From 25d301874f692498e565f13e958626cfc57340e0 Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Wed, 7 Jan 2026 09:00:47 +0100 Subject: [PATCH 1/6] Update quantity to 0.13 and remove typenum dependency (#328) --- CHANGELOG.md | 4 ++ Cargo.toml | 3 +- crates/feos-core/Cargo.toml | 1 - .../src/equation_of_state/residual.rs | 5 +- crates/feos-core/src/lib.rs | 55 +++++++++++-------- .../src/phase_equilibria/bubble_dew.rs | 3 +- crates/feos-core/src/state/builder.rs | 13 ++--- crates/feos-core/src/state/cache.rs | 4 +- crates/feos-core/src/state/mod.rs | 13 ++--- crates/feos-dft/Cargo.toml | 1 - crates/feos-dft/src/adsorption/pore.rs | 4 +- crates/feos-dft/src/pdgt.rs | 6 +- crates/feos-dft/src/profile/mod.rs | 7 +-- crates/feos/Cargo.toml | 1 - crates/feos/benches/dft_pore.rs | 3 +- crates/feos/benches/dual_numbers.rs | 3 +- crates/feos/benches/state_properties.rs | 5 +- crates/feos/src/association/mod.rs | 9 ++- crates/feos/src/epcsaft/eos/mod.rs | 7 +-- crates/feos/src/gc_pcsaft/eos/dispersion.rs | 5 +- crates/feos/src/gc_pcsaft/eos/hard_chain.rs | 5 +- crates/feos/src/gc_pcsaft/eos/mod.rs | 5 +- crates/feos/src/ideal_gas/dippr.rs | 7 +-- crates/feos/src/ideal_gas/joback.rs | 5 +- crates/feos/src/multiparameter/mod.rs | 5 +- crates/feos/src/pcsaft/eos/mod.rs | 14 ++--- crates/feos/src/pets/eos/mod.rs | 5 +- crates/feos/src/uvtheory/eos/mod.rs | 13 ++--- crates/feos/tests/gc_pcsaft/binary.rs | 5 +- crates/feos/tests/gc_pcsaft/dft.rs | 9 ++- crates/feos/tests/pcsaft/critical_point.rs | 5 +- crates/feos/tests/pcsaft/dft.rs | 11 ++-- .../tests/pcsaft/state_creation_mixture.rs | 7 +-- .../feos/tests/pcsaft/state_creation_pure.rs | 11 ++-- .../tests/saftvrmie/critical_properties.rs | 3 +- py-feos/Cargo.toml | 1 - py-feos/src/dft/profile.rs | 16 +++--- py-feos/src/eos/mod.rs | 4 +- py-feos/src/phase_equilibria.rs | 13 ++--- py-feos/src/state.rs | 13 +++-- 40 files changed, 149 insertions(+), 160 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c4b7d03..f395eb34a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. 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). +## [Breaking] +### Packaging +- Updated `quantity` dependency to 0.13 and removed the `typenum` dependency. [#323](https://github.com/feos-org/feos/pull/323) + ## [Unreleased] ### Added - Add Rayon global thread pool control via `FEOS_MAX_THREADS` and `set_num_threads()`/ `get_num_threads()` to Python. [#346](https://github.com/feos-org/feos/pull/346) diff --git a/Cargo.toml b/Cargo.toml index 5f4bd6586..de98c170f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ keywords = [ categories = ["science"] [workspace.dependencies] -quantity = "0.12" +quantity = "0.13" num-dual = "0.13" ndarray = "0.17" nalgebra = "0.34" @@ -33,7 +33,6 @@ serde = "1.0" serde_json = "1.0" indexmap = "2.0" itertools = "0.14" -typenum = "1.16" rayon = "1.11" petgraph = "0.8" rustdct = "0.7" diff --git a/crates/feos-core/Cargo.toml b/crates/feos-core/Cargo.toml index 94717bdb3..f33cd1255 100644 --- a/crates/feos-core/Cargo.toml +++ b/crates/feos-core/Cargo.toml @@ -25,7 +25,6 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["preserve_order"] } indexmap = { workspace = true, features = ["serde"] } rayon = { workspace = true, optional = true } -typenum = { workspace = true } itertools = { workspace = true } [dev-dependencies] diff --git a/crates/feos-core/src/equation_of_state/residual.rs b/crates/feos-core/src/equation_of_state/residual.rs index 1fe7bb56c..326aff883 100644 --- a/crates/feos-core/src/equation_of_state/residual.rs +++ b/crates/feos-core/src/equation_of_state/residual.rs @@ -3,9 +3,10 @@ use nalgebra::{DVector, DefaultAllocator, Dim, Dyn, OMatrix, OVector, U1, alloca use num_dual::{DualNum, Gradients, partial, partial2, second_derivative, third_derivative}; use quantity::ad::first_derivative; use quantity::*; -use std::ops::Deref; +use std::ops::{Deref, Div}; use std::sync::Arc; -use typenum::Quot; + +type Quot = >::Output; /// Molar weight of all components. /// diff --git a/crates/feos-core/src/lib.rs b/crates/feos-core/src/lib.rs index b883193ca..b51ae0653 100644 --- a/crates/feos-core/src/lib.rs +++ b/crates/feos-core/src/lib.rs @@ -3,7 +3,6 @@ #![warn(clippy::allow_attributes)] use quantity::{Quantity, SIUnit}; use std::ops::{Div, Mul}; -use typenum::Integer; /// Print messages with level `Verbosity::Iter` or higher. #[macro_export] @@ -132,20 +131,20 @@ const fn powi(x: f64, n: i32) -> f64 { /// Conversion between reduced units and SI units. pub trait ReferenceSystem { type Inner; - type T: Integer; - type L: Integer; - type M: Integer; - type I: Integer; - type THETA: Integer; - type N: Integer; - type J: Integer; - const FACTOR: f64 = powi(REFERENCE_VALUES[0], Self::T::I32) - * powi(REFERENCE_VALUES[1], Self::L::I32) - * powi(REFERENCE_VALUES[2], Self::M::I32) - * powi(REFERENCE_VALUES[3], Self::I::I32) - * powi(REFERENCE_VALUES[4], Self::THETA::I32) - * powi(REFERENCE_VALUES[5], Self::N::I32) - * powi(REFERENCE_VALUES[6], Self::J::I32); + const T: i8; + const L: i8; + const M: i8; + const I: i8; + const THETA: i8; + const N: i8; + const J: i8; + const FACTOR: f64 = powi(REFERENCE_VALUES[0], Self::T as i32) + * powi(REFERENCE_VALUES[1], Self::L as i32) + * powi(REFERENCE_VALUES[2], Self::M as i32) + * powi(REFERENCE_VALUES[3], Self::I as i32) + * powi(REFERENCE_VALUES[4], Self::THETA as i32) + * powi(REFERENCE_VALUES[5], Self::N as i32) + * powi(REFERENCE_VALUES[6], Self::J as i32); fn from_reduced(value: Self::Inner) -> Self where @@ -161,17 +160,25 @@ pub trait ReferenceSystem { } /// Conversion to and from reduced units -impl - ReferenceSystem for Quantity> +impl< + Inner, + const T: i8, + const L: i8, + const M: i8, + const I: i8, + const THETA: i8, + const N: i8, + const J: i8, +> ReferenceSystem for Quantity> { type Inner = Inner; - type T = T; - type L = L; - type M = M; - type I = I; - type THETA = THETA; - type N = N; - type J = J; + const T: i8 = T; + const L: i8 = L; + const M: i8 = M; + const I: i8 = I; + const THETA: i8 = THETA; + const N: i8 = N; + const J: i8 = J; fn from_reduced(value: Inner) -> Self where Inner: Mul, diff --git a/crates/feos-core/src/phase_equilibria/bubble_dew.rs b/crates/feos-core/src/phase_equilibria/bubble_dew.rs index 9433720cf..a50cc46a1 100644 --- a/crates/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/crates/feos-core/src/phase_equilibria/bubble_dew.rs @@ -12,7 +12,6 @@ use ndarray::Array1; use num_dual::linalg::LU; use num_dual::{DualNum, DualStruct, Gradients}; use quantity::{Density, Dimensionless, Moles, Pressure, Quantity, RGAS, SIUnit, Temperature}; -use typenum::{N1, N2, P1, Z0}; const MAX_ITER_INNER: usize = 5; const TOL_INNER: f64 = 1e-9; @@ -94,7 +93,7 @@ impl + Copy> TemperatureOrPressure for Temperature { // used instead of the explicit unit. Maybe the type is too complicated for the // compiler? impl + Copy> TemperatureOrPressure - for Quantity> + for Quantity> { type Other = Temperature; const IDENTIFIER: &'static str = "pressure"; diff --git a/crates/feos-core/src/state/builder.rs b/crates/feos-core/src/state/builder.rs index 02c61c93d..c418a7c1f 100644 --- a/crates/feos-core/src/state/builder.rs +++ b/crates/feos-core/src/state/builder.rs @@ -14,25 +14,24 @@ use quantity::*; /// # use quantity::*; /// # use nalgebra::dvector; /// # use approx::assert_relative_eq; -/// # use typenum::P3; /// # fn main() -> FeosResult<()> { /// // Create a state for given T,V,N /// let eos = &PengRobinson::new(PengRobinsonParameters::new_simple(&[369.8], &[41.9 * 1e5], &[0.15], &[15.0])?); /// let state = StateBuilder::new(&eos) /// .temperature(300.0 * KELVIN) -/// .volume(12.5 * METER.powi::()) +/// .volume(12.5 * METER.powi::<3>()) /// .moles(&(dvector![2.5] * MOL)) /// .build()?; -/// assert_eq!(state.density, 0.2 * MOL / METER.powi::()); +/// assert_eq!(state.density, 0.2 * MOL / METER.powi::<3>()); /// /// // For a pure component, the composition does not need to be specified. /// let eos = &PengRobinson::new(PengRobinsonParameters::new_simple(&[369.8], &[41.9 * 1e5], &[0.15], &[15.0])?); /// let state = StateBuilder::new(&eos) /// .temperature(300.0 * KELVIN) -/// .volume(12.5 * METER.powi::()) +/// .volume(12.5 * METER.powi::<3>()) /// .total_moles(2.5 * MOL) /// .build()?; -/// assert_eq!(state.density, 0.2 * MOL / METER.powi::()); +/// assert_eq!(state.density, 0.2 * MOL / METER.powi::<3>()); /// /// // The state can be constructed without providing any extensive property. /// let eos = &PengRobinson::new( @@ -45,10 +44,10 @@ use quantity::*; /// ); /// let state = StateBuilder::new(&eos) /// .temperature(300.0 * KELVIN) -/// .partial_density(&(dvector![0.2, 0.6] * MOL / METER.powi::())) +/// .partial_density(&(dvector![0.2, 0.6] * MOL / METER.powi::<3>())) /// .build()?; /// assert_relative_eq!(state.molefracs, dvector![0.25, 0.75]); -/// assert_relative_eq!(state.density, 0.8 * MOL / METER.powi::()); +/// assert_relative_eq!(state.density, 0.8 * MOL / METER.powi::<3>()); /// # Ok(()) /// # } /// ``` diff --git a/crates/feos-core/src/state/cache.rs b/crates/feos-core/src/state/cache.rs index 2ee54a737..b9a1326f7 100644 --- a/crates/feos-core/src/state/cache.rs +++ b/crates/feos-core/src/state/cache.rs @@ -1,8 +1,10 @@ use nalgebra::allocator::Allocator; use nalgebra::{DefaultAllocator, Dim, OVector, Scalar}; use quantity::*; +use std::ops::Sub; use std::sync::OnceLock; -use typenum::Diff; + +type Diff = >::Output; #[derive(Clone, Debug)] #[expect(clippy::type_complexity)] diff --git a/crates/feos-core/src/state/mod.rs b/crates/feos-core/src/state/mod.rs index 4f896d72e..5da7a43c5 100644 --- a/crates/feos-core/src/state/mod.rs +++ b/crates/feos-core/src/state/mod.rs @@ -798,12 +798,11 @@ mod critical_point; mod tests { use super::*; use nalgebra::dvector; - use typenum::P3; #[test] fn test_validate() { let temperature = 298.15 * KELVIN; - let density = 3000.0 * MOL / METER.powi::(); + let density = 3000.0 * MOL / METER.powi::<3>(); let molefracs = dvector![0.03, 0.02, 0.05]; assert!(validate(temperature, density, &molefracs).is_ok()); } @@ -811,7 +810,7 @@ mod tests { #[test] fn test_negative_temperature() { let temperature = -298.15 * KELVIN; - let density = 3000.0 * MOL / METER.powi::(); + let density = 3000.0 * MOL / METER.powi::<3>(); let molefracs = dvector![0.03, 0.02, 0.05]; assert!(validate(temperature, density, &molefracs).is_err()); } @@ -819,7 +818,7 @@ mod tests { #[test] fn test_nan_temperature() { let temperature = f64::NAN * KELVIN; - let density = 3000.0 * MOL / METER.powi::(); + let density = 3000.0 * MOL / METER.powi::<3>(); let molefracs = dvector![0.03, 0.02, 0.05]; assert!(validate(temperature, density, &molefracs).is_err()); } @@ -827,7 +826,7 @@ mod tests { #[test] fn test_negative_mole_number() { let temperature = 298.15 * KELVIN; - let density = 3000.0 * MOL / METER.powi::(); + let density = 3000.0 * MOL / METER.powi::<3>(); let molefracs = dvector![-0.03, 0.02, 0.05]; assert!(validate(temperature, density, &molefracs).is_err()); } @@ -835,7 +834,7 @@ mod tests { #[test] fn test_nan_mole_number() { let temperature = 298.15 * KELVIN; - let density = 3000.0 * MOL / METER.powi::(); + let density = 3000.0 * MOL / METER.powi::<3>(); let molefracs = dvector![f64::NAN, 0.02, 0.05]; assert!(validate(temperature, density, &molefracs).is_err()); } @@ -843,7 +842,7 @@ mod tests { #[test] fn test_negative_density() { let temperature = 298.15 * KELVIN; - let density = -3000.0 * MOL / METER.powi::(); + let density = -3000.0 * MOL / METER.powi::<3>(); let molefracs = dvector![0.01, 0.02, 0.05]; assert!(validate(temperature, density, &molefracs).is_err()); } diff --git a/crates/feos-dft/Cargo.toml b/crates/feos-dft/Cargo.toml index 9a010add0..2e9e52a7f 100644 --- a/crates/feos-dft/Cargo.toml +++ b/crates/feos-dft/Cargo.toml @@ -25,7 +25,6 @@ num-traits = { workspace = true } libm = { workspace = true } gauss-quad = { workspace = true, optional = true } petgraph = { workspace = true } -typenum = { workspace = true } feos-core = { workspace = true } diff --git a/crates/feos-dft/src/adsorption/pore.rs b/crates/feos-dft/src/adsorption/pore.rs index c7602b7d5..faf19e62d 100644 --- a/crates/feos-dft/src/adsorption/pore.rs +++ b/crates/feos-dft/src/adsorption/pore.rs @@ -19,12 +19,12 @@ use quantity::{ Temperature, Volume, }; use rustdct::DctNum; -use typenum::Diff; +use std::ops::Sub; const POTENTIAL_OFFSET: f64 = 2.0; const DEFAULT_GRID_POINTS: usize = 2048; -pub type _HenryCoefficient = Diff<_Moles, _Pressure>; +pub type _HenryCoefficient = <_Moles as Sub<_Pressure>>::Output; pub type HenryCoefficient = Quantity; /// Parameters required to specify a 1D pore. diff --git a/crates/feos-dft/src/pdgt.rs b/crates/feos-dft/src/pdgt.rs index d3a9fa30a..47ad359eb 100644 --- a/crates/feos-dft/src/pdgt.rs +++ b/crates/feos-dft/src/pdgt.rs @@ -10,7 +10,9 @@ use quantity::{ Temperature, }; use std::ops::{Add, AddAssign, Sub}; -use typenum::{Diff, P2, Sum}; + +type Sum = >::Output; +type Diff = >::Output; type _InfluenceParameter = Diff, _Density>; type InfluenceParameter = Quantity; @@ -218,7 +220,7 @@ pub trait PdgtFunctionalProperties: HelmholtzEnergyFunctional { // calculate interfacial width let w_temp = integrate_trapezoidal(&rho_r * &*z * z_int, dx); - *w = (24.0 * (w_temp - 0.5 * ze.powi::())).sqrt(); + *w = (24.0 * (w_temp - 0.5 * ze.powi::<2>())).sqrt(); // shift density profile *z -= ze; diff --git a/crates/feos-dft/src/profile/mod.rs b/crates/feos-dft/src/profile/mod.rs index 7bc9b7ac9..edac0e7b2 100644 --- a/crates/feos-dft/src/profile/mod.rs +++ b/crates/feos-dft/src/profile/mod.rs @@ -12,7 +12,6 @@ use num_dual::DualNum; use quantity::{_Volume, DEGREES, Density, Length, Moles, Quantity, Temperature, Volume}; use std::ops::{Add, MulAssign}; use std::sync::Arc; -use typenum::Sum; mod properties; @@ -304,7 +303,7 @@ where pub fn integrate, U>( &self, profile: &Quantity, U>, - ) -> Quantity> + ) -> Quantity>::Output> where _Volume: Add, { @@ -322,7 +321,7 @@ where pub fn integrate_comp, U>( &self, profile: &Quantity, U>, - ) -> Quantity, Sum<_Volume, U>> + ) -> Quantity, <_Volume as Add>::Output> where _Volume: Add, { @@ -335,7 +334,7 @@ where pub fn integrate_segments, U>( &self, profile: &Quantity, U>, - ) -> Quantity, Sum<_Volume, U>> + ) -> Quantity, <_Volume as Add>::Output> where _Volume: Add, { diff --git a/crates/feos/Cargo.toml b/crates/feos/Cargo.toml index 5965e4354..28e6e6109 100644 --- a/crates/feos/Cargo.toml +++ b/crates/feos/Cargo.toml @@ -26,7 +26,6 @@ serde_json = { workspace = true } indexmap = { workspace = true } rayon = { workspace = true, optional = true } itertools = { workspace = true } -typenum = { workspace = true } feos-core = { workspace = true } feos-derive = { workspace = true, optional = true } diff --git a/crates/feos/benches/dft_pore.rs b/crates/feos/benches/dft_pore.rs index 440c3648e..667c5ff12 100644 --- a/crates/feos/benches/dft_pore.rs +++ b/crates/feos/benches/dft_pore.rs @@ -10,7 +10,6 @@ use feos::hard_sphere::{FMTFunctional, FMTVersion}; use feos::pcsaft::{PcSaftFunctional, PcSaftParameters}; use nalgebra::dvector; use quantity::{ANGSTROM, KELVIN, NAV}; -use typenum::P3; fn fmt(c: &mut Criterion) { let mut group = c.benchmark_group("DFT_pore_fmt"); @@ -23,7 +22,7 @@ fn fmt(c: &mut Criterion) { None, None, ); - let bulk = State::new_pure(&func, KELVIN, 0.75 / NAV / ANGSTROM.powi::()).unwrap(); + let bulk = State::new_pure(&func, KELVIN, 0.75 / NAV / ANGSTROM.powi::<3>()).unwrap(); group.bench_function("liquid", |b| { b.iter(|| pore.initialize(&bulk, None, None).unwrap().solve(None)) }); diff --git a/crates/feos/benches/dual_numbers.rs b/crates/feos/benches/dual_numbers.rs index 6ec66a63c..454c6828f 100644 --- a/crates/feos/benches/dual_numbers.rs +++ b/crates/feos/benches/dual_numbers.rs @@ -13,7 +13,6 @@ use feos_core::parameter::PureRecord; use nalgebra::{DVector, Dyn, dvector}; use num_dual::{Dual2_64, Dual3_64, Dual64, DualNum, HyperDual64}; use quantity::*; -use typenum::P3; /// Helper function to create a state for given parameters. /// - temperature is 80% of critical temperature, @@ -129,7 +128,7 @@ fn methane_co2_pcsaft(c: &mut Criterion) { // 230 K, 50 bar, x0 = 0.15 let temperature = 230.0 * KELVIN; - let density = 24.16896 * KILO * MOL / METER.powi::(); + let density = 24.16896 * KILO * MOL / METER.powi::<3>(); let volume = 10.0 * MOL / density; let x = dvector![0.15, 0.85]; let moles = &x * 10.0 * MOL; diff --git a/crates/feos/benches/state_properties.rs b/crates/feos/benches/state_properties.rs index 401addf03..916f094ed 100644 --- a/crates/feos/benches/state_properties.rs +++ b/crates/feos/benches/state_properties.rs @@ -5,7 +5,6 @@ use feos::core::{Contributions, Residual, State}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use nalgebra::{DVector, dvector}; use quantity::*; -use typenum::P3; /// Evaluate a property of a state given the EoS, the property to compute, /// temperature, volume, moles, and the contributions to consider. @@ -42,7 +41,7 @@ fn properties_pcsaft(c: &mut Criterion) { .unwrap(); let eos = PcSaft::new(parameters); let t = 300.0 * KELVIN; - let density = 71.18 * KILO * MOL / METER.powi::(); + let density = 71.18 * KILO * MOL / METER.powi::<3>(); let v = 100.0 * MOL / density; let x = dvector![1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]; let m = &x * 100.0 * MOL; @@ -92,7 +91,7 @@ fn properties_pcsaft_polar(c: &mut Criterion) { .unwrap(); let eos = PcSaft::new(parameters); let t = 300.0 * KELVIN; - let density = 71.18 * KILO * MOL / METER.powi::(); + let density = 71.18 * KILO * MOL / METER.powi::<3>(); let v = 100.0 * MOL / density; let x = dvector![1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]; let m = &x * 100.0 * MOL; diff --git a/crates/feos/src/association/mod.rs b/crates/feos/src/association/mod.rs index a12e12e14..72bd97a38 100644 --- a/crates/feos/src/association/mod.rs +++ b/crates/feos/src/association/mod.rs @@ -18,7 +18,7 @@ pub use dft::YuWuAssociationFunctional; /// [AssociationStrength::association_strength] multiplies the model-specific /// site-site association strength [AssociationStrength::association_strength_ij] /// with the contact value of the hard-sphere pair correlation function. -/// +/// /// For implementations that require a different form, /// [AssociationStrength::association_strength] can be overwritten. pub trait AssociationStrength: HardSphereProperties { @@ -463,7 +463,6 @@ mod tests_gc_pcsaft { use nalgebra::dvector; use num_dual::Dual64; use quantity::{METER, MOL, PASCAL, Pressure}; - use typenum::P3; #[test] fn test_assoc_propanol() { @@ -471,7 +470,7 @@ mod tests_gc_pcsaft { let params = GcPcSaftEosParameters::new(¶meters); let contrib = Association::new(50, 1e-10); let temperature = 300.0; - let volume = Dual64::from_re(METER.powi::().to_reduced()).derivative(); + let volume = Dual64::from_re(METER.powi::<3>().to_reduced()).derivative(); let moles = Dual64::from_re((1.5 * MOL).to_reduced()); let molar_volume = volume / moles; let state = StateHD::new( @@ -499,7 +498,7 @@ mod tests_gc_pcsaft { let params = GcPcSaftEosParameters::new(¶meters); let contrib = Association::new_cross_association(50, 1e-10); let temperature = 300.0; - let volume = Dual64::from_re(METER.powi::().to_reduced()).derivative(); + let volume = Dual64::from_re(METER.powi::<3>().to_reduced()).derivative(); let moles = Dual64::from_re((1.5 * MOL).to_reduced()); let molar_volume = volume / moles; let state = StateHD::new( @@ -527,7 +526,7 @@ mod tests_gc_pcsaft { let params = GcPcSaftEosParameters::new(¶meters); let contrib = Association::new(50, 1e-10); let temperature = 300.0; - let volume = Dual64::from_re(METER.powi::().to_reduced()).derivative(); + let volume = Dual64::from_re(METER.powi::<3>().to_reduced()).derivative(); let moles = (dvector![1.5, 2.5] * MOL).to_reduced().map(Dual64::from_re); let total_moles = moles.sum(); let molar_volume = volume / total_moles; diff --git a/crates/feos/src/epcsaft/eos/mod.rs b/crates/feos/src/epcsaft/eos/mod.rs index 06aa87b63..03a2713d0 100644 --- a/crates/feos/src/epcsaft/eos/mod.rs +++ b/crates/feos/src/epcsaft/eos/mod.rs @@ -173,13 +173,12 @@ mod tests { use approx::assert_relative_eq; use feos_core::*; use nalgebra::dvector; - use typenum::P3; #[test] fn ideal_gas_pressure() { let e = ElectrolytePcSaft::new(propane_parameters()).unwrap(); let t = 200.0 * KELVIN; - let v = 1e-3 * METER.powi::(); + let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&&e, t, v, &n).unwrap(); let p_ig = s.total_moles * RGAS * t / v; @@ -195,7 +194,7 @@ mod tests { fn ideal_gas_heat_capacity_joback() { let e = ElectrolytePcSaft::new(propane_parameters()).unwrap(); let t = 200.0 * KELVIN; - let v = 1e-3 * METER.powi::(); + let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&&e, t, v, &n).unwrap(); let p_ig = s.total_moles * RGAS * t / v; @@ -282,7 +281,7 @@ mod tests { let e2 = ElectrolytePcSaft::new(butane_parameters()).unwrap(); let e12 = ElectrolytePcSaft::new(propane_butane_parameters()).unwrap(); let t = 300.0 * KELVIN; - let v = 0.02456883872966545 * METER.powi::(); + let v = 0.02456883872966545 * METER.powi::<3>(); let m1 = dvector![2.0] * MOL; let m1m = dvector![2.0, 0.0] * MOL; let m2m = dvector![0.0, 2.0] * MOL; diff --git a/crates/feos/src/gc_pcsaft/eos/dispersion.rs b/crates/feos/src/gc_pcsaft/eos/dispersion.rs index a1a8169bf..3f464507d 100644 --- a/crates/feos/src/gc_pcsaft/eos/dispersion.rs +++ b/crates/feos/src/gc_pcsaft/eos/dispersion.rs @@ -129,13 +129,12 @@ mod test { use nalgebra::dvector; use num_dual::Dual64; use quantity::{METER, MOL, PASCAL, Pressure}; - use typenum::P3; #[test] fn test_dispersion_propane() { let parameters = propane(); let temperature = 300.0; - let volume = Dual64::from_re(METER.powi::().to_reduced()).derivative(); + let volume = Dual64::from_re(METER.powi::<3>().to_reduced()).derivative(); let moles = Dual64::from_re((1.5 * MOL).to_reduced()); let molar_volume = volume / moles; let state = StateHD::new( @@ -153,7 +152,7 @@ mod test { fn test_dispersion_propanol() { let parameters = GcPcSaftEosParameters::new(&propanol()); let temperature = 300.0; - let volume = Dual64::from_re(METER.powi::().to_reduced()).derivative(); + let volume = Dual64::from_re(METER.powi::<3>().to_reduced()).derivative(); let moles = Dual64::from_re((1.5 * MOL).to_reduced()); let molar_volume = volume / moles; let state = StateHD::new( diff --git a/crates/feos/src/gc_pcsaft/eos/hard_chain.rs b/crates/feos/src/gc_pcsaft/eos/hard_chain.rs index 50b3c4afa..f67dffda4 100644 --- a/crates/feos/src/gc_pcsaft/eos/hard_chain.rs +++ b/crates/feos/src/gc_pcsaft/eos/hard_chain.rs @@ -42,13 +42,12 @@ mod test { use nalgebra::dvector; use num_dual::Dual64; use quantity::{METER, MOL, PASCAL, Pressure}; - use typenum::P3; #[test] fn test_hc_propane() { let parameters = propane(); let temperature = 300.0; - let volume = METER.powi::().to_reduced(); + let volume = METER.powi::<3>().to_reduced(); let volume = Dual64::from_re(volume).derivative(); let moles = (1.5 * MOL).to_reduced(); let state = StateHD::new( @@ -70,7 +69,7 @@ mod test { fn test_hc_propanol() { let parameters = GcPcSaftEosParameters::new(&propanol()); let temperature = 300.0; - let volume = METER.powi::().to_reduced(); + let volume = METER.powi::<3>().to_reduced(); let volume = Dual64::from_re(volume).derivative(); let moles = (1.5 * MOL).to_reduced(); let state = StateHD::new( diff --git a/crates/feos/src/gc_pcsaft/eos/mod.rs b/crates/feos/src/gc_pcsaft/eos/mod.rs index 4198da197..ae6acf960 100644 --- a/crates/feos/src/gc_pcsaft/eos/mod.rs +++ b/crates/feos/src/gc_pcsaft/eos/mod.rs @@ -150,13 +150,12 @@ mod test { use nalgebra::dvector; use num_dual::Dual64; use quantity::{METER, MOL, PASCAL, Pressure}; - use typenum::P3; #[test] fn hs_propane() { let parameters = propane(); let temperature = 300.0; - let volume = Dual64::from_re(METER.powi::().to_reduced()).derivative(); + let volume = Dual64::from_re(METER.powi::<3>().to_reduced()).derivative(); let moles = Dual64::from_re((1.5 * MOL).to_reduced()); let molar_volume = volume / moles; let state = StateHD::new( @@ -174,7 +173,7 @@ mod test { fn hs_propanol() { let parameters = GcPcSaftEosParameters::new(&propanol()); let temperature = 300.0; - let volume = Dual64::from_re(METER.powi::().to_reduced()).derivative(); + let volume = Dual64::from_re(METER.powi::<3>().to_reduced()).derivative(); let moles = Dual64::from_re((1.5 * MOL).to_reduced()); let molar_volume = volume / moles; let state = StateHD::new( diff --git a/crates/feos/src/ideal_gas/dippr.rs b/crates/feos/src/ideal_gas/dippr.rs index 927dd2e07..7553e376c 100644 --- a/crates/feos/src/ideal_gas/dippr.rs +++ b/crates/feos/src/ideal_gas/dippr.rs @@ -159,7 +159,6 @@ mod tests { use feos_core::{Contributions, EquationOfState, StateBuilder}; use num_dual::first_derivative; use quantity::*; - use typenum::P3; use super::*; @@ -173,7 +172,7 @@ mod tests { let dippr = Dippr::new(DipprParameters::new_pure(record.clone())?); let eos = EquationOfState::ideal_gas(dippr.clone()); let temperature = 300.0 * KELVIN; - let volume = METER.powi::(); + let volume = METER.powi::<3>(); let state = StateBuilder::new(&&eos) .temperature(temperature) .volume(volume) @@ -217,7 +216,7 @@ mod tests { let dippr = Dippr::new(DipprParameters::new_pure(record.clone())?); let eos = EquationOfState::ideal_gas(dippr.clone()); let temperature = 300.0 * KELVIN; - let volume = METER.powi::(); + let volume = METER.powi::<3>(); let state = StateBuilder::new(&&eos) .temperature(temperature) .volume(volume) @@ -263,7 +262,7 @@ mod tests { let dippr = Dippr::new(DipprParameters::new_pure(record.clone())?); let eos = EquationOfState::ideal_gas(dippr.clone()); let temperature = 20.0 * KELVIN; - let volume = METER.powi::(); + let volume = METER.powi::<3>(); let state = StateBuilder::new(&&eos) .temperature(temperature) .volume(volume) diff --git a/crates/feos/src/ideal_gas/joback.rs b/crates/feos/src/ideal_gas/joback.rs index bef674722..742c6ef2e 100644 --- a/crates/feos/src/ideal_gas/joback.rs +++ b/crates/feos/src/ideal_gas/joback.rs @@ -179,7 +179,6 @@ mod tests { use nalgebra::dvector; use quantity::*; use std::collections::HashMap; - use typenum::P3; use super::*; @@ -263,7 +262,7 @@ mod tests { let pr = PureRecord::new(Identifier::default(), 1.0, jr); let joback = Joback::new(JobackParameters::new_pure(pr)?); let eos = EquationOfState::ideal_gas(joback); - let state = State::new_pure(&&eos, 1000.0 * KELVIN, 1.0 * MOL / METER.powi::())?; + let state = State::new_pure(&&eos, 1000.0 * KELVIN, 1.0 * MOL / METER.powi::<3>())?; assert!( ((state.molar_isobaric_heat_capacity(Contributions::IdealGas) / (JOULE / MOL / KELVIN)) @@ -294,7 +293,7 @@ mod tests { )?); let eos = EquationOfState::ideal_gas(joback.clone()); let temperature = 300.0 * KELVIN; - let volume = METER.powi::(); + let volume = METER.powi::<3>(); let moles = &dvector![1.0, 3.0] * MOL; let state = StateBuilder::new(&&eos) .temperature(temperature) diff --git a/crates/feos/src/multiparameter/mod.rs b/crates/feos/src/multiparameter/mod.rs index f8ab47634..68bbd98b8 100644 --- a/crates/feos/src/multiparameter/mod.rs +++ b/crates/feos/src/multiparameter/mod.rs @@ -138,7 +138,6 @@ mod test { use nalgebra::{Dyn, SVector, U2, dvector}; use num_dual::{Dual2Vec, hessian}; use quantity::{GRAM, KELVIN, KILO, KILOGRAM, METER, MOL, RGAS}; - use typenum::P3; use super::*; @@ -262,7 +261,7 @@ mod test { #[test] fn test_ideal_gas_hack() { let t = 647. * KELVIN; - let rho = 358. * KILOGRAM / METER.powi::(); + let rho = 358. * KILOGRAM / METER.powi::<3>(); let eos = &water(); let mw = eos.molar_weight.get(0); let moles = dvector![1.8] * MOL; @@ -270,7 +269,7 @@ mod test { let phi_feos = (a_feos / RGAS / moles.sum() / t).into_value(); println!("A: {a_feos}"); println!("phi(feos): {phi_feos}"); - let delta = (rho / (eos.rhoc * MOL / METER.powi::() * mw)).into_value(); + let delta = (rho / (eos.rhoc * MOL / METER.powi::<3>() * mw)).into_value(); let tau = (eos.tc * KELVIN / t).into_value(); let phi = eos.ideal_gas[0] .terms diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index aea6b6890..6b0aa8782 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -9,7 +9,6 @@ use num_dual::{DualNum, partial2}; use quantity::ad::first_derivative; use quantity::*; use std::f64::consts::{FRAC_PI_6, PI}; -use typenum::P2; pub(crate) mod dispersion; pub(crate) mod hard_chain; @@ -241,7 +240,7 @@ impl EntropyScaling for PcSaft { let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); 5.0 / 16.0 * (mw.get(i) * KB / NAV * temperature / PI).sqrt() / omega22(tr) - / (p.sigma[i] * ANGSTROM).powi::() + / (p.sigma[i] * ANGSTROM).powi::<2>() }) .collect(); let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; @@ -291,7 +290,7 @@ impl EntropyScaling for PcSaft { let res: Vec<_> = (0..self.components()) .map(|i| { let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); - 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi::() / omega11(tr) / (density * NAV) + 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi::<2>() / omega11(tr) / (density * NAV) * (temperature * RGAS / PI / mw.get(i) / p.m[i]).sqrt() }) .collect(); @@ -393,13 +392,12 @@ mod tests { use feos_core::*; use nalgebra::dvector; use quantity::{BAR, KELVIN, METER, PASCAL, RGAS}; - use typenum::{P2, P3}; #[test] fn ideal_gas_pressure() { let e = &propane_parameters(); let t = 200.0 * KELVIN; - let v = 1e-3 * METER.powi::(); + let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&e, t, v, &n).unwrap(); let p_ig = s.total_moles * RGAS * t / v; @@ -415,7 +413,7 @@ mod tests { fn ideal_gas_heat_capacity_joback() { let e = &propane_parameters(); let t = 200.0 * KELVIN; - let v = 1e-3 * METER.powi::(); + let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&e, t, v, &n).unwrap(); let p_ig = s.total_moles * RGAS * t / v; @@ -504,7 +502,7 @@ mod tests { let e2 = &butane_parameters(); let e12 = &propane_butane_parameters(); let t = 300.0 * KELVIN; - let v = 0.02456883872966545 * METER.powi::(); + let v = 0.02456883872966545 * METER.powi::<3>(); let m1 = dvector![2.0] * MOL; let m1m = dvector![2.0, 0.0] * MOL; let m2m = dvector![0.0, 2.0] * MOL; @@ -577,7 +575,7 @@ mod tests { let s = State::new_npt(&e, t, p, &n, None)?; assert_relative_eq!( s.diffusion(), - 0.01505 * (CENTI * METER).powi::() / SECOND, + 0.01505 * (CENTI * METER).powi::<2>() / SECOND, epsilon = 1e-5 ); assert_relative_eq!( diff --git a/crates/feos/src/pets/eos/mod.rs b/crates/feos/src/pets/eos/mod.rs index 0ad191726..f1d1d76cb 100644 --- a/crates/feos/src/pets/eos/mod.rs +++ b/crates/feos/src/pets/eos/mod.rs @@ -144,13 +144,12 @@ mod tests { use feos_core::{Contributions, PhaseEquilibrium, State, StateHD}; use nalgebra::dvector; use quantity::{BAR, KELVIN, METER, MOL, RGAS}; - use typenum::P3; #[test] fn ideal_gas_pressure() { let e = &Pets::new(argon_parameters()); let t = 200.0 * KELVIN; - let v = 1e-3 * METER.powi::(); + let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&e, t, v, &n).unwrap(); let p_ig = s.total_moles * RGAS * t / v; @@ -212,7 +211,7 @@ mod tests { let e2 = &Pets::new(krypton_parameters()); let e12 = &Pets::new(argon_krypton_parameters()); let t = 300.0 * KELVIN; - let v = 0.02456883872966545 * METER.powi::(); + let v = 0.02456883872966545 * METER.powi::<3>(); let m1 = dvector![2.0] * MOL; let m1m = dvector![2.0, 0.0] * MOL; let m2m = dvector![0.0, 2.0] * MOL; diff --git a/crates/feos/src/uvtheory/eos/mod.rs b/crates/feos/src/uvtheory/eos/mod.rs index bc38dcd33..9fa963ae2 100644 --- a/crates/feos/src/uvtheory/eos/mod.rs +++ b/crates/feos/src/uvtheory/eos/mod.rs @@ -110,7 +110,6 @@ mod test { use feos_core::{FeosResult, State}; use nalgebra::dvector; use quantity::{ANGSTROM, KELVIN, MOL, NAV, RGAS}; - use typenum::P3; #[test] fn helmholtz_energy_pure_wca() -> FeosResult<()> { @@ -123,7 +122,7 @@ mod test { let reduced_density = 1.0; let temperature = reduced_temperature * eps_k * KELVIN; let moles = dvector![2.0] * MOL; - let volume = (sig * ANGSTROM).powi::() / reduced_density * NAV * 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.residual_molar_helmholtz_energy() / (RGAS * temperature)).into_value(); assert_relative_eq!(a, 2.972986567516, max_relative = 1e-12); //wca @@ -147,7 +146,7 @@ mod test { let reduced_density = 1.0; let temperature = reduced_temperature * eps_k * KELVIN; let moles = dvector![2.0] * MOL; - let volume = (sig * ANGSTROM).powi::() / reduced_density * NAV * 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.residual_molar_helmholtz_energy() / (RGAS * temperature)).into_value(); @@ -173,7 +172,7 @@ mod test { let reduced_density = 0.5; let temperature = reduced_temperature * eps_k * KELVIN; let moles = dvector![2.0] * MOL; - let volume = (sig * ANGSTROM).powi::() / reduced_density * NAV * 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.residual_molar_helmholtz_energy() / (RGAS * temperature)).into_value(); dbg!(a); @@ -209,7 +208,7 @@ mod test { let reduced_density = 1.0; let moles = dvector![1.7, 0.3] * MOL; let total_moles = moles.sum(); - let volume = (sig_x * ANGSTROM).powi::() / reduced_density * NAV * total_moles; + let volume = (sig_x * ANGSTROM).powi::<3>() / reduced_density * NAV * total_moles; // EoS let options = UVTheoryOptions { @@ -242,7 +241,7 @@ mod test { let reduced_density = 0.9; let moles = dvector![0.4, 0.6] * MOL; let total_moles = moles.sum(); - let volume = (p.sigma[0] * ANGSTROM).powi::() / reduced_density * NAV * total_moles; + let volume = (p.sigma[0] * ANGSTROM).powi::<3>() / reduced_density * NAV * total_moles; // EoS let eos_wca = &UVTheory::new(parameters); @@ -267,7 +266,7 @@ mod test { // state let reduced_temperature = 1.5; let t_x = reduced_temperature * p.epsilon_k[0] * KELVIN; - let sigma_x_3 = (0.4 + 0.6 * 8.0) * ANGSTROM.powi::(); + let sigma_x_3 = (0.4 + 0.6 * 8.0) * ANGSTROM.powi::<3>(); let density = 0.52000000000000002 / sigma_x_3; let moles = dvector![0.4, 0.6] * MOL; let total_moles = moles.sum(); diff --git a/crates/feos/tests/gc_pcsaft/binary.rs b/crates/feos/tests/gc_pcsaft/binary.rs index 0419a8987..e9b9ed434 100644 --- a/crates/feos/tests/gc_pcsaft/binary.rs +++ b/crates/feos/tests/gc_pcsaft/binary.rs @@ -6,7 +6,6 @@ use feos_core::parameter::IdentifierOption; use feos_core::{Contributions, FeosResult, State}; use nalgebra::dvector; use quantity::{KELVIN, METER, MOL}; -use typenum::P3; #[test] fn test_binary() -> FeosResult<()> { @@ -70,9 +69,9 @@ fn test_polar_term() -> FeosResult<()> { let eos1 = &GcPcSaft::new(parameters1); let eos2 = &GcPcSaft::new(parameters2); let moles = dvector![0.5, 0.5] * MOL; - let p1 = State::new_nvt(&eos1, 300.0 * KELVIN, METER.powi::(), &moles)? + let p1 = State::new_nvt(&eos1, 300.0 * KELVIN, METER.powi::<3>(), &moles)? .pressure(Contributions::Total); - let p2 = State::new_nvt(&eos2, 300.0 * KELVIN, METER.powi::(), &moles)? + let p2 = State::new_nvt(&eos2, 300.0 * KELVIN, METER.powi::<3>(), &moles)? .pressure(Contributions::Total); println!("{p1} {p2}"); assert_eq!(p1, p2); diff --git a/crates/feos/tests/gc_pcsaft/dft.rs b/crates/feos/tests/gc_pcsaft/dft.rs index 5d245bfb1..a9813c56e 100644 --- a/crates/feos/tests/gc_pcsaft/dft.rs +++ b/crates/feos/tests/gc_pcsaft/dft.rs @@ -10,7 +10,6 @@ use feos_dft::{DFTSolver, Geometry}; use nalgebra::dvector; use quantity::*; use std::error::Error; -use typenum::P3; #[test] #[allow(non_snake_case)] @@ -40,7 +39,7 @@ fn test_bulk_implementation() -> Result<(), Box> { let eos = GcPcSaft::new(parameters); let func = GcPcSaftFunctional::new(parameters_func); let t = 200.0 * KELVIN; - let v = 0.002 * METER.powi::() * NAV / NAV_old; + let v = 0.002 * METER.powi::<3>() * NAV / NAV_old; let n = dvector![1.5] * MOL; let state_eos = State::new_nvt(&&eos, t, v, &n)?; let state_func = State::new_nvt(&&func, t, v, &n)?; @@ -123,7 +122,7 @@ fn test_bulk_association() -> Result<(), Box> { let func = GcPcSaftFunctional::new(func_parameters); let t = 200.0 * KELVIN; - let v = 0.002 * METER.powi::(); + let v = 0.002 * METER.powi::<3>(); let n = dvector![1.5] * MOL; let state_eos = State::new_nvt(&&eos, t, v, &n)?; let state_func = State::new_nvt(&&func, t, v, &n)?; @@ -171,13 +170,13 @@ fn test_dft() -> Result<(), Box> { assert_relative_eq!( vle.vapor().density, - 12.8820179191167643 * MOL / METER.powi::() * NAV_old / NAV, + 12.8820179191167643 * MOL / METER.powi::<3>() * NAV_old / NAV, max_relative = 1e-13, ); assert_relative_eq!( vle.liquid().density, - 13.2705903446123212 * KILO * MOL / METER.powi::() * NAV_old / NAV, + 13.2705903446123212 * KILO * MOL / METER.powi::<3>() * NAV_old / NAV, max_relative = 1e-13, ); diff --git a/crates/feos/tests/pcsaft/critical_point.rs b/crates/feos/tests/pcsaft/critical_point.rs index 48d36045c..9183e5e5d 100644 --- a/crates/feos/tests/pcsaft/critical_point.rs +++ b/crates/feos/tests/pcsaft/critical_point.rs @@ -6,7 +6,6 @@ use nalgebra::dvector; use quantity::*; use std::error::Error; use std::sync::Arc; -use typenum::P3; #[test] fn test_critical_point_pure() -> Result<(), Box> { @@ -22,7 +21,7 @@ fn test_critical_point_pure() -> Result<(), Box> { assert_relative_eq!(cp.temperature, 375.12441 * KELVIN, max_relative = 1e-8); assert_relative_eq!( cp.density, - 4733.00377 * MOL / METER.powi::(), + 4733.00377 * MOL / METER.powi::<3>(), max_relative = 1e-6 ); Ok(()) @@ -43,7 +42,7 @@ fn test_critical_point_mix() -> Result<(), Box> { assert_relative_eq!(cp.temperature, 407.93481 * KELVIN, max_relative = 1e-8); assert_relative_eq!( cp.density, - 4265.50745 * MOL / METER.powi::(), + 4265.50745 * MOL / METER.powi::<3>(), max_relative = 1e-6 ); Ok(()) diff --git a/crates/feos/tests/pcsaft/dft.rs b/crates/feos/tests/pcsaft/dft.rs index b6c6f3880..e8d057e47 100644 --- a/crates/feos/tests/pcsaft/dft.rs +++ b/crates/feos/tests/pcsaft/dft.rs @@ -12,7 +12,6 @@ use nalgebra::dvector; use ndarray::Axis; use quantity::*; use std::error::Error; -use typenum::P3; fn parameters(comp: &str) -> FeosResult { PcSaftParameters::from_json( @@ -35,7 +34,7 @@ fn test_bulk_implementations() -> Result<(), Box> { let func_full = PcSaftFunctional::new_full(parameters("water_np")?, FMTVersion::KierlikRosinberg); let t = 300.0 * KELVIN; - let v = 0.002 * METER.powi::() * NAV / NAV_old; + let v = 0.002 * METER.powi::<3>() * NAV / NAV_old; let n = dvector![1.5] * MOL; let state = State::new_nvt(&&eos, t, v, &n)?; let state_pure = State::new_nvt(&&func_pure, t, v, &n)?; @@ -135,7 +134,7 @@ fn test_dft_propane() -> Result<(), Box> { (&func_full_vec).solve_pdgt(&vle_full_vec, 198, 0, None)?.1 ); - let vapor_density = 12.2557486248527745 * MOL / METER.powi::() * NAV_old / NAV; + let vapor_density = 12.2557486248527745 * MOL / METER.powi::<3>() * NAV_old / NAV; assert_relative_eq!( vle_pure.vapor().density, vapor_density, @@ -152,7 +151,7 @@ fn test_dft_propane() -> Result<(), Box> { max_relative = 1e-13, ); - let liquid_density = 13.8941749145544549 * KILO * MOL / METER.powi::() * NAV_old / NAV; + let liquid_density = 13.8941749145544549 * KILO * MOL / METER.powi::<3>() * NAV_old / NAV; assert_relative_eq!( vle_pure.liquid().density, liquid_density, @@ -254,7 +253,7 @@ fn test_dft_water() -> Result<(), Box> { vle_full_vec.liquid().density ); - let vapor_density = 75.8045715345905222 * MOL / METER.powi::() * NAV_old / NAV; + let vapor_density = 75.8045715345905222 * MOL / METER.powi::<3>() * NAV_old / NAV; assert_relative_eq!( vle_pure.vapor().density, vapor_density, @@ -266,7 +265,7 @@ fn test_dft_water() -> Result<(), Box> { max_relative = 1e-13, ); - let liquid_density = 47.8480850281608454 * KILO * MOL / METER.powi::() * NAV_old / NAV; + let liquid_density = 47.8480850281608454 * KILO * MOL / METER.powi::<3>() * NAV_old / NAV; assert_relative_eq!( vle_pure.liquid().density, liquid_density, diff --git a/crates/feos/tests/pcsaft/state_creation_mixture.rs b/crates/feos/tests/pcsaft/state_creation_mixture.rs index e30d4058c..b7b93c73a 100644 --- a/crates/feos/tests/pcsaft/state_creation_mixture.rs +++ b/crates/feos/tests/pcsaft/state_creation_mixture.rs @@ -6,7 +6,6 @@ use feos_core::{Contributions, EquationOfState, FeosResult, StateBuilder}; use nalgebra::dvector; use quantity::*; use std::error::Error; -use typenum::P3; fn propane_butane_parameters() -> FeosResult<(PcSaftParameters, Vec)> { let saft = PcSaftParameters::from_json( @@ -61,7 +60,7 @@ fn pressure_entropy_molefracs() -> Result<(), Box> { fn volume_temperature_molefracs() -> Result<(), Box> { let saft = PcSaft::new(propane_butane_parameters()?.0); let temperature = 300.0 * KELVIN; - let volume = 1.5e-3 * METER.powi::(); + let volume = 1.5e-3 * METER.powi::<3>(); let moles = MOL; let x = dvector![0.3, 0.7]; let state = StateBuilder::new(&&saft) @@ -79,7 +78,7 @@ fn temperature_partial_density() -> Result<(), Box> { let saft = PcSaft::new(propane_butane_parameters()?.0); let temperature = 300.0 * KELVIN; let x = dvector![0.3, 0.7]; - let partial_density = x.clone() * MOL / METER.powi::(); + let partial_density = x.clone() * MOL / METER.powi::<3>(); let density = partial_density.sum(); let state = StateBuilder::new(&&saft) .temperature(temperature) @@ -98,7 +97,7 @@ fn temperature_density_molefracs() -> Result<(), Box> { let saft = PcSaft::new(propane_butane_parameters()?.0); let temperature = 300.0 * KELVIN; let x = dvector![0.3, 0.7]; - let density = MOL / METER.powi::(); + let density = MOL / METER.powi::<3>(); let state = StateBuilder::new(&&saft) .temperature(temperature) .density(density) diff --git a/crates/feos/tests/pcsaft/state_creation_pure.rs b/crates/feos/tests/pcsaft/state_creation_pure.rs index c8fea1780..d5bc9769e 100644 --- a/crates/feos/tests/pcsaft/state_creation_pure.rs +++ b/crates/feos/tests/pcsaft/state_creation_pure.rs @@ -7,7 +7,6 @@ use feos_core::{ }; use quantity::*; use std::error::Error; -use typenum::P3; fn propane_parameters() -> FeosResult<(PcSaftParameters, Vec)> { let saft = PcSaftParameters::from_json( @@ -29,7 +28,7 @@ fn propane_parameters() -> FeosResult<(PcSaftParameters, Vec)> { fn temperature_volume() -> Result<(), Box> { let saft = PcSaft::new(propane_parameters()?.0); let temperature = 300.0 * KELVIN; - let volume = 1.5e-3 * METER.powi::(); + let volume = 1.5e-3 * METER.powi::<3>(); let moles = MOL; let state = StateBuilder::new(&&saft) .temperature(temperature) @@ -44,7 +43,7 @@ fn temperature_volume() -> Result<(), Box> { fn temperature_density() -> Result<(), Box> { let saft = PcSaft::new(propane_parameters()?.0); let temperature = 300.0 * KELVIN; - let density = MOL / METER.powi::(); + let density = MOL / METER.powi::<3>(); let state = StateBuilder::new(&&saft) .temperature(temperature) .density(density) @@ -58,7 +57,7 @@ fn temperature_total_moles_volume() -> Result<(), Box> { let saft = PcSaft::new(propane_parameters()?.0); let temperature = 300.0 * KELVIN; let total_moles = MOL; - let volume = METER.powi::(); + let volume = METER.powi::<3>(); let state = StateBuilder::new(&&saft) .temperature(temperature) .volume(volume) @@ -74,7 +73,7 @@ fn temperature_total_moles_density() -> Result<(), Box> { let saft = PcSaft::new(propane_parameters()?.0); let temperature = 300.0 * KELVIN; let total_moles = MOL; - let density = MOL / METER.powi::(); + let density = MOL / METER.powi::<3>(); let state = StateBuilder::new(&&saft) .temperature(temperature) .density(density) @@ -131,7 +130,7 @@ fn pressure_temperature_initial_density() -> Result<(), Box> { let state = StateBuilder::new(&&saft) .temperature(temperature) .pressure(pressure) - .initial_density(MOL / METER.powi::()) + .initial_density(MOL / METER.powi::<3>()) .build()?; assert_relative_eq!( state.pressure(Contributions::Total), diff --git a/crates/feos/tests/saftvrmie/critical_properties.rs b/crates/feos/tests/saftvrmie/critical_properties.rs index 3a4a585ed..c78eb110a 100644 --- a/crates/feos/tests/saftvrmie/critical_properties.rs +++ b/crates/feos/tests/saftvrmie/critical_properties.rs @@ -3,12 +3,11 @@ use feos::saftvrmie::{SaftVRMie, test_utils}; use feos_core::{SolverOptions, State}; use quantity::*; use std::collections::HashMap; -use typenum::P3; /// Critical data reported in Lafitte et al. pub fn critical_data() -> HashMap<&'static str, (Temperature, Pressure, MassDensity)> { let mut data = HashMap::new(); - let kg_m3 = KILOGRAM / METER.powi::(); + let kg_m3 = KILOGRAM / METER.powi::<3>(); let mpa = MEGA * PASCAL; let k = KELVIN; diff --git a/py-feos/Cargo.toml b/py-feos/Cargo.toml index 0ab58879b..c06369b72 100644 --- a/py-feos/Cargo.toml +++ b/py-feos/Cargo.toml @@ -35,7 +35,6 @@ serde_json = { workspace = true } indexmap = { workspace = true } rayon = { workspace = true, optional = true } itertools = { workspace = true } -typenum = { workspace = true } paste = { workspace = true } feos = { workspace = true } diff --git a/py-feos/src/dft/profile.rs b/py-feos/src/dft/profile.rs index 20fd4ed19..426d5b099 100644 --- a/py-feos/src/dft/profile.rs +++ b/py-feos/src/dft/profile.rs @@ -1,5 +1,7 @@ macro_rules! impl_profile { ($struct:ident, $arr:ident, $arr2:ident, $si_arr:ident, $si_arr2:ident, $py_arr2:ident, [$([$ind:expr, $ax:ident]),+]$(, $si_arr3:ident)?) => { + + #[pymethods] impl $struct { /// Calculate the residual for the given profile. @@ -118,7 +120,7 @@ macro_rules! impl_profile { fn entropy_density( &mut self, contributions: PyContributions, - ) -> PyResult>, Volume>> { + ) -> PyResult<> as std::ops::Div>::Output> { Ok(self.0.profile.entropy_density(contributions.into()).map_err(PyFeosError::from)?) } @@ -166,33 +168,33 @@ macro_rules! impl_profile { } $( #[getter] - fn get_drho_dmu(&self) -> PyResult>, MolarEnergy>> { + fn get_drho_dmu(&self) -> PyResult<> as std::ops::Div>::Output> { Ok(self.0.profile.drho_dmu().map_err(PyFeosError::from)?) } )? #[getter] - fn get_dn_dmu(&self) -> PyResult>, MolarEnergy>> { + fn get_dn_dmu(&self) -> PyResult<> as std::ops::Div>::Output> { Ok(self.0.profile.dn_dmu().map_err(PyFeosError::from)?) } #[getter] - fn get_drho_dp(&self) -> PyResult>, Pressure>> { + fn get_drho_dp(&self) -> PyResult<> as std::ops::Div>::Output> { Ok(self.0.profile.drho_dp().map_err(PyFeosError::from)?) } #[getter] - fn get_dn_dp(&self) -> PyResult>, Pressure>> { + fn get_dn_dp(&self) -> PyResult<> as std::ops::Div>::Output> { Ok(self.0.profile.dn_dp().map_err(PyFeosError::from)?) } #[getter] - fn get_drho_dt(&self) -> PyResult>, Temperature>> { + fn get_drho_dt(&self) -> PyResult<> as std::ops::Div>::Output> { Ok(self.0.profile.drho_dt().map_err(PyFeosError::from)?) } #[getter] - fn get_dn_dt(&self) -> PyResult>, Temperature>> { + fn get_dn_dt(&self) -> PyResult<> as std::ops::Div>::Output> { Ok(self.0.profile.dn_dt().map_err(PyFeosError::from)?) } } diff --git a/py-feos/src/eos/mod.rs b/py-feos/src/eos/mod.rs index 86b6a1447..1147ac020 100644 --- a/py-feos/src/eos/mod.rs +++ b/py-feos/src/eos/mod.rs @@ -7,8 +7,10 @@ use nalgebra::{DVector, DVectorView, Dyn}; use numpy::{PyArray1, PyReadonlyArray1, ToPyArray}; use pyo3::prelude::*; use quantity::*; +use std::ops::Div; use std::sync::Arc; -use typenum::Quot; + +type Quot = >::Output; mod constructors; #[cfg(feature = "epcsaft")] diff --git a/py-feos/src/phase_equilibria.rs b/py-feos/src/phase_equilibria.rs index 77f95df3d..c3786e300 100644 --- a/py-feos/src/phase_equilibria.rs +++ b/py-feos/src/phase_equilibria.rs @@ -1,10 +1,10 @@ use crate::{ - eos::{parse_molefracs, PyEquationOfState}, + PyVerbosity, + eos::{PyEquationOfState, parse_molefracs}, error::PyFeosError, ideal_gas::IdealGasModel, residual::ResidualModel, state::{PyContributions, PyState, PyStateVec}, - PyVerbosity, }; use feos_core::{ Contributions, EquationOfState, PhaseDiagram, PhaseDiagramHetero, PhaseEquilibrium, ResidualDyn, @@ -17,7 +17,6 @@ use pyo3::prelude::*; use quantity::*; use std::ops::Deref; use std::sync::Arc; -use typenum::P3; /// A thermodynamic two phase equilibrium state. #[pyclass(name = "PhaseEquilibrium")] @@ -1082,7 +1081,7 @@ impl PyPhaseDiagram { self.0 .liquid() .density() - .convert_to(MOL / METER.powi::()) + .convert_to(MOL / METER.powi::<3>()) .into_raw_vec_and_offset() .0, ); @@ -1091,7 +1090,7 @@ impl PyPhaseDiagram { self.0 .vapor() .density() - .convert_to(MOL / METER.powi::()) + .convert_to(MOL / METER.powi::<3>()) .into_raw_vec_and_offset() .0, ); @@ -1137,7 +1136,7 @@ impl PyPhaseDiagram { self.0 .liquid() .mass_density() - .convert_to(KILOGRAM / METER.powi::()) + .convert_to(KILOGRAM / METER.powi::<3>()) .into_raw_vec_and_offset() .0, ); @@ -1146,7 +1145,7 @@ impl PyPhaseDiagram { self.0 .vapor() .mass_density() - .convert_to(KILOGRAM / METER.powi::()) + .convert_to(KILOGRAM / METER.powi::<3>()) .into_raw_vec_and_offset() .0, ); diff --git a/py-feos/src/state.rs b/py-feos/src/state.rs index 2b5492d85..e7b54211c 100644 --- a/py-feos/src/state.rs +++ b/py-feos/src/state.rs @@ -1,7 +1,7 @@ use crate::eos::parse_molefracs; use crate::{ - eos::PyEquationOfState, error::PyFeosError, ideal_gas::IdealGasModel, residual::ResidualModel, - PyVerbosity, + PyVerbosity, eos::PyEquationOfState, error::PyFeosError, ideal_gas::IdealGasModel, + residual::ResidualModel, }; use feos_core::{ Contributions, DensityInitialization, EquationOfState, FeosError, ResidualDyn, State, StateVec, @@ -13,9 +13,10 @@ use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; use quantity::*; use std::collections::HashMap; -use std::ops::{Deref, Neg, Sub}; +use std::ops::{Deref, Div, Neg, Sub}; use std::sync::Arc; -use typenum::{Quot, P3}; + +type Quot = >::Output; type DpDn = Quantity>::Output>; type InvT = Quantity::Output>; @@ -1633,7 +1634,7 @@ impl PyStateVec { String::from("density"), states .density() - .convert_to(MOL / METER.powi::()) + .convert_to(MOL / METER.powi::<3>()) .into_raw_vec_and_offset() .0, ); @@ -1658,7 +1659,7 @@ impl PyStateVec { String::from("mass density"), states .mass_density() - .convert_to(KILOGRAM / METER.powi::()) + .convert_to(KILOGRAM / METER.powi::<3>()) .into_raw_vec_and_offset() .0, ); From 0a47f60b0a20e2a0ff8e30537eeccdeefb519483 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 27 Jan 2026 09:50:15 +0100 Subject: [PATCH 2/6] Extend tp-flash to static arrays and AD --- .github/workflows/test.yml | 4 +- .github/workflows/wheels.yml | 4 +- CHANGELOG.md | 3 + crates/feos-core/src/phase_equilibria/mod.rs | 11 +- .../phase_equilibria/stability_analysis.rs | 25 ++- .../src/phase_equilibria/tp_flash.rs | 156 ++++++++++++++---- crates/feos/src/pcsaft/eos/mod.rs | 58 ++++++- 7 files changed, 207 insertions(+), 54 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7c7b1ad4a..6ea6192e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Test on: push: - branches: [main] + branches: [main, development] pull_request: - branches: [main] + branches: [main, development] env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c2fab4c05..41f501d43 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,9 +1,9 @@ name: Build Wheels on: push: - branches: [main] + branches: [main, development] pull_request: - branches: [main] + branches: [main, development] jobs: linux: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index f395eb34a..3ea62b79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ 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). ## [Breaking] +### Added +- Extended tp-flash algorithm to static numbers of components and enabled automatic differentiation for binary systems. [#336](https://github.com/feos-org/feos/pull/336) + ### Packaging - Updated `quantity` dependency to 0.13 and removed the `typenum` dependency. [#323](https://github.com/feos-org/feos/pull/323) diff --git a/crates/feos-core/src/phase_equilibria/mod.rs b/crates/feos-core/src/phase_equilibria/mod.rs index b27d97fbb..9683e49a1 100644 --- a/crates/feos-core/src/phase_equilibria/mod.rs +++ b/crates/feos-core/src/phase_equilibria/mod.rs @@ -3,8 +3,8 @@ use crate::errors::{FeosError, FeosResult}; use crate::state::{DensityInitialization, State}; use crate::{Contributions, ReferenceSystem}; use nalgebra::allocator::Allocator; -use nalgebra::{DVector, DefaultAllocator, Dim, Dyn, OVector}; -use num_dual::{DualNum, DualStruct}; +use nalgebra::{DefaultAllocator, Dim, Dyn, OVector}; +use num_dual::{DualNum, DualStruct, Gradients}; use quantity::{Energy, Moles, Pressure, RGAS, Temperature}; use std::fmt; use std::fmt::Write; @@ -168,7 +168,10 @@ where } } -impl PhaseEquilibrium { +impl, N: Gradients, const P: usize> PhaseEquilibrium +where + DefaultAllocator: Allocator, +{ pub(super) fn update_pressure( mut self, temperature: Temperature, @@ -189,7 +192,7 @@ impl PhaseEquilibrium { pub(super) fn update_moles( &mut self, pressure: Pressure, - moles: [&Moles>; P], + moles: [&Moles>; P], ) -> FeosResult<()> { for (i, s) in self.0.iter_mut().enumerate() { *s = State::new_npt( diff --git a/crates/feos-core/src/phase_equilibria/stability_analysis.rs b/crates/feos-core/src/phase_equilibria/stability_analysis.rs index e9af96e8e..69988f8c2 100644 --- a/crates/feos-core/src/phase_equilibria/stability_analysis.rs +++ b/crates/feos-core/src/phase_equilibria/stability_analysis.rs @@ -3,7 +3,9 @@ use crate::equation_of_state::Residual; use crate::errors::{FeosError, FeosResult}; use crate::state::{Contributions, DensityInitialization, State}; use crate::{ReferenceSystem, SolverOptions, Verbosity}; -use nalgebra::{DMatrix, DVector}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, OMatrix, OVector, U1}; +use num_dual::Gradients; use num_dual::linalg::LU; use num_dual::linalg::smallest_ev; use quantity::Moles; @@ -16,7 +18,10 @@ const MINIMIZE_KMAX: usize = 100; const ZERO_TPD: f64 = -1E-08; /// # Stability analysis -impl State { +impl, N: Gradients> State +where + DefaultAllocator: Allocator + Allocator, +{ /// Determine if the state is stable, i.e. if a phase split should /// occur or not. pub fn is_stable(&self, options: SolverOptions) -> FeosResult { @@ -26,7 +31,7 @@ impl State { /// Perform a stability analysis. The result is a list of [State]s with /// negative tangent plane distance (i.e. lower Gibbs energy) that can be /// used as initial estimates for a phase equilibrium calculation. - pub fn stability_analysis(&self, options: SolverOptions) -> FeosResult>> { + pub fn stability_analysis(&self, options: SolverOptions) -> FeosResult>> { let mut result = Vec::new(); for i_trial in 0..self.eos.components() + 1 { let phase = if i_trial == self.eos.components() { @@ -59,8 +64,9 @@ impl State { Ok(result) } - fn define_trial_state(&self, dominant_component: usize) -> FeosResult> { + fn define_trial_state(&self, dominant_component: usize) -> FeosResult> { let x_feed = &self.molefracs; + let (n, _) = x_feed.shape_generic(); let (x_trial, phase) = if dominant_component == self.eos.components() { // try an ideal vapor phase @@ -70,7 +76,7 @@ impl State { // try each component as nearly pure phase let factor = (1.0 - X_DOMINANT) / (x_feed.sum() - x_feed[dominant_component]); ( - DVector::from_fn(self.eos.components(), |i, _| { + OVector::from_fn_generic(n, U1, |i, _| { if i == dominant_component { X_DOMINANT } else { @@ -92,7 +98,7 @@ impl State { fn minimize_tpd( &self, - trial: &mut State, + trial: &mut State, options: SolverOptions, ) -> FeosResult<(Option, usize)> { let (max_iter, tol, verbosity) = options.unwrap_or(MINIMIZE_KMAX, MINIMIZE_TOL); @@ -154,9 +160,10 @@ impl State { Err(FeosError::NotConverged(String::from("stability analysis"))) } - fn stability_newton_step(&mut self, di: &DVector, tpd: &mut f64) -> FeosResult { + fn stability_newton_step(&mut self, di: &OVector, tpd: &mut f64) -> FeosResult { // save old values let tpd_old = *tpd; + let (n, _) = di.shape_generic(); // calculate residual and ideal hesse matrix let mut hesse = (self.dln_phi_dnj() * Moles::from_reduced(1.0)).into_value(); @@ -166,7 +173,7 @@ impl State { let sq_y = y.map(f64::sqrt); let gradient = (&ln_y + &lnphi - di).component_mul(&sq_y); - let hesse_ig = DMatrix::identity(self.eos.components(), self.eos.components()); + let hesse_ig = OMatrix::identity_generic(n, n); for i in 0..self.eos.components() { hesse.column_mut(i).component_mul_assign(&(sq_y[i] * &sq_y)); if y[i] > f64::EPSILON { @@ -181,7 +188,7 @@ impl State { // ! (3) objective function (tpd) does not descent // !----------------------------------------------------------------------------- let mut adjust_hessian = true; - let mut hessian: DMatrix; + let mut hessian: OMatrix; let mut eta_h = 1.0; while adjust_hessian { diff --git a/crates/feos-core/src/phase_equilibria/tp_flash.rs b/crates/feos-core/src/phase_equilibria/tp_flash.rs index baefea80b..cd9d9bea3 100644 --- a/crates/feos-core/src/phase_equilibria/tp_flash.rs +++ b/crates/feos-core/src/phase_equilibria/tp_flash.rs @@ -2,16 +2,22 @@ use super::PhaseEquilibrium; use crate::equation_of_state::Residual; use crate::errors::{FeosError, FeosResult}; use crate::state::{Contributions, State}; -use crate::{SolverOptions, Verbosity}; -use nalgebra::{DVector, Matrix3, Matrix4xX}; -use num_dual::{Dual, DualNum, first_derivative}; -use quantity::{Dimensionless, Moles, Pressure, Temperature}; +use crate::{ReferenceSystem, SolverOptions, Verbosity}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, Dim, Matrix3, OVector, SVector, U1, U2, vector}; +use num_dual::{ + Dual, Dual2Vec, DualNum, DualStruct, Gradients, first_derivative, implicit_derivative_sp, +}; +use quantity::{Dimensionless, MOL, MolarVolume, Moles, Pressure, Quantity, Temperature}; const MAX_ITER_TP: usize = 400; const TOL_TP: f64 = 1e-8; /// # Flash calculations -impl PhaseEquilibrium { +impl, N: Gradients> PhaseEquilibrium +where + DefaultAllocator: Allocator + Allocator, +{ /// Perform a Tp-flash calculation. If no initial values are /// given, the solution is initialized using a stability analysis. /// @@ -21,8 +27,8 @@ impl PhaseEquilibrium { eos: &E, temperature: Temperature, pressure: Pressure, - feed: &Moles>, - initial_state: Option<&PhaseEquilibrium>, + feed: &Moles>, + initial_state: Option<&PhaseEquilibrium>, options: SolverOptions, non_volatile_components: Option>, ) -> FeosResult { @@ -34,8 +40,76 @@ impl PhaseEquilibrium { } } +impl, D: DualNum + Copy> PhaseEquilibrium { + /// Perform a Tp-flash calculation for a binary mixture. + /// Compared to the version of the algorithm for a generic + /// number of components ([tp_flash](PhaseEquilibrium::tp_flash)), + /// this can be used in combination with automatic differentiation. + pub fn tp_flash_binary( + eos: &E, + temperature: Temperature, + pressure: Pressure, + feed: &Moles>, + options: SolverOptions, + ) -> FeosResult { + let z = feed.get(0).convert_into(feed.get(0) + feed.get(1)); + let total_moles = feed.sum(); + let moles = vector![z.re(), 1.0 - z.re()] * MOL; + let vle_re = State::new_npt(&eos.re(), temperature.re(), pressure.re(), &moles, None)? + .tp_flash(None, options, None)?; + + // implicit differentiation + + // specifications + let t = temperature.into_reduced(); + let p = pressure.into_reduced(); + + // molar volume and composition of the two phases + let variables = SVector::from([ + vle_re.liquid().density.into_reduced().recip(), + vle_re.vapor().density.into_reduced().recip(), + vle_re.liquid().molefracs[0], + vle_re.vapor().molefracs[0], + ]); + + // calculate derivatives for molar volumes and compositions (first component) + // with respect to t, p, or z or equation of state parameters + // using implicit differentiation of the minimum in the Gibbs energy + let [[v_l, v_v, x, y]] = implicit_derivative_sp( + |variables, &[t, p, z]: &[_; 3]| { + let [[v_l, v_v, x, y]] = variables.data.0; + let beta = (z - x) / (y - x); + let eos = eos.lift(); + let molar_gibbs_energy = |x: Dual2Vec<_, _, _>, v| { + let molefracs = vector![x, -x + 1.0]; + let a_res = eos.residual_molar_helmholtz_energy(t, v, &molefracs); + let a_ig = (x * (x / v).ln() - (x - 1.0) * ((-x + 1.0) / v).ln() - 1.0) * t; + a_res + a_ig + v * p + }; + // g = a + pv is the potential function for a tp flash using a Helmholtz energy model + // see https://www.sciencedirect.com/science/article/pii/S0378381299000928 + molar_gibbs_energy(y, v_v) * beta - molar_gibbs_energy(x, v_l) * (beta - 1.0) + }, + variables, + &[t, p, z], + ) + .data + .0; + let beta = (z - x) / (y - x); + let state = |x: D, v, phi| { + let volume = MolarVolume::from_reduced(v * phi) * total_moles; + let moles = Quantity::new(vector![x, -x + 1.0] * phi * total_moles.convert_into(MOL)); + State::new_nvt(eos, temperature, volume, &moles) + }; + Ok(Self([state(y, v_v, beta)?, state(x, v_l, -beta + 1.0)?])) + } +} + /// # Flash calculations -impl State { +impl, N: Gradients> State +where + DefaultAllocator: Allocator + Allocator, +{ /// Perform a Tp-flash calculation using the [State] as feed. /// If no initial values are given, the solution is initialized /// using a stability analysis. @@ -44,10 +118,10 @@ impl State { /// containing non-volatile components (e.g. ions). pub fn tp_flash( &self, - initial_state: Option<&PhaseEquilibrium>, + initial_state: Option<&PhaseEquilibrium>, options: SolverOptions, non_volatile_components: Option>, - ) -> FeosResult> { + ) -> FeosResult> { // initialization if let Some(init) = initial_state { let vle = self.tp_flash_( @@ -76,10 +150,10 @@ impl State { pub fn tp_flash_( &self, - mut new_vle_state: PhaseEquilibrium, + mut new_vle_state: PhaseEquilibrium, options: SolverOptions, non_volatile_components: Option>, - ) -> FeosResult> { + ) -> FeosResult> { // set options let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_TP, TOL_TP); @@ -92,8 +166,8 @@ impl State { verbosity, " {:4} | | {:10.8?} | {:10.8?}", 0, - new_vle_state.vapor().molefracs.data.as_vec(), - new_vle_state.liquid().molefracs.data.as_vec(), + new_vle_state.vapor().molefracs.as_slice(), + new_vle_state.liquid().molefracs.as_slice(), ); let mut iter = 0; @@ -169,7 +243,7 @@ impl State { Ok(new_vle_state) } - fn tangent_plane_distance(&self, trial_state: &State) -> f64 { + fn tangent_plane_distance(&self, trial_state: &State) -> f64 { let ln_phi_z = self.ln_phi(); let ln_phi_w = trial_state.ln_phi(); let z = &self.molefracs; @@ -178,20 +252,23 @@ impl State { } } -impl PhaseEquilibrium { +impl, N: Gradients> PhaseEquilibrium +where + DefaultAllocator: Allocator + Allocator, +{ fn accelerated_successive_substitution( &mut self, - feed_state: &State, + feed_state: &State, iter: &mut usize, max_iter: usize, tol: f64, verbosity: Verbosity, non_volatile_components: &Option>, ) -> FeosResult<()> { + let (n, _) = feed_state.molefracs.shape_generic(); for _ in 0..max_iter { // do 5 successive substitution steps and check for convergence - let mut k_vec = Matrix4xX::zeros(self.vapor().eos.components()); - // let mut k_vec = Array::zeros((4, self.vapor().eos.components())); + let mut k_vec = std::array::repeat(OVector::zeros_generic(n, U1)); if self.successive_substitution( feed_state, 5, @@ -213,16 +290,19 @@ impl PhaseEquilibrium { let gibbs = self.total_gibbs_energy(); // extrapolate K values - let delta_vec = k_vec.rows_range(1..) - k_vec.rows_range(..3); - let delta = Matrix3::from_fn(|i, j| delta_vec.row(i).dot(&delta_vec.row(j))); + let delta_vec = [ + &k_vec[1] - &k_vec[0], + &k_vec[2] - &k_vec[1], + &k_vec[3] - &k_vec[2], + ]; + let delta = Matrix3::from_fn(|i, j| delta_vec[i].dot(&delta_vec[j])); let d = delta[(0, 1)] * delta[(0, 1)] - delta[(0, 0)] * delta[(1, 1)]; let a = (delta[(0, 2)] * delta[(0, 1)] - delta[(1, 2)] * delta[(0, 0)]) / d; let b = (delta[(1, 2)] * delta[(0, 1)] - delta[(0, 2)] * delta[(1, 1)]) / d; - let mut k = (k_vec.row(3) - + ((b * delta_vec.row(1) + (a + b) * delta_vec.row(2)) / (1.0 - a - b))) - .map(f64::exp) - .transpose(); + let mut k = (&k_vec[3] + + ((b * &delta_vec[1] + (a + b) * &delta_vec[2]) / (1.0 - a - b))) + .map(f64::exp); // Set k = 0 for non-volatile components if let Some(nvc) = non_volatile_components.as_ref() { @@ -245,10 +325,10 @@ impl PhaseEquilibrium { #[expect(clippy::too_many_arguments)] fn successive_substitution( &mut self, - feed_state: &State, + feed_state: &State, iterations: usize, iter: &mut usize, - k_vec: &mut Option<&mut Matrix4xX>, + k_vec: &mut Option<&mut [OVector; 4]>, abs_tol: f64, verbosity: Verbosity, non_volatile_components: &Option>, @@ -278,8 +358,8 @@ impl PhaseEquilibrium { " {:4} | {:14.8e} | {:.8?} | {:.8?}", iter, res, - self.vapor().molefracs.data.as_vec(), - self.liquid().molefracs.data.as_vec(), + self.vapor().molefracs.as_slice(), + self.liquid().molefracs.as_slice(), ); if res < abs_tol { return Ok(true); @@ -289,16 +369,13 @@ impl PhaseEquilibrium { if let Some(k_vec) = k_vec && i >= iterations - 3 { - k_vec.set_row( - i + 3 - iterations, - &k.map(|ki| if ki > 0.0 { ki.ln() } else { 0.0 }).transpose(), - ); + k_vec[i + 3 - iterations] = k.map(|ki| if ki > 0.0 { ki.ln() } else { 0.0 }); } } Ok(false) } - fn update_states(&mut self, feed_state: &State, k: &DVector) -> FeosResult<()> { + fn update_states(&mut self, feed_state: &State, k: &OVector) -> FeosResult<()> { // calculate vapor phase fraction using Rachford-Rice algorithm let mut beta = self.vapor_phase_fraction(); beta = rachford_rice(&feed_state.molefracs, k, Some(beta))?; @@ -314,7 +391,7 @@ impl PhaseEquilibrium { Ok(()) } - fn vle_init_stability(feed_state: &State) -> FeosResult<(Self, Option)> { + fn vle_init_stability(feed_state: &State) -> FeosResult<(Self, Option)> { let mut stable_states = feed_state.stability_analysis(SolverOptions::default())?; let state1 = stable_states.pop(); let state2 = stable_states.pop(); @@ -331,7 +408,14 @@ impl PhaseEquilibrium { } } -fn rachford_rice(feed: &DVector, k: &DVector, beta_in: Option) -> FeosResult { +fn rachford_rice( + feed: &OVector, + k: &OVector, + beta_in: Option, +) -> FeosResult +where + DefaultAllocator: Allocator, +{ const MAX_ITER: usize = 10; const ABS_TOL: f64 = 1e-6; diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index 6b0aa8782..6514be72d 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -596,7 +596,7 @@ mod tests_parameter_fit { use super::*; use approx::assert_relative_eq; use feos_core::DensityInitialization::Liquid; - use feos_core::{Contributions, PropertiesAD, ReferenceSystem}; + use feos_core::{Contributions, PropertiesAD, ReferenceSystem, SolverOptions}; use feos_core::{FeosResult, ParametersAD, PhaseEquilibrium, State}; use nalgebra::{U1, U3, U8, vector}; use num_dual::{DualStruct, DualVec, partial}; @@ -1023,4 +1023,60 @@ mod tests_parameter_fit { assert_relative_eq!(grad, dt_h, max_relative = 1e-7); Ok(()) } + + #[test] + fn test_tp_flash() -> FeosResult<()> { + let (pcsaft, _) = pcsaft_binary()?; + let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); + let temperature = 500.0 * KELVIN; + let pressure = 44.6 * BAR; + let x = vector![0.5, 0.5]; + let vle = PhaseEquilibrium::tp_flash_binary( + &pcsaft_ad, + Temperature::from_inner(&temperature), + Pressure::from_inner(&pressure), + &Moles::from_inner(&(x * MOL)), + SolverOptions { + verbosity: feos_core::Verbosity::Iter, + tol: Some(1e-10), + ..Default::default() + }, + )?; + let beta = vle + .vapor() + .total_moles + .convert_into(vle.vapor().total_moles + vle.liquid().total_moles); + let (beta, [[grad]]) = (beta.re, beta.eps.unwrap_generic(U1, U1).data.0); + + println!("{beta:.5}"); + println!("{grad:.5?}"); + + let (params, mut kij) = pcsaft.0; + let h = 1e-7; + kij += h; + let pcsaft_h = PcSaftBinary::new(params, kij); + let vle = PhaseEquilibrium::tp_flash_binary( + &pcsaft_h, + temperature, + pressure, + &(x * MOL), + SolverOptions { + tol: Some(1e-10), + ..Default::default() + }, + )?; + let beta_h = vle + .vapor() + .total_moles + .convert_into(vle.vapor().total_moles + vle.liquid().total_moles); + let dbeta_h = (beta_h - beta) / h; + println!( + "k_ij: {:11.5} {:11.5} {:.3e}", + dbeta_h, + grad, + ((dbeta_h - grad) / grad).abs() + ); + assert_relative_eq!(grad, dbeta_h, max_relative = 1e-4); + Ok(()) + } } From cd3e2160aa7d8d49e18d319c3d5b9fe407ac0059 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 27 Jan 2026 15:57:59 +0100 Subject: [PATCH 3/6] Enable AD for pure-component VLEs with given pressure --- CHANGELOG.md | 2 + crates/feos-core/src/ad/mod.rs | 66 ++- .../src/equation_of_state/residual.rs | 36 +- crates/feos-core/src/phase_equilibria/mod.rs | 16 +- .../src/phase_equilibria/vle_pure.rs | 432 ++++++++++-------- .../src/state/residual_properties.rs | 9 - crates/feos/src/pcsaft/eos/mod.rs | 44 ++ py-feos/src/ad/mod.rs | 28 +- py-feos/src/lib.rs | 1 + 9 files changed, 413 insertions(+), 221 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea62b79e..444607c36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Breaking] ### Added - Extended tp-flash algorithm to static numbers of components and enabled automatic differentiation for binary systems. [#336](https://github.com/feos-org/feos/pull/336) +- Rewrote `PhaseEquilibrium::pure_p` to mirror `pure_t` and enable automatic differentiation. [#337](https://github.com/feos-org/feos/pull/337) +- Added `boiling_temperature` to the list of properties for parallel evaluations of gradients. [#337](https://github.com/feos-org/feos/pull/337) ### Packaging - Updated `quantity` dependency to 0.13 and removed the `typenum` dependency. [#323](https://github.com/feos-org/feos/pull/323) diff --git a/crates/feos-core/src/ad/mod.rs b/crates/feos-core/src/ad/mod.rs index ce92d05d3..308e722d1 100644 --- a/crates/feos-core/src/ad/mod.rs +++ b/crates/feos-core/src/ad/mod.rs @@ -4,7 +4,7 @@ use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::{Const, SVector, U1, U2}; #[cfg(feature = "rayon")] use ndarray::{Array1, Array2, ArrayView2, Zip}; -use num_dual::{Derivative, DualSVec, DualStruct}; +use num_dual::{Derivative, DualNum, DualSVec, DualStruct, first_derivative, partial2}; use quantity::{Density, Pressure, Temperature}; #[cfg(feature = "rayon")] use quantity::{KELVIN, KILO, METER, MOL, PASCAL}; @@ -66,6 +66,50 @@ pub trait PropertiesAD { Ok(Pressure::from_reduced(p)) } + fn boiling_temperature( + &self, + pressure: Pressure, + ) -> FeosResult>> + where + Self: Residual>, + { + let eos_f64 = self.re(); + let (temperature, [vapor_density, liquid_density]) = + PhaseEquilibrium::pure_p(&eos_f64, pressure, None, Default::default())?; + + // implicit differentiation is implemented here instead of just calling pure_t with dual + // numbers, because for the first derivative, we can avoid calculating density derivatives. + let t = temperature.into_reduced(); + let v1 = 1.0 / liquid_density.to_reduced(); + let v2 = 1.0 / vapor_density.to_reduced(); + let p = pressure.into_reduced(); + let t = Gradient::from(t); + let t = t + { + let v1 = Gradient::from(v1); + let v2 = Gradient::from(v2); + let p = Gradient::from(p); + let x = Self::pure_molefracs(); + + let residual_entropy = |v| { + let (a, s) = first_derivative( + partial2( + |t, &v, x| self.lift().residual_molar_helmholtz_energy(t, v, x), + &v, + &x, + ), + t, + ); + (a, -s) + }; + let (a1, s1) = residual_entropy(v1); + let (a2, s2) = residual_entropy(v2); + + let ln_rho = (v1 / v2).ln(); + (p * (v2 - v1) + (a2 - a1 + t * ln_rho)) / (s2 - s1 - ln_rho) + }; + Ok(Temperature::from_reduced(t)) + } + fn equilibrium_liquid_density( &self, temperature: Temperature, @@ -111,6 +155,26 @@ pub trait PropertiesAD { ) } + #[cfg(feature = "rayon")] + fn boiling_temperature_parallel( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, + ) -> (Array1, Array2, Array1) + where + Self: ParametersAD<1>, + { + parallelize::<_, Self, _, _>( + parameter_names, + parameters, + input, + |eos: &Self::Lifted>, inp| { + eos.boiling_temperature(inp[0] * PASCAL) + .map(|p| p.convert_into(KELVIN)) + }, + ) + } + #[cfg(feature = "rayon")] fn liquid_density_parallel( parameter_names: [String; P], diff --git a/crates/feos-core/src/equation_of_state/residual.rs b/crates/feos-core/src/equation_of_state/residual.rs index 326aff883..9de69d3db 100644 --- a/crates/feos-core/src/equation_of_state/residual.rs +++ b/crates/feos-core/src/equation_of_state/residual.rs @@ -1,6 +1,9 @@ use crate::{FeosError, FeosResult, ReferenceSystem, state::StateHD}; +use nalgebra::SVector; use nalgebra::{DVector, DefaultAllocator, Dim, Dyn, OMatrix, OVector, U1, allocator::Allocator}; -use num_dual::{DualNum, Gradients, partial, partial2, second_derivative, third_derivative}; +use num_dual::{ + DualNum, Gradients, hessian, partial, partial2, second_derivative, third_derivative, +}; use quantity::ad::first_derivative; use quantity::*; use std::ops::{Deref, Div}; @@ -313,12 +316,41 @@ where molar_volume, ); ( - a * density, + a, -da + temperature * density, molar_volume * molar_volume * d2a + temperature, ) } + /// calculates a_res, p, s_res, dp_drho, dp_dt + fn p_dpdrho_dpdt( + &self, + temperature: D, + density: D, + molefracs: &OVector, + ) -> (D, D, D, D, D) { + let molar_volume = density.recip(); + let (a, da, d2a) = hessian::<_, _, _, nalgebra::U2, _>( + partial( + |vt: SVector<_, 2>, x: &OVector<_, N>| { + let [[v, t]] = vt.data.0; + self.lift().residual_molar_helmholtz_energy(t, v, x) + }, + molefracs, + ), + &SVector::from([molar_volume, temperature]), + ); + let [[da_dv, da_dt]] = da.data.0; + let [[d2a_dv2, d2a_dvdt], _] = d2a.data.0; + ( + a, + -da_dv + temperature * density, + -da_dt, + molar_volume * molar_volume * d2a_dv2 + temperature, + -d2a_dvdt + density, + ) + } + /// calculates p, dp_drho, d2p_drho2 fn p_dpdrho_d2pdrho2( &self, diff --git a/crates/feos-core/src/phase_equilibria/mod.rs b/crates/feos-core/src/phase_equilibria/mod.rs index 9683e49a1..4a10aa0fe 100644 --- a/crates/feos-core/src/phase_equilibria/mod.rs +++ b/crates/feos-core/src/phase_equilibria/mod.rs @@ -1,5 +1,5 @@ use crate::equation_of_state::Residual; -use crate::errors::{FeosError, FeosResult}; +use crate::errors::FeosResult; use crate::state::{DensityInitialization, State}; use crate::{Contributions, ReferenceSystem}; use nalgebra::allocator::Allocator; @@ -44,12 +44,6 @@ pub struct PhaseEquilibrium + C where DefaultAllocator: Allocator; -// impl Clone for PhaseEquilibrium { -// fn clone(&self) -> Self { -// Self(self.0.clone()) -// } -// } - impl fmt::Display for PhaseEquilibrium { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (i, s) in self.0.iter().enumerate() { @@ -224,14 +218,6 @@ impl, N: Dim> PhaseEquilibrium where DefaultAllocator: Allocator, { - pub(super) fn check_trivial_solution(self) -> FeosResult { - if Self::is_trivial_solution(self.vapor(), self.liquid()) { - Err(FeosError::TrivialSolution) - } else { - Ok(self) - } - } - /// Check if the two states form a trivial solution pub fn is_trivial_solution(state1: &State, state2: &State) -> bool { let rho1 = state1.molefracs.clone() * state1.density.into_reduced(); diff --git a/crates/feos-core/src/phase_equilibria/vle_pure.rs b/crates/feos-core/src/phase_equilibria/vle_pure.rs index 5e58601af..e3575c094 100644 --- a/crates/feos-core/src/phase_equilibria/vle_pure.rs +++ b/crates/feos-core/src/phase_equilibria/vle_pure.rs @@ -5,40 +5,37 @@ use crate::errors::{FeosError, FeosResult}; use crate::state::{Contributions, DensityInitialization, State}; use crate::{ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity}; use nalgebra::allocator::Allocator; -use nalgebra::{DVector, DefaultAllocator, Dim, dvector}; -use num_dual::{DualNum, DualStruct, Gradients}; -use quantity::{Density, Pressure, RGAS, Temperature}; +use nalgebra::{DVector, DefaultAllocator, Dim, SVector, U1, U2}; +use num_dual::{DualNum, DualStruct, Gradients, gradient, partial}; +use quantity::{Density, Pressure, Temperature}; const SCALE_T_NEW: f64 = 0.7; const MAX_ITER_PURE: usize = 50; const TOL_PURE: f64 = 1e-12; /// # Pure component phase equilibria -impl PhaseEquilibrium { +impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium +where + DefaultAllocator: Allocator + Allocator + Allocator, +{ /// Calculate a phase equilibrium for a pure component. - pub fn pure( + pub fn pure>( eos: &E, temperature_or_pressure: TP, initial_state: Option<&Self>, options: SolverOptions, ) -> FeosResult { - if let Some(t) = temperature_or_pressure.temperature() { + let (t, rho) = if let Some(t) = temperature_or_pressure.temperature() { let (_, rho) = Self::pure_t(eos, t, initial_state, options)?; - Ok(Self(rho.map(|r| { - State::new_intensive(eos, t, r, &dvector![1.0]).unwrap() - }))) + (t, rho) } else if let Some(p) = temperature_or_pressure.pressure() { - Self::pure_p(eos, p, initial_state, options) + Self::pure_p(eos, p, initial_state, options)? } else { unreachable!() - } + }; + Ok(Self(rho.map(|r| State::new_pure(eos, t, r).unwrap()))) } -} -impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium -where - DefaultAllocator: Allocator, -{ /// Calculate a phase equilibrium for a pure component /// and given temperature. pub fn pure_t( @@ -89,9 +86,9 @@ where for _ in 0..D::NDERIV { let v_l = liquid_density.recip(); let v_v = vapor_density.recip(); - let (f_l, p_l, dp_l) = eos.p_dpdrho(t, liquid_density, &x); - let (f_v, p_v, dp_v) = eos.p_dpdrho(t, vapor_density, &x); - pressure = -(f_l * v_l - f_v * v_v + t * (v_v / v_l).ln()) / (v_l - v_v); + let (a_l, p_l, dp_l) = eos.p_dpdrho(t, liquid_density, &x); + let (a_v, p_v, dp_v) = eos.p_dpdrho(t, vapor_density, &x); + pressure = -(a_l - a_v + t * (v_v / v_l).ln()) / (v_l - v_v); liquid_density += (pressure - p_l) / dp_l; vapor_density += (pressure - p_v) / dp_v; } @@ -133,15 +130,14 @@ where for i in 1..=max_iter { // calculate properties - let (f_l_res, p_l, p_rho_l) = eos.p_dpdrho(temperature, liquid_density, &x); - let (f_v_res, p_v, p_rho_v) = eos.p_dpdrho(temperature, vapor_density, &x); + let (a_l_res, p_l, p_rho_l) = eos.p_dpdrho(temperature, liquid_density, &x); + let (a_v_res, p_v, p_rho_v) = eos.p_dpdrho(temperature, vapor_density, &x); // Estimate the new pressure let v_v = vapor_density.recip(); let v_l = liquid_density.recip(); let delta_v = v_v - v_l; - let delta_a = - f_v_res * v_v - f_l_res * v_l + temperature * (vapor_density / liquid_density).ln(); + let delta_a = a_v_res - a_l_res + temperature * (vapor_density / liquid_density).ln(); let mut p_new = -delta_a / delta_v; // If the pressure becomes negative, assume the gas phase is ideal. The @@ -238,198 +234,248 @@ where Ok((p, [rho_v, rho_l])) } -impl PhaseEquilibrium { - fn new_pt(eos: &E, temperature: Temperature, pressure: Pressure) -> FeosResult { - let liquid = State::new_xpt( - eos, - temperature, - pressure, - &dvector![1.0], - Some(DensityInitialization::Liquid), - )?; - let vapor = State::new_xpt( - eos, - temperature, - pressure, - &dvector![1.0], - Some(DensityInitialization::Vapor), - )?; - Ok(Self([vapor, liquid])) - } - +impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium +where + DefaultAllocator: Allocator + Allocator + Allocator, +{ /// Calculate a phase equilibrium for a pure component /// and given pressure. - fn pure_p( + pub fn pure_p( eos: &E, - pressure: Pressure, + pressure: Pressure, initial_state: Option<&Self>, options: SolverOptions, - ) -> FeosResult { - let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE); + ) -> FeosResult<(Temperature, [Density; 2])> { + let eos_f64 = eos.re(); + let p = pressure.into_reduced(); // Initialize the phase equilibrium - let mut vle = match initial_state { - Some(init) => init - .clone() - .update_pressure(init.vapor().temperature, pressure)?, - None => PhaseEquilibrium::init_pure_p(eos, pressure)?, + let vle = match initial_state { + Some(init) => ( + init.vapor().temperature.into_reduced().re(), + [ + init.vapor().density.into_reduced().re(), + init.liquid().density.into_reduced().re(), + ], + ), + None => init_pure_p(&eos_f64, pressure.re())?, }; + let (t, [rho_v, rho_l]) = iterate_pure_p(&eos_f64, p.re(), vle, options)?; + // Implicit differentiation + let mut temperature = D::from(t); + let mut vapor_density = D::from(rho_v); + let mut liquid_density = D::from(rho_l); + let x = E::pure_molefracs(); + for _ in 0..D::NDERIV { + let v_l = liquid_density.recip(); + let v_v = vapor_density.recip(); + let (a_l, p_l, s_l, p_rho_l, p_t_l) = + eos.p_dpdrho_dpdt(temperature, liquid_density, &x); + let (a_v, p_v, s_v, p_rho_v, p_t_v) = eos.p_dpdrho_dpdt(temperature, vapor_density, &x); + let ln_rho = (v_l / v_v).ln(); + let delta_t = + (p * (v_v - v_l) + (a_v - a_l + temperature * ln_rho)) / (s_v - s_l - ln_rho); + temperature += delta_t; + liquid_density += (p - p_l - p_t_l * delta_t) / p_rho_l; + vapor_density += (p - p_v - p_t_v * delta_t) / p_rho_v; + } + Ok(( + Temperature::from_reduced(temperature), + [ + Density::from_reduced(vapor_density), + Density::from_reduced(liquid_density), + ], + )) + } +} + +/// Calculate a phase equilibrium for a pure component +/// and given pressure. +fn iterate_pure_p, N: Dim>( + eos: &E, + pressure: f64, + (mut temperature, [mut vapor_density, mut liquid_density]): (f64, [f64; 2]), + options: SolverOptions, +) -> FeosResult<(f64, [f64; 2])> +where + DefaultAllocator: Allocator, +{ + let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE); + let x = E::pure_molefracs(); + + log_iter!( + verbosity, + " iter | residual | temperature | liquid density | vapor density " + ); + log_iter!(verbosity, "{:-<89}", ""); + log_iter!( + verbosity, + " {:4} | | {:13.8} | {:12.8} | {:12.8}", + 0, + Temperature::from_reduced(temperature), + Density::from_reduced(liquid_density), + Density::from_reduced(vapor_density) + ); + for i in 1..=max_iter { + // calculate properties + let (a_l_res, p_l, s_l_res, p_rho_l, p_t_l) = + eos.p_dpdrho_dpdt(temperature, liquid_density, &x); + let (a_v_res, p_v, s_v_res, p_rho_v, p_t_v) = + eos.p_dpdrho_dpdt(temperature, vapor_density, &x); + + // calculate the molar volumes + let v_l = liquid_density.recip(); + let v_v = vapor_density.recip(); + + // estimate the temperature steps + let ln_rho = (v_l / v_v).ln(); + let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + temperature * ln_rho)) + / (s_v_res - s_l_res - ln_rho); + temperature += delta_t; + + // calculate Newton steps for the densities and update state. + let rho_l = liquid_density + (pressure - p_l - p_t_l * delta_t) / p_rho_l; + let rho_v = vapor_density + (pressure - p_v - p_t_v * delta_t) / p_rho_v; + + if rho_l.is_sign_negative() || rho_v.is_sign_negative() || delta_t.abs() > 1.0 { + // if densities are negative or the temperature step is large use density iteration instead + liquid_density = _density_iteration( + eos, + temperature, + pressure, + &x, + DensityInitialization::InitialDensity(liquid_density), + )?; + vapor_density = _density_iteration( + eos, + temperature, + pressure, + &x, + DensityInitialization::InitialDensity(vapor_density), + )?; + } else { + liquid_density = rho_l; + vapor_density = rho_v; + } + + // check for trivial solution + if (vapor_density / liquid_density - 1.0).abs() < TRIVIAL_REL_DEVIATION { + return Err(FeosError::TrivialSolution); + } + + // check for convergence + let res = delta_t.abs(); log_iter!( verbosity, - " iter | residual | temperature | liquid density | vapor density " - ); - log_iter!(verbosity, "{:-<89}", ""); - log_iter!( - verbosity, - " {:4} | | {:13.8} | {:12.8} | {:12.8}", - 0, - vle.vapor().temperature, - vle.liquid().density, - vle.vapor().density + " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}", + i, + res, + Temperature::from_reduced(temperature), + Density::from_reduced(liquid_density), + Density::from_reduced(vapor_density) ); - for i in 1..=max_iter { - // calculate the pressures and derivatives - let (p_l, p_rho_l) = vle.liquid().p_dpdrho(); - let (p_v, p_rho_v) = vle.vapor().p_dpdrho(); - let p_t_l = vle.liquid().dp_dt(Contributions::Total); - let p_t_v = vle.vapor().dp_dt(Contributions::Total); - - // calculate the residual molar entropies (already cached) - let s_l_res = vle.liquid().residual_molar_entropy(); - let s_v_res = vle.vapor().residual_molar_entropy(); - - // calculate the residual molar Helmholtz energies (already cached) - let a_l_res = vle.liquid().residual_molar_helmholtz_energy(); - let a_v_res = vle.vapor().residual_molar_helmholtz_energy(); - - // 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 kt = RGAS * vle.vapor().temperature; - let ln_rho = (v_l / v_v).into_value().ln(); - let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + kt * ln_rho)) - / (s_v_res - s_l_res - RGAS * ln_rho); - let t_new = vle.vapor().temperature + delta_t; - - // calculate Newton steps for the densities and update state. - let rho_l = vle.liquid().density + (pressure - p_l - p_t_l * delta_t) / p_rho_l; - let rho_v = vle.vapor().density + (pressure - p_v - p_t_v * delta_t) / p_rho_v; - - if rho_l.is_sign_negative() - || rho_v.is_sign_negative() - || delta_t.abs() > Temperature::from_reduced(1.0) - { - // if densities are negative or the temperature step is large use density iteration instead - vle = vle - .update_pressure(t_new, pressure)? - .check_trivial_solution()?; - } else { - // update state - vle = Self([ - State::new_pure(eos, t_new, rho_v)?, - State::new_pure(eos, t_new, rho_l)?, - ]); - } - - // check for convergence - let res = delta_t.abs(); - log_iter!( + if res < temperature * tol { + log_result!( verbosity, - " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}", - i, - res, - vle.vapor().temperature, - vle.liquid().density, - vle.vapor().density + "PhaseEquilibrium::pure_p: calculation converged in {} step(s)\n", + i ); - if res < vle.vapor().temperature * tol { - log_result!( - verbosity, - "PhaseEquilibrium::pure_p: calculation converged in {} step(s)\n", - i - ); - return Ok(vle); - } + return Ok((temperature, [vapor_density, liquid_density])); } - Err(FeosError::NotConverged("pure_p".to_owned())) } + Err(FeosError::NotConverged("pure_p".to_owned())) +} - /// Initialize a new VLE for a pure substance for a given pressure. - fn init_pure_p(eos: &E, pressure: Pressure) -> FeosResult { - let trial_temperatures = [ - Temperature::from_reduced(300.0), - Temperature::from_reduced(500.0), - Temperature::from_reduced(200.0), - ]; - let x = dvector![1.0]; - let mut vle = None; - let mut t0 = Temperature::from_reduced(1.0); - for t in trial_temperatures.iter() { - t0 = *t; - let _vle = PhaseEquilibrium::new_pt(eos, *t, pressure)?; - if !Self::is_trivial_solution(_vle.vapor(), _vle.liquid()) { - return Ok(_vle); +/// Initialize a new VLE for a pure substance for a given pressure. +fn init_pure_p, N: Gradients>( + eos: &E, + pressure: Pressure, +) -> FeosResult<(f64, [f64; 2])> +where + DefaultAllocator: Allocator + Allocator + Allocator, +{ + let trial_temperatures = [300.0, 500.0, 200.0]; + let p = pressure.into_reduced(); + let x = E::pure_molefracs(); + let mut vle = None; + for t in trial_temperatures { + let liquid_density = _density_iteration(eos, t, p, &x, DensityInitialization::Liquid)?; + let vapor_density = _density_iteration(eos, t, p, &x, DensityInitialization::Vapor)?; + let _vle = (t, [vapor_density, liquid_density]); + if (vapor_density / liquid_density - 1.0).abs() >= TRIVIAL_REL_DEVIATION { + return Ok(_vle); + } + vle = Some(_vle); + } + let Some((t0, [mut rho_v, mut rho_l])) = vle else { + unreachable!() + }; + let [mut t_v, mut t_l] = [t0, t0]; + + let cp = State::critical_point(eos, None, None, None, SolverOptions::default())?; + let cp_density = cp.density.into_reduced(); + if pressure > cp.pressure(Contributions::Total) { + return Err(FeosError::SuperCritical); + }; + + if rho_v < cp_density { + // reduce temperature of liquid phase... + for _ in 0..8 { + t_l *= SCALE_T_NEW; + rho_l = _density_iteration(eos, t_l, p, &x, DensityInitialization::Liquid)?; + if rho_l > cp_density { + break; } - vle = Some(_vle); } - - let cp = State::critical_point(eos, None, None, None, SolverOptions::default())?; - if pressure > cp.pressure(Contributions::Total) { - return Err(FeosError::SuperCritical); - }; - if let Some(mut e) = vle { - if e.vapor().density < cp.density { - for _ in 0..8 { - t0 *= SCALE_T_NEW; - e.0[1] = - State::new_xpt(eos, t0, pressure, &x, Some(DensityInitialization::Liquid))?; - if e.liquid().density > cp.density { - break; - } - } - } else { - for _ in 0..8 { - t0 /= SCALE_T_NEW; - e.0[0] = - State::new_xpt(eos, t0, pressure, &x, Some(DensityInitialization::Vapor))?; - if e.vapor().density < cp.density { - break; - } - } + } else { + // ...or increase temperature of vapor phase + for _ in 0..8 { + t_v /= SCALE_T_NEW; + rho_v = _density_iteration(eos, t_v, p, &x, DensityInitialization::Vapor)?; + if rho_v < cp_density { + break; } + } + } - for _ in 0..20 { - let h = |s: &State<_>| s.residual_enthalpy() + s.total_moles * RGAS * s.temperature; - t0 = (h(e.vapor()) - h(e.liquid())) - / (e.vapor().residual_entropy() - - e.liquid().residual_entropy() - - RGAS - * e.vapor().total_moles - * ((e.vapor().density / e.liquid().density).into_value().ln())); - let trial_state = - State::new_xpt(eos, t0, pressure, &x, Some(DensityInitialization::Vapor))?; - if trial_state.density < cp.density { - e.0[0] = trial_state; - } - let trial_state = - State::new_xpt(eos, t0, pressure, &x, Some(DensityInitialization::Liquid))?; - if trial_state.density > cp.density { - e.0[1] = trial_state; - } - if e.liquid().temperature == e.vapor().temperature { - return Ok(e); - } - } - Err(FeosError::IterationFailed( - "new_init_p: could not find proper initial state".to_owned(), - )) - } else { - unreachable!() + // determine new temperatures and assign them to either the liquid or the vapor phase until + // both phases have the same temperature + for _ in 0..20 { + let h_s = |t, v| { + let (a_res, da_res) = gradient::<_, _, _, U2, _>( + partial( + |t_v: SVector<_, _>, x| { + let [[t, v]] = t_v.data.0; + eos.lift().residual_molar_helmholtz_energy(t, v, x) + }, + &x, + ), + &SVector::from([t, v]), + ); + let [[da_res_dt, da_res_dv]] = da_res.data.0; + (a_res - t * da_res_dt - v * da_res_dv + t, -da_res_dt) + }; + let (h_l, s_l_res) = h_s(t_l, rho_l.recip()); + let (h_v, s_v_res) = h_s(t_v, rho_v.recip()); + let t = (h_v - h_l) / (s_v_res - s_l_res - (rho_v / rho_l).ln()); + let trial_density = _density_iteration(eos, t, p, &x, DensityInitialization::Vapor)?; + if trial_density < cp_density { + rho_v = trial_density; + t_v = t; + } + let trial_density = _density_iteration(eos, t, p, &x, DensityInitialization::Liquid)?; + if trial_density > cp_density { + rho_l = trial_density; + t_l = t; + } + if t_l == t_v { + return Ok((t_l, [rho_v, rho_l])); } } + Err(FeosError::IterationFailed( + "new_init_p: could not find proper initial state".to_owned(), + )) } impl PhaseEquilibrium { @@ -453,7 +499,7 @@ impl PhaseEquilibrium { .map(|i| { let pure_eos = eos.subset(&[i]); PhaseEquilibrium::pure_p(&pure_eos, pressure, None, SolverOptions::default()) - .map(|vle| vle.vapor().temperature) + .map(|(t, _)| t) .ok() }) .collect() diff --git a/crates/feos-core/src/state/residual_properties.rs b/crates/feos-core/src/state/residual_properties.rs index 554facf08..53695fdbe 100644 --- a/crates/feos-core/src/state/residual_properties.rs +++ b/crates/feos-core/src/state/residual_properties.rs @@ -233,15 +233,6 @@ where .into_value() } - // This function is designed specifically for use in density iterations - pub(crate) fn p_dpdrho(&self) -> (Pressure, as Div>>::Output) { - 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) -> MolarVolume> { -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total) diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index 6514be72d..45d3635da 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -685,6 +685,50 @@ mod tests_parameter_fit { Ok(()) } + #[test] + fn test_boiling_temperature_derivatives_fit() -> FeosResult<()> { + let pcsaft = pcsaft_non_assoc(); + let pcsaft_ad = pcsaft.named_derivatives(["m", "sigma", "epsilon_k"]); + let pressure = BAR; + let t = pcsaft_ad.boiling_temperature(pressure)?; + let t = t.convert_into(KELVIN); + let (t, grad) = (t.re, t.eps.unwrap_generic(U3, U1)); + + println!("{t:.5}"); + println!("{grad:.5?}"); + + let (t_check, _) = PhaseEquilibrium::pure_p( + &pcsaft_ad, + Pressure::from_inner(&pressure), + None, + Default::default(), + )?; + let t_check = t_check.convert_into(KELVIN); + let (t_check, grad_check) = (t_check.re, t_check.eps.unwrap_generic(U3, U1)); + println!("{t_check:.5}"); + println!("{grad_check:.5?}"); + assert_relative_eq!(t, t_check, max_relative = 1e-15); + assert_relative_eq!(grad, grad_check, max_relative = 1e-15); + + for (i, par) in ["m", "sigma", "epsilon_k"].into_iter().enumerate() { + let mut params = pcsaft.0; + let h = params[i] * 1e-8; + params[i] += h; + let pcsaft_h = PcSaftPure(params); + let (t_h, _) = PhaseEquilibrium::pure_p(&pcsaft_h, pressure, None, Default::default())?; + let dt_h = (t_h.convert_into(KELVIN) - t) / h; + let dt = grad[i]; + println!( + "{par:12}: {:11.5} {:11.5} {:.3e}", + dt_h, + dt, + ((dt_h - dt) / dt).abs() + ); + assert_relative_eq!(dt, dt_h, max_relative = 1e-6); + } + Ok(()) + } + #[test] fn test_equilibrium_liquid_density_derivatives_fit() -> FeosResult<()> { let pcsaft = pcsaft_non_assoc(); diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index 1286f6c14..c18d1eacb 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -57,6 +57,32 @@ pub fn vapor_pressure_derivatives<'py>( _vapor_pressure_derivatives(model, parameter_names, parameters, input) } +/// Calculate boiling temperatures and derivatives w.r.t. model parameters. +/// +/// Parameters +/// ---------- +/// model: EquationOfStateAD +/// The equation of state to use. +/// parameter_names: List[string] +/// The name of the parameters for which derivatives are calculated. +/// parameters: np.ndarray[float] +/// The parameters for every data point. +/// input: np.ndarray[float] +/// The pressure (in Pa) for every data point. +/// +/// Returns +/// ------- +/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The boiling temperature (in K), gradients, and convergence status. +#[pyfunction] +pub fn boiling_temperature_derivatives<'py>( + model: PyEquationOfStateAD, + parameter_names: Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, +) -> GradResult<'py> { + _boiling_temperature_derivatives(model, parameter_names, parameters, input) +} + /// Calculate liquid densities and derivatives w.r.t. model parameters. /// /// Parameters @@ -222,7 +248,7 @@ macro_rules! impl_evaluate_gradients { impl_evaluate_gradients!( pure, - [vapor_pressure, liquid_density, equilibrium_liquid_density], + [vapor_pressure, boiling_temperature, liquid_density, equilibrium_liquid_density], {PcSaftNonAssoc: PcSaftPure, PcSaftFull: PcSaftPure} ); diff --git a/py-feos/src/lib.rs b/py-feos/src/lib.rs index 0886c3eb0..948a24d6b 100644 --- a/py-feos/src/lib.rs +++ b/py-feos/src/lib.rs @@ -189,6 +189,7 @@ fn feos(m: &Bound<'_, PyModule>) -> PyResult<()> { #[cfg(feature = "ad")] { m.add_function(wrap_pyfunction!(ad::vapor_pressure_derivatives, m)?)?; + m.add_function(wrap_pyfunction!(ad::boiling_temperature_derivatives, m)?)?; m.add_function(wrap_pyfunction!(ad::liquid_density_derivatives, m)?)?; m.add_function(wrap_pyfunction!( ad::equilibrium_liquid_density_derivatives, From d97cb980dde161f56488a8b08852208b186e883b Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 27 Jan 2026 09:50:59 +0100 Subject: [PATCH 4/6] More rigorous treatment of extensive states --- crates/feos-core/src/ad/mod.rs | 18 +- crates/feos-core/src/cubic.rs | 2 +- crates/feos-core/src/density_iteration.rs | 2 +- crates/feos-core/src/equation_of_state/mod.rs | 17 +- .../src/equation_of_state/residual.rs | 180 +++---- crates/feos-core/src/errors.rs | 2 +- crates/feos-core/src/lib.rs | 32 +- .../src/phase_equilibria/bubble_dew.rs | 61 +-- crates/feos-core/src/phase_equilibria/mod.rs | 76 +-- .../phase_equilibria/phase_diagram_binary.rs | 93 ++-- .../phase_equilibria/phase_diagram_pure.rs | 4 +- .../src/phase_equilibria/phase_envelope.rs | 8 +- .../phase_equilibria/stability_analysis.rs | 10 +- .../src/phase_equilibria/tp_flash.rs | 32 +- .../src/phase_equilibria/vle_pure.rs | 19 +- crates/feos-core/src/state/builder.rs | 251 ---------- crates/feos-core/src/state/cache.rs | 14 +- crates/feos-core/src/state/composition.rs | 304 +++++++++++ crates/feos-core/src/state/critical_point.rs | 41 +- crates/feos-core/src/state/mod.rs | 473 +++++++----------- crates/feos-core/src/state/properties.rs | 106 ++-- .../src/state/residual_properties.rs | 276 +++++----- crates/feos-core/src/state/statevec.rs | 2 +- crates/feos-derive/src/residual.rs | 20 +- crates/feos-dft/src/adsorption/mod.rs | 93 ++-- crates/feos-dft/src/adsorption/pore.rs | 9 +- crates/feos-dft/src/interface/mod.rs | 13 +- crates/feos-dft/src/pdgt.rs | 6 +- crates/feos-dft/src/profile/mod.rs | 13 +- crates/feos-dft/src/profile/properties.rs | 4 +- .../src/solvation/pair_correlation.rs | 6 +- crates/feos/benches/contributions.rs | 2 +- crates/feos/benches/dft_pore.rs | 9 +- crates/feos/benches/dual_numbers.rs | 10 +- crates/feos/benches/dual_numbers_saftvrmie.rs | 10 +- crates/feos/benches/state_creation.rs | 10 +- crates/feos/src/epcsaft/eos/mod.rs | 6 +- crates/feos/src/gc_pcsaft/eos/ad.rs | 2 +- crates/feos/src/ideal_gas/dippr.rs | 20 +- crates/feos/src/ideal_gas/joback.rs | 12 +- crates/feos/src/lib.rs | 2 +- crates/feos/src/multiparameter/mod.rs | 15 +- crates/feos/src/pcsaft/eos/mod.rs | 77 ++- crates/feos/src/pcsaft/eos/pcsaft_binary.rs | 2 +- crates/feos/src/pcsaft/eos/pcsaft_pure.rs | 4 +- crates/feos/src/pets/eos/mod.rs | 2 +- crates/feos/src/uvtheory/eos/mod.rs | 5 +- crates/feos/tests/gc_pcsaft/binary.rs | 4 +- crates/feos/tests/gc_pcsaft/dft.rs | 11 +- crates/feos/tests/pcsaft/critical_point.rs | 8 +- crates/feos/tests/pcsaft/dft.rs | 28 +- crates/feos/tests/pcsaft/properties.rs | 18 +- .../tests/pcsaft/state_creation_mixture.rs | 43 +- .../feos/tests/pcsaft/state_creation_pure.rs | 202 +++----- .../tests/saftvrmie/critical_properties.rs | 2 +- py-feos/src/ad/mod.rs | 14 +- py-feos/src/dft/adsorption/mod.rs | 52 +- py-feos/src/eos/mod.rs | 167 ++++--- py-feos/src/parameter/mod.rs | 1 + py-feos/src/phase_equilibria.rs | 26 +- py-feos/src/state.rs | 116 ++--- 61 files changed, 1437 insertions(+), 1630 deletions(-) delete mode 100644 crates/feos-core/src/state/builder.rs create mode 100644 crates/feos-core/src/state/composition.rs diff --git a/crates/feos-core/src/ad/mod.rs b/crates/feos-core/src/ad/mod.rs index 308e722d1..aae9fc198 100644 --- a/crates/feos-core/src/ad/mod.rs +++ b/crates/feos-core/src/ad/mod.rs @@ -57,8 +57,8 @@ pub trait PropertiesAD { let v2 = Gradient::from(v2); let x = Self::pure_molefracs(); - let a1 = self.residual_molar_helmholtz_energy(t, v1, &x); - let a2 = self.residual_molar_helmholtz_energy(t, v2, &x); + let a1 = self.residual_helmholtz_energy(t, v1, &x); + let a2 = self.residual_helmholtz_energy(t, v2, &x); (a1, a2) }; @@ -93,7 +93,7 @@ pub trait PropertiesAD { let residual_entropy = |v| { let (a, s) = first_derivative( partial2( - |t, &v, x| self.lift().residual_molar_helmholtz_energy(t, v, x), + |t, &v, x| self.lift().residual_helmholtz_energy(t, v, x), &v, &x, ), @@ -248,16 +248,16 @@ pub trait PropertiesAD { let y = y.map(Gradient::from); let x = liquid_molefracs.map(Gradient::from); - let a_v = self.residual_molar_helmholtz_energy(t, v_v, &y); + let a_v = self.residual_helmholtz_energy(t, v_v, &y); let (p_l, mu_res_l, dp_l, dmu_l) = self.dmu_dv(t, v_l, &x); let vi_l = dmu_l / dp_l; let v_l = vi_l.dot(&y); let a_l = (mu_res_l - vi_l * p_l).dot(&y); (a_l, a_v, v_l, v_v) }; - let rho_l = vle.liquid().partial_density.to_reduced(); + let rho_l = vle.liquid().partial_density().to_reduced(); let rho_l = [rho_l[0], rho_l[1]]; - let rho_v = vle.vapor().partial_density.to_reduced(); + let rho_v = vle.vapor().partial_density().to_reduced(); let rho_v = [rho_v[0], rho_v[1]]; let p = -(a_v - a_l + t * (y[0] * (rho_v[0] / rho_l[0]).ln() + y[1] * (rho_v[1] / rho_l[1]).ln() - 1.0)) @@ -298,16 +298,16 @@ pub trait PropertiesAD { let x = x.map(Gradient::from); let y = vapor_molefracs.map(Gradient::from); - let a_l = self.residual_molar_helmholtz_energy(t, v_l, &x); + let a_l = self.residual_helmholtz_energy(t, v_l, &x); let (p_v, mu_res_v, dp_v, dmu_v) = self.dmu_dv(t, v_v, &y); let vi_v = dmu_v / dp_v; let v_v = vi_v.dot(&x); let a_v = (mu_res_v - vi_v * p_v).dot(&x); (a_l, a_v, v_l, v_v) }; - let rho_l = vle.liquid().partial_density.to_reduced(); + let rho_l = vle.liquid().partial_density().to_reduced(); let rho_l = [rho_l[0], rho_l[1]]; - let rho_v = vle.vapor().partial_density.to_reduced(); + let rho_v = vle.vapor().partial_density().to_reduced(); let rho_v = [rho_v[0], rho_v[1]]; let p = -(a_l - a_v + t * (x[0] * (rho_l[0] / rho_v[0]).ln() + x[1] * (rho_l[1] / rho_v[1]).ln() - 1.0)) diff --git a/crates/feos-core/src/cubic.rs b/crates/feos-core/src/cubic.rs index 562422b05..a12f615e5 100644 --- a/crates/feos-core/src/cubic.rs +++ b/crates/feos-core/src/cubic.rs @@ -221,7 +221,7 @@ mod tests { let parameters = PengRobinsonParameters::new_pure(propane)?; let pr = PengRobinson::new(parameters); let options = SolverOptions::new().verbosity(Verbosity::Iter); - let cp = State::critical_point(&&pr, None, None, None, options)?; + let cp = State::critical_point(&&pr, (), None, None, options)?; println!("{} {}", cp.temperature, cp.pressure(Contributions::Total)); assert_relative_eq!(cp.temperature, tc * KELVIN, max_relative = 1e-4); assert_relative_eq!( diff --git a/crates/feos-core/src/density_iteration.rs b/crates/feos-core/src/density_iteration.rs index a54cfd8b9..aaee68ec1 100644 --- a/crates/feos-core/src/density_iteration.rs +++ b/crates/feos-core/src/density_iteration.rs @@ -87,7 +87,7 @@ where let (a_res, da_res) = first_derivative( |molar_volume| { eos.lift() - .residual_molar_helmholtz_energy(t, molar_volume, &x) + .residual_helmholtz_energy(t, molar_volume, &x) }, molar_volume, ); diff --git a/crates/feos-core/src/equation_of_state/mod.rs b/crates/feos-core/src/equation_of_state/mod.rs index fa72f6997..dc849068f 100644 --- a/crates/feos-core/src/equation_of_state/mod.rs +++ b/crates/feos-core/src/equation_of_state/mod.rs @@ -1,9 +1,10 @@ -use crate::{ReferenceSystem, StateHD}; +use crate::ReferenceSystem; +use crate::state::StateHD; use nalgebra::{ Const, DVector, DefaultAllocator, Dim, Dyn, OVector, SVector, U1, allocator::Allocator, }; use num_dual::DualNum; -use quantity::{Energy, MolarEnergy, Moles, Temperature, Volume}; +use quantity::{Dimensionless, MolarEnergy, MolarVolume, Temperature}; use std::ops::Deref; mod residual; @@ -164,17 +165,17 @@ where fn ideal_gas_helmholtz_energy + Copy>( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, - ) -> Energy { + volume: MolarVolume, + moles: &OVector, + ) -> MolarEnergy { let total_moles = moles.sum(); let molefracs = moles / total_moles; - let molar_volume = volume / total_moles; + let molar_volume = volume.into_reduced() / total_moles; MolarEnergy::from_reduced(self.ideal_gas_molar_helmholtz_energy( temperature.into_reduced(), - molar_volume.into_reduced(), + molar_volume, &molefracs, - )) * total_moles + )) * Dimensionless::new(total_moles) } } diff --git a/crates/feos-core/src/equation_of_state/residual.rs b/crates/feos-core/src/equation_of_state/residual.rs index 9de69d3db..af5fc88f6 100644 --- a/crates/feos-core/src/equation_of_state/residual.rs +++ b/crates/feos-core/src/equation_of_state/residual.rs @@ -1,6 +1,7 @@ -use crate::{FeosError, FeosResult, ReferenceSystem, state::StateHD}; -use nalgebra::SVector; -use nalgebra::{DVector, DefaultAllocator, Dim, Dyn, OMatrix, OVector, U1, allocator::Allocator}; +use crate::state::StateHD; +use crate::{Composition, FeosResult, ReferenceSystem}; +use nalgebra::allocator::Allocator; +use nalgebra::{DVector, DefaultAllocator, Dim, Dyn, OMatrix, OVector, SVector, U1, U2}; use num_dual::{ DualNum, Gradients, hessian, partial, partial2, second_derivative, third_derivative, }; @@ -143,74 +144,42 @@ where .fold(D::zero(), |acc, (_, a)| acc + a) } - /// Evaluate the molar Helmholtz energy of each individual contribution - /// and return them together with a string representation of the contribution. - fn molar_helmholtz_energy_contributions( + /// Evaluate the Helmholtz energy of each individual contribution and return them + /// together with a string representation of the contribution. + fn helmholtz_energy_contributions( &self, temperature: D, - molar_volume: D, - molefracs: &OVector, + volume: D, + moles: &OVector, ) -> Vec<(&'static str, D)> { - let state = StateHD::new(temperature, molar_volume, molefracs); + let state = StateHD::new(temperature, volume, moles); self.reduced_helmholtz_energy_density_contributions(&state) .into_iter() - .map(|(n, f)| (n, f * temperature * molar_volume)) + .map(|(n, f)| (n, f * temperature * volume)) .collect() } - /// Evaluate the residual molar Helmholtz energy $a^\mathrm{res}$. - fn residual_molar_helmholtz_energy( - &self, - temperature: D, - molar_volume: D, - molefracs: &OVector, - ) -> D { - let state = StateHD::new(temperature, molar_volume, molefracs); - self.reduced_residual_helmholtz_energy_density(&state) * temperature * molar_volume - } - /// Evaluate the residual Helmholtz energy $A^\mathrm{res}$. fn residual_helmholtz_energy(&self, temperature: D, volume: D, moles: &OVector) -> D { - let state = StateHD::new_density(temperature, &(moles / volume)); + let state = StateHD::new(temperature, volume, moles); self.reduced_residual_helmholtz_energy_density(&state) * temperature * volume } - /// Evaluate the residual Helmholtz energy $A^\mathrm{res}$. - fn residual_helmholtz_energy_unit( + /// Evaluate the residual molar Helmholtz energy $a^\mathrm{res}$. + /// + /// The molefracs are treated as independently variable in order to + /// calculate derivatives like the chemical potential. + fn residual_molar_helmholtz_energy( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, - ) -> Energy { - let temperature = temperature.into_reduced(); - let total_moles = moles.sum(); - let molar_volume = (volume / total_moles).into_reduced(); - let molefracs = moles / total_moles; - let state = StateHD::new(temperature, molar_volume, &molefracs); - Pressure::from_reduced(self.reduced_residual_helmholtz_energy_density(&state) * temperature) - * volume - } - - /// Check if the provided optional molar concentration is consistent with the - /// equation of state. - /// - /// In general, the number of elements in `molefracs` needs to match the number - /// of components of the equation of state. For a pure component, however, - /// no molefracs need to be provided. - fn validate_molefracs(&self, molefracs: &Option>) -> FeosResult> { - let l = molefracs.as_ref().map_or(1, |m| m.len()); - if self.components() == l { - match molefracs { - Some(m) => Ok(m.clone()), - None => Ok(OVector::from_element_generic( - N::from_usize(1), - U1, - D::one(), - )), - } - } else { - Err(FeosError::IncompatibleComponents(self.components(), l)) - } + molar_volume: MolarVolume, + molefracs: &OVector, + ) -> MolarEnergy { + MolarEnergy::from_reduced(self.residual_helmholtz_energy( + temperature.into_reduced(), + molar_volume.into_reduced(), + molefracs, + )) } /// Calculate the maximum density. @@ -219,18 +188,18 @@ where /// 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, molefracs: &Option>) -> FeosResult> { - let x = self.validate_molefracs(molefracs)?; + fn max_density>(&self, composition: X) -> FeosResult> { + let (x, _) = composition.into_molefracs(self)?; Ok(Density::from_reduced(self.compute_max_density(&x))) } /// Calculate the second virial coefficient $B(T)$ - fn second_virial_coefficient( + fn second_virial_coefficient>( &self, temperature: Temperature, - molefracs: &Option>, - ) -> MolarVolume { - let x = self.validate_molefracs(molefracs).unwrap(); + composition: X, + ) -> FeosResult> { + let (x, _) = composition.into_molefracs(self)?; let (_, _, d2f) = second_derivative( partial2( |rho, &t, x| { @@ -244,16 +213,16 @@ where D::from(0.0), ); - Quantity::from_reduced(d2f * 0.5) + Ok(Quantity::from_reduced(d2f * 0.5)) } /// Calculate the third virial coefficient $C(T)$ - fn third_virial_coefficient( + fn third_virial_coefficient>( &self, temperature: Temperature, - molefracs: &Option>, - ) -> Quot, Density> { - let x = self.validate_molefracs(molefracs).unwrap(); + composition: X, + ) -> FeosResult, Density>> { + let (x, _) = composition.into_molefracs(self)?; let (_, _, _, d3f) = third_derivative( partial2( |rho, &t, x| { @@ -267,36 +236,43 @@ where D::from(0.0), ); - Quantity::from_reduced(d3f / 3.0) + Ok(Quantity::from_reduced(d3f / 3.0)) } /// Calculate the temperature derivative of the second virial coefficient $B'(T)$ - fn second_virial_coefficient_temperature_derivative( + fn second_virial_coefficient_temperature_derivative>( &self, temperature: Temperature, - molefracs: &Option>, - ) -> Quot, Temperature> { + composition: X, + ) -> FeosResult, Temperature>> { + let (molefracs, _) = composition.into_molefracs(self)?; let (_, db_dt) = first_derivative( partial( - |t, x| self.lift().second_virial_coefficient(t, x), - molefracs, + |t, x: &OVector<_, _>| self.lift().second_virial_coefficient(t, x).unwrap(), + &molefracs, ), temperature, ); - db_dt + Ok(db_dt) } /// Calculate the temperature derivative of the third virial coefficient $C'(T)$ - fn third_virial_coefficient_temperature_derivative( + #[expect(clippy::type_complexity)] + fn third_virial_coefficient_temperature_derivative>( &self, temperature: Temperature, - molefracs: &Option>, - ) -> Quot, Density>, Temperature> { + composition: X, + ) -> FeosResult, Density>, Temperature>> { + let (molefracs, _) = composition.into_molefracs(self)?; let (_, dc_dt) = first_derivative( - partial(|t, x| self.lift().third_virial_coefficient(t, x), molefracs), + partial( + // TODO: Fallible partial would be nice here... + |t, x: &OVector<_, _>| self.lift().third_virial_coefficient(t, x).unwrap(), + &molefracs, + ), temperature, ); - dc_dt + Ok(dc_dt) } // The following methods are used in phase equilibrium algorithms @@ -306,10 +282,7 @@ where let molar_volume = density.recip(); let (a, da, d2a) = second_derivative( partial2( - |molar_volume, &t, x| { - self.lift() - .residual_molar_helmholtz_energy(t, molar_volume, x) - }, + |molar_volume, &t, x| self.lift().residual_helmholtz_energy(t, molar_volume, x), &temperature, molefracs, ), @@ -330,11 +303,11 @@ where molefracs: &OVector, ) -> (D, D, D, D, D) { let molar_volume = density.recip(); - let (a, da, d2a) = hessian::<_, _, _, nalgebra::U2, _>( + let (a, da, d2a) = hessian::<_, _, _, U2, _>( partial( |vt: SVector<_, 2>, x: &OVector<_, N>| { let [[v, t]] = vt.data.0; - self.lift().residual_molar_helmholtz_energy(t, v, x) + self.lift().residual_helmholtz_energy(t, v, x) }, molefracs, ), @@ -361,10 +334,7 @@ where let molar_volume = density.recip(); let (_, da, d2a, d3a) = third_derivative( partial2( - |molar_volume, &t, x| { - self.lift() - .residual_molar_helmholtz_energy(t, molar_volume, x) - }, + |molar_volume, &t, x| self.lift().residual_helmholtz_energy(t, molar_volume, x), &temperature, molefracs, ), @@ -455,22 +425,22 @@ where fn viscosity_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &OVector, ) -> Viscosity; fn viscosity_correlation(&self, s_res: D, x: &OVector) -> D; fn diffusion_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &OVector, ) -> Diffusivity; fn diffusion_correlation(&self, s_res: D, x: &OVector) -> D; fn thermal_conductivity_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &OVector, ) -> ThermalConductivity; fn thermal_conductivity_correlation(&self, s_res: D, x: &OVector) -> D; } @@ -483,10 +453,11 @@ where fn viscosity_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &OVector, ) -> Viscosity { - self.deref().viscosity_reference(temperature, volume, moles) + self.deref() + .viscosity_reference(temperature, molar_volume, molefracs) } fn viscosity_correlation(&self, s_res: D, x: &OVector) -> D { self.deref().viscosity_correlation(s_res, x) @@ -494,10 +465,11 @@ where fn diffusion_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &OVector, ) -> Diffusivity { - self.deref().diffusion_reference(temperature, volume, moles) + self.deref() + .diffusion_reference(temperature, molar_volume, molefracs) } fn diffusion_correlation(&self, s_res: D, x: &OVector) -> D { self.deref().diffusion_correlation(s_res, x) @@ -505,11 +477,11 @@ where fn thermal_conductivity_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &OVector, ) -> ThermalConductivity { self.deref() - .thermal_conductivity_reference(temperature, volume, moles) + .thermal_conductivity_reference(temperature, molar_volume, molefracs) } fn thermal_conductivity_correlation(&self, s_res: D, x: &OVector) -> D { self.deref().thermal_conductivity_correlation(s_res, x) diff --git a/crates/feos-core/src/errors.rs b/crates/feos-core/src/errors.rs index 24834480f..f02946bc0 100644 --- a/crates/feos-core/src/errors.rs +++ b/crates/feos-core/src/errors.rs @@ -22,7 +22,7 @@ pub enum FeosError { IncompatibleComponents(usize, usize), #[error("Invalid state in {0}: {1} = {2}.")] InvalidState(String, String, f64), - #[error("Undetermined state: {0}.")] + #[error("Undetermined state: {0}")] UndeterminedState(String), #[error("System is supercritical.")] SuperCritical, diff --git a/crates/feos-core/src/lib.rs b/crates/feos-core/src/lib.rs index b51ae0653..a2552007d 100644 --- a/crates/feos-core/src/lib.rs +++ b/crates/feos-core/src/lib.rs @@ -41,7 +41,7 @@ pub use errors::{FeosError, FeosResult}; #[cfg(feature = "ndarray")] pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero}; pub use phase_equilibria::{PhaseEquilibrium, TemperatureOrPressure}; -pub use state::{Contributions, DensityInitialization, State, StateBuilder, StateHD, StateVec}; +pub use state::{Composition, Contributions, DensityInitialization, State, StateHD, StateVec}; /// Level of detail in the iteration output. #[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Default)] @@ -205,7 +205,7 @@ impl< mod tests { use crate::Contributions; use crate::FeosResult; - use crate::StateBuilder; + use crate::State; use crate::cubic::*; use crate::equation_of_state::{EquationOfState, IdealGas}; use crate::parameter::*; @@ -268,20 +268,12 @@ mod tests { let parameters = PengRobinsonParameters::new_pure(propane.clone())?; let residual = PengRobinson::new(parameters); - let sr = StateBuilder::new(&&residual) - .temperature(300.0 * KELVIN) - .pressure(1.0 * BAR) - .total_moles(2.0 * MOL) - .build()?; + let sr = State::new_npt(&&residual, 300.0 * KELVIN, 1.0 * BAR, 2.0 * MOL, None)?; let parameters = PengRobinsonParameters::new_pure(propane.clone())?; let residual = PengRobinson::new(parameters); let eos = EquationOfState::new(vec![NoIdealGas], residual); - let s = StateBuilder::new(&&eos) - .temperature(300.0 * KELVIN) - .pressure(1.0 * BAR) - .total_moles(2.0 * MOL) - .build()?; + let s = State::new_npt(&&eos, 300.0 * KELVIN, 1.0 * BAR, 2.0 * MOL, None)?; // pressure assert_relative_eq!( @@ -348,7 +340,7 @@ mod tests { ); assert_relative_eq!( s.gibbs_energy(Contributions::Residual) - - s.total_moles + - s.total_moles() * RGAS * s.temperature * s.compressibility(Contributions::Total).ln(), @@ -424,13 +416,13 @@ mod tests { max_relative = 1e-15 ); assert_relative_eq!( - s.dp_dni(Contributions::Total), - sr.dp_dni(Contributions::Total), + s.n_dp_dni(Contributions::Total), + sr.n_dp_dni(Contributions::Total), max_relative = 1e-15 ); assert_relative_eq!( - s.dp_dni(Contributions::Residual), - sr.dp_dni(Contributions::Residual), + s.n_dp_dni(Contributions::Residual), + sr.n_dp_dni(Contributions::Residual), max_relative = 1e-15 ); @@ -448,8 +440,8 @@ mod tests { max_relative = 1e-15 ); assert_relative_eq!( - s.dmu_dni(Contributions::Residual), - sr.dmu_dni(Contributions::Residual), + s.n_dmu_dni(Contributions::Residual), + sr.n_dmu_dni(Contributions::Residual), max_relative = 1e-15 ); assert_relative_eq!( @@ -462,7 +454,7 @@ mod tests { 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.n_dln_phi_dnj(), sr.n_dln_phi_dnj(), max_relative = 1e-15); assert_relative_eq!( s.thermodynamic_factor(), sr.thermodynamic_factor(), diff --git a/crates/feos-core/src/phase_equilibria/bubble_dew.rs b/crates/feos-core/src/phase_equilibria/bubble_dew.rs index a50cc46a1..91ad57d3d 100644 --- a/crates/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/crates/feos-core/src/phase_equilibria/bubble_dew.rs @@ -11,7 +11,7 @@ use nalgebra::{DMatrix, DVector, DefaultAllocator, Dim, Dyn, OVector, U1}; use ndarray::Array1; use num_dual::linalg::LU; use num_dual::{DualNum, DualStruct, Gradients}; -use quantity::{Density, Dimensionless, Moles, Pressure, Quantity, RGAS, SIUnit, Temperature}; +use quantity::{Density, Dimensionless, Pressure, Quantity, RGAS, SIUnit, Temperature}; const MAX_ITER_INNER: usize = 5; const TOL_INNER: f64 = 1e-9; @@ -333,7 +333,7 @@ where ) }; } - let state1 = State::new_intensive( + let state1 = State::new( eos, Temperature::from_reduced(t), Density::from_reduced(molar_volume.recip()), @@ -341,11 +341,11 @@ where )?; let rho2_total = rho2.sum(); let x2 = rho2 / rho2_total; - let state2 = State::new_intensive( + let state2 = State::new( eos, Temperature::from_reduced(t), Density::from_reduced(rho2_total), - &x2, + x2, )?; Ok(PhaseEquilibrium(if bubble { @@ -578,8 +578,8 @@ where } else { let mut t = temperature.into_reduced(); let mut p = pressure.into_reduced(); - let mut molar_volume = state1.density.into_reduced().recip(); - let mut rho2 = state2.partial_density.to_reduced(); + let mut molar_volume = state1.molar_volume.into_reduced(); + let mut rho2 = state2.partial_density().to_reduced(); let err = if iterate_p { Self::newton_step_t( &state1.eos, @@ -603,8 +603,19 @@ where }; *temperature = Temperature::from_reduced(t); *pressure = Pressure::from_reduced(p); - state1.density = Density::from_reduced(molar_volume.recip()); - state2.partial_density = Density::from_reduced(rho2); + state1 = State::new( + &state1.eos, + *temperature, + Density::from_reduced(molar_volume.recip()), + molefracs_spec, + )?; + let density = rho2.sum(); + state2 = State::new( + &state2.eos, + *temperature, + Density::from_reduced(density), + rho2 / density, + )?; Ok(err) }?; @@ -627,7 +638,7 @@ where ); Ok(( state1.density.into_reduced().recip(), - state2.partial_density.to_reduced(), + state2.partial_density().to_reduced(), )) } else { // not converged, return error @@ -752,7 +763,7 @@ where liquid_molefracs: &OVector, ) -> FeosResult<(Pressure, OVector)> { let density = 0.75 * Density::from_reduced(eos.compute_max_density(liquid_molefracs)); - let liquid = State::new_intensive(eos, temperature, density, liquid_molefracs)?; + let liquid = State::new(eos, temperature, density, liquid_molefracs)?; let v_l = liquid.partial_molar_volume(); let p_l = liquid.pressure(Contributions::Total); let mu_l = liquid.residual_chemical_potential(); @@ -776,7 +787,7 @@ where let mut x = vapor_molefracs.clone(); for _ in 0..5 { let density = Density::from_reduced(0.75 * eos.compute_max_density(&x)); - let liquid = State::new_intensive(eos, temperature, density, &x)?; + let liquid = State::new(eos, temperature, density, x)?; let v_l = liquid.partial_molar_volume(); let p_l = liquid.pressure(Contributions::Total); let mu_l = liquid.residual_chemical_potential(); @@ -804,7 +815,7 @@ where temperature: Temperature, molefracs: &OVector, ) -> FeosResult { - let [sp_v, sp_l] = State::spinodal(eos, temperature, Some(molefracs), Default::default())?; + let [sp_v, sp_l] = State::spinodal(eos, temperature, molefracs, Default::default())?; let pv = sp_v.pressure(Contributions::Total); let pl = sp_l.pressure(Contributions::Total); Ok(0.5 * (Pressure::from_reduced(0.0).max(pl) + pv)) @@ -818,7 +829,7 @@ where vapor_molefracs: Option<&OVector>, ) -> FeosResult<[State; 2]> { let liquid_state = - State::new_xpt(eos, temperature, pressure, liquid_molefracs, Some(Liquid))?; + State::new_npt(eos, temperature, pressure, liquid_molefracs, Some(Liquid))?; let xv = match vapor_molefracs { Some(xv) => xv.clone(), None => liquid_state @@ -826,7 +837,7 @@ where .map(f64::exp) .component_mul(liquid_molefracs), }; - let vapor_state = State::new_xpt(eos, temperature, pressure, &xv, Some(Vapor))?; + let vapor_state = State::new_npt(eos, temperature, pressure, xv, Some(Vapor))?; Ok([liquid_state, vapor_state]) } @@ -837,13 +848,7 @@ where vapor_molefracs: &OVector, liquid_molefracs: Option<&OVector>, ) -> FeosResult<[State; 2]> { - let vapor_state = State::new_npt( - eos, - temperature, - pressure, - &Moles::from_reduced(vapor_molefracs.clone()), - Some(Vapor), - )?; + let vapor_state = State::new_npt(eos, temperature, pressure, vapor_molefracs, Some(Vapor))?; let xl = match liquid_molefracs { Some(xl) => xl.clone(), None => { @@ -851,13 +856,13 @@ where .ln_phi() .map(f64::exp) .component_mul(vapor_molefracs); - let liquid_state = State::new_xpt(eos, temperature, pressure, &xl, Some(Liquid))?; + let liquid_state = State::new_npt(eos, temperature, pressure, xl, Some(Liquid))?; (vapor_state.ln_phi() - liquid_state.ln_phi()) .map(f64::exp) .component_mul(vapor_molefracs) } }; - let liquid_state = State::new_xpt(eos, temperature, pressure, &xl, Some(Liquid))?; + let liquid_state = State::new_npt(eos, temperature, pressure, xl, Some(Liquid))?; Ok([vapor_state, liquid_state]) } @@ -866,20 +871,20 @@ where pressure: Pressure, state1: &mut State, state2: &mut State, - moles_state2: Option<&Moles>>, + molefracs_state2: Option<&OVector>, ) -> FeosResult<()> { *state1 = State::new_npt( &state1.eos, temperature, pressure, - &state1.moles, + &state1.molefracs, Some(InitialDensity(state1.density)), )?; *state2 = State::new_npt( &state2.eos, temperature, pressure, - moles_state2.unwrap_or(&state2.moles), + molefracs_state2.unwrap_or(&state2.molefracs), Some(InitialDensity(state2.density)), )?; Ok(()) @@ -908,11 +913,11 @@ where "", "" ); - *state2 = State::new_xpt( + *state2 = State::new_npt( &state2.eos, state2.temperature, state2.pressure(Contributions::Total), - &x2, + x2, Some(InitialDensity(state2.density)), )?; Ok(err_out) diff --git a/crates/feos-core/src/phase_equilibria/mod.rs b/crates/feos-core/src/phase_equilibria/mod.rs index 4a10aa0fe..95671deb2 100644 --- a/crates/feos-core/src/phase_equilibria/mod.rs +++ b/crates/feos-core/src/phase_equilibria/mod.rs @@ -126,39 +126,41 @@ where Self([vapor, liquid]) } - /// Creates a new PhaseEquilibrium that contains two states at the - /// specified temperature, pressure and molefracs. - /// - /// The constructor can be used in custom phase equilibrium solvers or, - /// e.g., to generate initial guesses for an actual VLE solver. - /// In general, the two states generated are NOT in an equilibrium. - pub fn new_xpt( - eos: &E, - temperature: Temperature, - pressure: Pressure, - vapor_molefracs: &OVector, - liquid_molefracs: &OVector, - ) -> FeosResult { - let liquid = State::new_xpt( - eos, - temperature, - pressure, - liquid_molefracs, - Some(DensityInitialization::Liquid), - )?; - let vapor = State::new_xpt( - eos, - temperature, - pressure, - vapor_molefracs, - Some(DensityInitialization::Vapor), - )?; - Ok(Self([vapor, liquid])) - } - - pub(super) fn vapor_phase_fraction(&self) -> f64 { - (self.vapor().total_moles / (self.vapor().total_moles + self.liquid().total_moles)) - .into_value() + // /// Creates a new PhaseEquilibrium that contains two states at the + // /// specified temperature, pressure and molefracs. + // /// + // /// The constructor can be used in custom phase equilibrium solvers or, + // /// e.g., to generate initial guesses for an actual VLE solver. + // /// In general, the two states generated are NOT in an equilibrium. + // pub fn new_xpt( + // eos: &E, + // temperature: Temperature, + // pressure: Pressure, + // vapor_molefracs: &OVector, + // liquid_molefracs: &OVector, + // ) -> FeosResult { + // let liquid = State::new_xpt( + // eos, + // temperature, + // pressure, + // liquid_molefracs, + // Some(DensityInitialization::Liquid), + // )?; + // let vapor = State::new_xpt( + // eos, + // temperature, + // pressure, + // vapor_molefracs, + // Some(DensityInitialization::Vapor), + // )?; + // Ok(Self([vapor, liquid])) + // } + + pub(super) fn vapor_phase_fraction(&self) -> Option { + self.vapor() + .total_moles + .zip(self.liquid().total_moles) + .map(|(v, l)| (v / (l + v)).into_value()) } } @@ -176,7 +178,7 @@ where &s.eos, temperature, pressure, - &s.moles, + &*s, Some(DensityInitialization::InitialDensity(s.density)), )?; } @@ -203,10 +205,10 @@ where // Total Gibbs energy excluding the constant contribution RT sum_i N_i ln(\Lambda_i^3) pub(super) fn total_gibbs_energy(&self) -> Energy { self.0.iter().fold(Energy::from_reduced(0.0), |acc, s| { - let ln_rho_m1 = s.partial_density.to_reduced().map(|r| r.ln() - 1.0); + let ln_rho_m1 = s.partial_density().to_reduced().map(|r| r.ln() - 1.0); acc + s.residual_helmholtz_energy() - + s.pressure(Contributions::Total) * s.volume - + RGAS * s.temperature * s.total_moles * s.molefracs.dot(&ln_rho_m1) + + s.pressure(Contributions::Total) * s.volume() + + RGAS * s.temperature * s.total_moles() * s.molefracs.dot(&ln_rho_m1) }) } } diff --git a/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs b/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs index aaa717858..92e6932a5 100644 --- a/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs +++ b/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs @@ -1,7 +1,7 @@ use super::bubble_dew::TemperatureOrPressure; use super::{PhaseDiagram, PhaseEquilibrium}; use crate::errors::{FeosError, FeosResult}; -use crate::state::{Contributions, DensityInitialization::Vapor, State, StateBuilder}; +use crate::state::{Contributions, DensityInitialization::Vapor, State}; use crate::{ReferenceSystem, Residual, SolverOptions, Subset}; use nalgebra::{DVector, dvector, matrix, stack, vector}; use ndarray::{Array1, s}; @@ -558,21 +558,21 @@ impl PhaseEquilibrium { let p0 = (vle1.vapor().pressure(Contributions::Total) + vle2.vapor().pressure(Contributions::Total)) * 0.5; - let nv0 = (&vle1.vapor().moles + &vle2.vapor().moles) * 0.5; - let mut v = State::new_npt(eos, temperature, p0, &nv0, Some(Vapor))?; + let y0 = (&vle1.vapor().molefracs + &vle2.vapor().molefracs) * 0.5; + let mut v = State::new_npt(eos, temperature, p0, y0, Some(Vapor))?; for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) { // calculate properties - let dmu_drho_l1 = (l1.dmu_dni(Contributions::Total) * l1.volume).to_reduced(); - let dmu_drho_l2 = (l2.dmu_dni(Contributions::Total) * l2.volume).to_reduced(); - let dmu_drho_v = (v.dmu_dni(Contributions::Total) * v.volume).to_reduced(); - let dp_drho_l1 = (l1.dp_dni(Contributions::Total) * l1.volume) + let dmu_drho_l1 = (l1.n_dmu_dni(Contributions::Total) * l1.molar_volume).to_reduced(); + let dmu_drho_l2 = (l2.n_dmu_dni(Contributions::Total) * l2.molar_volume).to_reduced(); + let dmu_drho_v = (v.n_dmu_dni(Contributions::Total) * v.molar_volume).to_reduced(); + let dp_drho_l1 = (l1.n_dp_dni(Contributions::Total) * l1.molar_volume) .to_reduced() .transpose(); - let dp_drho_l2 = (l2.dp_dni(Contributions::Total) * l2.volume) + let dp_drho_l2 = (l2.n_dp_dni(Contributions::Total) * l2.molar_volume) .to_reduced() .transpose(); - let dp_drho_v = (v.dp_dni(Contributions::Total) * v.volume) + let dp_drho_v = (v.n_dp_dni(Contributions::Total) * v.molar_volume) .to_reduced() .transpose(); let mu_l1_res = l1.residual_chemical_potential().to_reduced(); @@ -585,15 +585,15 @@ impl PhaseEquilibrium { // calculate residual let delta_l1v_mu_ig = (RGAS * v.temperature).to_reduced() * (l1 - .partial_density + .partial_density() .to_reduced() - .component_div(&v.partial_density.to_reduced())) + .component_div(&v.partial_density().to_reduced())) .map(f64::ln); let delta_l2v_mu_ig = (RGAS * v.temperature).to_reduced() * (l2 - .partial_density + .partial_density() .to_reduced() - .component_div(&v.partial_density.to_reduced())) + .component_div(&v.partial_density().to_reduced())) .map(f64::ln); let res = stack![ mu_l1_res - &mu_v_res + delta_l1v_mu_ig; @@ -620,11 +620,11 @@ impl PhaseEquilibrium { // apply Newton step let rho_l1 = - &l1.partial_density - &Density::from_reduced(dx.rows_range(0..2).into_owned()); + &l1.partial_density() - &Density::from_reduced(dx.rows_range(0..2).into_owned()); let rho_l2 = - &l2.partial_density - &Density::from_reduced(dx.rows_range(2..4).into_owned()); + &l2.partial_density() - &Density::from_reduced(dx.rows_range(2..4).into_owned()); let rho_v = - &v.partial_density - &Density::from_reduced(dx.rows_range(4..6).into_owned()); + &v.partial_density() - &Density::from_reduced(dx.rows_range(4..6).into_owned()); // check for negative densities for i in 0..2 { @@ -639,18 +639,9 @@ impl PhaseEquilibrium { } // update states - l1 = StateBuilder::new(eos) - .temperature(temperature) - .partial_density(&rho_l1) - .build()?; - l2 = StateBuilder::new(eos) - .temperature(temperature) - .partial_density(&rho_l2) - .build()?; - v = StateBuilder::new(eos) - .temperature(temperature) - .partial_density(&rho_v) - .build()?; + l1 = State::new_density(eos, temperature, rho_l1)?; + l2 = State::new_density(eos, temperature, rho_l2)?; + v = State::new_density(eos, temperature, rho_v)?; } Err(FeosError::NotConverged(String::from( "PhaseEquilibrium::heteroazeotrope_t", @@ -680,24 +671,24 @@ impl PhaseEquilibrium { let mut l1 = vle1.liquid().clone(); let mut l2 = vle2.liquid().clone(); let t0 = (vle1.vapor().temperature + vle2.vapor().temperature) * 0.5; - let nv0 = (&vle1.vapor().moles + &vle2.vapor().moles) * 0.5; - let mut v = State::new_npt(eos, t0, pressure, &nv0, Some(Vapor))?; + let y0 = (&vle1.vapor().molefracs + &vle2.vapor().molefracs) * 0.5; + let mut v = State::new_npt(eos, t0, pressure, y0, Some(Vapor))?; for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) { // calculate properties - let dmu_drho_l1 = (l1.dmu_dni(Contributions::Total) * l1.volume).to_reduced(); - let dmu_drho_l2 = (l2.dmu_dni(Contributions::Total) * l2.volume).to_reduced(); - let dmu_drho_v = (v.dmu_dni(Contributions::Total) * v.volume).to_reduced(); + let dmu_drho_l1 = (l1.n_dmu_dni(Contributions::Total) * l1.molar_volume).to_reduced(); + let dmu_drho_l2 = (l2.n_dmu_dni(Contributions::Total) * l2.molar_volume).to_reduced(); + let dmu_drho_v = (v.n_dmu_dni(Contributions::Total) * v.molar_volume).to_reduced(); let dmu_res_dt_l1 = (l1.dmu_res_dt()).to_reduced(); let dmu_res_dt_l2 = (l2.dmu_res_dt()).to_reduced(); let dmu_res_dt_v = (v.dmu_res_dt()).to_reduced(); - let dp_drho_l1 = (l1.dp_dni(Contributions::Total) * l1.volume) + let dp_drho_l1 = (l1.n_dp_dni(Contributions::Total) * l1.molar_volume) .to_reduced() .transpose(); - let dp_drho_l2 = (l2.dp_dni(Contributions::Total) * l2.volume) + let dp_drho_l2 = (l2.n_dp_dni(Contributions::Total) * l2.molar_volume) .to_reduced() .transpose(); - let dp_drho_v = (v.dp_dni(Contributions::Total) * v.volume) + let dp_drho_v = (v.n_dp_dni(Contributions::Total) * v.molar_volume) .to_reduced() .transpose(); let dp_dt_l1 = (l1.dp_dt(Contributions::Total)).to_reduced(); @@ -712,14 +703,14 @@ impl PhaseEquilibrium { // calculate residual let delta_l1v_dmu_ig_dt = l1 - .partial_density + .partial_density() .to_reduced() - .component_div(&v.partial_density.to_reduced()) + .component_div(&v.partial_density().to_reduced()) .map(f64::ln); let delta_l2v_dmu_ig_dt = l2 - .partial_density + .partial_density() .to_reduced() - .component_div(&v.partial_density.to_reduced()) + .component_div(&v.partial_density().to_reduced()) .map(f64::ln); let delta_l1v_mu_ig = (RGAS * v.temperature).to_reduced() * &delta_l1v_dmu_ig_dt; let delta_l2v_mu_ig = (RGAS * v.temperature).to_reduced() * &delta_l2v_dmu_ig_dt; @@ -749,10 +740,11 @@ impl PhaseEquilibrium { // apply Newton step let rho_l1 = - l1.partial_density - Density::from_reduced(dx.rows_range(0..2).into_owned()); + l1.partial_density() - Density::from_reduced(dx.rows_range(0..2).into_owned()); let rho_l2 = - l2.partial_density - Density::from_reduced(dx.rows_range(2..4).into_owned()); - let rho_v = v.partial_density - Density::from_reduced(dx.rows_range(4..6).into_owned()); + l2.partial_density() - Density::from_reduced(dx.rows_range(2..4).into_owned()); + let rho_v = + v.partial_density() - Density::from_reduced(dx.rows_range(4..6).into_owned()); let t = v.temperature - Temperature::from_reduced(dx[6]); // check for negative densities and temperatures @@ -769,18 +761,9 @@ impl PhaseEquilibrium { } // update states - l1 = StateBuilder::new(eos) - .temperature(t) - .partial_density(&rho_l1) - .build()?; - l2 = StateBuilder::new(eos) - .temperature(t) - .partial_density(&rho_l2) - .build()?; - v = StateBuilder::new(eos) - .temperature(t) - .partial_density(&rho_v) - .build()?; + l1 = State::new_density(eos, t, rho_l1)?; + l2 = State::new_density(eos, t, rho_l2)?; + v = State::new_density(eos, t, rho_v)?; } Err(FeosError::NotConverged(String::from( "PhaseEquilibrium::heteroazeotrope_p", diff --git a/crates/feos-core/src/phase_equilibria/phase_diagram_pure.rs b/crates/feos-core/src/phase_equilibria/phase_diagram_pure.rs index daeb12132..2382a974c 100644 --- a/crates/feos-core/src/phase_equilibria/phase_diagram_pure.rs +++ b/crates/feos-core/src/phase_equilibria/phase_diagram_pure.rs @@ -37,7 +37,7 @@ impl PhaseDiagram { let sc = State::critical_point( eos, - None, + (), critical_temperature, None, SolverOptions::default(), @@ -104,7 +104,7 @@ impl PhaseDiagram { { let sc = State::critical_point( eos, - None, + (), critical_temperature, None, SolverOptions::default(), diff --git a/crates/feos-core/src/phase_equilibria/phase_envelope.rs b/crates/feos-core/src/phase_equilibria/phase_envelope.rs index 89610e538..0f01e7288 100644 --- a/crates/feos-core/src/phase_equilibria/phase_envelope.rs +++ b/crates/feos-core/src/phase_equilibria/phase_envelope.rs @@ -20,7 +20,7 @@ impl PhaseDiagram { let sc = State::critical_point( eos, - Some(molefracs), + molefracs, critical_temperature, None, SolverOptions::default(), @@ -69,7 +69,7 @@ impl PhaseDiagram { let sc = State::critical_point( eos, - Some(molefracs), + molefracs, critical_temperature, None, SolverOptions::default(), @@ -134,7 +134,7 @@ impl PhaseDiagram { let sc = State::critical_point( eos, - Some(molefracs), + molefracs, critical_temperature, None, SolverOptions::default(), @@ -145,7 +145,7 @@ impl PhaseDiagram { let temperatures = Temperature::linspace(min_temperature, max_temperature, npoints - 1); for ti in &temperatures { - let spinodal = State::spinodal(eos, ti, Some(molefracs), options).ok(); + let spinodal = State::spinodal(eos, ti, molefracs, options).ok(); if let Some(spinodal) = spinodal { states.push(PhaseEquilibrium(spinodal)); } diff --git a/crates/feos-core/src/phase_equilibria/stability_analysis.rs b/crates/feos-core/src/phase_equilibria/stability_analysis.rs index 69988f8c2..950f05026 100644 --- a/crates/feos-core/src/phase_equilibria/stability_analysis.rs +++ b/crates/feos-core/src/phase_equilibria/stability_analysis.rs @@ -91,7 +91,7 @@ where &self.eos, self.temperature, self.pressure(Contributions::Total), - &Moles::from_reduced(x_trial), + Moles::from_reduced(x_trial), Some(phase), ) } @@ -122,7 +122,7 @@ where &trial.eos, trial.temperature, trial.pressure(Contributions::Total), - &Moles::from_reduced(y), + Moles::from_reduced(y), Some(DensityInitialization::InitialDensity(trial.density)), )?; if (i > 4 && error > scaled_tol) || (tpd > tpd_old + 1E-05 && i > 2) { @@ -166,9 +166,9 @@ where let (n, _) = di.shape_generic(); // calculate residual and ideal hesse matrix - let mut hesse = (self.dln_phi_dnj() * Moles::from_reduced(1.0)).into_value(); + let mut hesse = self.n_dln_phi_dnj() / self.total_moles().into_reduced(); let lnphi = self.ln_phi(); - let y = self.moles.to_reduced(); + let y = self.moles().into_reduced(); let ln_y = y.map(|y| if y > f64::EPSILON { y.ln() } else { 0.0 }); let sq_y = y.map(f64::sqrt); let gradient = (&ln_y + &lnphi - di).component_mul(&sq_y); @@ -228,7 +228,7 @@ where &self.eos, self.temperature, self.pressure(Contributions::Total), - &Moles::from_reduced(y), + Moles::from_reduced(y), Some(DensityInitialization::InitialDensity(self.density)), )?; } diff --git a/crates/feos-core/src/phase_equilibria/tp_flash.rs b/crates/feos-core/src/phase_equilibria/tp_flash.rs index cd9d9bea3..e955acaf6 100644 --- a/crates/feos-core/src/phase_equilibria/tp_flash.rs +++ b/crates/feos-core/src/phase_equilibria/tp_flash.rs @@ -55,7 +55,7 @@ impl, D: DualNum + Copy> PhaseEquilibrium { let z = feed.get(0).convert_into(feed.get(0) + feed.get(1)); let total_moles = feed.sum(); let moles = vector![z.re(), 1.0 - z.re()] * MOL; - let vle_re = State::new_npt(&eos.re(), temperature.re(), pressure.re(), &moles, None)? + let vle_re = State::new_npt(&eos.re(), temperature.re(), pressure.re(), moles, None)? .tp_flash(None, options, None)?; // implicit differentiation @@ -82,7 +82,7 @@ impl, D: DualNum + Copy> PhaseEquilibrium { let eos = eos.lift(); let molar_gibbs_energy = |x: Dual2Vec<_, _, _>, v| { let molefracs = vector![x, -x + 1.0]; - let a_res = eos.residual_molar_helmholtz_energy(t, v, &molefracs); + let a_res = eos.residual_helmholtz_energy(t, v, &molefracs); let a_ig = (x * (x / v).ln() - (x - 1.0) * ((-x + 1.0) / v).ln() - 1.0) * t; a_res + a_ig + v * p }; @@ -99,7 +99,7 @@ impl, D: DualNum + Copy> PhaseEquilibrium { let state = |x: D, v, phi| { let volume = MolarVolume::from_reduced(v * phi) * total_moles; let moles = Quantity::new(vector![x, -x + 1.0] * phi * total_moles.convert_into(MOL)); - State::new_nvt(eos, temperature, volume, &moles) + State::new_nvt(eos, temperature, volume, moles) }; Ok(Self([state(y, v_v, beta)?, state(x, v_l, -beta + 1.0)?])) } @@ -184,7 +184,9 @@ where )?; // check convergence - let beta = new_vle_state.vapor_phase_fraction(); + // unwrap is safe here, because after the first successive substitution step the + // phase amounts in new_vle_state are known. + let beta = new_vle_state.vapor_phase_fraction().unwrap(); let tpd = [ self.tangent_plane_distance(new_vle_state.vapor()), self.tangent_plane_distance(new_vle_state.liquid()), @@ -377,16 +379,22 @@ where fn update_states(&mut self, feed_state: &State, k: &OVector) -> FeosResult<()> { // calculate vapor phase fraction using Rachford-Rice algorithm - let mut beta = self.vapor_phase_fraction(); - beta = rachford_rice(&feed_state.molefracs, k, Some(beta))?; + let beta = self.vapor_phase_fraction(); + let beta = rachford_rice(&feed_state.molefracs, k, beta)?; // update VLE - let v = feed_state.moles.clone().component_mul(&Dimensionless::new( - k.map(|k| beta * k / (1.0 - beta + beta * k)), - )); - let l = feed_state.moles.clone().component_mul(&Dimensionless::new( - k.map(|k| (1.0 - beta) / (1.0 - beta + beta * k)), - )); + let v = feed_state + .moles() + .clone() + .component_mul(&Dimensionless::new( + k.map(|k| beta * k / (1.0 - beta + beta * k)), + )); + let l = feed_state + .moles() + .clone() + .component_mul(&Dimensionless::new( + k.map(|k| (1.0 - beta) / (1.0 - beta + beta * k)), + )); self.update_moles(feed_state.pressure(Contributions::Total), [&v, &l])?; Ok(()) } diff --git a/crates/feos-core/src/phase_equilibria/vle_pure.rs b/crates/feos-core/src/phase_equilibria/vle_pure.rs index e3575c094..63756fd5b 100644 --- a/crates/feos-core/src/phase_equilibria/vle_pure.rs +++ b/crates/feos-core/src/phase_equilibria/vle_pure.rs @@ -3,7 +3,7 @@ use crate::density_iteration::{_density_iteration, _pressure_spinodal}; use crate::equation_of_state::{Residual, Subset}; use crate::errors::{FeosError, FeosResult}; use crate::state::{Contributions, DensityInitialization, State}; -use crate::{ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity}; +use crate::{Composition, ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity}; use nalgebra::allocator::Allocator; use nalgebra::{DVector, DefaultAllocator, Dim, SVector, U1, U2}; use num_dual::{DualNum, DualStruct, Gradients, gradient, partial}; @@ -17,6 +17,7 @@ const TOL_PURE: f64 = 1e-12; impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator + Allocator + Allocator, + (): Composition + Composition, { /// Calculate a phase equilibrium for a pure component. pub fn pure>( @@ -207,7 +208,7 @@ where let x = E::pure_molefracs(); let v = (0.75 * eos.compute_max_density(&x)).recip(); let t = temperature.into_reduced(); - let a_res = eos.residual_molar_helmholtz_energy(t, v, &x); + let a_res = eos.residual_helmholtz_energy(t, v, &x); let p = t / v * (a_res / t - 1.0).exp(); let rho_v = p / t; let rho_l = v.recip(); @@ -237,6 +238,7 @@ where impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator + Allocator + Allocator, + (): Composition, { /// Calculate a phase equilibrium for a pure component /// and given pressure. @@ -394,6 +396,7 @@ fn init_pure_p, N: Gradients>( ) -> FeosResult<(f64, [f64; 2])> where DefaultAllocator: Allocator + Allocator + Allocator, + (): Composition, { let trial_temperatures = [300.0, 500.0, 200.0]; let p = pressure.into_reduced(); @@ -413,7 +416,7 @@ where }; let [mut t_v, mut t_l] = [t0, t0]; - let cp = State::critical_point(eos, None, None, None, SolverOptions::default())?; + let cp = State::critical_point(eos, (), None, None, SolverOptions::default())?; let cp_density = cp.density.into_reduced(); if pressure > cp.pressure(Contributions::Total) { return Err(FeosError::SuperCritical); @@ -447,7 +450,7 @@ where partial( |t_v: SVector<_, _>, x| { let [[t, v]] = t_v.data.0; - eos.lift().residual_molar_helmholtz_energy(t, v, x) + eos.lift().residual_helmholtz_energy(t, v, x) }, &x, ), @@ -525,17 +528,17 @@ impl PhaseEquilibrium { let mut molefracs_liquid = molefracs_vapor.clone(); molefracs_vapor[i] = 1.0; molefracs_liquid[i] = 1.0; - let vapor = State::new_intensive( + let vapor = State::new( eos, vle_pure.vapor().temperature, vle_pure.vapor().density, - &molefracs_vapor, + molefracs_vapor, )?; - let liquid = State::new_intensive( + let liquid = State::new( eos, vle_pure.liquid().temperature, vle_pure.liquid().density, - &molefracs_liquid, + molefracs_liquid, )?; Ok(PhaseEquilibrium::from_states(vapor, liquid)) }) diff --git a/crates/feos-core/src/state/builder.rs b/crates/feos-core/src/state/builder.rs deleted file mode 100644 index c418a7c1f..000000000 --- a/crates/feos-core/src/state/builder.rs +++ /dev/null @@ -1,251 +0,0 @@ -use super::{DensityInitialization, State}; -use crate::Total; -use crate::equation_of_state::Residual; -use crate::errors::FeosResult; -use nalgebra::DVector; -use quantity::*; - -/// A simple tool to construct [State]s with arbitrary input parameters. -/// -/// # Examples -/// ``` -/// # use feos_core::{FeosResult, StateBuilder}; -/// # use feos_core::cubic::{PengRobinson, PengRobinsonParameters}; -/// # use quantity::*; -/// # use nalgebra::dvector; -/// # use approx::assert_relative_eq; -/// # fn main() -> FeosResult<()> { -/// // Create a state for given T,V,N -/// let eos = &PengRobinson::new(PengRobinsonParameters::new_simple(&[369.8], &[41.9 * 1e5], &[0.15], &[15.0])?); -/// let state = StateBuilder::new(&eos) -/// .temperature(300.0 * KELVIN) -/// .volume(12.5 * METER.powi::<3>()) -/// .moles(&(dvector![2.5] * MOL)) -/// .build()?; -/// assert_eq!(state.density, 0.2 * MOL / METER.powi::<3>()); -/// -/// // For a pure component, the composition does not need to be specified. -/// let eos = &PengRobinson::new(PengRobinsonParameters::new_simple(&[369.8], &[41.9 * 1e5], &[0.15], &[15.0])?); -/// let state = StateBuilder::new(&eos) -/// .temperature(300.0 * KELVIN) -/// .volume(12.5 * METER.powi::<3>()) -/// .total_moles(2.5 * MOL) -/// .build()?; -/// assert_eq!(state.density, 0.2 * MOL / METER.powi::<3>()); -/// -/// // The state can be constructed without providing any extensive property. -/// let eos = &PengRobinson::new( -/// PengRobinsonParameters::new_simple( -/// &[369.8, 305.4], -/// &[41.9 * 1e5, 48.2 * 1e5], -/// &[0.15, 0.10], -/// &[15.0, 30.0] -/// )? -/// ); -/// let state = StateBuilder::new(&eos) -/// .temperature(300.0 * KELVIN) -/// .partial_density(&(dvector![0.2, 0.6] * MOL / METER.powi::<3>())) -/// .build()?; -/// assert_relative_eq!(state.molefracs, dvector![0.25, 0.75]); -/// assert_relative_eq!(state.density, 0.8 * MOL / METER.powi::<3>()); -/// # Ok(()) -/// # } -/// ``` -#[derive(Clone)] -pub struct StateBuilder<'a, E, const IG: bool> { - eos: &'a E, - temperature: Option, - volume: Option, - density: Option, - partial_density: Option<&'a Density>>, - total_moles: Option, - moles: Option<&'a Moles>>, - molefracs: Option<&'a DVector>, - pressure: Option, - molar_enthalpy: Option, - molar_entropy: Option, - molar_internal_energy: Option, - density_initialization: Option, - initial_temperature: Option, -} - -impl<'a, E: Residual> StateBuilder<'a, E, false> { - /// Create a new `StateBuilder` for the given equation of state. - pub fn new(eos: &'a E) -> Self { - StateBuilder { - eos, - temperature: None, - volume: None, - density: None, - partial_density: None, - total_moles: None, - moles: None, - molefracs: None, - pressure: None, - molar_enthalpy: None, - molar_entropy: None, - molar_internal_energy: None, - density_initialization: None, - initial_temperature: None, - } - } -} - -impl<'a, E: Residual, const IG: bool> StateBuilder<'a, E, IG> { - /// Provide the temperature for the new state. - pub fn temperature(mut self, temperature: Temperature) -> Self { - self.temperature = Some(temperature); - self - } - - /// Provide the volume for the new state. - pub fn volume(mut self, volume: Volume) -> Self { - self.volume = Some(volume); - self - } - - /// Provide the density for the new state. - pub fn density(mut self, density: Density) -> Self { - self.density = Some(density); - self - } - - /// Provide partial densities for the new state. - pub fn partial_density(mut self, partial_density: &'a Density>) -> Self { - self.partial_density = Some(partial_density); - self - } - - /// Provide the total moles for the new state. - pub fn total_moles(mut self, total_moles: Moles) -> Self { - self.total_moles = Some(total_moles); - self - } - - /// Provide the moles for the new state. - pub fn moles(mut self, moles: &'a Moles>) -> Self { - self.moles = Some(moles); - self - } - - /// Provide the molefracs for the new state. - pub fn molefracs(mut self, molefracs: &'a DVector) -> Self { - self.molefracs = Some(molefracs); - self - } - - /// Provide the pressure for the new state. - pub fn pressure(mut self, pressure: Pressure) -> Self { - self.pressure = Some(pressure); - self - } - - /// Specify a vapor state. - pub fn vapor(mut self) -> Self { - self.density_initialization = Some(DensityInitialization::Vapor); - self - } - - /// Specify a liquid state. - pub fn liquid(mut self) -> Self { - self.density_initialization = Some(DensityInitialization::Liquid); - self - } - - /// Provide an initial density used in density iterations. - pub fn initial_density(mut self, initial_density: Density) -> Self { - self.density_initialization = Some(DensityInitialization::InitialDensity(initial_density)); - self - } -} - -impl<'a, E: Total, const IG: bool> StateBuilder<'a, E, IG> { - /// Provide the molar enthalpy for the new state. - pub fn molar_enthalpy(mut self, molar_enthalpy: MolarEnergy) -> 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: MolarEntropy) -> 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: MolarEnergy, - ) -> 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: Temperature, - ) -> StateBuilder<'a, E, true> { - self.initial_temperature = Some(initial_temperature); - 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 StateBuilder<'_, E, false> { - /// Try to build the state with the given inputs. - pub fn build(self) -> FeosResult> { - 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 StateBuilder<'_, E, true> { - /// Try to build the state with the given inputs. - pub fn build(self) -> FeosResult> { - State::new_full( - self.eos, - self.temperature, - self.volume, - self.density, - self.partial_density, - self.total_moles, - self.moles, - self.molefracs, - self.pressure, - self.molar_enthalpy, - self.molar_entropy, - self.molar_internal_energy, - self.density_initialization, - self.initial_temperature, - ) - } -} diff --git a/crates/feos-core/src/state/cache.rs b/crates/feos-core/src/state/cache.rs index b9a1326f7..cefc798d8 100644 --- a/crates/feos-core/src/state/cache.rs +++ b/crates/feos-core/src/state/cache.rs @@ -12,17 +12,17 @@ pub struct Cache where DefaultAllocator: Allocator, { - pub a: OnceLock>, - pub da_dt: OnceLock>, + pub a: OnceLock>, + pub da_dt: OnceLock>, pub da_dv: OnceLock>, pub da_dn: OnceLock>>, - pub d2a_dt2: OnceLock>>, - pub d2a_dv2: OnceLock>>, + pub d2a_dt2: OnceLock>>, + pub d2a_dv2: OnceLock>>, pub d2a_dtdv: OnceLock>>, pub d2a_dndt: OnceLock, Diff<_MolarEnergy, _Temperature>>>, - pub d2a_dndv: OnceLock, Diff<_MolarEnergy, _Volume>>>, - pub d3a_dt3: OnceLock, _Temperature>>>, - pub d3a_dv3: OnceLock, _Volume>>>, + pub d2a_dndv: OnceLock, Diff<_Energy, _Volume>>>, + pub d3a_dt3: OnceLock, _Temperature>>>, + pub d3a_dv3: OnceLock, _MolarVolume>>>, } impl Cache diff --git a/crates/feos-core/src/state/composition.rs b/crates/feos-core/src/state/composition.rs new file mode 100644 index 000000000..0ad47ac9e --- /dev/null +++ b/crates/feos-core/src/state/composition.rs @@ -0,0 +1,304 @@ +use super::State; +use crate::equation_of_state::Residual; +use crate::{FeosError, FeosResult}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, Dim, Dyn, OVector, U1, U2, dvector, vector}; +use num_dual::{DualNum, DualStruct}; +use quantity::{Density, Moles, Quantity, SIUnit}; + +pub trait Composition + Copy, N: Dim> +where + DefaultAllocator: Allocator, +{ + #[expect(clippy::type_complexity)] + fn into_molefracs>( + self, + eos: &E, + ) -> FeosResult<(OVector, Option>)>; + fn density(&self) -> Option> { + None + } +} + +pub trait FullComposition + Copy, N: Dim>: Composition +where + DefaultAllocator: Allocator, +{ + fn into_moles>(self, eos: &E) -> FeosResult<(OVector, Moles)>; +} + +// trivial implementations +impl + Copy, N: Dim> Composition for (OVector, Moles) +where + DefaultAllocator: Allocator, +{ + fn into_molefracs>( + self, + _: &E, + ) -> FeosResult<(OVector, Option>)> { + Ok((self.0, Some(self.1))) + } +} + +impl + Copy, N: Dim> FullComposition for (OVector, Moles) +where + DefaultAllocator: Allocator, +{ + fn into_moles>(self, _: &E) -> FeosResult<(OVector, Moles)> { + Ok((self.0, self.1)) + } +} + +impl + Copy, N: Dim> Composition for (OVector, Option>) +where + DefaultAllocator: Allocator, +{ + fn into_molefracs>( + self, + _: &E, + ) -> FeosResult<(OVector, Option>)> { + Ok((self.0, self.1)) + } +} + +// copy the composition from a given state +impl + Copy, N: Dim> Composition for &State +where + DefaultAllocator: Allocator, +{ + fn into_molefracs>( + self, + _: &E1, + ) -> FeosResult<(OVector, Option>)> { + Ok(((self.molefracs.clone()), self.total_moles)) + } +} + +// a pure component needs no specification +impl + Copy> Composition for () { + fn into_molefracs>( + self, + _: &E, + ) -> FeosResult<(OVector, Option>)> { + Ok(((vector![D::one()]), None)) + } +} +impl + Copy> Composition for () { + fn into_molefracs>( + self, + eos: &E, + ) -> FeosResult<(OVector, Option>)> { + if eos.components() == 1 { + Ok(((dvector![D::one()]), None)) + } else { + Err(FeosError::UndeterminedState( + "The composition needs to be specified for a system with more than one component." + .into(), + )) + } + } +} + +// a binary mixture can be specified by a scalar (x1) +impl + Copy> Composition for D { + fn into_molefracs>( + self, + _: &E, + ) -> FeosResult<(OVector, Option>)> { + Ok(((vector![self, -self + 1.0]), None)) + } +} + +// this cannot be implemented generically for D due to mising specialization +impl Composition for f64 { + fn into_molefracs( + self, + eos: &E, + ) -> FeosResult<(OVector, Option)> { + if eos.components() == 2 { + Ok(((dvector![self, 1.0 - self]), None)) + } else { + Err(FeosError::UndeterminedState(format!( + "A scalar ({}) can only be used to specify a binary mixture!", + self + ))) + } + } +} + +// a pure component can be specified by the total mole number +impl + Copy> Composition for Moles { + fn into_molefracs>( + self, + _: &E, + ) -> FeosResult<(OVector, Option>)> { + Ok(((vector![D::one()]), Some(self))) + } +} + +impl + Copy> FullComposition for Moles { + fn into_moles>(self, _: &E) -> FeosResult<(OVector, Moles)> { + Ok(((vector![D::one()]), self)) + } +} + +impl + Copy> Composition for Moles { + fn into_molefracs>( + self, + eos: &E, + ) -> FeosResult<(OVector, Option>)> { + if eos.components() == 1 { + Ok(((dvector![D::one()]), Some(self))) + } else { + Err(FeosError::UndeterminedState(format!( + "A single mole number ({}) can only be used to specify a pure component!", + self.re() + ))) + } + } +} + +impl + Copy> FullComposition for Moles { + fn into_moles>(self, eos: &E) -> FeosResult<(OVector, Moles)> { + if eos.components() == 1 { + Ok(((dvector![D::one()]), self)) + } else { + Err(FeosError::UndeterminedState(format!( + "A single mole number ({}) can only be used to specify a pure component!", + self.re() + ))) + } + } +} + +// the mixture can be specified by its molefractions +// +// for a dynamic number of components, it is also possible to specify only the +// N-1 first components +impl + Copy, N: Dim> Composition for OVector +where + DefaultAllocator: Allocator, +{ + fn into_molefracs>( + self, + eos: &E, + ) -> FeosResult<(OVector, Option>)> { + (&self).into_molefracs(eos) + } +} + +impl + Copy, N: Dim> Composition for &OVector +where + DefaultAllocator: Allocator, +{ + fn into_molefracs>( + self, + eos: &E, + ) -> FeosResult<(OVector, Option>)> { + let sum = self.sum(); + if eos.components() == self.len() { + Ok(((self.clone() / sum), None)) + } else if eos.components() == self.len() + 1 { + let mut x = OVector::zeros_generic(N::from_usize(eos.components()), U1); + for i in 0..self.len() { + x[i] = self[i]; + } + x[self.len()] = -sum + 1.0; + Ok(((x), None)) + } else { + Err(FeosError::UndeterminedState(format!( + "The length of the composition vector ({}) does not match the number of components ({})!", + self.len(), + eos.components() + ))) + } + } +} + +// the mixture can be specified by its moles +impl + Copy, N: Dim> Composition for Moles> +where + DefaultAllocator: Allocator, +{ + fn into_molefracs>( + self, + eos: &E, + ) -> FeosResult<(OVector, Option>)> { + (&self).into_molefracs(eos) + } +} + +impl + Copy, N: Dim> FullComposition for Moles> +where + DefaultAllocator: Allocator, +{ + fn into_moles>(self, eos: &E) -> FeosResult<(OVector, Moles)> { + (&self).into_moles(eos) + } +} + +impl + Copy, N: Dim> Composition for &Moles> +where + DefaultAllocator: Allocator, +{ + fn into_molefracs>( + self, + eos: &E, + ) -> FeosResult<(OVector, Option>)> { + if eos.components() == self.len() { + let total_moles = self.sum(); + Ok(((self.convert_to(total_moles)), Some(total_moles))) + } else { + Err(FeosError::UndeterminedState(format!( + "The length of the composition vector ({}) does not match the number of components ({})!", + self.len(), + eos.components() + ))) + } + } +} + +impl + Copy, N: Dim> FullComposition for &Moles> +where + DefaultAllocator: Allocator, +{ + fn into_moles>(self, eos: &E) -> FeosResult<(OVector, Moles)> { + if eos.components() == self.len() { + let total_moles = self.sum(); + Ok(((self.convert_to(total_moles)), total_moles)) + } else { + Err(FeosError::UndeterminedState(format!( + "The length of the composition vector ({}) does not match the number of components ({})!", + self.len(), + eos.components() + ))) + } + } +} + +// the mixture can be specified with the partial density +impl + Copy, N: Dim> Composition + for Quantity, SIUnit<0, -3, 0, 0, 0, 1, 0>> +where + DefaultAllocator: Allocator, +{ + fn into_molefracs>( + self, + eos: &E, + ) -> FeosResult<(OVector, Option>)> { + if eos.components() == self.len() { + let density = self.sum(); + Ok(((self.convert_to(density)), None)) + } else { + panic!( + "The length of the composition vector ({}) does not match the number of components ({})!", + self.len(), + eos.components() + ) + } + } + + fn density(&self) -> Option> { + Some(self.sum()) + } +} diff --git a/crates/feos-core/src/state/critical_point.rs b/crates/feos-core/src/state/critical_point.rs index e77b95299..4de9f9562 100644 --- a/crates/feos-core/src/state/critical_point.rs +++ b/crates/feos-core/src/state/critical_point.rs @@ -1,4 +1,4 @@ -use super::{DensityInitialization, State}; +use super::{Composition, DensityInitialization, State}; use crate::equation_of_state::Residual; use crate::errors::{FeosError, FeosResult}; use crate::{ReferenceSystem, SolverOptions, Subset, TemperatureOrPressure, Verbosity}; @@ -31,14 +31,14 @@ impl State { let pure_eos = eos.subset(&[i]); let cp = State::critical_point( &pure_eos, - None, + (), initial_temperature, initial_density, options, )?; let mut molefracs = DVector::zeros(eos.components()); molefracs[i] = 1.0; - State::new_intensive(eos, cp.temperature, cp.density, &molefracs) + State::new(eos, cp.temperature, cp.density, molefracs) }) .collect() } @@ -81,7 +81,7 @@ where ); let density = rho[0] + rho[1]; let molefracs = OVector::from_fn_generic(n, U1, |i, _| rho[i] / density); - Self::new_intensive(eos, t, Density::from_reduced(density), &molefracs) + Self::new(eos, t, Density::from_reduced(density), molefracs) } else if let Some(p) = temperature_or_pressure.pressure() { let x = critical_point_binary_p( &eos_re, @@ -103,22 +103,22 @@ where let density = trho[1] + trho[2]; let molefracs = OVector::from_fn_generic(n, U1, |i, _| trho[i + 1] / density); let t = Temperature::from_reduced(trho[0]); - Self::new_intensive(eos, t, Density::from_reduced(density), &molefracs) + Self::new(eos, t, Density::from_reduced(density), molefracs) } else { unreachable!() } } /// Calculate the critical point of a system for given moles. - pub fn critical_point( + pub fn critical_point>( eos: &E, - molefracs: Option<&OVector>, + composition: X, initial_temperature: Option, initial_density: Option, options: SolverOptions, ) -> FeosResult { let eos_re = eos.re(); - let molefracs = molefracs.map_or_else(E::pure_molefracs, |x| x.clone()); + let (molefracs, total_moles) = composition.into_molefracs(eos)?; let x = &molefracs.map(|x| x.re()); let rho_init = initial_density.map(|r| r.into_reduced()); let trial_temperatures = [300.0, 700.0, 500.0]; @@ -144,11 +144,11 @@ where t_rho[1], &molefracs, ); - Self::new_intensive( + Self::new( eos, Temperature::from_reduced(temperature), Density::from_reduced(density), - &molefracs, + (molefracs, total_moles), ) } } @@ -411,18 +411,18 @@ impl, N: Gradients> State where DefaultAllocator: Allocator + Allocator + Allocator, { - pub fn spinodal( + pub fn spinodal + Clone>( eos: &E, temperature: Temperature, - molefracs: Option<&OVector>, + composition: X, options: SolverOptions, ) -> FeosResult<[Self; 2]> { - let critical_point = Self::critical_point(eos, molefracs, None, None, options)?; - let molefracs = molefracs.map_or_else(E::pure_molefracs, |x| x.clone()); + let critical_point = Self::critical_point(eos, composition, None, None, options)?; + let molefracs = &critical_point.molefracs; let spinodal_vapor = Self::calculate_spinodal( eos, temperature, - &molefracs, + molefracs, DensityInitialization::Vapor, options, )?; @@ -430,7 +430,7 @@ where let spinodal_liquid = Self::calculate_spinodal( eos, temperature, - &molefracs, + molefracs, DensityInitialization::InitialDensity(rho), options, )?; @@ -500,12 +500,7 @@ where "Spinodal calculation converged in {} step(s)\n", i ); - return Self::new_intensive( - eos, - temperature, - Density::from_reduced(rho), - molefracs, - ); + return Self::new(eos, temperature, Density::from_reduced(rho), molefracs); } } Err(FeosError::SuperCritical) @@ -571,7 +566,7 @@ where // calculate pressure let a = partial2( - |v, &t, x| eos.lift().residual_molar_helmholtz_energy(t, v, x), + |v, &t, x| eos.lift().residual_helmholtz_energy(t, v, x), &temperature, &molefracs, ); diff --git a/crates/feos-core/src/state/mod.rs b/crates/feos-core/src/state/mod.rs index 5da7a43c5..ec99d4f19 100644 --- a/crates/feos-core/src/state/mod.rs +++ b/crates/feos-core/src/state/mod.rs @@ -11,19 +11,19 @@ use crate::equation_of_state::Residual; use crate::errors::{FeosError, FeosResult}; use crate::{ReferenceSystem, Total}; use nalgebra::allocator::Allocator; -use nalgebra::{DefaultAllocator, Dim, Dyn, OVector, U1}; +use nalgebra::{DefaultAllocator, Dim, Dyn, OVector}; use num_dual::*; use quantity::*; use std::fmt; use std::ops::Sub; -mod builder; mod cache; +mod composition; mod properties; mod residual_properties; mod statevec; -pub use builder::StateBuilder; pub(crate) use cache::Cache; +pub use composition::{Composition, FullComposition}; pub use statevec::StateVec; /// Possible contributions that can be computed. @@ -70,8 +70,6 @@ where { /// temperature in Kelvin pub temperature: D, - // /// volume in Angstrom^3 - // pub molar_volume: D, /// mole fractions pub molefracs: OVector, /// partial number densities in Angstrom^-3 @@ -82,15 +80,9 @@ impl + Copy> StateHD where DefaultAllocator: Allocator, { - /// Create a new `StateHD` for given temperature, molar volume and composition. - pub fn new(temperature: D, molar_volume: D, molefracs: &OVector) -> Self { - let partial_density = molefracs / molar_volume; - - Self { - temperature, - molefracs: molefracs.clone(), - partial_density, - } + /// Create a new `StateHD` for given temperature, volume and composition. + pub fn new(temperature: D, volume: D, moles: &OVector) -> Self { + Self::new_density(temperature, &(moles / volume)) } /// Create a new `StateHD` for given temperature and partial densities @@ -144,7 +136,7 @@ where /// + [State constructors](#state-constructors) /// + [Stability analysis](#stability-analysis) /// + [Flash calculations](#flash-calculations) -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State + Copy = f64> where DefaultAllocator: Allocator, @@ -153,14 +145,10 @@ where pub eos: E, /// Temperature $T$ pub temperature: Temperature, - /// Volume $V$ - pub volume: Volume, - /// Mole numbers $N_i$ - pub moles: Moles>, + /// Molar volume $v=\frac{V}{N}$ + pub molar_volume: MolarVolume, /// Total number of moles $N=\sum_iN_i$ - pub total_moles: Moles, - /// Partial densities $\rho_i=\frac{N_i}{V}$ - pub partial_density: Density>, + pub total_moles: Option>, /// Total density $\rho=\frac{N}{V}=\sum_i\rho_i$ pub density: Density, /// Mole fractions $x_i=\frac{N_i}{N}=\frac{\rho_i}{\rho}$ @@ -169,22 +157,38 @@ where cache: Cache, } -impl + Copy> Clone for State +impl + Copy> State where DefaultAllocator: Allocator, { - fn clone(&self) -> Self { - Self { - eos: self.eos.clone(), - temperature: self.temperature, - volume: self.volume, - moles: self.moles.clone(), - total_moles: self.total_moles, - partial_density: self.partial_density.clone(), - density: self.density, - molefracs: self.molefracs.clone(), - cache: self.cache.clone(), - } + /// Set the total amount of substance to the given value. + /// + /// This method does not introduce inconsistencies, because the + /// total moles are the only field that stores information about + /// the size of the state. + pub fn set_total_moles(mut self, total_moles: Moles) -> State { + self.total_moles = Some(total_moles); + self + } + + /// Partial densities $\rho_i=\frac{N_i}{V}$ + pub fn partial_density(&self) -> Density> { + Dimensionless::new(&self.molefracs) * self.density + } + + /// Mole numbers $N_i$ + pub fn moles(&self) -> Moles> { + Dimensionless::new(&self.molefracs) * self.total_moles() + } + + /// Total moles $N=\sum_iN_i$ + pub fn total_moles(&self) -> Moles { + self.total_moles.expect("Extensive properties can only be evaluated for states that are initialized with extensive properties!") + } + + /// Volume $V$ + pub fn volume(&self) -> Volume { + self.molar_volume * self.total_moles() } } @@ -221,83 +225,76 @@ where /// This function will perform a validation of the given properties, i.e. test for signs /// and if values are finite. It will **not** validate physics, i.e. if the resulting /// densities are below the maximum packing fraction. - pub fn new_nvt( + pub fn new_nvt>( eos: &E, temperature: Temperature, volume: Volume, - moles: &Moles>, + composition: X, ) -> FeosResult { - let total_moles = moles.sum(); - let molefracs = (moles / total_moles).into_value(); + let (molefracs, total_moles) = composition.into_moles(eos)?; let density = total_moles / volume; - validate(temperature, density, &molefracs)?; + Self::new(eos, temperature, density, (molefracs, total_moles)) + } - Ok(Self::new_unchecked( - eos, - temperature, - density, - total_moles, - &molefracs, - )) + /// Return a new `State` given a temperature and the partial density of all components. + /// + /// This function will perform a validation of the given properties, i.e. test for signs + /// and if values are finite. It will **not** validate physics, i.e. if the resulting + /// densities are below the maximum packing fraction. + pub fn new_density( + eos: &E, + temperature: Temperature, + partial_density: Density>, + ) -> FeosResult { + let density = partial_density.sum(); + Self::new(eos, temperature, density, partial_density) } - /// Return a new `State` for which the total amount of substance is unspecified. + /// Return a new `State` for a pure component given a temperature and a density. /// - /// Internally the total number of moles will be set to 1 mol. + /// This function will perform a validation of the given properties, i.e. test for signs + /// and if values are finite. It will **not** validate physics, i.e. if the resulting + /// densities are below the maximum packing fraction. + pub fn new_pure(eos: &E, temperature: Temperature, density: Density) -> FeosResult + where + (): Composition, + { + Self::new(eos, temperature, density, ()) + } + + /// Return a new `State` given a temperature, a density and the composition. /// /// This function will perform a validation of the given properties, i.e. test for signs /// and if values are finite. It will **not** validate physics, i.e. if the resulting /// densities are below the maximum packing fraction. - pub fn new_intensive( + pub fn new>( eos: &E, temperature: Temperature, density: Density, - molefracs: &OVector, + composition: X, ) -> FeosResult { - validate(temperature, density, molefracs)?; - let total_moles = Moles::new(D::one()); - Ok(Self::new_unchecked( - eos, - temperature, - density, - total_moles, - molefracs, - )) + let (molefracs, total_moles) = composition.into_molefracs(eos)?; + Self::_new(eos, temperature, density, molefracs, total_moles) } - fn new_unchecked( + fn _new( eos: &E, temperature: Temperature, density: Density, - total_moles: Moles, - molefracs: &OVector, - ) -> Self { - let volume = total_moles / density; - let moles = Dimensionless::new(molefracs.clone()) * total_moles; - let partial_density = moles.clone() / volume; - - State { + molefracs: OVector, + total_moles: Option>, + ) -> FeosResult { + let molar_volume = density.inv(); + validate(temperature, density, &molefracs)?; + Ok(State { eos: eos.clone(), temperature, - volume, - moles, - total_moles, - partial_density, + molar_volume, density, - molefracs: molefracs.clone(), + molefracs, + total_moles, cache: Cache::new(), - } - } - - /// Return a new `State` for a pure component given a temperature and a density. The moles - /// are set to the reference value for each component. - /// - /// This function will perform a validation of the given properties, i.e. test for signs - /// and if values are finite. It will **not** validate physics, i.e. if the resulting - /// densities are below the maximum packing fraction. - pub fn new_pure(eos: &E, temperature: Temperature, density: Density) -> FeosResult { - let molefracs = OVector::from_element_generic(N::from_usize(1), U1, D::one()); - Self::new_intensive(eos, temperature, density, &molefracs) + }) } /// Return a new `State` for the combination of inputs. @@ -313,200 +310,105 @@ where /// # Errors /// /// When the state cannot be created using the combination of inputs. - #[expect(clippy::too_many_arguments)] - pub fn new( + pub fn build>( eos: &E, - temperature: Option>, + temperature: Temperature, volume: Option>, density: Option>, - partial_density: Option<&Density>>, - total_moles: Option>, - moles: Option<&Moles>>, - molefracs: Option<&OVector>, + composition: X, pressure: Option>, density_initialization: Option, ) -> FeosResult { - Self::_new( + Self::_build( eos, temperature, volume, density, - partial_density, - total_moles, - moles, - molefracs, + composition, pressure, density_initialization, )? - .map_err(|_| FeosError::UndeterminedState(String::from("Missing input parameters."))) + .ok_or_else(|| FeosError::UndeterminedState(String::from("Missing input parameters."))) } - #[expect(clippy::too_many_arguments)] - #[expect(clippy::type_complexity)] - fn _new( + fn _build>( eos: &E, - temperature: Option>, + temperature: Temperature, volume: Option>, density: Option>, - partial_density: Option<&Density>>, - total_moles: Option>, - moles: Option<&Moles>>, - molefracs: Option<&OVector>, + composition: X, pressure: Option>, density_initialization: Option, - ) -> FeosResult>>>> { - // check for density - if density.and(partial_density).is_some() { + ) -> FeosResult> { + // check if density is given twice + if density.and(composition.density()).is_some() { return Err(FeosError::UndeterminedState(String::from( "Both density and partial density given.", ))); } - let rho = density.or_else(|| partial_density.map(|pd| pd.sum())); - - // check for total moles - if moles.and(total_moles).is_some() { - return Err(FeosError::UndeterminedState(String::from( - "Both moles and total moles given.", - ))); - } - let mut n = total_moles.or_else(|| moles.map(|m| m.sum())); - - // check if total moles can be inferred from volume - if rho.and(n).and(volume).is_some() { - return Err(FeosError::UndeterminedState(String::from( + let density = density.or_else(|| composition.density()); + + // unwrap composition + let (x, n) = composition.into_molefracs(eos)?; + + let t = temperature; + let di = density_initialization; + // find the appropriate state constructor + match (volume, density, n, pressure) { + (None, None, None, None) => Ok(None), + (None, None, Some(_), None) => Ok(None), + (Some(_), None, None, None) => Ok(None), + (None, None, _, Some(p)) => State::new_npt(eos, t, p, (x, n), di).map(Some), + (None, Some(d), _, None) => State::new(eos, t, d, (x, n)).map(Some), + (Some(v), None, None, Some(p)) => State::new_tpvx(eos, t, p, v, x, di).map(Some), + (Some(v), None, Some(n), None) => State::new_nvt(eos, t, v, (x, n)).map(Some), + (Some(v), Some(d), None, None) => State::new_nvt(eos, t, v, (x, d * v)).map(Some), + (Some(_), Some(_), Some(_), _) => Err(FeosError::UndeterminedState(String::from( "Density is overdetermined.", - ))); + ))), + (_, _, _, Some(_)) => Err(FeosError::UndeterminedState(String::from( + "Pressure is overdetermined.", + ))), } - n = n.or_else(|| rho.and_then(|d| volume.map(|v| v * d))); - - // check for composition - if partial_density.and(moles).is_some() { - return Err(FeosError::UndeterminedState(String::from( - "Composition is overdetermined.", - ))); - } - let x = partial_density - .map(|pd| pd / pd.sum()) - .or_else(|| moles.map(|ms| ms / ms.sum())) - .map(Quantity::into_value); - let x_u = match (x, molefracs, eos.components()) { - (Some(_), Some(_), _) => { - return Err(FeosError::UndeterminedState(String::from( - "Composition is overdetermined.", - ))); - } - (Some(x), None, _) => x, - (None, Some(x), _) => x.clone(), - (None, None, 1) => OVector::from_element_generic(N::from_usize(1), U1, D::from(1.0)), - _ => { - return Err(FeosError::UndeterminedState(String::from( - "Missing composition.", - ))); - } - }; - let x_u = &x_u / x_u.sum(); - - // If no extensive property is given, moles is set to the reference value. - if let (None, None) = (volume, n) { - n = Some(Moles::from_reduced(D::from(1.0))) - } - let n_i = n.map(|n| Dimensionless::new(&x_u) * n); - let v = volume.or_else(|| rho.and_then(|d| n.map(|n| n / d))); - - // check if new state can be created using default constructor - if let (Some(v), Some(t), Some(n_i)) = (v, temperature, &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 Ok(Ok(State::new_npt(eos, t, p, n_i, density_initialization)?)); - } - if let (Some(p), Some(t), Some(v)) = (pressure, temperature, v) { - return Ok(Ok(State::new_npvx( - eos, - t, - p, - v, - &x_u, - density_initialization, - )?)); - } - Ok(Err(n_i.to_owned())) } /// Return a new `State` using a density iteration. [DensityInitialization] is used to /// influence the calculation with respect to the possible solutions. - pub fn new_npt( + pub fn new_npt>( eos: &E, temperature: Temperature, pressure: Pressure, - moles: &Moles>, - density_initialization: Option, - ) -> FeosResult { - let total_moles = moles.sum(); - let molefracs = (moles / total_moles).into_value(); - let density = Self::new_xpt( - eos, - temperature, - pressure, - &molefracs, - density_initialization, - )? - .density; - Ok(Self::new_unchecked( - eos, - temperature, - density, - total_moles, - &molefracs, - )) - } - - /// Return a new `State` using a density iteration. [DensityInitialization] is used to - /// influence the calculation with respect to the possible solutions. - pub fn new_xpt( - eos: &E, - temperature: Temperature, - pressure: Pressure, - molefracs: &OVector, + composition: X, density_initialization: Option, ) -> FeosResult { + let (molefracs, total_moles) = composition.into_molefracs(eos)?; density_iteration( eos, temperature, pressure, - molefracs, + &molefracs, density_initialization, ) - .and_then(|density| Self::new_intensive(eos, temperature, density, molefracs)) + .and_then(|density| Self::_new(eos, temperature, density, molefracs, total_moles)) } /// Return a new `State` for given pressure $p$, volume $V$, temperature $T$ and composition $x_i$. - pub fn new_npvx( + pub fn new_tpvx( eos: &E, temperature: Temperature, pressure: Pressure, volume: Volume, - molefracs: &OVector, + molefracs: OVector, density_initialization: Option, ) -> FeosResult { - let density = Self::new_xpt( + let density = density_iteration( eos, temperature, pressure, - molefracs, + &molefracs, density_initialization, - )? - .density; - let total_moles = density * volume; - Ok(Self::new_unchecked( - eos, - temperature, - density, - total_moles, - molefracs, - )) + )?; + Self::new_nvt(eos, temperature, volume, (molefracs, density * volume)) } } @@ -529,15 +431,12 @@ where /// /// When the state cannot be created using the combination of inputs. #[expect(clippy::too_many_arguments)] - pub fn new_full( + pub fn build_full + Clone>( eos: &E, temperature: Option>, volume: Option>, density: Option>, - partial_density: Option<&Density>>, - total_moles: Option>, - moles: Option<&Moles>>, - molefracs: Option<&OVector>, + composition: X, pressure: Option>, molar_enthalpy: Option>, molar_entropy: Option>, @@ -545,38 +444,42 @@ where density_initialization: Option, initial_temperature: Option>, ) -> FeosResult { - let state = Self::_new( - eos, - temperature, - volume, - density, - partial_density, - total_moles, - moles, - molefracs, - pressure, - density_initialization, - )?; + let state = if let Some(temperature) = temperature { + Self::_build( + eos, + temperature, + volume, + density, + composition.clone(), + pressure, + density_initialization, + )? + } else { + None + }; let ti = initial_temperature; match state { - Ok(state) => Ok(state), - Err(n_i) => { + Some(state) => Ok(state), + None => { // 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(h)) = (pressure, molar_enthalpy) { + return State::new_nph(eos, p, h, composition, 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(p), Some(s)) = (pressure, molar_entropy) { + return State::new_nps(eos, p, s, composition, 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(h)) = (temperature, molar_enthalpy) { + return State::new_nth(eos, t, h, composition, 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(t), Some(s)) = (temperature, molar_entropy) { + return State::new_nts(eos, t, s, composition, 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); + if let (Some(u), Some(v)) = (molar_internal_energy, volume) { + let (molefracs, total_moles) = composition.into_molefracs(eos)?; + if let Some(n) = total_moles { + return State::new_nvu(eos, v, u, (molefracs, n), ti); + } } Err(FeosError::UndeterminedState(String::from( "Missing input parameters.", @@ -586,18 +489,18 @@ where } /// Return a new `State` for given pressure $p$ and molar enthalpy $h$. - pub fn new_nph( + pub fn new_nph + Clone>( eos: &E, pressure: Pressure, molar_enthalpy: MolarEnergy, - moles: &Moles>, + composition: X, density_initialization: Option, initial_temperature: Option>, ) -> FeosResult { let t0 = initial_temperature.unwrap_or(Temperature::from_reduced(D::from(298.15))); let mut density = density_initialization; let f = |x0| { - let s = State::new_npt(eos, x0, pressure, moles, density)?; + let s = State::new_npt(eos, x0, pressure, composition.clone(), density)?; let dfx = s.molar_isobaric_heat_capacity(Contributions::Total); let fx = s.molar_enthalpy(Contributions::Total) - molar_enthalpy; density = Some(DensityInitialization::InitialDensity(s.density.re())); @@ -607,28 +510,27 @@ where } /// Return a new `State` for given temperature $T$ and molar enthalpy $h$. - pub fn new_nth( + pub fn new_nth + Clone>( eos: &E, temperature: Temperature, molar_enthalpy: MolarEnergy, - moles: &Moles>, + composition: X, density_initialization: Option, ) -> FeosResult { - let x = moles.convert_to(moles.sum()); + let (x, _) = composition.clone().into_molefracs(eos)?; let rho0 = match density_initialization { Some(DensityInitialization::InitialDensity(r)) => { Density::from_reduced(D::from(r.into_reduced())) } - Some(DensityInitialization::Liquid) => eos.max_density(&Some(x))?, - Some(DensityInitialization::Vapor) => eos.max_density(&Some(x))? * 1.0e-5, - None => eos.max_density(&Some(x))? * 0.01, + Some(DensityInitialization::Liquid) => eos.max_density(&x)?, + Some(DensityInitialization::Vapor) => eos.max_density(&x)? * 1.0e-5, + None => eos.max_density(&x)? * 0.01, }; - let n_inv = moles.sum().inv(); - let f = |x0| { - let s = State::new_nvt(eos, temperature, moles.sum() / x0, moles)?; - let dfx = -s.volume / s.density - * n_inv - * (s.volume * s.dp_dv(Contributions::Total) + let f = |rho| { + let s = State::new(eos, temperature, rho, composition.clone())?; + let dfx = -s.molar_volume + * s.molar_volume + * (s.molar_volume * s.dp_dv(Contributions::Total) + temperature * s.dp_dt(Contributions::Total)); let fx = s.molar_enthalpy(Contributions::Total) - molar_enthalpy; Ok((fx, dfx, s)) @@ -637,26 +539,25 @@ where } /// Return a new `State` for given temperature $T$ and molar entropy $s$. - pub fn new_nts( + pub fn new_nts + Clone>( eos: &E, temperature: Temperature, molar_entropy: MolarEntropy, - moles: &Moles>, + composition: X, density_initialization: Option, ) -> FeosResult { - let x = moles.convert_to(moles.sum()); + let (x, _) = composition.clone().into_molefracs(eos)?; let rho0 = match density_initialization { Some(DensityInitialization::InitialDensity(r)) => { Density::from_reduced(D::from(r.into_reduced())) } - Some(DensityInitialization::Liquid) => eos.max_density(&Some(x))?, - Some(DensityInitialization::Vapor) => eos.max_density(&Some(x))? * 1.0e-5, - None => eos.max_density(&Some(x))? * 0.01, + Some(DensityInitialization::Liquid) => eos.max_density(&x)?, + Some(DensityInitialization::Vapor) => eos.max_density(&x)? * 1.0e-5, + None => eos.max_density(&x)? * 0.01, }; - let n_inv = moles.sum().inv(); - let f = |x0| { - let s = State::new_nvt(eos, temperature, moles.sum() / x0, moles)?; - let dfx = -n_inv * s.volume / s.density * s.dp_dt(Contributions::Total); + let f = |rho| { + let s = State::new(eos, temperature, rho, composition.clone())?; + let dfx = -s.molar_volume * s.molar_volume * s.dp_dt(Contributions::Total); let fx = s.molar_entropy(Contributions::Total) - molar_entropy; Ok((fx, dfx, s)) }; @@ -664,18 +565,18 @@ where } /// Return a new `State` for given pressure $p$ and molar entropy $s$. - pub fn new_nps( + pub fn new_nps + Clone>( eos: &E, pressure: Pressure, molar_entropy: MolarEntropy, - moles: &Moles>, + composition: X, density_initialization: Option, initial_temperature: Option>, ) -> FeosResult { let t0 = initial_temperature.unwrap_or(Temperature::from_reduced(D::from(298.15))); let mut density = density_initialization; let f = |x0| { - let s = State::new_npt(eos, x0, pressure, moles, density)?; + let s = State::new_npt(eos, x0, pressure, composition.clone(), density)?; let dfx = s.molar_isobaric_heat_capacity(Contributions::Total) / s.temperature; let fx = s.molar_entropy(Contributions::Total) - molar_entropy; density = Some(DensityInitialization::InitialDensity(s.density.re())); @@ -685,16 +586,16 @@ where } /// Return a new `State` for given volume $V$ and molar internal energy $u$. - pub fn new_nvu( + pub fn new_nvu + Clone>( eos: &E, volume: Volume, molar_internal_energy: MolarEnergy, - moles: &Moles>, + composition: X, initial_temperature: Option>, ) -> FeosResult { let t0 = initial_temperature.unwrap_or(Temperature::from_reduced(D::from(298.15))); let f = |x0| { - let s = State::new_nvt(eos, x0, volume, moles)?; + let s = State::new_nvt(eos, x0, volume, composition.clone())?; let fx = s.molar_internal_energy(Contributions::Total) - molar_internal_energy; let dfx = s.molar_isochoric_heat_capacity(Contributions::Total); Ok((fx, dfx, s)) diff --git a/crates/feos-core/src/state/properties.rs b/crates/feos-core/src/state/properties.rs index f45bfb751..c93c1e77d 100644 --- a/crates/feos-core/src/state/properties.rs +++ b/crates/feos-core/src/state/properties.rs @@ -20,11 +20,11 @@ where let ideal_gas = || { quantity::ad::gradient_copy( partial2( - |n, &t, &v| self.eos.ideal_gas_helmholtz_energy(t, v, &n), + |n: Dimensionless<_>, &t, &v| self.eos.ideal_gas_helmholtz_energy(t, v, &n), &self.temperature, - &self.volume, + &self.molar_volume, ), - &self.moles, + &Dimensionless::new(self.molefracs.clone()), ) .1 }; @@ -37,10 +37,15 @@ where let ideal_gas = || { quantity::ad::partial_hessian_copy( partial( - |(n, t), &v| self.eos.ideal_gas_helmholtz_energy(t, v, &n), - &self.volume, + |(n, t): (Dimensionless<_>, _), &v| { + self.eos.ideal_gas_helmholtz_energy(t, v, &n) + }, + &self.molar_volume, + ), + ( + &Dimensionless::new(self.molefracs.clone()), + self.temperature, ), - (&self.moles, self.temperature), ) .3 }; @@ -49,7 +54,7 @@ where /// Molar isochoric heat capacity: $c_v=\left(\frac{\partial u}{\partial T}\right)_{V,N_i}$ pub fn molar_isochoric_heat_capacity(&self, contributions: Contributions) -> MolarEntropy { - self.temperature * self.ds_dt(contributions) / self.total_moles + self.temperature * self.ds_dt(contributions) } /// Partial derivative of the molar isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V}{\partial T}\right)_{V,N_i}$ @@ -57,8 +62,7 @@ where &self, contributions: Contributions, ) -> as Div>>::Output { - (self.temperature * self.d2s_dt2(contributions) + self.ds_dt(contributions)) - / self.total_moles + self.temperature * self.d2s_dt2(contributions) + self.ds_dt(contributions) } /// Molar isobaric heat capacity: $c_p=\left(\frac{\partial h}{\partial T}\right)_{p,N_i}$ @@ -66,7 +70,7 @@ where match contributions { Contributions::Residual => self.residual_molar_isobaric_heat_capacity(), _ => { - self.temperature / self.total_moles + self.temperature * (self.ds_dt(contributions) - (self.dp_dt(contributions) * self.dp_dt(contributions)) / self.dp_dv(contributions)) @@ -76,13 +80,18 @@ where /// Entropy: $S=-\left(\frac{\partial A}{\partial T}\right)_{V,N_i}$ pub fn entropy(&self, contributions: Contributions) -> Entropy { - let residual = || self.residual_entropy(); + self.molar_entropy(contributions) * self.total_moles() + } + + /// Molar entropy: $s=\frac{S}{N}$ + pub fn molar_entropy(&self, contributions: Contributions) -> MolarEntropy { + let residual = || self.residual_molar_entropy(); let ideal_gas = || { -quantity::ad::first_derivative( partial2( |t, &v, n| self.eos.ideal_gas_helmholtz_energy(t, v, n), - &self.volume, - &self.moles, + &self.molar_volume, + &self.molefracs, ), self.temperature, ) @@ -91,29 +100,24 @@ where Self::contributions(ideal_gas, residual, contributions) } - /// Molar entropy: $s=\frac{S}{N}$ - pub fn molar_entropy(&self, contributions: Contributions) -> MolarEntropy { - self.entropy(contributions) / self.total_moles - } - /// Partial molar entropy: $s_i=\left(\frac{\partial S}{\partial N_i}\right)_{T,p,N_j}$ pub fn partial_molar_entropy(&self) -> MolarEntropy> { let c = Contributions::Total; - -(self.dmu_dt(c) + self.dp_dni(c) * (self.dp_dt(c) / self.dp_dv(c))) + -(self.dmu_dt(c) + self.n_dp_dni(c) * (self.dp_dt(c) / self.dp_dv(c))) } - /// Partial derivative of the entropy w.r.t. temperature: $\left(\frac{\partial S}{\partial T}\right)_{V,N_i}$ + /// Partial derivative of the molar entropy w.r.t. temperature: $\left(\frac{\partial s}{\partial T}\right)_{V,N_i}$ pub fn ds_dt( &self, contributions: Contributions, - ) -> as Div>>::Output { + ) -> as Div>>::Output { let residual = || self.ds_res_dt(); let ideal_gas = || { -quantity::ad::second_derivative( partial2( |t, &v, n| self.eos.ideal_gas_helmholtz_energy(t, v, n), - &self.volume, - &self.moles, + &self.molar_volume, + &self.molefracs, ), self.temperature, ) @@ -122,18 +126,18 @@ where Self::contributions(ideal_gas, residual, contributions) } - /// Second partial derivative of the entropy w.r.t. temperature: $\left(\frac{\partial^2 S}{\partial T^2}\right)_{V,N_i}$ + /// Second partial derivative of the molar entropy w.r.t. temperature: $\left(\frac{\partial^2 s}{\partial T^2}\right)_{V,N_i}$ pub fn d2s_dt2( &self, contributions: Contributions, - ) -> < as Div>>::Output as Div>>::Output { + ) -> < as Div>>::Output as Div>>::Output { let residual = || self.d2s_res_dt2(); let ideal_gas = || { -quantity::ad::third_derivative( partial2( |t, &v, n| self.eos.ideal_gas_helmholtz_energy(t, v, n), - &self.volume, - &self.moles, + &self.molar_volume, + &self.molefracs, ), self.temperature, ) @@ -144,14 +148,14 @@ where /// Enthalpy: $H=A+TS+pV$ pub fn enthalpy(&self, contributions: Contributions) -> Energy { - self.temperature * self.entropy(contributions) - + self.helmholtz_energy(contributions) - + self.pressure(contributions) * self.volume + self.molar_enthalpy(contributions) * self.total_moles() } /// Molar enthalpy: $h=\frac{H}{N}$ pub fn molar_enthalpy(&self, contributions: Contributions) -> MolarEnergy { - self.enthalpy(contributions) / self.total_moles + self.temperature * self.molar_entropy(contributions) + + self.molar_helmholtz_energy(contributions) + + self.pressure(contributions) * self.molar_volume } /// Partial molar enthalpy: $h_i=\left(\frac{\partial H}{\partial N_i}\right)_{T,p,N_j}$ @@ -163,13 +167,18 @@ where /// Helmholtz energy: $A$ pub fn helmholtz_energy(&self, contributions: Contributions) -> Energy { - let residual = || self.residual_helmholtz_energy(); + self.molar_helmholtz_energy(contributions) * self.total_moles() + } + + /// Molar Helmholtz energy: $a=\frac{A}{N}$ + pub fn molar_helmholtz_energy(&self, contributions: Contributions) -> MolarEnergy { + let residual = || self.residual_molar_helmholtz_energy(); let ideal_gas = || { quantity::ad::zeroth_derivative( partial2( |t, &v, n| self.eos.ideal_gas_helmholtz_energy(t, v, n), - &self.volume, - &self.moles, + &self.molar_volume, + &self.molefracs, ), self.temperature, ) @@ -177,43 +186,40 @@ where Self::contributions(ideal_gas, residual, contributions) } - /// Molar Helmholtz energy: $a=\frac{A}{N}$ - pub fn molar_helmholtz_energy(&self, contributions: Contributions) -> MolarEnergy { - self.helmholtz_energy(contributions) / self.total_moles - } - /// Internal energy: $U=A+TS$ pub fn internal_energy(&self, contributions: Contributions) -> Energy { - self.temperature * self.entropy(contributions) + self.helmholtz_energy(contributions) + self.molar_internal_energy(contributions) * self.total_moles() } /// Molar internal energy: $u=\frac{U}{N}$ pub fn molar_internal_energy(&self, contributions: Contributions) -> MolarEnergy { - self.internal_energy(contributions) / self.total_moles + self.temperature * self.molar_entropy(contributions) + + self.molar_helmholtz_energy(contributions) } /// Gibbs energy: $G=A+pV$ pub fn gibbs_energy(&self, contributions: Contributions) -> Energy { - self.pressure(contributions) * self.volume + self.helmholtz_energy(contributions) + self.molar_gibbs_energy(contributions) * self.total_moles() } /// Molar Gibbs energy: $g=\frac{G}{N}$ pub fn molar_gibbs_energy(&self, contributions: Contributions) -> MolarEnergy { - self.gibbs_energy(contributions) / self.total_moles + self.pressure(contributions) * self.molar_volume + + self.molar_helmholtz_energy(contributions) } /// Joule Thomson coefficient: $\mu_{JT}=\left(\frac{\partial T}{\partial p}\right)_{H,N_i}$ pub fn joule_thomson(&self) -> as Div>>::Output { let c = Contributions::Total; - -(self.volume + self.temperature * self.dp_dt(c) / self.dp_dv(c)) - / (self.total_moles * self.molar_isobaric_heat_capacity(c)) + -(self.molar_volume + self.temperature * self.dp_dt(c) / self.dp_dv(c)) + / self.molar_isobaric_heat_capacity(c) } /// Isentropic compressibility: $\kappa_s=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{S,N_i}$ pub fn isentropic_compressibility(&self) -> InvP { let c = Contributions::Total; -self.molar_isochoric_heat_capacity(c) - / (self.molar_isobaric_heat_capacity(c) * self.dp_dv(c) * self.volume) + / (self.molar_isobaric_heat_capacity(c) * self.dp_dv(c) * self.molar_volume) } /// Isenthalpic compressibility: $\kappa_H=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{H,N_i}$ @@ -224,7 +230,7 @@ where /// Thermal expansivity: $\alpha_p=-\frac{1}{V}\left(\frac{\partial V}{\partial T}\right)_{p,N_i}$ pub fn thermal_expansivity(&self) -> InvT { let c = Contributions::Total; - -self.dp_dt(c) / self.dp_dv(c) / self.volume + -self.dp_dt(c) / (self.dp_dv(c) * self.molar_volume) } /// Grueneisen parameter: $\phi=V\left(\frac{\partial p}{\partial U}\right)_{V,n_i}=\frac{v}{c_v}\left(\frac{\partial p}{\partial T}\right)_{v,n_i}=\frac{\rho}{T}\left(\frac{\partial T}{\partial \rho}\right)_{s, n_i}$ @@ -251,11 +257,7 @@ where )); } if let Contributions::Residual | Contributions::Total = contributions { - res.extend( - self.eos - .lift() - .molar_helmholtz_energy_contributions(t, v, &x), - ); + res.extend(self.eos.lift().helmholtz_energy_contributions(t, v, &x)); } res.into_iter() .map(|(s, v)| (s, MolarEnergy::from_reduced(v.eps))) diff --git a/crates/feos-core/src/state/residual_properties.rs b/crates/feos-core/src/state/residual_properties.rs index 53695fdbe..4a6b8f015 100644 --- a/crates/feos-core/src/state/residual_properties.rs +++ b/crates/feos-core/src/state/residual_properties.rs @@ -7,11 +7,8 @@ use num_dual::{Dual, DualNum, Gradients, partial, partial2}; use quantity::*; use std::ops::{Add, Div, Neg, Sub}; -type DpDn = Quantity>::Output>; -type DeDn = Quantity>::Output>; type InvT = Quantity::Output>; type InvP = Quantity::Output>; -type InvM = Quantity::Output>; type POverT = Quantity>::Output>; /// # State properties @@ -38,25 +35,33 @@ where /// Residual Helmholtz energy $A^\text{res}$ pub fn residual_helmholtz_energy(&self) -> Energy { - *self.cache.a.get_or_init(|| { - self.eos - .residual_helmholtz_energy_unit(self.temperature, self.volume, &self.moles) - }) + self.residual_molar_helmholtz_energy() * self.total_moles() } /// Residual molar Helmholtz energy $a^\text{res}$ pub fn residual_molar_helmholtz_energy(&self) -> MolarEnergy { - self.residual_helmholtz_energy() / self.total_moles + *self.cache.a.get_or_init(|| { + self.eos.residual_molar_helmholtz_energy( + self.temperature, + self.molar_volume, + &self.molefracs, + ) + }) } /// Residual entropy $S^\text{res}=\left(\frac{\partial A^\text{res}}{\partial T}\right)_{V,N_i}$ pub fn residual_entropy(&self) -> Entropy { + self.residual_molar_entropy() * self.total_moles() + } + + /// Residual molar entropy $s^\text{res}=\left(\frac{\partial a^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn residual_molar_entropy(&self) -> MolarEntropy { -*self.cache.da_dt.get_or_init(|| { let (a, da_dt) = quantity::ad::first_derivative( partial2( - |t, &v, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n), - &self.volume, - &self.moles, + |t, &v, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n), + &self.molar_volume, + &self.molefracs, ), self.temperature, ); @@ -65,11 +70,6 @@ where }) } - /// Residual entropy $s^\text{res}=\left(\frac{\partial a^\text{res}}{\partial T}\right)_{V,N_i}$ - pub fn residual_molar_entropy(&self) -> MolarEntropy { - self.residual_entropy() / self.total_moles - } - /// Pressure: $p=-\left(\frac{\partial A}{\partial V}\right)_{T,N_i}$ pub fn pressure(&self, contributions: Contributions) -> Pressure { let ideal_gas = || self.density * RGAS * self.temperature; @@ -77,11 +77,11 @@ where -*self.cache.da_dv.get_or_init(|| { let (a, da_dv) = quantity::ad::first_derivative( partial2( - |v, &t, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n), + |v, &t, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n), &self.temperature, - &self.moles, + &self.molefracs, ), - self.volume, + self.molar_volume, ); let _ = self.cache.a.set(a); da_dv @@ -97,11 +97,13 @@ where .get_or_init(|| { let (a, mu) = quantity::ad::gradient_copy( partial2( - |n, &t, &v| self.eos.lift().residual_helmholtz_energy_unit(t, v, &n), + |n: Dimensionless<_>, &t, &v| { + self.eos.lift().residual_molar_helmholtz_energy(t, v, &n) + }, &self.temperature, - &self.volume, + &self.molar_volume, ), - &self.moles, + &Dimensionless::new(self.molefracs.clone()), ); let _ = self.cache.a.set(a); mu @@ -116,18 +118,21 @@ where // 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) -> as Div>>::Output { - let ideal_gas = || -self.density * RGAS * self.temperature / self.volume; + /// Partial derivative of pressure w.r.t. molar volume: $\left(\frac{\partial p}{\partial v}\right)_{T,N_i}$ + pub fn dp_dv( + &self, + contributions: Contributions, + ) -> as Div>>::Output { + let ideal_gas = || -self.density * RGAS * self.temperature / self.molar_volume; let residual = || { -*self.cache.d2a_dv2.get_or_init(|| { let (a, da_dv, d2a_dv2) = quantity::ad::second_derivative( partial2( - |v, &t, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n), + |v, &t, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n), &self.temperature, - &self.moles, + &self.molefracs, ), - self.volume, + self.molar_volume, ); let _ = self.cache.a.set(a); let _ = self.cache.da_dv.set(da_dv); @@ -142,7 +147,7 @@ where &self, contributions: Contributions, ) -> as Div>>::Output { - -self.volume / self.density * self.dp_dv(contributions) + -self.molar_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}$ @@ -152,10 +157,10 @@ where -*self.cache.d2a_dtdv.get_or_init(|| { let (a, da_dt, da_dv, d2a_dtdv) = quantity::ad::second_partial_derivative( partial( - |(t, v), n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n), - &self.moles, + |(t, v), n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n), + &self.molefracs, ), - (self.temperature, self.volume), + (self.temperature, self.molar_volume), ); let _ = self.cache.a.set(a); let _ = self.cache.da_dt.set(da_dt); @@ -166,18 +171,23 @@ where 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) -> DpDn> { + /// Partial derivative of pressure w.r.t. moles: $N\left(\frac{\partial p}{\partial N_i}\right)_{T,V,N_j}$ + pub fn n_dp_dni(&self, contributions: Contributions) -> Pressure> { let residual = -self .cache .d2a_dndv .get_or_init(|| { let (a, da_dn, da_dv, dmu_dv) = quantity::ad::partial_hessian_copy( partial( - |(n, v), &t| self.eos.lift().residual_helmholtz_energy_unit(t, v, &n), + |(n, v): (Dimensionless<_>, _), &t| { + self.eos.lift().residual_molar_helmholtz_energy(t, v, &n) + }, &self.temperature, ), - (&self.moles, self.volume), + ( + &Dimensionless::new(self.molefracs.clone()), + self.molar_volume, + ), ); let _ = self.cache.a.set(a); let _ = self.cache.da_dn.set(da_dn); @@ -186,7 +196,7 @@ where }) .clone(); let (r, c) = residual.shape_generic(); - let ideal_gas = || self.temperature / self.volume * RGAS; + let ideal_gas = || self.temperature * self.density * RGAS; Quantity::from_fn_generic(r, c, |i, _| { Self::contributions(ideal_gas, || residual.get(i), contributions) }) @@ -196,18 +206,19 @@ where pub fn d2p_dv2( &self, contributions: Contributions, - ) -> < as Div>>::Output as Div>>::Output { - let ideal_gas = - || self.density * RGAS * self.temperature / (self.volume * self.volume) * 2.0; + ) -> < as Div>>::Output as Div>>::Output { + let ideal_gas = || { + self.density * RGAS * self.temperature / (self.molar_volume * self.molar_volume) * 2.0 + }; let residual = || { -*self.cache.d3a_dv3.get_or_init(|| { let (a, da_dv, d2a_dv2, d3a_dv3) = quantity::ad::third_derivative( partial2( - |v, &t, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n), + |v, &t, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n), &self.temperature, - &self.moles, + &self.molefracs, ), - self.volume, + self.molar_volume, ); let _ = self.cache.a.set(a); let _ = self.cache.da_dv.set(da_dv); @@ -223,59 +234,62 @@ where &self, contributions: Contributions, ) -> < as Div>>::Output as Div>>::Output { - self.volume / (self.density * self.density) - * (self.volume * self.d2p_dv2(contributions) + self.dp_dv(contributions) * 2.0) + self.molar_volume.powi::<3>() + * (self.molar_volume * self.d2p_dv2(contributions) + self.dp_dv(contributions) * 2.0) } /// Structure factor: $S(0)=k_BT\left(\frac{\partial\rho}{\partial p}\right)_{T,N_i}$ pub fn structure_factor(&self) -> D { - -(self.temperature * self.density * RGAS / (self.volume * self.dp_dv(Contributions::Total))) - .into_value() + -(self.temperature * self.density * RGAS + / (self.molar_volume * self.dp_dv(Contributions::Total))) + .into_value() } /// Partial molar volume: $v_i=\left(\frac{\partial V}{\partial N_i}\right)_{T,p,N_j}$ pub fn partial_molar_volume(&self) -> MolarVolume> { - -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total) + -self.n_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) -> DeDn> + /// Partial derivative of chemical potential w.r.t. moles: $N\left(\frac{\partial\mu_i}{\partial N_j}\right)_{T,V,N_k}$ + pub fn n_dmu_dni(&self, contributions: Contributions) -> MolarEnergy> where DefaultAllocator: Allocator, { let (a, da_dn, d2a_dn2) = quantity::ad::hessian_copy( partial2( - |n, &t, &v| self.eos.lift().residual_helmholtz_energy_unit(t, v, &n), + |n: Dimensionless<_>, &t, &v| { + self.eos.lift().residual_molar_helmholtz_energy(t, v, &n) + }, &self.temperature, - &self.volume, + &self.molar_volume, ), - &self.moles, + &Dimensionless::new(self.molefracs.clone()), ); let _ = self.cache.a.set(a); let _ = self.cache.da_dn.set(da_dn); let residual = || d2a_dn2; let ideal_gas = || { Dimensionless::new(OMatrix::from_diagonal(&self.molefracs.map(|x| x.recip()))) - * (self.temperature * RGAS / self.total_moles) + * (self.temperature * RGAS) }; Self::contributions(ideal_gas, residual, contributions) } /// Isothermal compressibility: $\kappa_T=-\frac{1}{V}\left(\frac{\partial V}{\partial p}\right)_{T,N_i}$ pub fn isothermal_compressibility(&self) -> InvP { - -(self.dp_dv(Contributions::Total) * self.volume).inv() + -(self.dp_dv(Contributions::Total) * self.molar_volume).inv() } // 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) -> as Div>>::Output { + /// Partial derivative of the residual molar entropy w.r.t. temperature: $\left(\frac{\partial s^\text{res}}{\partial T}\right)_{V,N_i}$ + pub fn ds_res_dt(&self) -> as Div>>::Output { -*self.cache.d2a_dt2.get_or_init(|| { let (a, da_dt, d2a_dt2) = quantity::ad::second_derivative( partial2( - |t, &v, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n), - &self.volume, - &self.moles, + |t, &v, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n), + &self.molar_volume, + &self.molefracs, ), self.temperature, ); @@ -285,16 +299,16 @@ where }) } - /// Second partial derivative of the residual entropy w.r.t. temperature: $\left(\frac{\partial^2S^\text{res}}{\partial T^2}\right)_{V,N_i}$ + /// Second partial derivative of the residual molar entropy w.r.t. temperature: $\left(\frac{\partial^2s^\text{res}}{\partial T^2}\right)_{V,N_i}$ pub fn d2s_res_dt2( &self, - ) -> < as Div>>::Output as Div>>::Output { + ) -> < as Div>>::Output as Div>>::Output { -*self.cache.d3a_dt3.get_or_init(|| { let (a, da_dt, d2a_dt2, d3a_dt3) = quantity::ad::third_derivative( partial2( - |t, &v, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n), - &self.volume, - &self.moles, + |t, &v, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n), + &self.molar_volume, + &self.molefracs, ), self.temperature, ); @@ -312,10 +326,15 @@ where .get_or_init(|| { let (a, da_dn, da_dt, d2a_dndt) = quantity::ad::partial_hessian_copy( partial( - |(n, t), &v| self.eos.lift().residual_helmholtz_energy_unit(t, v, &n), - &self.volume, + |(n, t): (Dimensionless<_>, _), &v| { + self.eos.lift().residual_molar_helmholtz_energy(t, v, &n) + }, + &self.molar_volume, + ), + ( + &Dimensionless::new(self.molefracs.clone()), + self.temperature, ), - (&self.moles, self.temperature), ); let _ = self.cache.a.set(a); let _ = self.cache.da_dn.set(da_dn); @@ -350,17 +369,19 @@ where .add_scalar(-self.pressure(Contributions::Total).inv()) } - /// 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) -> InvM> + /// Partial derivative of the logarithm of the fugacity coefficient w.r.t. moles: $N\left(\frac{\partial\ln\varphi_i}{\partial N_j}\right)_{T,p,N_k}$ + pub fn n_dln_phi_dnj(&self) -> OMatrix where DefaultAllocator: Allocator, { - let dmu_dni = self.dmu_dni(Contributions::Residual); - let dp_dni = self.dp_dni(Contributions::Total); + let dmu_dni = self.n_dmu_dni(Contributions::Residual); + let dp_dni = self.n_dp_dni(Contributions::Total); let dp_dv = self.dp_dv(Contributions::Total); let (r, c) = dmu_dni.shape_generic(); let dp_dn_2 = Quantity::from_fn_generic(r, c, |i, j| dp_dni.get(i) * dp_dni.get(j)); - ((dmu_dni + dp_dn_2 / dp_dv) / (self.temperature * RGAS)).add_scalar(self.total_moles.inv()) + ((dmu_dni + dp_dn_2 / dp_dv) / (self.temperature * RGAS)) + .into_value() + .add_scalar(D::from(1.0)) } } @@ -371,11 +392,11 @@ impl State { (0..self.eos.components()) .map(|i| { let eos = self.eos.subset(&[i]); - let state = State::new_xpt( + let state = State::new_npt( &eos, self.temperature, pressure, - &dvector![1.0], + dvector![1.0], Some(crate::DensityInitialization::Liquid), )?; Ok(state.ln_phi()[0]) @@ -424,12 +445,7 @@ impl State { }?; // Calculate the liquid state including the Henry components - let liquid = State::new_nvt( - eos, - temperature, - vle.liquid().volume, - &(molefracs * vle.liquid().total_moles), - )?; + let liquid = State::new(eos, temperature, vle.liquid().density, molefracs.clone())?; // Calculate the vapor state including the Henry components let mut molefracs_vapor = molefracs.clone(); @@ -437,12 +453,7 @@ impl State { .into_iter() .zip(&vle.vapor().molefracs) .for_each(|(i, &y)| molefracs_vapor[i] = y); - let vapor = State::new_nvt( - eos, - temperature, - vle.vapor().volume, - &(molefracs_vapor * vle.vapor().total_moles), - )?; + let vapor = State::new(eos, temperature, vle.vapor().density, molefracs.clone())?; // Determine the Henry's law coefficients and return only those of the Henry components let p = vle.vapor().pressure(Contributions::Total).into_reduced(); @@ -465,11 +476,10 @@ impl State { impl State { /// 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) -> DMatrix { - let dln_phi_dnj = (self.dln_phi_dnj() * Moles::from_reduced(1.0)).into_value(); - let moles = &self.molefracs * self.total_moles.into_reduced(); + let dln_phi_dnj = self.n_dln_phi_dnj(); let n = self.eos.components() - 1; DMatrix::from_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 } + dln_phi_dnj[(i, j)] - dln_phi_dnj[(i, n)] + if i == j { 1.0 } else { 0.0 } }) } } @@ -480,63 +490,62 @@ where { /// Residual molar isochoric heat capacity: $c_v^\text{res}=\left(\frac{\partial u^\text{res}}{\partial T}\right)_{V,N_i}$ pub fn residual_molar_isochoric_heat_capacity(&self) -> MolarEntropy { - self.ds_res_dt() * self.temperature / self.total_moles + self.ds_res_dt() * self.temperature } /// Partial derivative of the residual molar isochoric heat capacity w.r.t. temperature: $\left(\frac{\partial c_V^\text{res}}{\partial T}\right)_{V,N_i}$ pub fn dc_v_res_dt(&self) -> as Div>>::Output { - (self.temperature * self.d2s_res_dt2() + self.ds_res_dt()) / self.total_moles + self.temperature * self.d2s_res_dt2() + self.ds_res_dt() } /// Residual molar isobaric heat capacity: $c_p^\text{res}=\left(\frac{\partial h^\text{res}}{\partial T}\right)_{p,N_i}$ pub fn residual_molar_isobaric_heat_capacity(&self) -> MolarEntropy { let dp_dt = self.dp_dt(Contributions::Total); - self.temperature / self.total_moles - * (self.ds_res_dt() - dp_dt * dp_dt / self.dp_dv(Contributions::Total)) + self.temperature * (self.ds_res_dt() - dp_dt * dp_dt / self.dp_dv(Contributions::Total)) - RGAS } /// Residual enthalpy: $H^\text{res}(T,p,\mathbf{n})=A^\text{res}+TS^\text{res}+p^\text{res}V$ pub fn residual_enthalpy(&self) -> Energy { - self.temperature * self.residual_entropy() - + self.residual_helmholtz_energy() - + self.pressure(Contributions::Residual) * self.volume + self.residual_molar_enthalpy() * self.total_moles() } /// Residual molar enthalpy: $h^\text{res}(T,p,\mathbf{n})=a^\text{res}+Ts^\text{res}+p^\text{res}v$ pub fn residual_molar_enthalpy(&self) -> MolarEnergy { - self.residual_enthalpy() / self.total_moles + self.temperature * self.residual_molar_entropy() + + self.residual_molar_helmholtz_energy() + + self.pressure(Contributions::Residual) * self.molar_volume } /// Residual internal energy: $U^\text{res}(T,V,\mathbf{n})=A^\text{res}+TS^\text{res}$ pub fn residual_internal_energy(&self) -> Energy { - self.temperature * self.residual_entropy() + self.residual_helmholtz_energy() + self.residual_molar_internal_energy() * self.total_moles() } /// Residual molar internal energy: $u^\text{res}(T,V,\mathbf{n})=a^\text{res}+Ts^\text{res}$ pub fn residual_molar_internal_energy(&self) -> MolarEnergy { - self.residual_internal_energy() / self.total_moles + self.temperature * self.residual_molar_entropy() + self.residual_molar_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) -> Energy { - self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy() - - self.total_moles - * RGAS - * self.temperature - * Dimensionless::new(self.compressibility(Contributions::Total).ln()) + self.residual_molar_gibbs_energy() * self.total_moles() } /// Residual Gibbs energy: $g^\text{res}(T,p,\mathbf{n})=a^\text{res}+p^\text{res}v-RT \ln Z$ pub fn residual_molar_gibbs_energy(&self) -> MolarEnergy { - self.residual_gibbs_energy() / self.total_moles + self.pressure(Contributions::Residual) * self.molar_volume + + self.residual_molar_helmholtz_energy() + - self.temperature + * RGAS + * Dimensionless::new(self.compressibility(Contributions::Total).ln()) } /// Molar Helmholtz energy $a^\text{res}$ evaluated for each residual contribution of the equation of state. pub fn residual_molar_helmholtz_energy_contributions( &self, ) -> Vec<(&'static str, MolarEnergy)> { - let residual_contributions = self.eos.molar_helmholtz_energy_contributions( + let residual_contributions = self.eos.helmholtz_energy_contributions( self.temperature.into_reduced(), self.density.into_reduced().recip(), &self.molefracs, @@ -557,10 +566,7 @@ where let v = Dual::from_re(self.temperature.into_reduced()); let mut x = self.molefracs.map(Dual::from_re); x[component].eps = D::one(); - let contributions = self - .eos - .lift() - .molar_helmholtz_energy_contributions(t, v, &x); + let contributions = self.eos.lift().helmholtz_energy_contributions(t, v, &x); let mut res = Vec::with_capacity(contributions.len()); for (s, v) in contributions { res.push((s, MolarEnergy::from_reduced(v.eps))); @@ -573,10 +579,7 @@ where let t = Dual::from_re(self.temperature.into_reduced()); let v = Dual::from_re(self.density.into_reduced().recip()).derivative(); let x = self.molefracs.map(Dual::from_re); - let contributions = self - .eos - .lift() - .molar_helmholtz_energy_contributions(t, v, &x); + let contributions = self.eos.lift().helmholtz_energy_contributions(t, v, &x); let mut res = Vec::with_capacity(contributions.len() + 1); res.push(("Ideal gas", self.density * RGAS * self.temperature)); for (s, v) in contributions { @@ -599,12 +602,15 @@ where /// Mass of each component: $m_i=n_iMW_i$ pub fn mass(&self) -> Mass> { - self.eos.molar_weight().component_mul(&self.moles) + self.eos + .molar_weight() + .component_mul(&Dimensionless::new(self.molefracs.clone())) + * self.total_moles() } /// Total mass: $m=\sum_im_i=nMW$ pub fn total_mass(&self) -> Mass { - self.total_moles * self.total_molar_weight() + self.total_molar_weight() * self.total_moles() } /// Mass density: $\rho^{(m)}=\frac{m}{V}$ @@ -614,7 +620,10 @@ where /// Mass fractions: $w_i=\frac{m_i}{m}$ pub fn massfracs(&self) -> OVector { - (self.mass() / self.total_mass()).into_value() + self.eos + .molar_weight() + .convert_into(self.total_molar_weight()) + .component_mul(&self.molefracs) } } @@ -630,7 +639,7 @@ where pub fn viscosity(&self) -> Viscosity { let s = self.residual_molar_entropy().into_reduced(); self.eos - .viscosity_reference(self.temperature, self.volume, &self.moles) + .viscosity_reference(self.temperature, self.molar_volume, &self.molefracs) * Dimensionless::new(self.eos.viscosity_correlation(s, &self.molefracs).exp()) } @@ -646,14 +655,14 @@ where /// Return the viscosity reference as used in entropy scaling. pub fn viscosity_reference(&self) -> Viscosity { self.eos - .viscosity_reference(self.temperature, self.volume, &self.moles) + .viscosity_reference(self.temperature, self.molar_volume, &self.molefracs) } /// Return the diffusion via entropy scaling. pub fn diffusion(&self) -> Diffusivity { let s = self.residual_molar_entropy().into_reduced(); self.eos - .diffusion_reference(self.temperature, self.volume, &self.moles) + .diffusion_reference(self.temperature, self.molar_volume, &self.molefracs) * Dimensionless::new(self.eos.diffusion_correlation(s, &self.molefracs).exp()) } @@ -669,19 +678,21 @@ where /// Return the diffusion reference as used in entropy scaling. pub fn diffusion_reference(&self) -> Diffusivity { self.eos - .diffusion_reference(self.temperature, self.volume, &self.moles) + .diffusion_reference(self.temperature, self.molar_volume, &self.molefracs) } /// Return the thermal conductivity via entropy scaling. pub fn thermal_conductivity(&self) -> ThermalConductivity { let s = self.residual_molar_entropy().into_reduced(); - self.eos - .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) - * Dimensionless::new( - self.eos - .thermal_conductivity_correlation(s, &self.molefracs) - .exp(), - ) + self.eos.thermal_conductivity_reference( + self.temperature, + self.molar_volume, + &self.molefracs, + ) * Dimensionless::new( + self.eos + .thermal_conductivity_correlation(s, &self.molefracs) + .exp(), + ) } /// Return the logarithm of the reduced thermal conductivity. @@ -696,7 +707,10 @@ where /// Return the thermal conductivity reference as used in entropy scaling. pub fn thermal_conductivity_reference(&self) -> ThermalConductivity { - self.eos - .thermal_conductivity_reference(self.temperature, self.volume, &self.moles) + self.eos.thermal_conductivity_reference( + self.temperature, + self.molar_volume, + &self.molefracs, + ) } } diff --git a/crates/feos-core/src/state/statevec.rs b/crates/feos-core/src/state/statevec.rs index a62fe9413..63d51b3fd 100644 --- a/crates/feos-core/src/state/statevec.rs +++ b/crates/feos-core/src/state/statevec.rs @@ -63,7 +63,7 @@ impl StateVec<'_, E> { pub fn moles(&self) -> Moles> { Moles::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].moles.get(j) + self.0[i].moles().get(j) }) } diff --git a/crates/feos-derive/src/residual.rs b/crates/feos-derive/src/residual.rs index 12e4fe862..df3f5367f 100644 --- a/crates/feos-derive/src/residual.rs +++ b/crates/feos-derive/src/residual.rs @@ -1,4 +1,4 @@ -use super::{implement, OPT_IMPLS}; +use super::{OPT_IMPLS, implement}; use quote::quote; use syn::DeriveInput; @@ -201,19 +201,19 @@ fn impl_entropy_scaling( let name = &v.ident; if implement("entropy_scaling", v, &OPT_IMPLS)? { etar.push(quote! { - Self::#name(eos) => eos.viscosity_reference(temperature, volume, moles) + Self::#name(eos) => eos.viscosity_reference(temperature, molar_volume, molefracs) }); etac.push(quote! { Self::#name(eos) => eos.viscosity_correlation(s_res, x) }); dr.push(quote! { - Self::#name(eos) => eos.diffusion_reference(temperature, volume, moles) + Self::#name(eos) => eos.diffusion_reference(temperature, molar_volume, molefracs) }); 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) + Self::#name(eos) => eos.thermal_conductivity_reference(temperature, molar_volume, molefracs) }); thcc.push(quote! { Self::#name(eos) => eos.thermal_conductivity_correlation(s_res, x) @@ -245,8 +245,8 @@ fn impl_entropy_scaling( fn viscosity_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &DVector, ) -> Viscosity { match self { #(#etar,)* @@ -262,8 +262,8 @@ fn impl_entropy_scaling( fn diffusion_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &DVector, ) -> Diffusivity { match self { #(#dr,)* @@ -279,8 +279,8 @@ fn impl_entropy_scaling( fn thermal_conductivity_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &DVector, ) -> ThermalConductivity { match self { #(#thcr,)* diff --git a/crates/feos-dft/src/adsorption/mod.rs b/crates/feos-dft/src/adsorption/mod.rs index 9962f27a5..bf9ddabe0 100644 --- a/crates/feos-dft/src/adsorption/mod.rs +++ b/crates/feos-dft/src/adsorption/mod.rs @@ -1,11 +1,12 @@ //! Adsorption profiles and isotherms. use super::functional::HelmholtzEnergyFunctional; use super::solver::DFTSolver; +use feos_core::DensityInitialization::{Liquid, Vapor}; use feos_core::{ - Contributions, DensityInitialization, FeosError, FeosResult, ReferenceSystem, SolverOptions, - State, StateBuilder, + Composition, Contributions, DensityInitialization, FeosError, FeosResult, ReferenceSystem, + SolverOptions, State, }; -use nalgebra::{DMatrix, DVector}; +use nalgebra::{DMatrix, DVector, Dyn}; use ndarray::{Array1, Array2, Dimension, Ix1, Ix3, RemoveAxis}; use quantity::{Energy, MolarEnergy, Moles, Pressure, Temperature}; use std::iter; @@ -53,12 +54,12 @@ where } /// Calculate an adsorption isotherm (starting at low pressure) - pub fn adsorption_isotherm>( + pub fn adsorption_isotherm, X: Composition + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, pore: &S, - molefracs: &Option>, + composition: X, solver: Option<&DFTSolver>, ) -> FeosResult> { Self::isotherm( @@ -66,19 +67,19 @@ where temperature, pressure, pore, - molefracs, + composition, DensityInitialization::Vapor, solver, ) } /// Calculate an desorption isotherm (starting at high pressure) - pub fn desorption_isotherm>( + pub fn desorption_isotherm, X: Composition + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, pore: &S, - molefracs: &Option>, + composition: X, solver: Option<&DFTSolver>, ) -> FeosResult> { let pressure = pressure.into_iter().rev().collect(); @@ -87,7 +88,7 @@ where temperature, &pressure, pore, - molefracs, + composition, DensityInitialization::Liquid, solver, )?; @@ -98,12 +99,12 @@ where } /// Calculate an equilibrium isotherm - pub fn equilibrium_isotherm>( + pub fn equilibrium_isotherm, X: Composition + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, pore: &S, - molefracs: &Option>, + composition: X, solver: Option<&DFTSolver>, ) -> FeosResult> { let (p_min, p_max) = (pressure.get(0), pressure.get(pressure.len() - 1)); @@ -113,7 +114,7 @@ where p_min, p_max, pore, - molefracs, + composition.clone(), solver, SolverOptions::default(), ); @@ -132,7 +133,7 @@ where temperature, &p_ads, pore, - molefracs, + composition.clone(), solver, )? .profiles; @@ -141,7 +142,7 @@ where temperature, &p_des, pore, - molefracs, + composition, solver, )? .profiles; @@ -155,7 +156,7 @@ where temperature, pressure, pore, - molefracs, + composition.clone(), solver, )?; let desorption = Self::desorption_isotherm( @@ -163,7 +164,7 @@ where temperature, pressure, pore, - molefracs, + composition, solver, )?; let omega_a = adsorption.grand_potential(); @@ -181,25 +182,24 @@ where } } - fn isotherm>( + fn isotherm, X: Composition + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, pore: &S, - molefracs: &Option>, + composition: X, density_initialization: DensityInitialization, solver: Option<&DFTSolver>, ) -> FeosResult> { - let x = functional.validate_molefracs(molefracs)?; let mut profiles: Vec>> = Vec::with_capacity(pressure.len()); // On the first iteration, initialize the density profile according to the direction // and calculate the external potential once. - let mut bulk = State::new_xpt( + let mut bulk = State::new_npt( functional, temperature, pressure.get(0), - &x, + composition.clone(), Some(density_initialization), )?; if functional.components() > 1 && !bulk.is_stable(SolverOptions::default())? { @@ -215,11 +215,13 @@ where let mut old_density = Some(&profile.density); for i in 0..pressure.len() { - let mut bulk = StateBuilder::new(functional) - .temperature(temperature) - .pressure(pressure.get(i)) - .molefracs(&x) - .build()?; + let mut bulk = State::new_npt( + functional, + temperature, + pressure.get(i), + composition.clone(), + None, + )?; if functional.components() > 1 && !bulk.is_stable(SolverOptions::default())? { bulk = bulk .tp_flash(None, SolverOptions::default(), None)? @@ -243,37 +245,21 @@ where /// Calculate the phase transition from an empty to a filled pore. #[expect(clippy::too_many_arguments)] - pub fn phase_equilibrium>( + pub fn phase_equilibrium, X: Composition + Clone>( functional: &F, temperature: Temperature, p_min: Pressure, p_max: Pressure, pore: &S, - molefracs: &Option>, + composition: X, solver: Option<&DFTSolver>, options: SolverOptions, ) -> FeosResult> { - let x = functional.validate_molefracs(molefracs)?; - + let x = composition; // calculate density profiles for the minimum and maximum pressure - let vapor_bulk = StateBuilder::new(functional) - .temperature(temperature) - .pressure(p_min) - .molefracs(&x) - .vapor() - .build()?; - let bulk_init = StateBuilder::new(functional) - .temperature(temperature) - .pressure(p_max) - .molefracs(&x) - .liquid() - .build()?; - let liquid_bulk = StateBuilder::new(functional) - .temperature(temperature) - .pressure(p_max) - .molefracs(&x) - .vapor() - .build()?; + let vapor_bulk = State::new_npt(functional, temperature, p_min, x.clone(), Some(Vapor))?; + let bulk_init = State::new_npt(functional, temperature, p_max, x.clone(), Some(Liquid))?; + let liquid_bulk = State::new_npt(functional, temperature, p_max, x.clone(), Some(Vapor))?; let mut vapor = pore.initialize(&vapor_bulk, None, None)?.solve(solver)?; let mut liquid = pore.initialize(&bulk_init, None, None)?.solve(solver)?; @@ -287,18 +273,13 @@ where / (n_dp_drho_v / vapor_bulk.density - n_dp_drho_l / liquid_bulk.density); // update filled pore with limited step size - let mut bulk = StateBuilder::new(functional) - .temperature(temperature) - .pressure(p_max) - .molefracs(&x) - .vapor() - .build()?; + let mut bulk = State::new_npt(functional, temperature, p_max, x.clone(), Some(Vapor))?; let rho0 = liquid_bulk.density; let steps = (10.0 * (rho - rho0) / rho0).into_value().abs().ceil() as usize; let delta_rho = (rho - rho0) / steps as f64; for i in 1..=steps { let rho_i = rho0 + i as f64 * delta_rho; - bulk = State::new_intensive(functional, temperature, rho_i, &x)?; + bulk = State::new(functional, temperature, rho_i, x.clone())?; liquid = liquid.update_bulk(&bulk).solve(solver)?; } @@ -324,7 +305,7 @@ where rho += delta_rho; // update bulk phase - bulk = State::new_intensive(functional, temperature, rho, &x)?; + bulk = State::new(functional, temperature, rho, x.clone())?; } Err(FeosError::NotConverged( "Adsorption::phase_equilibrium".into(), diff --git a/crates/feos-dft/src/adsorption/pore.rs b/crates/feos-dft/src/adsorption/pore.rs index faf19e62d..ddf2f7479 100644 --- a/crates/feos-dft/src/adsorption/pore.rs +++ b/crates/feos-dft/src/adsorption/pore.rs @@ -6,9 +6,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, FeosResult, ReferenceSystem, ResidualDyn, State, StateBuilder, StateHD, -}; +use feos_core::{Contributions, FeosResult, ReferenceSystem, ResidualDyn, State, StateHD}; use nalgebra::{DVector, dvector}; use ndarray::prelude::*; use ndarray::{Axis as Axis_nd, RemoveAxis}; @@ -69,10 +67,7 @@ pub trait PoreSpecification { where D::Larger: Dimension, { - let bulk = StateBuilder::new(&&Helium) - .temperature(298.0 * KELVIN) - .density(Density::from_reduced(1.0)) - .build()?; + let bulk = State::new_pure(&&Helium, 298.0 * KELVIN, Density::from_reduced(1.0))?; let pore = self.initialize(&bulk, None, None)?; let pot = Dimensionless::from_reduced( pore.profile diff --git a/crates/feos-dft/src/interface/mod.rs b/crates/feos-dft/src/interface/mod.rs index 6697535d0..1e9f02b99 100644 --- a/crates/feos-dft/src/interface/mod.rs +++ b/crates/feos-dft/src/interface/mod.rs @@ -84,8 +84,8 @@ impl PlanarInterface { let reduced_temperature = (vle.vapor().temperature / critical_temperature).into_value(); profile.profile.density = Density::from_shape_fn(profile.profile.density.raw_dim(), |(i, z)| { - let rho_v = profile.vle.vapor().partial_density.get(indices[i]); - let rho_l = profile.vle.liquid().partial_density.get(indices[i]); + let rho_v = profile.vle.vapor().partial_density().get(indices[i]); + let rho_l = profile.vle.liquid().partial_density().get(indices[i]); 0.5 * (rho_l - rho_v) * (sign * (profile.profile.grid.grids()[0][z] - z0) / 3.0 * (2.4728 - 2.3625 * reduced_temperature)) @@ -343,8 +343,9 @@ fn interp_symmetric( radius: Length, ) -> FeosResult>> { let reduced_density = Array2::from_shape_fn(rho_pdgt.raw_dim(), |(i, j)| { - ((rho_pdgt.get((i, j)) - vle_pdgt.vapor().partial_density.get(i)) - / (vle_pdgt.liquid().partial_density.get(i) - vle_pdgt.vapor().partial_density.get(i))) + ((rho_pdgt.get((i, j)) - vle_pdgt.vapor().partial_density().get(i)) + / (vle_pdgt.liquid().partial_density().get(i) + - vle_pdgt.vapor().partial_density().get(i))) .into_value() - 0.5 }); @@ -371,8 +372,8 @@ fn interp_symmetric( reduced_density.raw_dim(), |(i, j)| { reduced_density[(i, j)] - * (vle.liquid().partial_density.get(i) - vle.vapor().partial_density.get(i)) - + vle.vapor().partial_density.get(i) + * (vle.liquid().partial_density().get(i) - vle.vapor().partial_density().get(i)) + + vle.vapor().partial_density().get(i) }, )) } diff --git a/crates/feos-dft/src/pdgt.rs b/crates/feos-dft/src/pdgt.rs index 47ad359eb..eb7cfe0f2 100644 --- a/crates/feos-dft/src/pdgt.rs +++ b/crates/feos-dft/src/pdgt.rs @@ -185,7 +185,7 @@ pub trait PdgtFunctionalProperties: HelmholtzEnergyFunctional { let mu_res = vle.vapor().residual_chemical_potential(); for i in 0..self.components() { let rhoi = density.index_axis(Axis(0), i).to_owned(); - let rhoi_b = vle.vapor().partial_density.get(i); + let rhoi_b = vle.vapor().partial_density().get(i); let mui_res = mu_res.get(i); let kt = RGAS * vle.vapor().temperature; delta_omega += @@ -198,8 +198,8 @@ pub trait PdgtFunctionalProperties: HelmholtzEnergyFunctional { let drho = gradient( &density, -dx, - &vle.liquid().partial_density, - &vle.vapor().partial_density, + &vle.liquid().partial_density(), + &vle.vapor().partial_density(), ); // calculate integrand diff --git a/crates/feos-dft/src/profile/mod.rs b/crates/feos-dft/src/profile/mod.rs index edac0e7b2..941a3818b 100644 --- a/crates/feos-dft/src/profile/mod.rs +++ b/crates/feos-dft/src/profile/mod.rs @@ -232,7 +232,7 @@ where let mut bonds = bulk.eos.bond_integrals(t, &exp_dfdrho, convolver.as_ref()); bonds *= &exp_dfdrho; let mut density = Array::zeros(external_potential.raw_dim()); - let bulk_density = bulk.partial_density.to_reduced(); + let bulk_density = bulk.partial_density().into_reduced(); for (s, &c) in bulk.eos.component_index().iter().enumerate() { density.index_axis_mut(Axis_nd(0), s).assign( &(bonds.index_axis(Axis_nd(0), s).map(|is| is.min(1.0)) * bulk_density[c]), @@ -373,7 +373,7 @@ where pub fn residual(&self, log: bool) -> FeosResult<(Array, Array1, f64)> { // Read from profile let density = self.density.to_reduced(); - let partial_density = self.bulk.partial_density.to_reduced(); + let partial_density = self.bulk.partial_density().into_reduced(); let bulk_density = self .bulk .eos @@ -484,7 +484,7 @@ where // Read from profile let component_index = self.bulk.eos.component_index().into_owned(); let mut density = self.density.to_reduced(); - let partial_density = self.bulk.partial_density.to_reduced(); + let partial_density = self.bulk.partial_density().into_reduced(); let mut bulk_density = component_index .iter() .map(|&i| partial_density[i]) @@ -495,13 +495,12 @@ where // Update profile self.density = Density::from_reduced(density); - let volume = Volume::from_reduced(1.0); - let mut moles = self.bulk.moles.clone(); + let mut partial_density = self.bulk.partial_density(); bulk_density .into_iter() .enumerate() - .for_each(|(i, r)| moles.set(component_index[i], Density::from_reduced(r) * volume)); - self.bulk = State::new_nvt(&self.bulk.eos, self.bulk.temperature, volume, &moles)?; + .for_each(|(i, r)| partial_density.set(component_index[i], Density::from_reduced(r))); + self.bulk = State::new_density(&self.bulk.eos, self.bulk.temperature, partial_density)?; Ok(()) } diff --git a/crates/feos-dft/src/profile/properties.rs b/crates/feos-dft/src/profile/properties.rs index 225967409..6095fb172 100644 --- a/crates/feos-dft/src/profile/properties.rs +++ b/crates/feos-dft/src/profile/properties.rs @@ -298,7 +298,7 @@ where { fn density_derivative(&self, lhs: &Array) -> FeosResult> { let rho = self.density.to_reduced(); - let partial_density = self.bulk.partial_density.to_reduced(); + let partial_density = self.bulk.partial_density().into_reduced(); let rho_bulk = self .bulk .eos @@ -402,7 +402,7 @@ where dfdrho += &(&self.external_potential * t).mapv(|v| Dual64::from(v) / t_dual); // calculate bulk functional derivative - let partial_density = self.bulk.partial_density.to_reduced(); + let partial_density = self.bulk.partial_density().into_reduced(); let rho_bulk: Array1<_> = self .bulk .eos diff --git a/crates/feos-dft/src/solvation/pair_correlation.rs b/crates/feos-dft/src/solvation/pair_correlation.rs index d901887cb..25849af4f 100644 --- a/crates/feos-dft/src/solvation/pair_correlation.rs +++ b/crates/feos-dft/src/solvation/pair_correlation.rs @@ -59,12 +59,10 @@ impl PairCorrelation { self.profile.solve(solver, debug)?; // calculate pair correlation function + let partial_density = self.profile.bulk.partial_density(); self.pair_correlation_function = Some(Array::from_shape_fn( self.profile.density.raw_dim(), - |(i, j)| { - (self.profile.density.get((i, j)) / self.profile.bulk.partial_density.get(i)) - .into_value() - }, + |(i, j)| (self.profile.density.get((i, j)) / partial_density.get(i)).into_value(), )); // calculate self solvation free energy diff --git a/crates/feos/benches/contributions.rs b/crates/feos/benches/contributions.rs index 3b2e8b367..3e74bf68c 100644 --- a/crates/feos/benches/contributions.rs +++ b/crates/feos/benches/contributions.rs @@ -74,7 +74,7 @@ fn pcsaft(c: &mut Criterion) { State::new_npt(&&eos, t, p, &moles, Some(DensityInitialization::Liquid)).unwrap(); let temperature = Dual64::from(state.temperature.into_reduced()).derivative(); let molar_volume = Dual::from(1.0 / state.density.into_reduced()); - let moles = state.moles.to_reduced().map(Dual::from); + let moles = state.moles().to_reduced().map(Dual::from); // let state_hd = state.derive1(Derivative::DT); let name1 = comp1.identifier.name.as_deref().unwrap(); let name2 = comp2.identifier.name.as_deref().unwrap(); diff --git a/crates/feos/benches/dft_pore.rs b/crates/feos/benches/dft_pore.rs index 667c5ff12..98dcba4e2 100644 --- a/crates/feos/benches/dft_pore.rs +++ b/crates/feos/benches/dft_pore.rs @@ -2,7 +2,7 @@ //! in pores at different conditions. use criterion::{Criterion, criterion_group, criterion_main}; use feos::core::parameter::IdentifierOption; -use feos::core::{PhaseEquilibrium, State, StateBuilder}; +use feos::core::{PhaseEquilibrium, State}; use feos::dft::adsorption::{ExternalPotential, Pore1D, PoreSpecification}; use feos::dft::{DFTSolver, Geometry}; use feos::gc_pcsaft::{GcPcSaftFunctional, GcPcSaftParameters}; @@ -80,11 +80,8 @@ fn pcsaft(c: &mut Criterion) { group.bench_function("butane_pentane_liquid", |b| { b.iter(|| pore.initialize(bulk, None, None).unwrap().solve(None)) }); - let bulk = StateBuilder::new(&func) - .temperature(300.0 * KELVIN) - .partial_density(&(&vle.vapor().partial_density * 0.2)) - .build() - .unwrap(); + let bulk = + State::new_density(&func, 300.0 * KELVIN, vle.vapor().partial_density() * 0.2).unwrap(); group.bench_function("butane_pentane_vapor", |b| { b.iter(|| pore.initialize(&bulk, None, None).unwrap().solve(None)) }); diff --git a/crates/feos/benches/dual_numbers.rs b/crates/feos/benches/dual_numbers.rs index 454c6828f..c3c1bd07f 100644 --- a/crates/feos/benches/dual_numbers.rs +++ b/crates/feos/benches/dual_numbers.rs @@ -21,9 +21,9 @@ use quantity::*; fn state_pcsaft(n: usize, eos: &PcSaft) -> State<&PcSaft> { let moles = DVector::from_element(n, 1.0 / n as f64) * 10.0 * MOL; let molefracs = (&moles / moles.sum()).into_value(); - let cp = State::critical_point(&eos, Some(&molefracs), None, None, Default::default()).unwrap(); + let cp = State::critical_point(&eos, molefracs, None, None, Default::default()).unwrap(); let temperature = 0.8 * cp.temperature; - State::new_nvt(&eos, temperature, cp.volume, &moles).unwrap() + State::new_nvt(&eos, temperature, cp.volume(), moles).unwrap() } /// Residual Helmholtz energy given an equation of state and a StateHD. @@ -152,11 +152,11 @@ enum Derivative { /// Creates a [StateHD] cloning temperature, volume and moles. fn derive0(state: &State) -> StateHD { - let total_moles = state.total_moles.into_reduced(); + let total_moles = state.total_moles().into_reduced(); StateHD::new( state.temperature.into_reduced(), - state.volume.into_reduced() / total_moles, - &(state.moles.to_reduced() / total_moles), + state.volume().into_reduced() / total_moles, + &(state.moles().to_reduced() / total_moles), ) } diff --git a/crates/feos/benches/dual_numbers_saftvrmie.rs b/crates/feos/benches/dual_numbers_saftvrmie.rs index 0d6329974..e7409e307 100644 --- a/crates/feos/benches/dual_numbers_saftvrmie.rs +++ b/crates/feos/benches/dual_numbers_saftvrmie.rs @@ -18,9 +18,9 @@ use quantity::*; /// - molefracs (or moles) for equimolar mixture. fn state_saftvrmie(n: usize, eos: &SaftVRMie) -> State<&SaftVRMie> { let molefracs = DVector::from_element(n, 1.0 / n as f64); - let cp = State::critical_point(&eos, Some(&molefracs), None, None, Default::default()).unwrap(); + let cp = State::critical_point(&eos, &molefracs, None, None, Default::default()).unwrap(); let temperature = 0.8 * cp.temperature; - State::new_nvt(&eos, temperature, cp.volume, &(molefracs * 10. * MOL)).unwrap() + State::new_nvt(&eos, temperature, cp.volume(), &(molefracs * 10. * MOL)).unwrap() } /// Residual Helmholtz energy given an equation of state and a StateHD. @@ -100,11 +100,11 @@ enum Derivative { /// Creates a [StateHD] cloning temperature, volume and moles. fn derive0(state: &State) -> StateHD { - let total_moles = state.total_moles.into_reduced(); + let total_moles = state.total_moles().into_reduced(); StateHD::new( state.temperature.into_reduced(), - state.volume.into_reduced() / total_moles, - &(state.moles.to_reduced() / total_moles), + state.volume().into_reduced() / total_moles, + &(state.moles().to_reduced() / total_moles), ) } diff --git a/crates/feos/benches/state_creation.rs b/crates/feos/benches/state_creation.rs index 6110abd68..3aa08778e 100644 --- a/crates/feos/benches/state_creation.rs +++ b/crates/feos/benches/state_creation.rs @@ -22,7 +22,7 @@ fn npt( } /// Evaluate critical point constructor -fn critical_point((eos, n): (&E, Option<&DVector>)) { +fn critical_point((eos, n): (&E, &DVector)) { State::critical_point(eos, n, None, None, Default::default()).unwrap(); } @@ -69,7 +69,7 @@ fn bench_states(c: &mut Criterion, group_name: &str, eos: &E) { let ncomponents = eos.components(); let x = DVector::from_element(ncomponents, 1.0 / ncomponents as f64); let n = &x * 100.0 * MOL; - let crit = State::critical_point(eos, Some(&x), None, None, Default::default()).unwrap(); + let crit = State::critical_point(eos, &x, None, None, Default::default()).unwrap(); let vle = if ncomponents == 1 { PhaseEquilibrium::pure(eos, crit.temperature * 0.95, None, Default::default()).unwrap() } else { @@ -77,7 +77,7 @@ fn bench_states(c: &mut Criterion, group_name: &str, eos: &E) { eos, crit.temperature, crit.pressure(Contributions::Total) * 0.95, - &crit.moles, + &crit.moles(), None, Default::default(), None, @@ -108,9 +108,7 @@ fn bench_states(c: &mut Criterion, group_name: &str, eos: &E) { )) }) }); - group.bench_function("critical_point", |b| { - b.iter(|| critical_point((eos, Some(&x)))) - }); + group.bench_function("critical_point", |b| b.iter(|| critical_point((eos, &x)))); if ncomponents == 2 { group.bench_function("critical_point_binary_t", |b| { b.iter(|| critical_point_binary((eos, crit.temperature))) diff --git a/crates/feos/src/epcsaft/eos/mod.rs b/crates/feos/src/epcsaft/eos/mod.rs index 03a2713d0..2edba6f69 100644 --- a/crates/feos/src/epcsaft/eos/mod.rs +++ b/crates/feos/src/epcsaft/eos/mod.rs @@ -181,7 +181,7 @@ mod tests { let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&&e, t, v, &n).unwrap(); - let p_ig = s.total_moles * RGAS * t / v; + 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::Residual), @@ -197,7 +197,7 @@ mod tests { let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&&e, t, v, &n).unwrap(); - let p_ig = s.total_moles * RGAS * t / v; + 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::Residual), @@ -269,7 +269,7 @@ mod tests { fn critical_point() { let e = ElectrolytePcSaft::new(propane_parameters()).unwrap(); let t = 300.0 * KELVIN; - let cp = State::critical_point(&&e, None, Some(t), None, Default::default()); + let cp = State::critical_point(&&e, (), Some(t), None, Default::default()); if let Ok(v) = cp { assert_relative_eq!(v.temperature, 375.1244078318015 * KELVIN, epsilon = 1e-8) } diff --git a/crates/feos/src/gc_pcsaft/eos/ad.rs b/crates/feos/src/gc_pcsaft/eos/ad.rs index 252f9a061..72b251eaa 100644 --- a/crates/feos/src/gc_pcsaft/eos/ad.rs +++ b/crates/feos/src/gc_pcsaft/eos/ad.rs @@ -510,7 +510,7 @@ pub mod test { let eos_ad = GcPcSaftAD(params); let moles = vector![1.3] * KILO * MOL; - let state = State::new_nvt(&eos_ad, temperature, volume, &moles)?; + let state = State::new_nvt(&eos_ad, temperature, volume, moles)?; let a_ad = state.residual_molar_helmholtz_energy(); let mu_ad = state.residual_chemical_potential(); let p_ad = state.pressure(Total); diff --git a/crates/feos/src/ideal_gas/dippr.rs b/crates/feos/src/ideal_gas/dippr.rs index 7553e376c..89824b07a 100644 --- a/crates/feos/src/ideal_gas/dippr.rs +++ b/crates/feos/src/ideal_gas/dippr.rs @@ -156,7 +156,7 @@ impl IdealGas for Dippr { mod tests { use approx::assert_relative_eq; use feos_core::parameter::{Identifier, PureRecord}; - use feos_core::{Contributions, EquationOfState, StateBuilder}; + use feos_core::{Contributions, EquationOfState, State}; use num_dual::first_derivative; use quantity::*; @@ -173,11 +173,7 @@ mod tests { let eos = EquationOfState::ideal_gas(dippr.clone()); let temperature = 300.0 * KELVIN; let volume = METER.powi::<3>(); - let state = StateBuilder::new(&&eos) - .temperature(temperature) - .volume(volume) - .total_moles(MOL) - .build()?; + let state = State::new_nvt(&&eos, temperature, volume, MOL)?; let t = temperature.convert_to(KELVIN); let c_p_direct = record.model_record.c_p(t); @@ -217,11 +213,7 @@ mod tests { let eos = EquationOfState::ideal_gas(dippr.clone()); let temperature = 300.0 * KELVIN; let volume = METER.powi::<3>(); - let state = StateBuilder::new(&&eos) - .temperature(temperature) - .volume(volume) - .total_moles(MOL) - .build()?; + let state = State::new_nvt(&&eos, temperature, volume, MOL)?; let t = temperature.convert_to(KELVIN); let c_p_direct = record.model_record.c_p(t); @@ -263,11 +255,7 @@ mod tests { let eos = EquationOfState::ideal_gas(dippr.clone()); let temperature = 20.0 * KELVIN; let volume = METER.powi::<3>(); - let state = StateBuilder::new(&&eos) - .temperature(temperature) - .volume(volume) - .total_moles(MOL) - .build()?; + let state = State::new_nvt(&&eos, temperature, volume, MOL)?; let t = temperature.convert_to(KELVIN); let c_p_direct = record.model_record.c_p(t); diff --git a/crates/feos/src/ideal_gas/joback.rs b/crates/feos/src/ideal_gas/joback.rs index 742c6ef2e..8f2279d5b 100644 --- a/crates/feos/src/ideal_gas/joback.rs +++ b/crates/feos/src/ideal_gas/joback.rs @@ -172,10 +172,8 @@ const KB: f64 = 1.38064852e-23; #[cfg(test)] mod tests { use approx::assert_relative_eq; - use feos_core::{ - Contributions, EquationOfState, State, StateBuilder, - parameter::{ChemicalRecord, GroupCount, Identifier, PureRecord, SegmentRecord}, - }; + use feos_core::parameter::{ChemicalRecord, GroupCount, Identifier, PureRecord, SegmentRecord}; + use feos_core::{Contributions, EquationOfState, State}; use nalgebra::dvector; use quantity::*; use std::collections::HashMap; @@ -295,11 +293,7 @@ mod tests { let temperature = 300.0 * KELVIN; let volume = METER.powi::<3>(); let moles = &dvector![1.0, 3.0] * MOL; - let state = StateBuilder::new(&&eos) - .temperature(temperature) - .volume(volume) - .moles(&moles) - .build()?; + let state = State::new_nvt(&&eos, temperature, volume, moles)?; println!( "{} {}", Joback::molar_isobaric_heat_capacity(&joback, temperature, &state.molefracs)?, diff --git a/crates/feos/src/lib.rs b/crates/feos/src/lib.rs index eab5ae128..0fbdffa71 100644 --- a/crates/feos/src/lib.rs +++ b/crates/feos/src/lib.rs @@ -23,7 +23,7 @@ //! let saft = PcSaft::new(parameters); //! //! // Define thermodynamic conditions. -//! let critical_point = State::critical_point(&&saft, Some(&dvector![1.0]), None, None, Default::default())?; +//! let critical_point = State::critical_point(&&saft, dvector![1.0], None, None, Default::default())?; //! //! // Compute properties. //! let p = critical_point.pressure(Contributions::Total); diff --git a/crates/feos/src/multiparameter/mod.rs b/crates/feos/src/multiparameter/mod.rs index 68bbd98b8..b4a6c4f9f 100644 --- a/crates/feos/src/multiparameter/mod.rs +++ b/crates/feos/src/multiparameter/mod.rs @@ -265,8 +265,13 @@ mod test { let eos = &water(); let mw = eos.molar_weight.get(0); let moles = dvector![1.8] * MOL; - let a_feos = eos.ideal_gas_helmholtz_energy(t, moles.sum() * mw / rho, &moles); - let phi_feos = (a_feos / RGAS / moles.sum() / t).into_value(); + let total_moles = moles.sum(); + let a_feos = eos.ideal_gas_helmholtz_energy( + t, + moles.sum() * mw / rho / total_moles, + &moles.convert_into(total_moles), + ); + let phi_feos = (a_feos / RGAS / t).into_value(); println!("A: {a_feos}"); println!("phi(feos): {phi_feos}"); let delta = (rho / (eos.rhoc * MOL / METER.powi::<3>() * mw)).into_value(); @@ -288,15 +293,15 @@ mod test { ..Default::default() }; let cp: State<_, Dyn, f64> = - State::critical_point(&eos, None, Some(647. * KELVIN), None, options).unwrap(); + State::critical_point(&eos, (), Some(647. * KELVIN), None, options).unwrap(); println!("{cp}"); assert_relative_eq!(cp.temperature, eos.tc * KELVIN, max_relative = 1e-13); let cp: State<_, Dyn, f64> = - State::critical_point(&eos, None, None, None, Default::default()).unwrap(); + State::critical_point(&eos, (), None, None, Default::default()).unwrap(); println!("{cp}"); assert_relative_ne!(cp.temperature, eos.tc * KELVIN, max_relative = 1e-13); let cp: State<_, Dyn, f64> = - State::critical_point(&eos, None, Some(700.0 * KELVIN), None, Default::default()) + State::critical_point(&eos, (), Some(700.0 * KELVIN), None, Default::default()) .unwrap(); println!("{cp}"); assert_relative_eq!(cp.temperature, eos.tc * KELVIN, max_relative = 1e-13) diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index 45d3635da..752e7c6f6 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -229,12 +229,11 @@ impl EntropyScaling for PcSaft { fn viscosity_reference( &self, temperature: Temperature, - _: Volume, - moles: &Moles>, + _: MolarVolume, + molefracs: &DVector, ) -> Viscosity { let p = &self.params; let mw = &self.parameters.molar_weight; - let x = (moles / moles.sum()).into_value(); let ce: Vec<_> = (0..self.components()) .map(|i| { let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); @@ -247,14 +246,15 @@ impl EntropyScaling for PcSaft { for i in 0..self.components() { let denom: f64 = (0..self.components()) .map(|j| { - x[j] * (1.0 - + (ce[i] / ce[j]).into_value().sqrt() - * (mw.get(j) / mw.get(i)).powf(1.0 / 4.0)) - .powi(2) + molefracs[j] + * (1.0 + + (ce[i] / ce[j]).into_value().sqrt() + * (mw.get(j) / mw.get(i)).powf(1.0 / 4.0)) + .powi(2) / (8.0 * (1.0 + (mw.get(i) / mw.get(j)).into_value())).sqrt() }) .sum(); - ce_mix += ce[i] * x[i] / denom + ce_mix += ce[i] * molefracs[i] / denom } ce_mix } @@ -278,19 +278,19 @@ impl EntropyScaling for PcSaft { fn diffusion_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + _: &DVector, ) -> Diffusivity { if self.components() != 1 { panic!("Diffusion coefficients in PC-SAFT are only implemented for pure components!"); } let p = &self.params; let mw = &self.parameters.molar_weight; - let density = moles.sum() / volume; let res: Vec<_> = (0..self.components()) .map(|i| { let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); - 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi::<2>() / omega11(tr) / (density * NAV) + 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi::<2>() / omega11(tr) + * (molar_volume / NAV) * (temperature * RGAS / PI / mw.get(i) / p.m[i]).sqrt() }) .collect(); @@ -321,8 +321,8 @@ impl EntropyScaling for PcSaft { fn thermal_conductivity_reference( &self, temperature: Temperature, - volume: Volume, - moles: &Moles>, + molar_volume: MolarVolume, + molefracs: &DVector, ) -> ThermalConductivity { if self.components() != 1 { panic!("Thermal conductivity in PC-SAFT is only implemented for pure components!"); @@ -331,9 +331,9 @@ impl EntropyScaling for PcSaft { let mws = self.molar_weight(); let (_, s_res) = first_derivative( partial2( - |t, &v, n| -self.residual_helmholtz_energy_unit(t, v, n) / n.sum(), - &volume, - moles, + |t, &v, n| -self.residual_molar_helmholtz_energy(t, v, n), + &molar_volume, + molefracs, ), temperature, ); @@ -399,8 +399,8 @@ mod tests { let t = 200.0 * KELVIN; let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; - let s = State::new_nvt(&e, t, v, &n).unwrap(); - let p_ig = s.total_moles * RGAS * t / v; + 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::Residual), @@ -415,8 +415,8 @@ mod tests { let t = 200.0 * KELVIN; let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; - let s = State::new_nvt(&e, t, v, &n).unwrap(); - let p_ig = s.total_moles * RGAS * t / v; + 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::Residual), @@ -463,7 +463,7 @@ mod tests { let t = 300.0 * KELVIN; let p = BAR; let m = dvector![1.5] * MOL; - let s = State::new_npt(&e, t, p, &m, None); + let s = State::new_npt(&e, t, p, m, None); let p_calc = if let Ok(state) = s { state.pressure(Contributions::Total) } else { @@ -490,7 +490,7 @@ mod tests { fn critical_point() { let e = &propane_parameters(); let t = 300.0 * KELVIN; - let cp = State::critical_point(&e, None, Some(t), None, Default::default()); + let cp = State::critical_point(&e, (), Some(t), None, Default::default()); if let Ok(v) = cp { assert_relative_eq!(v.temperature, 375.1244078318015 * KELVIN, epsilon = 1e-8) } @@ -507,9 +507,9 @@ mod tests { let m1m = dvector![2.0, 0.0] * MOL; let m2m = dvector![0.0, 2.0] * MOL; let s1 = State::new_nvt(&e1, t, v, &m1).unwrap(); - let s2 = State::new_nvt(&e2, t, v, &m1).unwrap(); - let s1m = State::new_nvt(&e12, t, v, &m1m).unwrap(); - let s2m = State::new_nvt(&e12, t, v, &m2m).unwrap(); + let s2 = State::new_nvt(&e2, t, v, m1).unwrap(); + let s1m = State::new_nvt(&e12, t, v, m1m).unwrap(); + let s2m = State::new_nvt(&e12, t, v, m2m).unwrap(); assert_relative_eq!( s1.pressure(Contributions::Total), s1m.pressure(Contributions::Total), @@ -528,7 +528,7 @@ mod tests { let t = 300.0 * KELVIN; let p = BAR; let n = dvector![1.0] * MOL; - let s = State::new_npt(&e, t, p, &n, None)?; + let s = State::new_npt(&e, t, p, n, None)?; assert_relative_eq!( s.viscosity(), 0.00797 * MILLI * PASCAL * SECOND, @@ -536,7 +536,7 @@ mod tests { ); assert_relative_eq!( s.ln_viscosity_reduced(), - (s.viscosity() / e.viscosity_reference(s.temperature, s.volume, &s.moles)) + (s.viscosity() / e.viscosity_reference(s.temperature, s.molar_volume, &s.molefracs)) .into_value() .ln(), epsilon = 1e-15 @@ -553,15 +553,15 @@ mod tests { let t = 303.15 * KELVIN; let p = 500.0 * BAR; let n = dvector![0.25, 0.75] * MOL; - let viscosity_mix = State::new_npt(&e, t, p, &n, None)?.viscosity(); + let viscosity_mix = State::new_npt(&e, t, p, n, None)?.viscosity(); let viscosity_paper = 0.68298 * MILLI * PASCAL * SECOND; assert_relative_eq!(viscosity_paper, viscosity_mix, epsilon = 1e-8); // Make sure pure substance case is recovered let n_pseudo_mix = dvector![1.0, 0.0] * MOL; - let viscosity_pseudo_mix = State::new_npt(&e, t, p, &n_pseudo_mix, None)?.viscosity(); + let viscosity_pseudo_mix = State::new_npt(&e, t, p, n_pseudo_mix, None)?.viscosity(); let n_nonane = dvector![1.0] * MOL; - let viscosity_nonane = State::new_npt(&nonane, t, p, &n_nonane, None)?.viscosity(); + let viscosity_nonane = State::new_npt(&nonane, t, p, n_nonane, None)?.viscosity(); assert_relative_eq!(viscosity_pseudo_mix, viscosity_nonane, epsilon = 1e-15); Ok(()) } @@ -572,7 +572,7 @@ mod tests { let t = 300.0 * KELVIN; let p = BAR; let n = dvector![1.0] * MOL; - let s = State::new_npt(&e, t, p, &n, None)?; + let s = State::new_npt(&e, t, p, n, None)?; assert_relative_eq!( s.diffusion(), 0.01505 * (CENTI * METER).powi::<2>() / SECOND, @@ -580,7 +580,7 @@ mod tests { ); assert_relative_eq!( s.ln_diffusion_reduced(), - (s.diffusion() / e.diffusion_reference(s.temperature, s.volume, &s.moles)) + (s.diffusion() / e.diffusion_reference(s.temperature, s.molar_volume, &s.molefracs)) .into_value() .ln(), epsilon = 1e-15 @@ -787,14 +787,9 @@ mod tests_parameter_fit { let h = params[i] * 1e-7; params[i] += h; let pcsaft_h = PcSaftPure(params); - let rho_h = State::new_xpt( - &pcsaft_h, - temperature, - pressure, - &vector![1.0], - Some(Liquid), - )? - .density; + let rho_h = + State::new_npt(&pcsaft_h, temperature, pressure, vector![1.0], Some(Liquid))? + .density; let drho_h = (rho_h.convert_into(MOL / LITER) - rho) / h; let drho = grad[i]; println!( diff --git a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs index 3eecc9db4..71b15ae88 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs @@ -557,7 +557,7 @@ pub mod test { let h_feos = state.residual_molar_enthalpy(); let moles = vector![1.3, 2.5] * KILO * MOL; - let state = State::new_nvt(&pcsaft, temperature, volume, &moles)?; + let state = State::new_nvt(&pcsaft, temperature, volume, moles)?; let a_ad = state.residual_molar_helmholtz_energy(); let mu_ad = state.residual_chemical_potential(); let p_ad = state.pressure(Total); diff --git a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs index baa5bc10f..f9ea2983f 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs @@ -228,9 +228,9 @@ impl ParametersAD<1> for PcSaftPure { #[cfg(test)] pub mod test { - use crate::pcsaft::PcSaftRecord; use super::super::{PcSaft, PcSaftAssociationRecord, PcSaftParameters}; use super::*; + use crate::pcsaft::PcSaftRecord; use approx::assert_relative_eq; use feos_core::parameter::{AssociationRecord, PureRecord}; use feos_core::{Contributions::Total, FeosResult, State}; @@ -278,7 +278,7 @@ pub mod test { let h_feos = state.residual_molar_enthalpy(); let moles = vector![1.3] * KILO * MOL; - let state = State::new_nvt(&pcsaft, temperature, volume, &moles)?; + let state = State::new_nvt(&pcsaft, temperature, volume, moles)?; let a_ad = state.residual_molar_helmholtz_energy(); let mu_ad = state.residual_chemical_potential(); let p_ad = state.pressure(Total); diff --git a/crates/feos/src/pets/eos/mod.rs b/crates/feos/src/pets/eos/mod.rs index f1d1d76cb..e26b94960 100644 --- a/crates/feos/src/pets/eos/mod.rs +++ b/crates/feos/src/pets/eos/mod.rs @@ -152,7 +152,7 @@ mod tests { let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&e, t, v, &n).unwrap(); - let p_ig = s.total_moles * RGAS * t / v; + 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::Residual), diff --git a/crates/feos/src/uvtheory/eos/mod.rs b/crates/feos/src/uvtheory/eos/mod.rs index 9fa963ae2..8a660db81 100644 --- a/crates/feos/src/uvtheory/eos/mod.rs +++ b/crates/feos/src/uvtheory/eos/mod.rs @@ -246,8 +246,9 @@ mod test { // EoS let eos_wca = &UVTheory::new(parameters); let state_wca = State::new_nvt(&eos_wca, t_x, volume, &moles).unwrap(); - let a_wca = (state_wca.residual_helmholtz_energy() / (RGAS * t_x * state_wca.total_moles)) - .into_value(); + let a_wca = (state_wca.residual_helmholtz_energy() + / (RGAS * t_x * state_wca.total_moles())) + .into_value(); assert_relative_eq!(a_wca, -0.597791038364405, max_relative = 1e-5); Ok(()) diff --git a/crates/feos/tests/gc_pcsaft/binary.rs b/crates/feos/tests/gc_pcsaft/binary.rs index e9b9ed434..ce627ec8c 100644 --- a/crates/feos/tests/gc_pcsaft/binary.rs +++ b/crates/feos/tests/gc_pcsaft/binary.rs @@ -30,9 +30,9 @@ fn test_binary() -> FeosResult<()> { #[cfg(feature = "dft")] let func = &GcPcSaftFunctional::new(parameters_func); let molefracs = dvector![0.5, 0.5]; - let cp = State::critical_point(&eos, Some(&molefracs), None, None, Default::default())?; + let cp = State::critical_point(&eos, &molefracs, None, None, Default::default())?; #[cfg(feature = "dft")] - let cp_func = State::critical_point(&func, Some(&molefracs), None, None, Default::default())?; + let cp_func = State::critical_point(&func, molefracs, None, None, Default::default())?; println!("{}", cp.temperature); #[cfg(feature = "dft")] println!("{}", cp_func.temperature); diff --git a/crates/feos/tests/gc_pcsaft/dft.rs b/crates/feos/tests/gc_pcsaft/dft.rs index a9813c56e..ce5d103cb 100644 --- a/crates/feos/tests/gc_pcsaft/dft.rs +++ b/crates/feos/tests/gc_pcsaft/dft.rs @@ -3,7 +3,7 @@ use approx::assert_relative_eq; use feos::gc_pcsaft::{GcPcSaft, GcPcSaftFunctional, GcPcSaftParameters}; use feos_core::parameter::{ChemicalRecord, Identifier, IdentifierOption, SegmentRecord}; -use feos_core::{PhaseEquilibrium, State, StateBuilder, Verbosity}; +use feos_core::{PhaseEquilibrium, State, Verbosity}; use feos_dft::adsorption::{ExternalPotential, Pore1D, PoreSpecification}; use feos_dft::interface::PlanarInterface; use feos_dft::{DFTSolver, Geometry}; @@ -158,7 +158,7 @@ fn test_dft() -> Result<(), Box> { let t = 200.0 * KELVIN; let w = 150.0 * ANGSTROM; let points = 2048; - let tc = State::critical_point(&&func, None, None, None, Default::default())?.temperature; + let tc = State::critical_point(&&func, (), None, None, Default::default())?.temperature; let vle = PhaseEquilibrium::pure(&&func, t, None, Default::default())?; let profile = PlanarInterface::from_tanh(&vle, points, w, tc, false).solve(None)?; println!( @@ -216,10 +216,7 @@ fn test_dft_assoc() -> Result<(), Box> { let solver = DFTSolver::new(Some(Verbosity::Iter)) .picard_iteration(None, None, Some(1e-5), Some(0.05)) .anderson_mixing(None, None, None, None, None); - let bulk = StateBuilder::new(&func) - .temperature(t) - .pressure(5.0 * BAR) - .build()?; + let bulk = State::new_npt(&func, t, 5.0 * BAR, (), None)?; Pore1D::new( Geometry::Cartesian, 20.0 * ANGSTROM, @@ -252,7 +249,7 @@ fn test_dft_newton() -> Result<(), Box> { let t = 200.0 * KELVIN; let w = 150.0 * ANGSTROM; let points = 512; - let tc = State::critical_point(&&func, None, None, None, Default::default())?.temperature; + let tc = State::critical_point(&&func, (), None, None, Default::default())?.temperature; let vle = PhaseEquilibrium::pure(&&func, t, None, Default::default())?; let solver = DFTSolver::new(Some(Verbosity::Iter)) .picard_iteration(None, Some(10), None, None) diff --git a/crates/feos/tests/pcsaft/critical_point.rs b/crates/feos/tests/pcsaft/critical_point.rs index 9183e5e5d..f0f435b13 100644 --- a/crates/feos/tests/pcsaft/critical_point.rs +++ b/crates/feos/tests/pcsaft/critical_point.rs @@ -17,7 +17,7 @@ fn test_critical_point_pure() -> Result<(), Box> { )?; let saft = PcSaft::new(params); let t = 300.0 * KELVIN; - let cp = State::critical_point(&&saft, None, Some(t), None, Default::default())?; + let cp = State::critical_point(&&saft, (), Some(t), None, Default::default())?; assert_relative_eq!(cp.temperature, 375.12441 * KELVIN, max_relative = 1e-8); assert_relative_eq!( cp.density, @@ -38,7 +38,7 @@ fn test_critical_point_mix() -> Result<(), Box> { let saft = PcSaft::new(params); let t = 300.0 * KELVIN; let molefracs = dvector![0.5, 0.5]; - let cp = State::critical_point(&&saft, Some(&molefracs), Some(t), None, Default::default())?; + let cp = State::critical_point(&&saft, molefracs, Some(t), None, Default::default())?; assert_relative_eq!(cp.temperature, 407.93481 * KELVIN, max_relative = 1e-8); assert_relative_eq!( cp.density, @@ -64,10 +64,10 @@ fn test_critical_point_limits() -> Result<(), Box> { let cp_pure = State::critical_point_pure(&saft, None, None, options)?; println!("{} {}", cp_pure[0], cp_pure[1]); let molefracs = dvector![0.0, 1.0]; - let cp_2 = State::critical_point(&saft, Some(&molefracs), None, None, options)?; + let cp_2 = State::critical_point(&saft, &molefracs, None, None, options)?; println!("{}", cp_2); let molefracs = dvector![1.0, 0.0]; - let cp_1 = State::critical_point(&saft, Some(&molefracs), None, None, options)?; + let cp_1 = State::critical_point(&saft, &molefracs, None, None, options)?; println!("{}", cp_1); assert_eq!(cp_pure[0].temperature, cp_1.temperature); assert_eq!(cp_pure[0].density, cp_1.density); diff --git a/crates/feos/tests/pcsaft/dft.rs b/crates/feos/tests/pcsaft/dft.rs index e8d057e47..b04a40aa1 100644 --- a/crates/feos/tests/pcsaft/dft.rs +++ b/crates/feos/tests/pcsaft/dft.rs @@ -103,7 +103,7 @@ fn test_dft_propane() -> Result<(), Box> { let t = 200.0 * KELVIN; let w = 150.0 * ANGSTROM; let points = 2048; - let tc = State::critical_point(&&func_pure, None, None, None, Default::default())?.temperature; + let tc = State::critical_point(&&func_pure, (), None, None, Default::default())?.temperature; let vle_pure = PhaseEquilibrium::pure(&&func_pure, t, None, Default::default())?; let vle_full = PhaseEquilibrium::pure(&&func_full, t, None, Default::default())?; let vle_full_vec = PhaseEquilibrium::pure(&&func_full_vec, t, None, Default::default())?; @@ -213,7 +213,7 @@ fn test_dft_propane_newton() -> Result<(), Box> { let t = 200.0 * KELVIN; let w = 150.0 * ANGSTROM; let points = 512; - let tc = State::critical_point(&&func, None, None, None, Default::default())?.temperature; + let tc = State::critical_point(&&func, (), None, None, Default::default())?.temperature; let vle = PhaseEquilibrium::pure(&&func, t, None, Default::default())?; let solver = DFTSolver::new(Some(Verbosity::Iter)).newton(None, None, None, None); PlanarInterface::from_tanh(&vle, points, w, tc, false).solve(Some(&solver))?; @@ -234,7 +234,7 @@ fn test_dft_water() -> Result<(), Box> { let t = 400.0 * KELVIN; let w = 120.0 * ANGSTROM; let points = 2048; - let tc = State::critical_point(&&func_pure, None, None, None, Default::default())?.temperature; + let tc = State::critical_point(&&func_pure, (), None, None, Default::default())?.temperature; let vle_pure = PhaseEquilibrium::pure(&&func_pure, t, None, Default::default())?; let vle_full_vec = PhaseEquilibrium::pure(&&func_full_vec, t, None, Default::default())?; let profile_pure = PlanarInterface::from_tanh(&vle_pure, points, w, tc, false).solve(None)?; @@ -334,33 +334,39 @@ fn test_entropy_bulk_values() -> Result<(), Box> { println!("\nResidual:\n{s_res:?}"); println!( "liquid: {:?}, vapor: {:?}", - profile.vle.liquid().entropy(Contributions::Residual) / profile.vle.liquid().volume, - profile.vle.vapor().entropy(Contributions::Residual) / profile.vle.vapor().volume + profile.vle.liquid().molar_entropy(Contributions::Residual) + / profile.vle.liquid().molar_volume, + profile.vle.vapor().molar_entropy(Contributions::Residual) + / profile.vle.vapor().molar_volume ); println!("\nTotal:\n{s_tot:?}"); println!( "liquid: {:?}, vapor: {:?}", - profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, - profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume + profile.vle.liquid().molar_entropy(Contributions::Total) + / profile.vle.liquid().molar_volume, + profile.vle.vapor().molar_entropy(Contributions::Total) / profile.vle.vapor().molar_volume ); assert_relative_eq!( s_res.get(0), - profile.vle.liquid().entropy(Contributions::Residual) / profile.vle.liquid().volume, + profile.vle.liquid().molar_entropy(Contributions::Residual) + / profile.vle.liquid().molar_volume, max_relative = 1e-8, ); assert_relative_eq!( s_res.get(2047), - profile.vle.vapor().entropy(Contributions::Residual) / profile.vle.vapor().volume, + profile.vle.vapor().molar_entropy(Contributions::Residual) + / profile.vle.vapor().molar_volume, max_relative = 1e-8, ); assert_relative_eq!( s_tot.get(0), - profile.vle.liquid().entropy(Contributions::Total) / profile.vle.liquid().volume, + profile.vle.liquid().molar_entropy(Contributions::Total) + / profile.vle.liquid().molar_volume, max_relative = 1e-8, ); assert_relative_eq!( s_tot.get(2047), - profile.vle.vapor().entropy(Contributions::Total) / profile.vle.vapor().volume, + profile.vle.vapor().molar_entropy(Contributions::Total) / profile.vle.vapor().molar_volume, max_relative = 1e-8, ); Ok(()) diff --git a/crates/feos/tests/pcsaft/properties.rs b/crates/feos/tests/pcsaft/properties.rs index 3b05c86e1..489760e5e 100644 --- a/crates/feos/tests/pcsaft/properties.rs +++ b/crates/feos/tests/pcsaft/properties.rs @@ -1,7 +1,7 @@ use approx::assert_relative_eq; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::IdentifierOption; -use feos_core::{Residual, StateBuilder}; +use feos_core::{DensityInitialization::Vapor, Residual, State}; use nalgebra::dvector; use quantity::*; use std::error::Error; @@ -18,18 +18,8 @@ fn test_dln_phi_dp() -> Result<(), Box> { let t = 300.0 * KELVIN; let p = BAR; let h = 1e-1 * PASCAL; - let s = StateBuilder::new(&&saft) - .temperature(t) - .pressure(p) - .molefracs(&dvector![0.5, 0.5]) - .vapor() - .build()?; - let sh = StateBuilder::new(&&saft) - .temperature(t) - .pressure(p + h) - .molefracs(&dvector![0.5, 0.5]) - .vapor() - .build()?; + let s = State::new_npt(&&saft, t, p, dvector![0.5, 0.5], Some(Vapor))?; + let sh = State::new_npt(&&saft, t, p + h, dvector![0.5, 0.5], Some(Vapor))?; let ln_phi = s.ln_phi()[0]; let ln_phi_h = sh.ln_phi()[0]; @@ -48,7 +38,7 @@ fn test_virial_is_not_nan() -> Result<(), Box> { IdentifierOption::Name, )?; let saft = &PcSaft::new(params); - let virial_b = saft.second_virial_coefficient(300.0 * KELVIN, &None); + let virial_b = saft.second_virial_coefficient(300.0 * KELVIN, ())?; assert!(!virial_b.is_nan()); Ok(()) } diff --git a/crates/feos/tests/pcsaft/state_creation_mixture.rs b/crates/feos/tests/pcsaft/state_creation_mixture.rs index b7b93c73a..46e0442b8 100644 --- a/crates/feos/tests/pcsaft/state_creation_mixture.rs +++ b/crates/feos/tests/pcsaft/state_creation_mixture.rs @@ -2,7 +2,7 @@ use approx::assert_relative_eq; use feos::ideal_gas::{Joback, JobackParameters}; use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::IdentifierOption; -use feos_core::{Contributions, EquationOfState, FeosResult, StateBuilder}; +use feos_core::{Contributions, EquationOfState, FeosResult, State}; use nalgebra::dvector; use quantity::*; use std::error::Error; @@ -31,17 +31,9 @@ fn pressure_entropy_molefracs() -> Result<(), Box> { let pressure = BAR; let temperature = 300.0 * KELVIN; let x = dvector![0.3, 0.7]; - let state = StateBuilder::new(&&eos) - .temperature(temperature) - .pressure(pressure) - .molefracs(&x) - .build()?; + let state = State::new_npt(&&eos, temperature, pressure, &x, None)?; let molar_entropy = state.molar_entropy(Contributions::Total); - let state = StateBuilder::new(&&eos) - .pressure(pressure) - .molar_entropy(molar_entropy) - .molefracs(&x) - .build()?; + let state = State::new_nps(&&eos, pressure, molar_entropy, x, None, None)?; assert_relative_eq!( state.molar_entropy(Contributions::Total), molar_entropy, @@ -63,13 +55,8 @@ fn volume_temperature_molefracs() -> Result<(), Box> { let volume = 1.5e-3 * METER.powi::<3>(); let moles = MOL; let x = dvector![0.3, 0.7]; - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .volume(volume) - .total_moles(moles) - .molefracs(&x) - .build()?; - assert_relative_eq!(state.volume, volume, max_relative = 1e-10); + let state = State::new_nvt(&&saft, temperature, volume, (x, moles))?; + assert_relative_eq!(state.volume(), volume, max_relative = 1e-10); Ok(()) } @@ -80,15 +67,9 @@ fn temperature_partial_density() -> Result<(), Box> { let x = dvector![0.3, 0.7]; let partial_density = x.clone() * MOL / METER.powi::<3>(); let density = partial_density.sum(); - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .partial_density(&partial_density) - .build()?; + let state = State::new_density(&&saft, temperature, partial_density)?; assert_relative_eq!(x, state.molefracs, max_relative = 1e-10); assert_relative_eq!(density, state.density, max_relative = 1e-10); - // Zip::from(&state.partial_density.to_reduced(reference)) - // .and(&partial_density.into_value()?) - // .for_each(|&r1, &r2| assert_relative_eq!(r1, r2, max_relative = 1e-10)); Ok(()) } @@ -98,11 +79,7 @@ fn temperature_density_molefracs() -> Result<(), Box> { let temperature = 300.0 * KELVIN; let x = dvector![0.3, 0.7]; let density = MOL / METER.powi::<3>(); - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .density(density) - .molefracs(&x) - .build()?; + let state = State::new(&&saft, temperature, density, &x)?; state .molefracs .iter() @@ -118,11 +95,7 @@ fn temperature_pressure_molefracs() -> Result<(), Box> { let temperature = 300.0 * KELVIN; let pressure = BAR; let x = dvector![0.3, 0.7]; - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .pressure(pressure) - .molefracs(&x) - .build()?; + let state = State::new_npt(&&saft, temperature, pressure, &x, None)?; state .molefracs .iter() diff --git a/crates/feos/tests/pcsaft/state_creation_pure.rs b/crates/feos/tests/pcsaft/state_creation_pure.rs index d5bc9769e..cda35b75f 100644 --- a/crates/feos/tests/pcsaft/state_creation_pure.rs +++ b/crates/feos/tests/pcsaft/state_creation_pure.rs @@ -1,12 +1,10 @@ use approx::assert_relative_eq; use feos::ideal_gas::{Joback, JobackParameters}; use feos::pcsaft::{PcSaft, PcSaftParameters}; +use feos_core::DensityInitialization::{InitialDensity, Liquid, Vapor}; use feos_core::parameter::IdentifierOption; -use feos_core::{ - Contributions, EquationOfState, FeosResult, PhaseEquilibrium, State, StateBuilder, Total, -}; +use feos_core::{Contributions, EquationOfState, FeosResult, PhaseEquilibrium, State, Total}; use quantity::*; -use std::error::Error; fn propane_parameters() -> FeosResult<(PcSaftParameters, Vec)> { let saft = PcSaftParameters::from_json( @@ -25,77 +23,59 @@ fn propane_parameters() -> FeosResult<(PcSaftParameters, Vec)> { } #[test] -fn temperature_volume() -> Result<(), Box> { +fn temperature_volume() -> FeosResult<()> { let saft = PcSaft::new(propane_parameters()?.0); let temperature = 300.0 * KELVIN; let volume = 1.5e-3 * METER.powi::<3>(); let moles = MOL; - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .volume(volume) - .total_moles(moles) - .build()?; - assert_relative_eq!(state.volume, volume, max_relative = 1e-10); + let state = State::new_nvt(&&saft, temperature, volume, moles)?; + assert_relative_eq!(state.volume(), volume, max_relative = 1e-10); Ok(()) } #[test] -fn temperature_density() -> Result<(), Box> { +fn temperature_density() -> FeosResult<()> { let saft = PcSaft::new(propane_parameters()?.0); let temperature = 300.0 * KELVIN; let density = MOL / METER.powi::<3>(); - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .density(density) - .build()?; + let state = State::new_pure(&&saft, temperature, density)?; assert_relative_eq!(state.density, density, max_relative = 1e-10); Ok(()) } #[test] -fn temperature_total_moles_volume() -> Result<(), Box> { +fn temperature_total_moles_volume() -> FeosResult<()> { let saft = PcSaft::new(propane_parameters()?.0); let temperature = 300.0 * KELVIN; let total_moles = MOL; let volume = METER.powi::<3>(); - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .volume(volume) - .total_moles(total_moles) - .build()?; - assert_relative_eq!(state.volume, volume, max_relative = 1e-10); - assert_relative_eq!(state.total_moles, total_moles, max_relative = 1e-10); + let state = State::new_nvt(&&saft, temperature, volume, total_moles)?; + assert_relative_eq!(state.volume(), volume, max_relative = 1e-10); + assert_relative_eq!(state.total_moles(), total_moles, max_relative = 1e-10); Ok(()) } #[test] -fn temperature_total_moles_density() -> Result<(), Box> { +fn temperature_total_moles_density() -> FeosResult<()> { let saft = PcSaft::new(propane_parameters()?.0); let temperature = 300.0 * KELVIN; let total_moles = MOL; let density = MOL / METER.powi::<3>(); - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .density(density) - .total_moles(total_moles) - .build()?; + let state = State::new_pure(&&saft, temperature, density)?.set_total_moles(total_moles); assert_relative_eq!(state.density, density, max_relative = 1e-10); - assert_relative_eq!(state.total_moles, total_moles, max_relative = 1e-10); - assert_relative_eq!(state.volume, total_moles / density, max_relative = 1e-10); + assert_relative_eq!(state.total_moles(), total_moles, max_relative = 1e-10); + assert_relative_eq!(state.volume(), total_moles / density, max_relative = 1e-10); Ok(()) } // Pressure constructors #[test] -fn pressure_temperature() -> Result<(), Box> { +fn pressure_temperature() -> FeosResult<()> { let saft = PcSaft::new(propane_parameters()?.0); let pressure = BAR; let temperature = 300.0 * KELVIN; - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .pressure(pressure) - .build()?; + let state = State::new_npt(&&saft, temperature, pressure, (), None)?; assert_relative_eq!( state.pressure(Contributions::Total), pressure, @@ -105,15 +85,11 @@ fn pressure_temperature() -> Result<(), Box> { } #[test] -fn pressure_temperature_phase() -> Result<(), Box> { +fn pressure_temperature_phase() -> FeosResult<()> { let saft = PcSaft::new(propane_parameters()?.0); let pressure = BAR; let temperature = 300.0 * KELVIN; - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .pressure(pressure) - .liquid() - .build()?; + let state = State::new_npt(&&saft, temperature, pressure, (), Some(Liquid))?; assert_relative_eq!( state.pressure(Contributions::Total), pressure, @@ -123,15 +99,12 @@ fn pressure_temperature_phase() -> Result<(), Box> { } #[test] -fn pressure_temperature_initial_density() -> Result<(), Box> { +fn pressure_temperature_initial_density() -> FeosResult<()> { let saft = PcSaft::new(propane_parameters()?.0); let pressure = BAR; let temperature = 300.0 * KELVIN; - let state = StateBuilder::new(&&saft) - .temperature(temperature) - .pressure(pressure) - .initial_density(MOL / METER.powi::<3>()) - .build()?; + let init = Some(InitialDensity(MOL / METER.powi::<3>())); + let state = State::new_npt(&&saft, temperature, pressure, (), init)?; assert_relative_eq!( state.pressure(Contributions::Total), pressure, @@ -141,17 +114,13 @@ fn pressure_temperature_initial_density() -> Result<(), Box> { } #[test] -fn pressure_enthalpy_vapor() -> Result<(), Box> { +fn pressure_enthalpy_vapor() -> FeosResult<()> { let (saft_params, joback) = propane_parameters()?; let saft = PcSaft::new(saft_params); let eos = EquationOfState::new(joback, saft); let pressure = 0.3 * BAR; let molar_enthalpy = 2000.0 * JOULE / MOL; - let state = StateBuilder::new(&&eos) - .pressure(pressure) - .molar_enthalpy(molar_enthalpy) - .vapor() - .build()?; + let state = State::new_nph(&&eos, pressure, molar_enthalpy, (), Some(Vapor), None)?; assert_relative_eq!( state.molar_enthalpy(Contributions::Total), molar_enthalpy, @@ -163,11 +132,7 @@ fn pressure_enthalpy_vapor() -> Result<(), Box> { max_relative = 1e-10 ); - let state = StateBuilder::new(&&eos) - .volume(state.volume) - .temperature(state.temperature) - .moles(&state.moles) - .build()?; + let state = State::new(&&eos, state.temperature, state.density, state.molefracs)?; assert_relative_eq!( state.molar_enthalpy(Contributions::Total), molar_enthalpy, @@ -182,24 +147,22 @@ fn pressure_enthalpy_vapor() -> Result<(), Box> { } #[test] -fn density_internal_energy() -> Result<(), Box> { +fn density_internal_energy() -> FeosResult<()> { let (saft_params, joback) = propane_parameters()?; let saft = PcSaft::new(saft_params); let eos = EquationOfState::new(joback, saft); let pressure = 5.0 * BAR; let temperature = 315.0 * KELVIN; let total_moles = 2.5 * MOL; - let state = StateBuilder::new(&&eos) - .pressure(pressure) - .temperature(temperature) - .total_moles(total_moles) - .build()?; + let state = State::new_npt(&&eos, temperature, pressure, total_moles, None)?; let molar_internal_energy = state.molar_internal_energy(Contributions::Total); - let state_nvu = StateBuilder::new(&&eos) - .volume(state.volume) - .molar_internal_energy(molar_internal_energy) - .total_moles(total_moles) - .build()?; + let state_nvu = State::new_nvu( + &&eos, + state.volume(), + molar_internal_energy, + total_moles, + None, + )?; assert_relative_eq!( molar_internal_energy, state_nvu.molar_internal_energy(Contributions::Total), @@ -211,19 +174,21 @@ fn density_internal_energy() -> Result<(), Box> { } #[test] -fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { +fn pressure_enthalpy_total_moles_vapor() -> FeosResult<()> { let (saft_params, joback) = propane_parameters()?; let saft = PcSaft::new(saft_params); let eos = EquationOfState::new(joback, saft); let pressure = 0.3 * BAR; let molar_enthalpy = 2000.0 * JOULE / MOL; let total_moles = 2.5 * MOL; - let state = StateBuilder::new(&&eos) - .pressure(pressure) - .molar_enthalpy(molar_enthalpy) - .total_moles(total_moles) - .vapor() - .build()?; + let state = State::new_nph( + &&eos, + pressure, + molar_enthalpy, + total_moles, + Some(Vapor), + None, + )?; assert_relative_eq!( state.molar_enthalpy(Contributions::Total), molar_enthalpy, @@ -235,11 +200,12 @@ fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { max_relative = 1e-10 ); - let state = StateBuilder::new(&&eos) - .volume(state.volume) - .temperature(state.temperature) - .total_moles(state.total_moles) - .build()?; + let state = State::new_nvt( + &&eos, + state.temperature, + state.volume(), + state.total_moles(), + )?; assert_relative_eq!( state.molar_enthalpy(Contributions::Total), molar_enthalpy, @@ -254,17 +220,13 @@ fn pressure_enthalpy_total_moles_vapor() -> Result<(), Box> { } #[test] -fn pressure_entropy_vapor() -> Result<(), Box> { +fn pressure_entropy_vapor() -> FeosResult<()> { let (saft_params, joback) = propane_parameters()?; let saft = PcSaft::new(saft_params); let eos = EquationOfState::new(joback, saft); let pressure = 0.3 * BAR; let molar_entropy = -2.0 * JOULE / MOL / KELVIN; - let state = StateBuilder::new(&&eos) - .pressure(pressure) - .molar_entropy(molar_entropy) - .vapor() - .build()?; + let state = State::new_nps(&&eos, pressure, molar_entropy, (), Some(Vapor), None)?; assert_relative_eq!( state.molar_entropy(Contributions::Total), molar_entropy, @@ -276,11 +238,7 @@ fn pressure_entropy_vapor() -> Result<(), Box> { max_relative = 1e-10 ); - let state = StateBuilder::new(&&eos) - .volume(state.volume) - .temperature(state.temperature) - .moles(&state.moles) - .build()?; + let state = State::new(&&eos, state.temperature, state.density, state.molefracs)?; assert_relative_eq!( state.molar_entropy(Contributions::Total), molar_entropy, @@ -295,24 +253,20 @@ fn pressure_entropy_vapor() -> Result<(), Box> { } #[test] -fn temperature_entropy_vapor() -> Result<(), Box> { +fn temperature_entropy_vapor() -> FeosResult<()> { let (saft_params, joback) = propane_parameters()?; let saft = PcSaft::new(saft_params); let eos = EquationOfState::new(joback, saft); let pressure = 3.0 * BAR; let temperature = 315.15 * KELVIN; let total_moles = 3.0 * MOL; - let state = StateBuilder::new(&&eos) - .temperature(temperature) - .pressure(pressure) - .total_moles(total_moles) - .build()?; + let state = State::new_npt(&&eos, temperature, pressure, total_moles, None)?; let s = State::new_nts( &&eos, temperature, state.molar_entropy(Contributions::Total), - &state.moles, + state.moles(), None, )?; assert_relative_eq!( @@ -354,7 +308,7 @@ fn assert_multiple_states( } #[test] -fn test_consistency() -> Result<(), Box> { +fn test_consistency() -> FeosResult<()> { let (saft_params, joback) = propane_parameters()?; let saft = PcSaft::new(saft_params); let eos = EquationOfState::new(joback, saft); @@ -362,10 +316,7 @@ fn test_consistency() -> Result<(), Box> { let pressures = [1.0 * BAR, 2.0 * BAR, 3.0 * BAR]; for (&temperature, &pressure) in temperatures.iter().zip(pressures.iter()) { - let state = StateBuilder::new(&&eos) - .pressure(pressure) - .temperature(temperature) - .build()?; + let state = State::new_npt(&&eos, temperature, pressure, (), None)?; assert_relative_eq!( state.pressure(Contributions::Total), pressure, @@ -379,49 +330,26 @@ fn test_consistency() -> Result<(), Box> { let molar_entropy = state.molar_entropy(Contributions::Total); let density = state.density; - let state_tv = StateBuilder::new(&&eos) - .temperature(temperature) - .density(density) - .build()?; + let state_tv = State::new_pure(&&eos, temperature, density)?; let vle = PhaseEquilibrium::pure(&&eos, temperature, None, Default::default()); let eos = &eos; - let builder = if let Ok(ps) = vle { + let phase = if let Ok(ps) = vle { let p_sat = ps.liquid().pressure(Contributions::Total); - if pressure > p_sat { - StateBuilder::new(&eos).liquid() - } else { - StateBuilder::new(&eos).vapor() - } + if pressure > p_sat { Liquid } else { Vapor } } else { - StateBuilder::new(&eos).vapor() + Vapor }; - let state_ts = builder - .clone() - .temperature(temperature) - .molar_entropy(molar_entropy) - .build()?; + let state_ts = State::new_nts(&eos, temperature, molar_entropy, (), Some(phase))?; - let state_ps = builder - .clone() - .pressure(pressure) - .molar_entropy(molar_entropy) - .build()?; + let state_ps = State::new_nps(&eos, pressure, molar_entropy, (), Some(phase), None)?; dbg!("ph"); - let state_ph = builder - .clone() - .pressure(pressure) - .molar_enthalpy(molar_enthalpy) - .build()?; + let state_ph = State::new_nph(&eos, pressure, molar_enthalpy, (), Some(phase), None)?; dbg!("th"); - let state_th = builder - .clone() - .temperature(temperature) - .molar_enthalpy(molar_enthalpy) - .build()?; + let state_th = State::new_nth(&eos, temperature, molar_enthalpy, (), Some(phase))?; dbg!("assertions"); assert_multiple_states( diff --git a/crates/feos/tests/saftvrmie/critical_properties.rs b/crates/feos/tests/saftvrmie/critical_properties.rs index c78eb110a..1f3334ea3 100644 --- a/crates/feos/tests/saftvrmie/critical_properties.rs +++ b/crates/feos/tests/saftvrmie/critical_properties.rs @@ -52,7 +52,7 @@ fn critical_properties_pure() { let option = SolverOptions::default(); let p = parameters.remove(name).unwrap(); let eos = SaftVRMie::new(p); - let cp = State::critical_point(&&eos, None, t0, None, option).unwrap(); + let cp = State::critical_point(&&eos, (), t0, None, option).unwrap(); assert_relative_eq!(cp.temperature, data.0, max_relative = 2e-3); assert_relative_eq!( cp.pressure(feos_core::Contributions::Total), diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index c18d1eacb..75958d7ca 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -50,7 +50,7 @@ type GradResult<'py> = ( #[pyfunction] pub fn vapor_pressure_derivatives<'py>( model: PyEquationOfStateAD, - parameter_names: Bound<'py, PyAny>, + parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, input: PyReadonlyArray2, ) -> GradResult<'py> { @@ -102,7 +102,7 @@ pub fn boiling_temperature_derivatives<'py>( #[pyfunction] pub fn liquid_density_derivatives<'py>( model: PyEquationOfStateAD, - parameter_names: Bound<'py, PyAny>, + parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, input: PyReadonlyArray2, ) -> GradResult<'py> { @@ -128,7 +128,7 @@ pub fn liquid_density_derivatives<'py>( #[pyfunction] pub fn equilibrium_liquid_density_derivatives<'py>( model: PyEquationOfStateAD, - parameter_names: Bound<'py, PyAny>, + parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, input: PyReadonlyArray2, ) -> GradResult<'py> { @@ -155,7 +155,7 @@ pub fn equilibrium_liquid_density_derivatives<'py>( #[pyfunction] pub fn bubble_point_pressure_derivatives<'py>( model: PyEquationOfStateAD, - parameter_names: Bound<'py, PyAny>, + parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, input: PyReadonlyArray2, ) -> GradResult<'py> { @@ -182,7 +182,7 @@ pub fn bubble_point_pressure_derivatives<'py>( #[pyfunction] pub fn dew_point_pressure_derivatives<'py>( model: PyEquationOfStateAD, - parameter_names: Bound<'py, PyAny>, + parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, input: PyReadonlyArray2, ) -> GradResult<'py> { @@ -195,7 +195,7 @@ macro_rules! expand_models { #[pyfunction] fn [<_ $prop _derivatives>]<'py>( model: PyEquationOfStateAD, - parameter_names: Bound<'py, PyAny>, + parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, input: PyReadonlyArray2, ) -> GradResult<'py> { @@ -220,7 +220,7 @@ macro_rules! impl_evaluate_gradients { expand_models!($enum, $prop, $($model: $type),*); paste!( fn $prop<'py, R: ParametersAD<$n>>( - parameter_names: Bound<'py, PyAny>, + parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, input: PyReadonlyArray2, ) -> ( diff --git a/py-feos/src/dft/adsorption/mod.rs b/py-feos/src/dft/adsorption/mod.rs index 1f006be0a..1c5040003 100644 --- a/py-feos/src/dft/adsorption/mod.rs +++ b/py-feos/src/dft/adsorption/mod.rs @@ -1,9 +1,9 @@ use super::PyDFTSolver; -use crate::eos::{parse_molefracs, PyEquationOfState}; +use crate::PyVerbosity; +use crate::eos::{Compositions, PyEquationOfState}; use crate::error::PyFeosError; use crate::ideal_gas::IdealGasModel; use crate::residual::ResidualModel; -use crate::PyVerbosity; use feos_core::EquationOfState; use feos_dft::adsorption::{Adsorption, Adsorption1D, Adsorption3D}; use nalgebra::DMatrix; @@ -45,8 +45,8 @@ macro_rules! impl_adsorption_isotherm { /// The pressures for which the profiles are calculated. /// pore : Pore /// The pore parameters. - /// molefracs: numpy.ndarray[float], optional - /// For a mixture, the molefracs of the bulk system. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// The composition of the mixture. /// solver: DFTSolver, optional /// Custom solver options. /// @@ -55,14 +55,14 @@ macro_rules! impl_adsorption_isotherm { /// Adsorption /// #[staticmethod] - #[pyo3(text_signature = "(functional, temperature, pressure, pore, molefracs=None, solver=None)")] - #[pyo3(signature = (functional, temperature, pressure, pore, molefracs=None, solver=None))] + #[pyo3(text_signature = "(functional, temperature, pressure, pore, composition=None, solver=None)")] + #[pyo3(signature = (functional, temperature, pressure, pore, composition=None, solver=None))] fn adsorption_isotherm( functional: &PyEquationOfState, temperature: Temperature, pressure: Pressure>, pore: &$py_pore, - molefracs: Option>, + composition: Option<&Bound<'_, PyAny>>, solver: Option, ) -> PyResult { Ok(Self(Adsorption::adsorption_isotherm( @@ -70,7 +70,7 @@ macro_rules! impl_adsorption_isotherm { temperature, &pressure, &pore.0, - &parse_molefracs(molefracs), + Compositions::try_from(composition)?, solver.map(|s| s.0).as_ref(), ).map_err(PyFeosError::from)?)) } @@ -89,8 +89,8 @@ macro_rules! impl_adsorption_isotherm { /// The pressures for which the profiles are calculated. /// pore : Pore /// The pore parameters. - /// molefracs: numpy.ndarray[float], optional - /// For a mixture, the molefracs of the bulk system. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// The composition of the mixture. /// solver: DFTSolver, optional /// Custom solver options. /// @@ -99,14 +99,14 @@ macro_rules! impl_adsorption_isotherm { /// Adsorption /// #[staticmethod] - #[pyo3(text_signature = "(functional, temperature, pressure, pore, molefracs=None, solver=None)")] - #[pyo3(signature = (functional, temperature, pressure, pore, molefracs=None, solver=None))] + #[pyo3(text_signature = "(functional, temperature, pressure, pore, composition=None, solver=None)")] + #[pyo3(signature = (functional, temperature, pressure, pore, composition=None, solver=None))] fn desorption_isotherm( functional: &PyEquationOfState, temperature: Temperature, pressure: Pressure>, pore: &$py_pore, - molefracs: Option>, + composition: Option<&Bound<'_, PyAny>>, solver: Option, ) -> PyResult { Ok(Self(Adsorption::desorption_isotherm( @@ -114,7 +114,7 @@ macro_rules! impl_adsorption_isotherm { temperature, &pressure, &pore.0, - &parse_molefracs(molefracs), + Compositions::try_from(composition)?, solver.map(|s| s.0).as_ref(), ).map_err(PyFeosError::from)?)) } @@ -136,8 +136,8 @@ macro_rules! impl_adsorption_isotherm { /// The pressures for which the profiles are calculated. /// pore : Pore /// The pore parameters. - /// molefracs: numpy.ndarray[float], optional - /// For a mixture, the molefracs of the bulk system. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// The composition of the mixture. /// solver: DFTSolver, optional /// Custom solver options. /// @@ -146,14 +146,14 @@ macro_rules! impl_adsorption_isotherm { /// Adsorption /// #[staticmethod] - #[pyo3(text_signature = "(functional, temperature, pressure, pore, molefracs=None, solver=None)")] - #[pyo3(signature = (functional, temperature, pressure, pore, molefracs=None, solver=None))] + #[pyo3(text_signature = "(functional, temperature, pressure, pore, composition=None, solver=None)")] + #[pyo3(signature = (functional, temperature, pressure, pore, composition=None, solver=None))] fn equilibrium_isotherm( functional: &PyEquationOfState, temperature: Temperature, pressure: Pressure>, pore: &$py_pore, - molefracs: Option>, + composition: Option<&Bound<'_, PyAny>>, solver: Option, ) -> PyResult { Ok(Self(Adsorption::equilibrium_isotherm( @@ -161,7 +161,7 @@ macro_rules! impl_adsorption_isotherm { temperature, &pressure, &pore.0, - &parse_molefracs(molefracs), + Compositions::try_from(composition)?, solver.map(|s| s.0).as_ref(), ).map_err(PyFeosError::from)?)) } @@ -180,8 +180,8 @@ macro_rules! impl_adsorption_isotherm { /// A suitable upper limit for the pressure. /// pore : Pore /// The pore parameters. - /// molefracs: numpy.ndarray[float], optional - /// For a mixture, the molefracs of the bulk system. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// The composition of the mixture. /// solver: DFTSolver, optional /// Custom solver options. /// max_iter : int, optional @@ -196,8 +196,8 @@ macro_rules! impl_adsorption_isotherm { /// Adsorption /// #[staticmethod] - #[pyo3(text_signature = "(functional, temperature, p_min, p_max, pore, molefracs=None, solver=None, max_iter=None, tol=None, verbosity=None)")] - #[pyo3(signature = (functional, temperature, p_min, p_max, pore, molefracs=None, solver=None, max_iter=None, tol=None, verbosity=None))] + #[pyo3(text_signature = "(functional, temperature, p_min, p_max, pore, composition=None, solver=None, max_iter=None, tol=None, verbosity=None)")] + #[pyo3(signature = (functional, temperature, p_min, p_max, pore, composition=None, solver=None, max_iter=None, tol=None, verbosity=None))] #[expect(clippy::too_many_arguments)] fn phase_equilibrium( functional: &PyEquationOfState, @@ -205,7 +205,7 @@ macro_rules! impl_adsorption_isotherm { p_min: Pressure, p_max: Pressure, pore: &$py_pore, - molefracs: Option>, + composition: Option<&Bound<'_, PyAny>>, solver: Option, max_iter: Option, tol: Option, @@ -217,7 +217,7 @@ macro_rules! impl_adsorption_isotherm { p_min, p_max, &pore.0, - &parse_molefracs(molefracs), + Compositions::try_from(composition)?, solver.map(|s| s.0).as_ref(), (max_iter, tol, verbosity.map(|v| v.into())).into(), ).map_err(PyFeosError::from)?)) diff --git a/py-feos/src/eos/mod.rs b/py-feos/src/eos/mod.rs index 1147ac020..cd50de42f 100644 --- a/py-feos/src/eos/mod.rs +++ b/py-feos/src/eos/mod.rs @@ -3,9 +3,9 @@ use crate::ideal_gas::IdealGasModel; use crate::residual::ResidualModel; use feos_core::*; use indexmap::IndexMap; -use nalgebra::{DVector, DVectorView, Dyn}; +use nalgebra::{DVector, DVectorView, Dyn, U1}; use numpy::{PyArray1, PyReadonlyArray1, ToPyArray}; -use pyo3::prelude::*; +use pyo3::{exceptions::PyValueError, prelude::*}; use quantity::*; use std::ops::Div; use std::sync::Arc; @@ -58,17 +58,17 @@ impl PyEquationOfState { /// /// Parameters /// ---------- - /// molefracs : np.ndarray[float], optional + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional /// The composition of the mixture. /// /// Returns /// ------- /// SINumber - #[pyo3(text_signature = "(molefracs=None)", signature = (molefracs=None))] - fn max_density(&self, molefracs: Option>) -> PyResult { + #[pyo3(text_signature = "(composition=None)", signature = (composition=None))] + fn max_density(&self, composition: Option<&Bound<'_, PyAny>>) -> PyResult { Ok(self .0 - .max_density(&parse_molefracs(molefracs)) + .max_density(Compositions::try_from(composition)?) .map_err(PyFeosError::from)?) } @@ -78,20 +78,22 @@ impl PyEquationOfState { /// ---------- /// temperature : SINumber /// The temperature for which B should be computed. - /// molefracs : np.ndarray[float], optional + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional /// The composition of the mixture. /// /// Returns /// ------- /// SINumber - #[pyo3(text_signature = "(temperature, molefracs=None)", signature = (temperature, molefracs=None))] + #[pyo3(text_signature = "(temperature, composition=None)", signature = (temperature, composition=None))] fn second_virial_coefficient( &self, temperature: Temperature, - molefracs: Option>, - ) -> Quot { - self.0 - .second_virial_coefficient(temperature, &parse_molefracs(molefracs)) + composition: Option<&Bound<'_, PyAny>>, + ) -> PyResult> { + Ok(self + .0 + .second_virial_coefficient(temperature, Compositions::try_from(composition)?) + .map_err(PyFeosError::from)?) } /// Calculate the third Virial coefficient C(T,x). @@ -100,20 +102,22 @@ impl PyEquationOfState { /// ---------- /// temperature : SINumber /// The temperature for which C should be computed. - /// molefracs : np.ndarray[float], optional + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional /// The composition of the mixture. /// /// Returns /// ------- /// SINumber - #[pyo3(text_signature = "(temperature, molefracs=None)", signature = (temperature, molefracs=None))] + #[pyo3(text_signature = "(temperature, composition=None)", signature = (temperature, composition=None))] fn third_virial_coefficient( &self, temperature: Temperature, - molefracs: Option>, - ) -> Quot, Density> { - self.0 - .third_virial_coefficient(temperature, &parse_molefracs(molefracs)) + composition: Option<&Bound<'_, PyAny>>, + ) -> PyResult, Density>> { + Ok(self + .0 + .third_virial_coefficient(temperature, Compositions::try_from(composition)?) + .map_err(PyFeosError::from)?) } /// Calculate the derivative of the second Virial coefficient B(T,x) @@ -123,22 +127,25 @@ impl PyEquationOfState { /// ---------- /// temperature : SINumber /// The temperature for which B' should be computed. - /// molefracs : np.ndarray[float], optional + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional /// The composition of the mixture. /// /// Returns /// ------- /// SINumber - #[pyo3(text_signature = "(temperature, molefracs=None)", signature = (temperature, molefracs=None))] + #[pyo3(text_signature = "(temperature, composition=None)", signature = (temperature, composition=None))] fn second_virial_coefficient_temperature_derivative( &self, temperature: Temperature, - molefracs: Option>, - ) -> Quot, Temperature> { - self.0.second_virial_coefficient_temperature_derivative( - temperature, - &parse_molefracs(molefracs), - ) + composition: Option<&Bound<'_, PyAny>>, + ) -> PyResult, Temperature>> { + Ok(self + .0 + .second_virial_coefficient_temperature_derivative( + temperature, + Compositions::try_from(composition)?, + ) + .map_err(PyFeosError::from)?) } /// Calculate the derivative of the third Virial coefficient C(T,x) @@ -148,22 +155,26 @@ impl PyEquationOfState { /// ---------- /// temperature : SINumber /// The temperature for which C' should be computed. - /// molefracs : np.ndarray[float], optional + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional /// The composition of the mixture. /// /// Returns /// ------- /// SINumber - #[pyo3(text_signature = "(temperature, molefracs=None)", signature = (temperature, molefracs=None))] + #[expect(clippy::type_complexity)] + #[pyo3(text_signature = "(temperature, composition=None)", signature = (temperature, composition=None))] fn third_virial_coefficient_temperature_derivative( &self, temperature: Temperature, - molefracs: Option>, - ) -> Quot, Density>, Temperature> { - self.0.third_virial_coefficient_temperature_derivative( - temperature, - &parse_molefracs(molefracs), - ) + composition: Option<&Bound<'_, PyAny>>, + ) -> PyResult, Density>, Temperature>> { + Ok(self + .0 + .third_virial_coefficient_temperature_derivative( + temperature, + Compositions::try_from(composition)?, + ) + .map_err(PyFeosError::from)?) } } @@ -192,36 +203,64 @@ pub(crate) fn parse_molefracs(molefracs: Option>) -> Optio }) } -// impl_state_entropy_scaling!(EquationOfState, ResidualModel>, PyEquationOfState); -// impl_phase_equilibrium!(EquationOfState, ResidualModel>, PyEquationOfState); - -// #[cfg(feature = "estimator")] -// impl_estimator!(EquationOfState, ResidualModel>, PyEquationOfState); -// #[cfg(all(feature = "estimator", feature = "pcsaft"))] -// impl_estimator_entropy_scaling!(EquationOfState, ResidualModel>, PyEquationOfState); - -// #[pymodule] -// pub fn eos(m: &Bound<'_, PyModule>) -> PyResult<()> { -// m.add_class::()?; -// m.add_class::()?; - -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; +#[derive(Clone)] +pub enum Compositions { + None, + Scalar(f64), + TotalMoles(Moles), + Molefracs(DVector), + Moles(Moles>), + PartialDensity(Density>), +} -// #[cfg(feature = "estimator")] -// m.add_wrapped(wrap_pymodule!(estimator_eos))?; +impl Composition for Compositions { + fn into_molefracs>( + self, + eos: &E, + ) -> FeosResult<(DVector, Option>)> { + match self { + Self::None => ().into_molefracs(eos), + Self::Scalar(x) => x.into_molefracs(eos), + Self::TotalMoles(total_moles) => total_moles.into_molefracs(eos), + Self::Molefracs(molefracs) => molefracs.into_molefracs(eos), + Self::Moles(moles) => moles.into_molefracs(eos), + Self::PartialDensity(partial_density) => partial_density.into_molefracs(eos), + } + } -// Ok(()) -// } + fn density(&self) -> Option> { + if let Self::PartialDensity(partial_density) = self { + partial_density.density() + } else { + None + } + } +} -// #[cfg(feature = "estimator")] -// #[pymodule] -// pub fn estimator_eos(m: &Bound<'_, PyModule>) -> PyResult<()> { -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::() -// } +impl TryFrom>> for Compositions { + type Error = PyErr; + fn try_from(composition: Option<&Bound<'_, PyAny>>) -> PyResult { + let Some(composition) = composition else { + return Ok(Compositions::None); + }; + if let Ok(x) = composition.extract::>() + && let Some(x) = x.try_as_matrix::() + { + Ok(Compositions::Molefracs(x.clone_owned())) + } else if let Ok(x) = composition.extract::>() { + Ok(Compositions::Molefracs(DVector::from_vec(x))) + } else if let Ok(x) = composition.extract::() { + Ok(Compositions::Scalar(x)) + } else if let Ok(n) = composition.extract::>>() { + Ok(Compositions::Moles(n)) + } else if let Ok(n) = composition.extract::() { + Ok(Compositions::TotalMoles(n)) + } else if let Ok(rho) = composition.extract::>>() { + Ok(Compositions::PartialDensity(rho)) + } else { + Err(PyErr::new::(format!( + "failed to parse value '{composition}' as composition." + ))) + } + } +} diff --git a/py-feos/src/parameter/mod.rs b/py-feos/src/parameter/mod.rs index 115f8f79a..aabbed5ab 100644 --- a/py-feos/src/parameter/mod.rs +++ b/py-feos/src/parameter/mod.rs @@ -557,6 +557,7 @@ impl PyGcParameters { .map_err(PyFeosError::from)?) } + #[cfg(feature = "gc_pcsaft")] pub fn try_convert_heterosegmented( self, ) -> PyResult> diff --git a/py-feos/src/phase_equilibria.rs b/py-feos/src/phase_equilibria.rs index c3786e300..38d0d2d12 100644 --- a/py-feos/src/phase_equilibria.rs +++ b/py-feos/src/phase_equilibria.rs @@ -61,7 +61,7 @@ impl PyPhaseEquilibrium { #[pyo3(signature = (eos, temperature_or_pressure, initial_state=None, max_iter=None, tol=None, verbosity=None))] pub(crate) fn pure( eos: &PyEquationOfState, - temperature_or_pressure: Bound<'_, PyAny>, + temperature_or_pressure: &Bound<'_, PyAny>, initial_state: Option<&PyPhaseEquilibrium>, max_iter: Option, tol: Option, @@ -198,9 +198,9 @@ impl PyPhaseEquilibrium { #[expect(clippy::too_many_arguments)] pub(crate) fn bubble_point<'py>( eos: &PyEquationOfState, - temperature_or_pressure: Bound<'_, PyAny>, + temperature_or_pressure: &Bound<'_, PyAny>, liquid_molefracs: PyReadonlyArray1<'py, f64>, - tp_init: Option>, + tp_init: Option<&Bound<'_, PyAny>>, vapor_molefracs: Option>, max_iter_inner: Option, max_iter_outer: Option, @@ -286,9 +286,9 @@ impl PyPhaseEquilibrium { #[expect(clippy::too_many_arguments)] pub(crate) fn dew_point<'py>( eos: &PyEquationOfState, - temperature_or_pressure: Bound<'_, PyAny>, + temperature_or_pressure: &Bound<'_, PyAny>, vapor_molefracs: PyReadonlyArray1<'py, f64>, - tp_init: Option>, + tp_init: Option<&Bound<'_, PyAny>>, liquid_molefracs: Option>, max_iter_inner: Option, max_iter_outer: Option, @@ -398,7 +398,7 @@ impl PyPhaseEquilibrium { #[staticmethod] fn vle_pure_comps( eos: &PyEquationOfState, - temperature_or_pressure: Bound<'_, PyAny>, + temperature_or_pressure: &Bound<'_, PyAny>, ) -> PyResult>> { if let Ok(t) = temperature_or_pressure.extract::() { Ok(PhaseEquilibrium::vle_pure_comps(&eos.0, t) @@ -514,9 +514,9 @@ impl PyPhaseEquilibrium { #[expect(clippy::too_many_arguments)] fn heteroazeotrope( eos: &PyEquationOfState, - temperature_or_pressure: Bound<'_, PyAny>, + temperature_or_pressure: &Bound<'_, PyAny>, x_init: (f64, f64), - tp_init: Option>, + tp_init: Option<&Bound<'_, PyAny>>, max_iter: Option, tol: Option, verbosity: Option, @@ -1224,7 +1224,7 @@ impl PyPhaseDiagram { #[expect(clippy::too_many_arguments)] pub(crate) fn binary_vle( eos: &PyEquationOfState, - temperature_or_pressure: Bound<'_, PyAny>, + temperature_or_pressure: &Bound<'_, PyAny>, npoints: Option, x_lle: Option<(f64, f64)>, max_iter_inner: Option, @@ -1299,7 +1299,7 @@ impl PyPhaseDiagram { #[pyo3(signature = (eos, temperature_or_pressure, feed, min_tp, max_tp, npoints=None))] pub(crate) fn lle( eos: &PyEquationOfState, - temperature_or_pressure: Bound<'_, PyAny>, + temperature_or_pressure: &Bound<'_, PyAny>, feed: Moles>, min_tp: Bound<'_, PyAny>, max_tp: Bound<'_, PyAny>, @@ -1388,10 +1388,10 @@ impl PyPhaseDiagram { #[expect(clippy::too_many_arguments)] pub(crate) fn binary_vlle( eos: &PyEquationOfState, - temperature_or_pressure: Bound<'_, PyAny>, + temperature_or_pressure: &Bound<'_, PyAny>, x_lle: (f64, f64), - tp_lim_lle: Option>, - tp_init_vlle: Option>, + tp_lim_lle: Option<&Bound<'_, PyAny>>, + tp_init_vlle: Option<&Bound<'_, PyAny>>, npoints_vle: Option, npoints_lle: Option, max_iter_inner: Option, diff --git a/py-feos/src/state.rs b/py-feos/src/state.rs index e7b54211c..6d5ff09f8 100644 --- a/py-feos/src/state.rs +++ b/py-feos/src/state.rs @@ -1,4 +1,4 @@ -use crate::eos::parse_molefracs; +use crate::eos::{Compositions, parse_molefracs}; use crate::{ PyVerbosity, eos::PyEquationOfState, error::PyFeosError, ideal_gas::IdealGasModel, residual::ResidualModel, @@ -13,15 +13,13 @@ use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; use quantity::*; use std::collections::HashMap; -use std::ops::{Deref, Div, Neg, Sub}; +use std::ops::{Deref, Div, Neg}; use std::sync::Arc; type Quot = >::Output; -type DpDn = Quantity>::Output>; type InvT = Quantity::Output>; type InvP = Quantity::Output>; -type InvM = Quantity::Output>; /// Possible contributions that can be computed. #[derive(Clone, Copy, PartialEq)] @@ -69,14 +67,8 @@ impl From for Contributions { /// Volume. /// density : SINumber, optional /// Molar density. -/// partial_density : SIArray1, optional -/// Partial molar densities. -/// total_moles : SINumber, optional -/// Total amount of substance (of a mixture). -/// moles : SIArray1, optional -/// Amount of substance for each component. -/// molefracs : numpy.ndarray[float] -/// Molar fraction of each component. +/// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional +/// Composition of the mixture. /// pressure : SINumber, optional /// Pressure. /// molar_enthalpy : SINumber, optional @@ -110,19 +102,16 @@ pub struct PyState(pub State, ResidualMod impl PyState { #[new] #[pyo3( - text_signature = "(eos, temperature=None, volume=None, density=None, partial_density=None, total_moles=None, moles=None, molefracs=None, pressure=None, molar_enthalpy=None, molar_entropy=None, molar_internal_energy=None, density_initialization=None, initial_temperature=None)" + text_signature = "(eos, temperature=None, volume=None, density=None, composition=None, pressure=None, molar_enthalpy=None, molar_entropy=None, molar_internal_energy=None, density_initialization=None, initial_temperature=None)" )] - #[pyo3(signature = (eos, temperature=None, volume=None, density=None, partial_density=None, total_moles=None, moles=None, molefracs=None, pressure=None, molar_enthalpy=None, molar_entropy=None, molar_internal_energy=None, density_initialization=None, initial_temperature=None))] + #[pyo3(signature = (eos, temperature=None, volume=None, density=None, composition=None, pressure=None, molar_enthalpy=None, molar_entropy=None, molar_internal_energy=None, density_initialization=None, initial_temperature=None))] #[expect(clippy::too_many_arguments)] pub fn new<'py>( eos: &PyEquationOfState, temperature: Option, volume: Option, density: Option, - partial_density: Option>>, - total_moles: Option, - moles: Option>>, - molefracs: Option>, + composition: Option<&Bound<'py, PyAny>>, pressure: Option, molar_enthalpy: Option, molar_entropy: Option, @@ -130,7 +119,7 @@ impl PyState { density_initialization: Option<&Bound<'py, PyAny>>, initial_temperature: Option, ) -> PyResult { - let x = parse_molefracs(molefracs); + let composition = Compositions::try_from(composition)?; let density_init = if let Some(di) = density_initialization { if let Ok(d) = di.extract::().as_deref() { match d { @@ -147,25 +136,23 @@ impl PyState { } } else { Ok(None) - }; - let s = State::new_full( - &eos.0, - temperature, - volume, - density, - partial_density.as_ref(), - total_moles, - moles.as_ref(), - x.as_ref(), - pressure, - molar_enthalpy, - molar_entropy, - molar_internal_energy, - density_init?, - initial_temperature, - ) - .map_err(PyFeosError::from)?; - Ok(Self(s)) + }?; + Ok(Self( + State::build_full( + &eos.0, + temperature, + volume, + density, + composition, + pressure, + molar_enthalpy, + molar_entropy, + molar_internal_energy, + density_init, + initial_temperature, + ) + .map_err(PyFeosError::from)?, + )) } /// Return a list of thermodynamic state at critical conditions @@ -233,12 +220,12 @@ impl PyState { /// State : State at critical conditions. #[staticmethod] #[pyo3( - text_signature = "(eos, molefracs=None, initial_temperature=None, initial_density=None, max_iter=None, tol=None, verbosity=None)" + text_signature = "(eos, composition=None, initial_temperature=None, initial_density=None, max_iter=None, tol=None, verbosity=None)" )] - #[pyo3(signature = (eos, molefracs=None, initial_temperature=None, initial_density=None, max_iter=None, tol=None, verbosity=None))] + #[pyo3(signature = (eos, composition=None, initial_temperature=None, initial_density=None, max_iter=None, tol=None, verbosity=None))] fn critical_point<'py>( eos: &PyEquationOfState, - molefracs: Option>, + composition: Option<&Bound<'py, PyAny>>, initial_temperature: Option, initial_density: Option, max_iter: Option, @@ -248,7 +235,7 @@ impl PyState { Ok(PyState( State::critical_point( &eos.0, - parse_molefracs(molefracs).as_ref(), + Compositions::try_from(composition)?, initial_temperature, initial_density, (max_iter, tol, verbosity.map(|v| v.into())).into(), @@ -287,7 +274,7 @@ impl PyState { #[expect(clippy::too_many_arguments)] fn critical_point_binary( eos: &PyEquationOfState, - temperature_or_pressure: Bound<'_, PyAny>, + temperature_or_pressure: &Bound<'_, PyAny>, initial_temperature: Option, initial_molefracs: Option<[f64; 2]>, initial_density: Option, @@ -353,13 +340,13 @@ impl PyState { /// (State, State): Spinodal states. #[staticmethod] #[pyo3( - text_signature = "(eos, temperature, molefracs=None, max_iter=None, tol=None, verbosity=None)" + text_signature = "(eos, temperature, composition=None, max_iter=None, tol=None, verbosity=None)" )] - #[pyo3(signature = (eos, temperature, molefracs=None, max_iter=None, tol=None, verbosity=None))] + #[pyo3(signature = (eos, temperature, composition=None, max_iter=None, tol=None, verbosity=None))] fn spinodal<'py>( eos: &PyEquationOfState, temperature: Temperature, - molefracs: Option>, + composition: Option<&Bound<'py, PyAny>>, max_iter: Option, tol: Option, verbosity: Option, @@ -367,7 +354,7 @@ impl PyState { let [state1, state2] = State::spinodal( &eos.0, temperature, - parse_molefracs(molefracs).as_ref(), + Compositions::try_from(composition)?, (max_iter, tol, verbosity.map(|v| v.into())).into(), ) .map_err(PyFeosError::from)?; @@ -476,7 +463,7 @@ impl PyState { self.0.compressibility(contributions.into()) } - /// Return partial derivative of pressure w.r.t. volume. + /// Return partial derivative of pressure w.r.t. molar volume. /// /// Parameters /// ---------- @@ -488,7 +475,7 @@ impl PyState { /// ------- /// SINumber #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn dp_dv(&self, contributions: PyContributions) -> Quot { + fn dp_dv(&self, contributions: PyContributions) -> Quot { self.0.dp_dv(contributions.into()) } @@ -536,11 +523,11 @@ impl PyState { /// ------- /// SIArray1 #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn dp_dni(&self, contributions: PyContributions) -> DpDn> { - self.0.dp_dni(contributions.into()) + fn dp_dni(&self, contributions: PyContributions) -> Pressure> { + self.0.n_dp_dni(contributions.into()) } - /// Return second partial derivative of pressure w.r.t. volume. + /// Return second partial derivative of pressure w.r.t. molar volume. /// /// Parameters /// ---------- @@ -552,7 +539,10 @@ impl PyState { /// ------- /// SINumber #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn d2p_dv2(&self, contributions: PyContributions) -> Quot, Volume> { + fn d2p_dv2( + &self, + contributions: PyContributions, + ) -> Quot, MolarVolume> { self.0.d2p_dv2(contributions.into()) } @@ -652,8 +642,8 @@ impl PyState { /// ------- /// SIArray2 #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn dmu_dni(&self, contributions: PyContributions) -> Quot>, Moles> { - self.0.dmu_dni(contributions.into()) + fn n_dmu_dni(&self, contributions: PyContributions) -> MolarEnergy> { + self.0.n_dmu_dni(contributions.into()) } /// Return logarithmic fugacity coefficient. @@ -771,8 +761,8 @@ impl PyState { /// Returns /// ------- /// SIArray2 - fn dln_phi_dnj(&self) -> InvM> { - self.0.dln_phi_dnj() + fn n_dln_phi_dnj<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { + self.0.n_dln_phi_dnj().to_pyarray(py) } /// Return thermodynamic factor. @@ -848,7 +838,7 @@ impl PyState { self.0.entropy(contributions.into()) } - /// Return derivative of entropy with respect to temperature. + /// Return derivative of molar entropy with respect to temperature. /// /// Parameters /// ---------- @@ -860,7 +850,7 @@ impl PyState { /// ------- /// SINumber #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn ds_dt(&self, contributions: PyContributions) -> Quot { + fn ds_dt(&self, contributions: PyContributions) -> Quot { self.0.ds_dt(contributions.into()) } @@ -1357,7 +1347,7 @@ impl PyState { #[getter] fn get_total_moles(&self) -> Moles { - self.0.total_moles + self.0.total_moles() } #[getter] @@ -1367,7 +1357,7 @@ impl PyState { #[getter] fn get_volume(&self) -> Volume { - self.0.volume + self.0.volume() } #[getter] @@ -1377,12 +1367,12 @@ impl PyState { #[getter] fn get_moles(&self) -> Moles> { - self.0.moles.clone() + self.0.moles() } #[getter] fn get_partial_density(&self) -> Density> { - self.0.partial_density.clone() + self.0.partial_density() } #[getter] From c33df5910cff0b02fc73f8a6738c17965be6c79c Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 27 Jan 2026 12:36:56 +0100 Subject: [PATCH 5/6] include phase fraction in PhaseEquilibrium --- .github/workflows/test.yml | 2 +- .github/workflows/wheels.yml | 2 +- CHANGELOG.md | 9 +- crates/feos-core/src/ad/mod.rs | 32 ++- crates/feos-core/src/errors.rs | 4 + crates/feos-core/src/lib.rs | 22 +- .../src/phase_equilibria/bubble_dew.rs | 63 +++--- crates/feos-core/src/phase_equilibria/mod.rs | 200 +++++++++--------- .../phase_equilibria/phase_diagram_binary.rs | 16 +- .../phase_equilibria/phase_diagram_pure.rs | 4 +- .../src/phase_equilibria/phase_envelope.rs | 60 +++--- .../phase_equilibria/stability_analysis.rs | 6 +- .../src/phase_equilibria/tp_flash.rs | 161 +++++++++----- .../src/phase_equilibria/vle_pure.rs | 17 +- crates/feos-core/src/state/composition.rs | 94 +------- crates/feos-core/src/state/mod.rs | 37 ++-- crates/feos-core/src/state/properties.rs | 22 +- .../src/state/residual_properties.rs | 51 +++-- crates/feos-core/src/state/statevec.rs | 15 +- crates/feos/benches/contributions.rs | 2 +- crates/feos/benches/dft_pore.rs | 12 +- crates/feos/benches/dual_numbers.rs | 10 +- crates/feos/benches/dual_numbers_saftvrmie.rs | 13 +- crates/feos/benches/state_creation.rs | 2 +- crates/feos/src/epcsaft/eos/mod.rs | 4 +- crates/feos/src/pcsaft/eos/mod.rs | 82 +++---- crates/feos/src/pets/eos/mod.rs | 2 +- crates/feos/src/uvtheory/eos/mod.rs | 4 +- .../feos/tests/pcsaft/stability_analysis.rs | 5 +- .../tests/pcsaft/state_creation_mixture.rs | 2 +- .../feos/tests/pcsaft/state_creation_pure.rs | 18 +- py-feos/src/ad/mod.rs | 2 +- py-feos/src/eos/mod.rs | 12 -- py-feos/src/phase_equilibria.rs | 107 +++------- py-feos/src/state.rs | 99 ++++++--- 35 files changed, 573 insertions(+), 620 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ea6192e7..6c35188d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: Test on: push: - branches: [main, development] + branches: [main] pull_request: branches: [main, development] diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 41f501d43..197da9586 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,7 +1,7 @@ name: Build Wheels on: push: - branches: [main, development] + branches: [main] pull_request: branches: [main, development] jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 444607c36..513a7cc1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Breaking] ### Added - Extended tp-flash algorithm to static numbers of components and enabled automatic differentiation for binary systems. [#336](https://github.com/feos-org/feos/pull/336) -- Rewrote `PhaseEquilibrium::pure_p` to mirror `pure_t` and enable automatic differentiation. [#337](https://github.com/feos-org/feos/pull/337) +- Rewrote `PhaseEquilibrium::pure_p` to mirror `pure_t` and enabled automatic differentiation. [#337](https://github.com/feos-org/feos/pull/337) - Added `boiling_temperature` to the list of properties for parallel evaluations of gradients. [#337](https://github.com/feos-org/feos/pull/337) +- Added the `Composition` trait to allow more flexibility in the creation of states and phase equilibria. [#330](https://github.com/feos-org/feos/pull/330) + +### Changed +- Removed any assumptions about the total number of moles in a `State` or `PhaseEquilibrium`. Evaluating extensive properties now returns a `Result`. [#330](https://github.com/feos-org/feos/pull/330) + +### Removed +- Removed the `StateBuilder` struct, because it is mostly obsolete with the addition of the `Composition` trait. [#330](https://github.com/feos-org/feos/pull/330) ### Packaging - Updated `quantity` dependency to 0.13 and removed the `typenum` dependency. [#323](https://github.com/feos-org/feos/pull/323) diff --git a/crates/feos-core/src/ad/mod.rs b/crates/feos-core/src/ad/mod.rs index aae9fc198..f48fb153c 100644 --- a/crates/feos-core/src/ad/mod.rs +++ b/crates/feos-core/src/ad/mod.rs @@ -1,6 +1,6 @@ use crate::DensityInitialization::Liquid; use crate::density_iteration::density_iteration; -use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; +use crate::{Composition, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::{Const, SVector, U1, U2}; #[cfg(feature = "rayon")] use ndarray::{Array1, Array2, ArrayView2, Zip}; @@ -215,20 +215,21 @@ pub trait PropertiesAD { ) } - fn bubble_point_pressure( + fn bubble_point_pressure>( &self, temperature: Temperature, pressure: Option, - liquid_molefracs: SVector, + liquid_molefracs: X, ) -> FeosResult>> where Self: Residual>, { let eos_f64 = self.re(); + let (liquid_molefracs, _) = liquid_molefracs.into_molefracs(&eos_f64)?; let vle = PhaseEquilibrium::bubble_point( &eos_f64, temperature, - &liquid_molefracs, + liquid_molefracs, pressure, None, Default::default(), @@ -265,20 +266,21 @@ pub trait PropertiesAD { Ok(Pressure::from_reduced(p)) } - fn dew_point_pressure( + fn dew_point_pressure>( &self, temperature: Temperature, pressure: Option, - vapor_molefracs: SVector, + vapor_molefracs: X, ) -> FeosResult>> where Self: Residual>, { let eos_f64 = self.re(); + let (vapor_molefracs, _) = vapor_molefracs.into_molefracs(&eos_f64)?; let vle = PhaseEquilibrium::dew_point( &eos_f64, temperature, - &vapor_molefracs, + vapor_molefracs, pressure, None, Default::default(), @@ -329,12 +331,8 @@ pub trait PropertiesAD { parameters, input, |eos: &Self::Lifted>, inp| { - eos.bubble_point_pressure( - inp[0] * KELVIN, - Some(inp[2] * PASCAL), - SVector::from([inp[1], 1.0 - inp[1]]), - ) - .map(|p| p.convert_into(PASCAL)) + eos.bubble_point_pressure(inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) + .map(|p| p.convert_into(PASCAL)) }, ) } @@ -353,12 +351,8 @@ pub trait PropertiesAD { parameters, input, |eos: &Self::Lifted>, inp| { - eos.dew_point_pressure( - inp[0] * KELVIN, - Some(inp[2] * PASCAL), - SVector::from([inp[1], 1.0 - inp[1]]), - ) - .map(|p| p.convert_into(PASCAL)) + eos.dew_point_pressure(inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) + .map(|p| p.convert_into(PASCAL)) }, ) } diff --git a/crates/feos-core/src/errors.rs b/crates/feos-core/src/errors.rs index f02946bc0..eb7ea4147 100644 --- a/crates/feos-core/src/errors.rs +++ b/crates/feos-core/src/errors.rs @@ -24,6 +24,10 @@ pub enum FeosError { InvalidState(String, String, f64), #[error("Undetermined state: {0}")] UndeterminedState(String), + #[error( + "Extensive properties can only be evaluated for states that are initialized with extensive properties." + )] + IntensiveState, #[error("System is supercritical.")] SuperCritical, #[error("No phase split according to stability analysis.")] diff --git a/crates/feos-core/src/lib.rs b/crates/feos-core/src/lib.rs index a2552007d..48b8306f3 100644 --- a/crates/feos-core/src/lib.rs +++ b/crates/feos-core/src/lib.rs @@ -299,8 +299,8 @@ mod tests { // residual properties assert_relative_eq!( - s.helmholtz_energy(Contributions::Residual), - sr.residual_helmholtz_energy(), + s.helmholtz_energy(Contributions::Residual)?, + sr.residual_helmholtz_energy()?, max_relative = 1e-15 ); assert_relative_eq!( @@ -309,8 +309,8 @@ mod tests { max_relative = 1e-15 ); assert_relative_eq!( - s.entropy(Contributions::Residual), - sr.residual_entropy(), + s.entropy(Contributions::Residual)?, + sr.residual_entropy()?, max_relative = 1e-15 ); assert_relative_eq!( @@ -319,8 +319,8 @@ mod tests { max_relative = 1e-15 ); assert_relative_eq!( - s.enthalpy(Contributions::Residual), - sr.residual_enthalpy(), + s.enthalpy(Contributions::Residual)?, + sr.residual_enthalpy()?, max_relative = 1e-15 ); assert_relative_eq!( @@ -329,8 +329,8 @@ mod tests { max_relative = 1e-15 ); assert_relative_eq!( - s.internal_energy(Contributions::Residual), - sr.residual_internal_energy(), + s.internal_energy(Contributions::Residual)?, + sr.residual_internal_energy()?, max_relative = 1e-15 ); assert_relative_eq!( @@ -339,12 +339,12 @@ mod tests { max_relative = 1e-15 ); assert_relative_eq!( - s.gibbs_energy(Contributions::Residual) - - s.total_moles() + s.gibbs_energy(Contributions::Residual)? + - s.total_moles()? * RGAS * s.temperature * s.compressibility(Contributions::Total).ln(), - sr.residual_gibbs_energy(), + sr.residual_gibbs_energy()?, max_relative = 1e-15 ); assert_relative_eq!( diff --git a/crates/feos-core/src/phase_equilibria/bubble_dew.rs b/crates/feos-core/src/phase_equilibria/bubble_dew.rs index 91ad57d3d..05dd42849 100644 --- a/crates/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/crates/feos-core/src/phase_equilibria/bubble_dew.rs @@ -4,7 +4,7 @@ use crate::state::{ Contributions, DensityInitialization::{InitialDensity, Liquid, Vapor}, }; -use crate::{ReferenceSystem, Residual, SolverOptions, State, Verbosity}; +use crate::{Composition, ReferenceSystem, Residual, SolverOptions, State, Verbosity}; use nalgebra::allocator::Allocator; use nalgebra::{DMatrix, DVector, DefaultAllocator, Dim, Dyn, OVector, U1}; #[cfg(feature = "ndarray")] @@ -141,10 +141,10 @@ where { /// Calculate a phase equilibrium for a given temperature /// or pressure and composition of the liquid phase. - pub fn bubble_point>( + pub fn bubble_point, X: Composition>( eos: &E, temperature_or_pressure: TP, - liquid_molefracs: &OVector, + liquid_molefracs: X, tp_init: Option, vapor_molefracs: Option<&OVector>, options: (SolverOptions, SolverOptions), @@ -162,10 +162,10 @@ where /// Calculate a phase equilibrium for a given temperature /// or pressure and composition of the vapor phase. - pub fn dew_point>( + pub fn dew_point, X: Composition>( eos: &E, temperature_or_pressure: TP, - vapor_molefracs: &OVector, + vapor_molefracs: X, tp_init: Option, liquid_molefracs: Option<&OVector>, options: (SolverOptions, SolverOptions), @@ -181,35 +181,43 @@ where ) } - pub(super) fn bubble_dew_point>( + pub(super) fn bubble_dew_point, X: Composition>( eos: &E, temperature_or_pressure: TP, - vapor_molefracs: &OVector, + vapor_molefracs: X, tp_init: Option, liquid_molefracs: Option<&OVector>, bubble: bool, options: (SolverOptions, SolverOptions), ) -> FeosResult { - let (temperature, pressure, iterate_p) = - temperature_or_pressure.temperature_pressure(tp_init); - Self::bubble_dew_point_tp( - eos, - temperature, - pressure, - vapor_molefracs, - liquid_molefracs, - bubble, - iterate_p, - options, - ) + if eos.components() == 1 { + let mut vle = Self::pure(eos, temperature_or_pressure, None, options.1)?; + if bubble { + vle.phase_fractions = [D::from(0.0), D::from(1.0)]; + } + Ok(vle) + } else { + let (temperature, pressure, iterate_p) = + temperature_or_pressure.temperature_pressure(tp_init); + Self::bubble_dew_point_tp( + eos, + temperature, + pressure, + vapor_molefracs, + liquid_molefracs, + bubble, + iterate_p, + options, + ) + } } #[expect(clippy::too_many_arguments)] - fn bubble_dew_point_tp( + fn bubble_dew_point_tp>( eos: &E, temperature: Option>, pressure: Option>, - molefracs_spec: &OVector, + composition: X, molefracs_init: Option<&OVector>, bubble: bool, iterate_p: bool, @@ -218,6 +226,7 @@ where let eos_re = eos.re(); let mut temperature_re = temperature.map(|t| t.re()); let mut pressure_re = pressure.map(|p| p.re()); + let (molefracs_spec, total_moles) = composition.into_molefracs(eos)?; let molefracs_spec_re = molefracs_spec.map(|x| x.re()); let (v1, rho2) = if iterate_p { // temperature is specified @@ -315,7 +324,7 @@ where Self::newton_step_t( eos, t, - molefracs_spec, + &molefracs_spec, &mut p, &mut molar_volume, &mut rho2, @@ -325,7 +334,7 @@ where Self::newton_step_p( eos, &mut t, - molefracs_spec, + &molefracs_spec, p, &mut molar_volume, &mut rho2, @@ -348,11 +357,11 @@ where x2, )?; - Ok(PhaseEquilibrium(if bubble { - [state2, state1] + Ok(if bubble { + PhaseEquilibrium::with_vapor_phase_fraction(state2, state1, D::from(0.0), total_moles) } else { - [state1, state2] - })) + PhaseEquilibrium::with_vapor_phase_fraction(state1, state2, D::from(1.0), total_moles) + }) } fn newton_step_t( diff --git a/crates/feos-core/src/phase_equilibria/mod.rs b/crates/feos-core/src/phase_equilibria/mod.rs index 95671deb2..461a1a901 100644 --- a/crates/feos-core/src/phase_equilibria/mod.rs +++ b/crates/feos-core/src/phase_equilibria/mod.rs @@ -1,11 +1,12 @@ +use crate::FeosError; use crate::equation_of_state::Residual; use crate::errors::FeosResult; -use crate::state::{DensityInitialization, State}; -use crate::{Contributions, ReferenceSystem}; +use crate::state::State; +use crate::{Contributions::Total as Tot, ReferenceSystem, Total}; use nalgebra::allocator::Allocator; -use nalgebra::{DefaultAllocator, Dim, Dyn, OVector}; -use num_dual::{DualNum, DualStruct, Gradients}; -use quantity::{Energy, Moles, Pressure, RGAS, Temperature}; +use nalgebra::{DefaultAllocator, Dim, Dyn}; +use num_dual::{DualNum, Gradients}; +use quantity::{Dimensionless, Energy, Entropy, MolarEnergy, MolarEntropy, Moles}; use std::fmt; use std::fmt::Write; @@ -38,15 +39,18 @@ pub use phase_diagram_pure::PhaseDiagram; /// + [Pure component phase equilibria](#pure-component-phase-equilibria) /// + [Utility functions](#utility-functions) #[derive(Debug, Clone)] -pub struct PhaseEquilibrium + Copy = f64>( - pub [State; P], -) +pub struct PhaseEquilibrium + Copy = f64> where - DefaultAllocator: Allocator; + DefaultAllocator: Allocator, +{ + states: [State; P], + pub phase_fractions: [D; P], + total_moles: Option>, +} impl fmt::Display for PhaseEquilibrium { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (i, s) in self.0.iter().enumerate() { + for (i, s) in self.states.iter().enumerate() { writeln!(f, "phase {i}: {s}")?; } Ok(()) @@ -55,9 +59,9 @@ impl fmt::Display for PhaseEquilibrium { impl PhaseEquilibrium { pub fn _repr_markdown_(&self) -> String { - if self.0[0].eos.components() == 1 { + if self.states[0].eos.components() == 1 { let mut res = "||temperature|density|\n|-|-|-|\n".to_string(); - for (i, s) in self.0.iter().enumerate() { + for (i, s) in self.states.iter().enumerate() { writeln!( res, "|phase {}|{:.5}|{:.5}|", @@ -70,7 +74,7 @@ impl PhaseEquilibrium { res } else { let mut res = "||temperature|density|molefracs|\n|-|-|-|-|\n".to_string(); - for (i, s) in self.0.iter().enumerate() { + for (i, s) in self.states.iter().enumerate() { writeln!( res, "|phase {}|{:.5}|{:.5}|{:.5?}|", @@ -91,125 +95,115 @@ where DefaultAllocator: Allocator, { pub fn vapor(&self) -> &State { - &self.0[0] + &self.states[0] } pub fn liquid(&self) -> &State { - &self.0[1] + &self.states[1] + } + + pub fn vapor_phase_fraction(&self) -> D { + self.phase_fractions[0] } } impl PhaseEquilibrium { pub fn vapor(&self) -> &State { - &self.0[0] + &self.states[0] } pub fn liquid1(&self) -> &State { - &self.0[1] + &self.states[1] } pub fn liquid2(&self) -> &State { - &self.0[2] + &self.states[2] } } -impl, N: Dim> PhaseEquilibrium +impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator, { - pub(super) fn from_states(state1: State, state2: State) -> Self { - let (vapor, liquid) = if state1.density.re() < state2.density.re() { - (state1, state2) - } else { - (state2, state1) - }; - Self([vapor, liquid]) - } - - // /// Creates a new PhaseEquilibrium that contains two states at the - // /// specified temperature, pressure and molefracs. - // /// - // /// The constructor can be used in custom phase equilibrium solvers or, - // /// e.g., to generate initial guesses for an actual VLE solver. - // /// In general, the two states generated are NOT in an equilibrium. - // pub fn new_xpt( - // eos: &E, - // temperature: Temperature, - // pressure: Pressure, - // vapor_molefracs: &OVector, - // liquid_molefracs: &OVector, - // ) -> FeosResult { - // let liquid = State::new_xpt( - // eos, - // temperature, - // pressure, - // liquid_molefracs, - // Some(DensityInitialization::Liquid), - // )?; - // let vapor = State::new_xpt( - // eos, - // temperature, - // pressure, - // vapor_molefracs, - // Some(DensityInitialization::Vapor), - // )?; - // Ok(Self([vapor, liquid])) - // } - - pub(super) fn vapor_phase_fraction(&self) -> Option { - self.vapor() - .total_moles - .zip(self.liquid().total_moles) - .map(|(v, l)| (v / (l + v)).into_value()) + pub(super) fn single_phase(state: State) -> Self { + let total_moles = state.total_moles; + Self::with_vapor_phase_fraction(state.clone(), state, D::from(1.0), total_moles) + } + + pub(super) fn two_phase(vapor: State, liquid: State) -> Self { + let (beta, total_moles) = + if let (Some(nv), Some(nl)) = (vapor.total_moles, liquid.total_moles) { + (nv.convert_into(nl + nv), Some(nl + nv)) + } else { + (D::from(1.0), None) + }; + Self::with_vapor_phase_fraction(vapor, liquid, beta, total_moles) + } + + pub(super) fn with_vapor_phase_fraction( + vapor: State, + liquid: State, + vapor_phase_fraction: D, + total_moles: Option>, + ) -> Self { + Self { + states: [vapor, liquid], + phase_fractions: [vapor_phase_fraction, -vapor_phase_fraction + 1.0], + total_moles, + } } } -impl, N: Gradients, const P: usize> PhaseEquilibrium +impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator, { - pub(super) fn update_pressure( - mut self, - temperature: Temperature, - pressure: Pressure, - ) -> FeosResult { - for s in self.0.iter_mut() { - *s = State::new_npt( - &s.eos, - temperature, - pressure, - &*s, - Some(DensityInitialization::InitialDensity(s.density)), - )?; - } - Ok(self) - } - - pub(super) fn update_moles( - &mut self, - pressure: Pressure, - moles: [&Moles>; P], - ) -> FeosResult<()> { - for (i, s) in self.0.iter_mut().enumerate() { - *s = State::new_npt( - &s.eos, - s.temperature, - pressure, - moles[i], - Some(DensityInitialization::InitialDensity(s.density)), - )?; + pub(super) fn new( + vapor: State, + liquid1: State, + liquid2: State, + ) -> Self { + Self { + states: [vapor, liquid1, liquid2], + phase_fractions: [D::from(1.0), D::from(0.0), D::from(0.0)], + total_moles: None, } - Ok(()) + } +} + +impl, N: Gradients, const P: usize, D: DualNum + Copy> + PhaseEquilibrium +where + DefaultAllocator: Allocator, +{ + pub fn total_moles(&self) -> FeosResult> { + self.total_moles.ok_or(FeosError::IntensiveState) + } + + pub fn molar_enthalpy(&self) -> MolarEnergy { + self.states + .iter() + .zip(&self.phase_fractions) + .map(|(s, x)| s.molar_enthalpy(Tot) * Dimensionless::new(x)) + .reduce(|a, b| a + b) + .unwrap() + } + + pub fn enthalpy(&self) -> FeosResult> { + Ok(self.total_moles()? * self.molar_enthalpy()) + } + + pub fn molar_entropy(&self) -> MolarEntropy { + self.states + .iter() + .zip(&self.phase_fractions) + .map(|(s, x)| s.molar_entropy(Tot) * Dimensionless::new(x)) + .reduce(|a, b| a + b) + .unwrap() } - // Total Gibbs energy excluding the constant contribution RT sum_i N_i ln(\Lambda_i^3) - pub(super) fn total_gibbs_energy(&self) -> Energy { - self.0.iter().fold(Energy::from_reduced(0.0), |acc, s| { - let ln_rho_m1 = s.partial_density().to_reduced().map(|r| r.ln() - 1.0); - acc + s.residual_helmholtz_energy() - + s.pressure(Contributions::Total) * s.volume() - + RGAS * s.temperature * s.total_moles() * s.molefracs.dot(&ln_rho_m1) - }) + pub fn entropy(&self) -> FeosResult> { + Ok(self.total_moles()? * self.molar_entropy()) } } diff --git a/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs b/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs index 92e6932a5..81742b808 100644 --- a/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs +++ b/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs @@ -63,8 +63,9 @@ impl PhaseDiagram { None, SolverOptions::default(), )?; - let cp_vle = PhaseEquilibrium::from_states(cp.clone(), cp.clone()); - ([0.0, cp.molefracs[0]], (vle2, cp_vle), bubble) + let x_max = cp.molefracs[0]; + let cp_vle = PhaseEquilibrium::single_phase(cp); + ([0.0, x_max], (vle2, cp_vle), bubble) } [None, Some(vle1)] => { let cp = State::critical_point_binary( @@ -75,8 +76,9 @@ impl PhaseDiagram { None, SolverOptions::default(), )?; - let cp_vle = PhaseEquilibrium::from_states(cp.clone(), cp.clone()); - ([1.0, cp.molefracs[0]], (vle1, cp_vle), bubble) + let x_min = cp.molefracs[0]; + let cp_vle = PhaseEquilibrium::single_phase(cp); + ([1.0, x_min], (vle1, cp_vle), bubble) } [Some(vle2), Some(vle1)] => ([0.0, 1.0], (vle2, vle1), true), }; @@ -201,7 +203,7 @@ fn iterate_vle( let vle = PhaseEquilibrium::bubble_dew_point( eos, tp, - &dvector![*xi, 1.0 - xi], + dvector![*xi, 1.0 - xi], tp_old, y_old.as_ref(), bubble, @@ -604,7 +606,7 @@ impl PhaseEquilibrium { // check for convergence if res.norm() < options.tol.unwrap_or(TOL_HETERO) { - return Ok(Self([v, l1, l2])); + return Ok(Self::new(v, l1, l2)); } // calculate Jacobian @@ -724,7 +726,7 @@ impl PhaseEquilibrium { // check for convergence if res.norm() < options.tol.unwrap_or(TOL_HETERO) { - return Ok(Self([v, l1, l2])); + return Ok(Self::new(v, l1, l2)); } let jacobian = stack![ diff --git a/crates/feos-core/src/phase_equilibria/phase_diagram_pure.rs b/crates/feos-core/src/phase_equilibria/phase_diagram_pure.rs index 2382a974c..ab2c37cd0 100644 --- a/crates/feos-core/src/phase_equilibria/phase_diagram_pure.rs +++ b/crates/feos-core/src/phase_equilibria/phase_diagram_pure.rs @@ -54,7 +54,7 @@ impl PhaseDiagram { states.push(vle.clone()); } } - states.push(PhaseEquilibrium::from_states(sc.clone(), sc)); + states.push(PhaseEquilibrium::single_phase(sc)); Ok(PhaseDiagram::new(states)) } @@ -127,7 +127,7 @@ impl PhaseDiagram { .collect() }); - states.push(PhaseEquilibrium::from_states(sc.clone(), sc)); + states.push(PhaseEquilibrium::single_phase(sc)); Ok(PhaseDiagram::new(states)) } } diff --git a/crates/feos-core/src/phase_equilibria/phase_envelope.rs b/crates/feos-core/src/phase_equilibria/phase_envelope.rs index 0f01e7288..7f118a3cd 100644 --- a/crates/feos-core/src/phase_equilibria/phase_envelope.rs +++ b/crates/feos-core/src/phase_equilibria/phase_envelope.rs @@ -1,16 +1,16 @@ use super::{PhaseDiagram, PhaseEquilibrium}; -use crate::SolverOptions; use crate::equation_of_state::Residual; use crate::errors::FeosResult; use crate::state::{Contributions, State}; -use nalgebra::DVector; +use crate::{Composition, SolverOptions}; +use nalgebra::Dyn; use quantity::{Pressure, Temperature}; impl PhaseDiagram { /// Calculate the bubble point line of a mixture with given composition. - pub fn bubble_point_line( + pub fn bubble_point_line + Clone>( eos: &E, - molefracs: &DVector, + composition: X, min_temperature: Temperature, npoints: usize, critical_temperature: Option, @@ -20,7 +20,7 @@ impl PhaseDiagram { let sc = State::critical_point( eos, - molefracs, + composition.clone(), critical_temperature, None, SolverOptions::default(), @@ -40,7 +40,7 @@ impl PhaseDiagram { vle = PhaseEquilibrium::bubble_point( eos, ti, - molefracs, + composition.clone(), p_init, vapor_molefracs, options, @@ -51,15 +51,15 @@ impl PhaseDiagram { states.push(vle.clone()); } } - states.push(PhaseEquilibrium::from_states(sc.clone(), sc)); + states.push(PhaseEquilibrium::single_phase(sc)); Ok(PhaseDiagram::new(states)) } /// Calculate the dew point line of a mixture with given composition. - pub fn dew_point_line( + pub fn dew_point_line + Clone>( eos: &E, - molefracs: &DVector, + composition: X, min_temperature: Temperature, npoints: usize, critical_temperature: Option, @@ -69,7 +69,7 @@ impl PhaseDiagram { let sc = State::critical_point( eos, - molefracs, + composition.clone(), critical_temperature, None, SolverOptions::default(), @@ -86,9 +86,15 @@ impl PhaseDiagram { .as_ref() .map(|vle| vle.vapor().pressure(Contributions::Total)); let liquid_molefracs = vle.as_ref().map(|vle| &vle.liquid().molefracs); - vle = - PhaseEquilibrium::dew_point(eos, ti, molefracs, p_init, liquid_molefracs, options) - .ok(); + vle = PhaseEquilibrium::dew_point( + eos, + ti, + composition.clone(), + p_init, + liquid_molefracs, + options, + ) + .ok(); if let Some(vle) = vle.as_ref() { states.push(vle.clone()); } @@ -108,23 +114,29 @@ impl PhaseDiagram { for pi in &pressures { let t_init = vle.as_ref().map(|vle| vle.vapor().temperature); let liquid_molefracs = vle.as_ref().map(|vle| &vle.liquid().molefracs); - vle = - PhaseEquilibrium::dew_point(eos, pi, molefracs, t_init, liquid_molefracs, options) - .ok(); + vle = PhaseEquilibrium::dew_point( + eos, + pi, + composition.clone(), + t_init, + liquid_molefracs, + options, + ) + .ok(); if let Some(vle) = vle.as_ref() { states.push(vle.clone()); } } - states.push(PhaseEquilibrium::from_states(sc.clone(), sc)); + states.push(PhaseEquilibrium::single_phase(sc)); Ok(PhaseDiagram::new(states)) } /// Calculate the spinodal lines for a mixture with fixed composition. - pub fn spinodal( + pub fn spinodal + Clone>( eos: &E, - molefracs: &DVector, + composition: X, min_temperature: Temperature, npoints: usize, critical_temperature: Option, @@ -134,7 +146,7 @@ impl PhaseDiagram { let sc = State::critical_point( eos, - molefracs, + composition.clone(), critical_temperature, None, SolverOptions::default(), @@ -145,12 +157,12 @@ impl PhaseDiagram { let temperatures = Temperature::linspace(min_temperature, max_temperature, npoints - 1); for ti in &temperatures { - let spinodal = State::spinodal(eos, ti, molefracs, options).ok(); - if let Some(spinodal) = spinodal { - states.push(PhaseEquilibrium(spinodal)); + let spinodal = State::spinodal(eos, ti, composition.clone(), options).ok(); + if let Some([sp_v, sp_l]) = spinodal { + states.push(PhaseEquilibrium::two_phase(sp_v, sp_l)); } } - states.push(PhaseEquilibrium::from_states(sc.clone(), sc)); + states.push(PhaseEquilibrium::single_phase(sc)); Ok(PhaseDiagram::new(states)) } diff --git a/crates/feos-core/src/phase_equilibria/stability_analysis.rs b/crates/feos-core/src/phase_equilibria/stability_analysis.rs index 950f05026..ec2329da6 100644 --- a/crates/feos-core/src/phase_equilibria/stability_analysis.rs +++ b/crates/feos-core/src/phase_equilibria/stability_analysis.rs @@ -166,9 +166,11 @@ where let (n, _) = di.shape_generic(); // calculate residual and ideal hesse matrix - let mut hesse = self.n_dln_phi_dnj() / self.total_moles().into_reduced(); + // TODO: this should not require extensive properties, but I couldn't rewrite it + // quickly without breaking it. + let mut hesse = self.n_dln_phi_dnj() / self.total_moles().unwrap().into_reduced(); let lnphi = self.ln_phi(); - let y = self.moles().into_reduced(); + let y = self.moles().unwrap().into_reduced(); let ln_y = y.map(|y| if y > f64::EPSILON { y.ln() } else { 0.0 }); let sq_y = y.map(f64::sqrt); let gradient = (&ln_y + &lnphi - di).component_mul(&sq_y); diff --git a/crates/feos-core/src/phase_equilibria/tp_flash.rs b/crates/feos-core/src/phase_equilibria/tp_flash.rs index e955acaf6..88b30679e 100644 --- a/crates/feos-core/src/phase_equilibria/tp_flash.rs +++ b/crates/feos-core/src/phase_equilibria/tp_flash.rs @@ -2,13 +2,13 @@ use super::PhaseEquilibrium; use crate::equation_of_state::Residual; use crate::errors::{FeosError, FeosResult}; use crate::state::{Contributions, State}; -use crate::{ReferenceSystem, SolverOptions, Verbosity}; +use crate::{Composition, DensityInitialization, ReferenceSystem, SolverOptions, Verbosity}; use nalgebra::allocator::Allocator; use nalgebra::{DefaultAllocator, Dim, Matrix3, OVector, SVector, U1, U2, vector}; use num_dual::{ Dual, Dual2Vec, DualNum, DualStruct, Gradients, first_derivative, implicit_derivative_sp, }; -use quantity::{Dimensionless, MOL, MolarVolume, Moles, Pressure, Quantity, Temperature}; +use quantity::{MolarEnergy, MolarVolume, Pressure, RGAS, Temperature}; const MAX_ITER_TP: usize = 400; const TOL_TP: f64 = 1e-8; @@ -23,11 +23,11 @@ where /// /// The algorithm can be use to calculate phase equilibria of systems /// containing non-volatile components (e.g. ions). - pub fn tp_flash( + pub fn tp_flash>( eos: &E, temperature: Temperature, pressure: Pressure, - feed: &Moles>, + feed: X, initial_state: Option<&PhaseEquilibrium>, options: SolverOptions, non_volatile_components: Option>, @@ -45,17 +45,16 @@ impl, D: DualNum + Copy> PhaseEquilibrium { /// Compared to the version of the algorithm for a generic /// number of components ([tp_flash](PhaseEquilibrium::tp_flash)), /// this can be used in combination with automatic differentiation. - pub fn tp_flash_binary( + pub fn tp_flash_binary>( eos: &E, temperature: Temperature, pressure: Pressure, - feed: &Moles>, + feed: X, options: SolverOptions, ) -> FeosResult { - let z = feed.get(0).convert_into(feed.get(0) + feed.get(1)); - let total_moles = feed.sum(); - let moles = vector![z.re(), 1.0 - z.re()] * MOL; - let vle_re = State::new_npt(&eos.re(), temperature.re(), pressure.re(), moles, None)? + let (feed, total_moles) = feed.into_molefracs(eos)?; + let z = feed[0]; + let vle_re = State::new_npt(&eos.re(), temperature.re(), pressure.re(), z.re(), None)? .tp_flash(None, options, None)?; // implicit differentiation @@ -96,12 +95,16 @@ impl, D: DualNum + Copy> PhaseEquilibrium { .data .0; let beta = (z - x) / (y - x); - let state = |x: D, v, phi| { - let volume = MolarVolume::from_reduced(v * phi) * total_moles; - let moles = Quantity::new(vector![x, -x + 1.0] * phi * total_moles.convert_into(MOL)); - State::new_nvt(eos, temperature, volume, moles) + let state = |x: D, v| { + let density = MolarVolume::from_reduced(v).inv(); + State::new(eos, temperature, density, x) }; - Ok(Self([state(y, v_v, beta)?, state(x, v_l, -beta + 1.0)?])) + Ok(Self::with_vapor_phase_fraction( + state(y, v_v)?, + state(x, v_l)?, + beta, + total_moles, + )) } } @@ -123,13 +126,15 @@ where non_volatile_components: Option>, ) -> FeosResult> { // initialization - if let Some(init) = initial_state { - let vle = self.tp_flash_( - init.clone() - .update_pressure(self.temperature, self.pressure(Contributions::Total))?, - options, - non_volatile_components.clone(), - ); + if let Some(initial_state) = initial_state { + let mut init = initial_state.clone(); + init.update_states( + self, + initial_state.vapor().molefracs.clone(), + initial_state.liquid().molefracs.clone(), + initial_state.vapor_phase_fraction(), + )?; + let vle = self.tp_flash_(init, options, non_volatile_components.clone()); if vle.is_ok() { return vle; } @@ -184,14 +189,12 @@ where )?; // check convergence - // unwrap is safe here, because after the first successive substitution step the - // phase amounts in new_vle_state are known. - let beta = new_vle_state.vapor_phase_fraction().unwrap(); let tpd = [ self.tangent_plane_distance(new_vle_state.vapor()), self.tangent_plane_distance(new_vle_state.liquid()), ]; - let dg = (1.0 - beta) * tpd[1] + beta * tpd[0]; + let b = new_vle_state.phase_fractions; + let dg = b[0] * tpd[0] + b[1] * tpd[1]; // fix if only tpd[1] is positive if tpd[0] < 0.0 && dg >= 0.0 { @@ -200,7 +203,7 @@ where if let Some(nvc) = non_volatile_components.as_ref() { nvc.iter().for_each(|&c| k[c] = 0.0); } - new_vle_state.update_states(self, &k)?; + new_vle_state.rachford_rice_inplace(self, &k)?; new_vle_state.successive_substitution( self, 1, @@ -219,7 +222,7 @@ where if let Some(nvc) = non_volatile_components.as_ref() { nvc.iter().for_each(|&c| k[c] = 0.0); } - new_vle_state.update_states(self, &k)?; + new_vle_state.rachford_rice_inplace(self, &k)?; new_vle_state.successive_substitution( self, 1, @@ -289,7 +292,7 @@ where } // calculate total Gibbs energy before the extrapolation - let gibbs = self.total_gibbs_energy(); + let gibbs = self.molar_gibbs_energy(); // extrapolate K values let delta_vec = [ @@ -316,8 +319,8 @@ where // calculate new states let mut trial_vle_state = self.clone(); - trial_vle_state.update_states(feed_state, &k)?; - if trial_vle_state.total_gibbs_energy() < gibbs { + trial_vle_state.rachford_rice_inplace(feed_state, &k)?; + if trial_vle_state.molar_gibbs_energy() < gibbs { *self = trial_vle_state; } } @@ -367,7 +370,7 @@ where return Ok(true); } - self.update_states(feed_state, &k)?; + self.rachford_rice_inplace(feed_state, &k)?; if let Some(k_vec) = k_vec && i >= iterations - 3 { @@ -377,25 +380,41 @@ where Ok(false) } - fn update_states(&mut self, feed_state: &State, k: &OVector) -> FeosResult<()> { + fn rachford_rice_inplace( + &mut self, + feed_state: &State, + k: &OVector, + ) -> FeosResult<()> { // calculate vapor phase fraction using Rachford-Rice algorithm - let beta = self.vapor_phase_fraction(); - let beta = rachford_rice(&feed_state.molefracs, k, beta)?; - - // update VLE - let v = feed_state - .moles() - .clone() - .component_mul(&Dimensionless::new( - k.map(|k| beta * k / (1.0 - beta + beta * k)), - )); - let l = feed_state - .moles() - .clone() - .component_mul(&Dimensionless::new( - k.map(|k| (1.0 - beta) / (1.0 - beta + beta * k)), - )); - self.update_moles(feed_state.pressure(Contributions::Total), [&v, &l])?; + let (b, [v, l]) = + rachford_rice(&feed_state.molefracs, k, Some(self.vapor_phase_fraction()))?; + self.update_states(feed_state, v, l, b) + } + + fn update_states( + &mut self, + feed_state: &State, + vapor_molefracs: OVector, + liquid_molefracs: OVector, + beta: f64, + ) -> FeosResult<()> { + let vapor = State::new_npt( + &feed_state.eos, + feed_state.temperature, + feed_state.pressure(Contributions::Total), + vapor_molefracs, + Some(DensityInitialization::InitialDensity(self.vapor().density)), + )?; + let liquid = State::new_npt( + &feed_state.eos, + feed_state.temperature, + feed_state.pressure(Contributions::Total), + liquid_molefracs, + Some(DensityInitialization::InitialDensity(self.liquid().density)), + )?; + + *self = Self::with_vapor_phase_fraction(vapor, liquid, beta, feed_state.total_moles); + Ok(()) } @@ -404,23 +423,42 @@ where let state1 = stable_states.pop(); let state2 = stable_states.pop(); if let Some(s1) = state1 { - let init1 = Self::from_states(s1.clone(), feed_state.clone()); - if let Some(s2) = state2 { - Ok((Self::from_states(s1, s2), Some(init1))) + let init1 = if s1.density < feed_state.density { + Self::two_phase(s1.clone(), feed_state.clone()) } else { - Ok((init1, None)) - } + Self::two_phase(feed_state.clone(), s1.clone()) + }; + let init2 = state2.map(|s2| { + if s1.density < s2.density { + Self::two_phase(s1.clone(), s2.clone()) + } else { + Self::two_phase(s2.clone(), s1.clone()) + } + }); + Ok((init1, init2)) } else { Err(FeosError::NoPhaseSplit) } } + + // Total molar Gibbs energy excluding the constant contribution RT sum_i x_i ln(\Lambda_i^3) + fn molar_gibbs_energy(&self) -> MolarEnergy { + self.states + .iter() + .fold(MolarEnergy::from_reduced(0.0), |acc, s| { + let ln_rho_m1 = s.partial_density().to_reduced().map(|r| r.ln() - 1.0); + acc + s.residual_molar_helmholtz_energy() + + s.pressure(Contributions::Total) * s.molar_volume + + RGAS * s.temperature * s.molefracs.dot(&ln_rho_m1) + }) + } } fn rachford_rice( feed: &OVector, k: &OVector, beta_in: Option, -) -> FeosResult +) -> FeosResult<(f64, [OVector; 2])> where DefaultAllocator: Allocator, { @@ -494,9 +532,16 @@ where beta = 0.5 * (beta_min + beta_max); } if dbeta.abs() < ABS_TOL { - return Ok(beta); + // update VLE + let v = feed.component_mul(&k.map(|k| beta * k / (1.0 - beta + beta * k))); + let l = feed.component_mul(&k.map(|k| (1.0 - beta) / (1.0 - beta + beta * k))); + return Ok((beta, [v, l])); } } - Ok(beta) + // update VLE + let v = feed.component_mul(&k.map(|k| beta * k / (1.0 - beta + beta * k))); + let l = feed.component_mul(&k.map(|k| (1.0 - beta) / (1.0 - beta + beta * k))); + + Ok((beta, [v, l])) } diff --git a/crates/feos-core/src/phase_equilibria/vle_pure.rs b/crates/feos-core/src/phase_equilibria/vle_pure.rs index 63756fd5b..f58fc6e21 100644 --- a/crates/feos-core/src/phase_equilibria/vle_pure.rs +++ b/crates/feos-core/src/phase_equilibria/vle_pure.rs @@ -3,7 +3,7 @@ use crate::density_iteration::{_density_iteration, _pressure_spinodal}; use crate::equation_of_state::{Residual, Subset}; use crate::errors::{FeosError, FeosResult}; use crate::state::{Contributions, DensityInitialization, State}; -use crate::{Composition, ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity}; +use crate::{ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity}; use nalgebra::allocator::Allocator; use nalgebra::{DVector, DefaultAllocator, Dim, SVector, U1, U2}; use num_dual::{DualNum, DualStruct, Gradients, gradient, partial}; @@ -17,7 +17,6 @@ const TOL_PURE: f64 = 1e-12; impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator + Allocator + Allocator, - (): Composition + Composition, { /// Calculate a phase equilibrium for a pure component. pub fn pure>( @@ -26,7 +25,7 @@ where initial_state: Option<&Self>, options: SolverOptions, ) -> FeosResult { - let (t, rho) = if let Some(t) = temperature_or_pressure.temperature() { + let (t, [rho_v, rho_l]) = if let Some(t) = temperature_or_pressure.temperature() { let (_, rho) = Self::pure_t(eos, t, initial_state, options)?; (t, rho) } else if let Some(p) = temperature_or_pressure.pressure() { @@ -34,7 +33,11 @@ where } else { unreachable!() }; - Ok(Self(rho.map(|r| State::new_pure(eos, t, r).unwrap()))) + let x = E::pure_molefracs(); + Ok(Self::two_phase( + State::new(eos, t, rho_v, &x)?, + State::new(eos, t, rho_l, x)?, + )) } /// Calculate a phase equilibrium for a pure component @@ -238,7 +241,6 @@ where impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator + Allocator + Allocator, - (): Composition, { /// Calculate a phase equilibrium for a pure component /// and given pressure. @@ -396,7 +398,6 @@ fn init_pure_p, N: Gradients>( ) -> FeosResult<(f64, [f64; 2])> where DefaultAllocator: Allocator + Allocator + Allocator, - (): Composition, { let trial_temperatures = [300.0, 500.0, 200.0]; let p = pressure.into_reduced(); @@ -416,7 +417,7 @@ where }; let [mut t_v, mut t_l] = [t0, t0]; - let cp = State::critical_point(eos, (), None, None, SolverOptions::default())?; + let cp = State::critical_point(eos, &x, None, None, SolverOptions::default())?; let cp_density = cp.density.into_reduced(); if pressure > cp.pressure(Contributions::Total) { return Err(FeosError::SuperCritical); @@ -540,7 +541,7 @@ impl PhaseEquilibrium { vle_pure.liquid().density, molefracs_liquid, )?; - Ok(PhaseEquilibrium::from_states(vapor, liquid)) + Ok(PhaseEquilibrium::two_phase(vapor, liquid)) }) .ok() }) diff --git a/crates/feos-core/src/state/composition.rs b/crates/feos-core/src/state/composition.rs index 0ad47ac9e..f6a190e33 100644 --- a/crates/feos-core/src/state/composition.rs +++ b/crates/feos-core/src/state/composition.rs @@ -4,7 +4,7 @@ use crate::{FeosError, FeosResult}; use nalgebra::allocator::Allocator; use nalgebra::{DefaultAllocator, Dim, Dyn, OVector, U1, U2, dvector, vector}; use num_dual::{DualNum, DualStruct}; -use quantity::{Density, Moles, Quantity, SIUnit}; +use quantity::Moles; pub trait Composition + Copy, N: Dim> where @@ -15,16 +15,6 @@ where self, eos: &E, ) -> FeosResult<(OVector, Option>)>; - fn density(&self) -> Option> { - None - } -} - -pub trait FullComposition + Copy, N: Dim>: Composition -where - DefaultAllocator: Allocator, -{ - fn into_moles>(self, eos: &E) -> FeosResult<(OVector, Moles)>; } // trivial implementations @@ -40,15 +30,6 @@ where } } -impl + Copy, N: Dim> FullComposition for (OVector, Moles) -where - DefaultAllocator: Allocator, -{ - fn into_moles>(self, _: &E) -> FeosResult<(OVector, Moles)> { - Ok((self.0, self.1)) - } -} - impl + Copy, N: Dim> Composition for (OVector, Option>) where DefaultAllocator: Allocator, @@ -136,12 +117,6 @@ impl + Copy> Composition for Moles { } } -impl + Copy> FullComposition for Moles { - fn into_moles>(self, _: &E) -> FeosResult<(OVector, Moles)> { - Ok(((vector![D::one()]), self)) - } -} - impl + Copy> Composition for Moles { fn into_molefracs>( self, @@ -158,19 +133,6 @@ impl + Copy> Composition for Moles { } } -impl + Copy> FullComposition for Moles { - fn into_moles>(self, eos: &E) -> FeosResult<(OVector, Moles)> { - if eos.components() == 1 { - Ok(((dvector![D::one()]), self)) - } else { - Err(FeosError::UndeterminedState(format!( - "A single mole number ({}) can only be used to specify a pure component!", - self.re() - ))) - } - } -} - // the mixture can be specified by its molefractions // // for a dynamic number of components, it is also possible to specify only the @@ -228,15 +190,6 @@ where } } -impl + Copy, N: Dim> FullComposition for Moles> -where - DefaultAllocator: Allocator, -{ - fn into_moles>(self, eos: &E) -> FeosResult<(OVector, Moles)> { - (&self).into_moles(eos) - } -} - impl + Copy, N: Dim> Composition for &Moles> where DefaultAllocator: Allocator, @@ -257,48 +210,3 @@ where } } } - -impl + Copy, N: Dim> FullComposition for &Moles> -where - DefaultAllocator: Allocator, -{ - fn into_moles>(self, eos: &E) -> FeosResult<(OVector, Moles)> { - if eos.components() == self.len() { - let total_moles = self.sum(); - Ok(((self.convert_to(total_moles)), total_moles)) - } else { - Err(FeosError::UndeterminedState(format!( - "The length of the composition vector ({}) does not match the number of components ({})!", - self.len(), - eos.components() - ))) - } - } -} - -// the mixture can be specified with the partial density -impl + Copy, N: Dim> Composition - for Quantity, SIUnit<0, -3, 0, 0, 0, 1, 0>> -where - DefaultAllocator: Allocator, -{ - fn into_molefracs>( - self, - eos: &E, - ) -> FeosResult<(OVector, Option>)> { - if eos.components() == self.len() { - let density = self.sum(); - Ok(((self.convert_to(density)), None)) - } else { - panic!( - "The length of the composition vector ({}) does not match the number of components ({})!", - self.len(), - eos.components() - ) - } - } - - fn density(&self) -> Option> { - Some(self.sum()) - } -} diff --git a/crates/feos-core/src/state/mod.rs b/crates/feos-core/src/state/mod.rs index ec99d4f19..716468aa8 100644 --- a/crates/feos-core/src/state/mod.rs +++ b/crates/feos-core/src/state/mod.rs @@ -23,7 +23,7 @@ mod properties; mod residual_properties; mod statevec; pub(crate) use cache::Cache; -pub use composition::{Composition, FullComposition}; +pub use composition::Composition; pub use statevec::StateVec; /// Possible contributions that can be computed. @@ -177,18 +177,18 @@ where } /// Mole numbers $N_i$ - pub fn moles(&self) -> Moles> { - Dimensionless::new(&self.molefracs) * self.total_moles() + pub fn moles(&self) -> FeosResult>> { + Ok(Dimensionless::new(&self.molefracs) * self.total_moles()?) } /// Total moles $N=\sum_iN_i$ - pub fn total_moles(&self) -> Moles { - self.total_moles.expect("Extensive properties can only be evaluated for states that are initialized with extensive properties!") + pub fn total_moles(&self) -> FeosResult> { + self.total_moles.ok_or(FeosError::IntensiveState) } /// Volume $V$ - pub fn volume(&self) -> Volume { - self.molar_volume * self.total_moles() + pub fn volume(&self) -> FeosResult> { + Ok(self.molar_volume * self.total_moles()?) } } @@ -225,13 +225,19 @@ where /// This function will perform a validation of the given properties, i.e. test for signs /// and if values are finite. It will **not** validate physics, i.e. if the resulting /// densities are below the maximum packing fraction. - pub fn new_nvt>( + pub fn new_nvt>( eos: &E, temperature: Temperature, volume: Volume, composition: X, ) -> FeosResult { - let (molefracs, total_moles) = composition.into_moles(eos)?; + let (molefracs, total_moles) = composition.into_molefracs(eos)?; + let Some(total_moles) = total_moles else { + return Err(FeosError::UndeterminedState( + "Missing total mole number in the specification!".into(), + )); + }; + let density = total_moles / volume; Self::new(eos, temperature, density, (molefracs, total_moles)) } @@ -247,7 +253,8 @@ where partial_density: Density>, ) -> FeosResult { let density = partial_density.sum(); - Self::new(eos, temperature, density, partial_density) + let molefracs = partial_density.convert_into(density); + Self::new(eos, temperature, density, molefracs) } /// Return a new `State` for a pure component given a temperature and a density. @@ -340,14 +347,6 @@ where pressure: Option>, density_initialization: Option, ) -> FeosResult> { - // check if density is given twice - if density.and(composition.density()).is_some() { - return Err(FeosError::UndeterminedState(String::from( - "Both density and partial density given.", - ))); - } - let density = density.or_else(|| composition.density()); - // unwrap composition let (x, n) = composition.into_molefracs(eos)?; @@ -586,7 +585,7 @@ where } /// Return a new `State` for given volume $V$ and molar internal energy $u$. - pub fn new_nvu + Clone>( + pub fn new_nvu + Clone>( eos: &E, volume: Volume, molar_internal_energy: MolarEnergy, diff --git a/crates/feos-core/src/state/properties.rs b/crates/feos-core/src/state/properties.rs index c93c1e77d..fa0f53b4a 100644 --- a/crates/feos-core/src/state/properties.rs +++ b/crates/feos-core/src/state/properties.rs @@ -1,6 +1,6 @@ use super::{Contributions, State}; use crate::equation_of_state::{Molarweight, Total}; -use crate::{ReferenceSystem, Residual}; +use crate::{FeosResult, ReferenceSystem, Residual}; use nalgebra::allocator::Allocator; use nalgebra::{DefaultAllocator, OVector}; use num_dual::{Dual, DualNum, Gradients, partial, partial2}; @@ -79,8 +79,8 @@ where } /// Entropy: $S=-\left(\frac{\partial A}{\partial T}\right)_{V,N_i}$ - pub fn entropy(&self, contributions: Contributions) -> Entropy { - self.molar_entropy(contributions) * self.total_moles() + pub fn entropy(&self, contributions: Contributions) -> FeosResult> { + Ok(self.molar_entropy(contributions) * self.total_moles()?) } /// Molar entropy: $s=\frac{S}{N}$ @@ -147,8 +147,8 @@ where } /// Enthalpy: $H=A+TS+pV$ - pub fn enthalpy(&self, contributions: Contributions) -> Energy { - self.molar_enthalpy(contributions) * self.total_moles() + pub fn enthalpy(&self, contributions: Contributions) -> FeosResult> { + Ok(self.molar_enthalpy(contributions) * self.total_moles()?) } /// Molar enthalpy: $h=\frac{H}{N}$ @@ -166,8 +166,8 @@ where } /// Helmholtz energy: $A$ - pub fn helmholtz_energy(&self, contributions: Contributions) -> Energy { - self.molar_helmholtz_energy(contributions) * self.total_moles() + pub fn helmholtz_energy(&self, contributions: Contributions) -> FeosResult> { + Ok(self.molar_helmholtz_energy(contributions) * self.total_moles()?) } /// Molar Helmholtz energy: $a=\frac{A}{N}$ @@ -187,8 +187,8 @@ where } /// Internal energy: $U=A+TS$ - pub fn internal_energy(&self, contributions: Contributions) -> Energy { - self.molar_internal_energy(contributions) * self.total_moles() + pub fn internal_energy(&self, contributions: Contributions) -> FeosResult> { + Ok(self.molar_internal_energy(contributions) * self.total_moles()?) } /// Molar internal energy: $u=\frac{U}{N}$ @@ -198,8 +198,8 @@ where } /// Gibbs energy: $G=A+pV$ - pub fn gibbs_energy(&self, contributions: Contributions) -> Energy { - self.molar_gibbs_energy(contributions) * self.total_moles() + pub fn gibbs_energy(&self, contributions: Contributions) -> FeosResult> { + Ok(self.molar_gibbs_energy(contributions) * self.total_moles()?) } /// Molar Gibbs energy: $g=\frac{G}{N}$ diff --git a/crates/feos-core/src/state/residual_properties.rs b/crates/feos-core/src/state/residual_properties.rs index 4a6b8f015..184f1aa53 100644 --- a/crates/feos-core/src/state/residual_properties.rs +++ b/crates/feos-core/src/state/residual_properties.rs @@ -34,8 +34,8 @@ where } /// Residual Helmholtz energy $A^\text{res}$ - pub fn residual_helmholtz_energy(&self) -> Energy { - self.residual_molar_helmholtz_energy() * self.total_moles() + pub fn residual_helmholtz_energy(&self) -> FeosResult> { + Ok(self.residual_molar_helmholtz_energy() * self.total_moles()?) } /// Residual molar Helmholtz energy $a^\text{res}$ @@ -50,8 +50,8 @@ where } /// Residual entropy $S^\text{res}=\left(\frac{\partial A^\text{res}}{\partial T}\right)_{V,N_i}$ - pub fn residual_entropy(&self) -> Entropy { - self.residual_molar_entropy() * self.total_moles() + pub fn residual_entropy(&self) -> FeosResult> { + Ok(self.residual_molar_entropy() * self.total_moles()?) } /// Residual molar entropy $s^\text{res}=\left(\frac{\partial a^\text{res}}{\partial T}\right)_{V,N_i}$ @@ -431,18 +431,14 @@ impl State { .unzip(); let solvent_molefracs = DVector::from_vec(solvent_molefracs); let solvent = eos.subset(&solvent_comps); - let vle = if solvent_comps.len() == 1 { - PhaseEquilibrium::pure(&solvent, temperature, None, Default::default()) - } else { - PhaseEquilibrium::bubble_point( - &solvent, - temperature, - &solvent_molefracs, - None, - None, - Default::default(), - ) - }?; + let vle = PhaseEquilibrium::bubble_point( + &solvent, + temperature, + &solvent_molefracs, + None, + None, + Default::default(), + )?; // Calculate the liquid state including the Henry components let liquid = State::new(eos, temperature, vle.liquid().density, molefracs.clone())?; @@ -506,8 +502,8 @@ where } /// Residual enthalpy: $H^\text{res}(T,p,\mathbf{n})=A^\text{res}+TS^\text{res}+p^\text{res}V$ - pub fn residual_enthalpy(&self) -> Energy { - self.residual_molar_enthalpy() * self.total_moles() + pub fn residual_enthalpy(&self) -> FeosResult> { + Ok(self.residual_molar_enthalpy() * self.total_moles()?) } /// Residual molar enthalpy: $h^\text{res}(T,p,\mathbf{n})=a^\text{res}+Ts^\text{res}+p^\text{res}v$ @@ -518,8 +514,8 @@ where } /// Residual internal energy: $U^\text{res}(T,V,\mathbf{n})=A^\text{res}+TS^\text{res}$ - pub fn residual_internal_energy(&self) -> Energy { - self.residual_molar_internal_energy() * self.total_moles() + pub fn residual_internal_energy(&self) -> FeosResult> { + Ok(self.residual_molar_internal_energy() * self.total_moles()?) } /// Residual molar internal energy: $u^\text{res}(T,V,\mathbf{n})=a^\text{res}+Ts^\text{res}$ @@ -528,8 +524,8 @@ where } /// 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) -> Energy { - self.residual_molar_gibbs_energy() * self.total_moles() + pub fn residual_gibbs_energy(&self) -> FeosResult> { + Ok(self.residual_molar_gibbs_energy() * self.total_moles()?) } /// Residual Gibbs energy: $g^\text{res}(T,p,\mathbf{n})=a^\text{res}+p^\text{res}v-RT \ln Z$ @@ -601,16 +597,17 @@ where } /// Mass of each component: $m_i=n_iMW_i$ - pub fn mass(&self) -> Mass> { - self.eos + pub fn mass(&self) -> FeosResult>> { + Ok(self + .eos .molar_weight() .component_mul(&Dimensionless::new(self.molefracs.clone())) - * self.total_moles() + * self.total_moles()?) } /// Total mass: $m=\sum_im_i=nMW$ - pub fn total_mass(&self) -> Mass { - self.total_molar_weight() * self.total_moles() + pub fn total_mass(&self) -> FeosResult> { + Ok(self.total_molar_weight() * self.total_moles()?) } /// Mass density: $\rho^{(m)}=\frac{m}{V}$ diff --git a/crates/feos-core/src/state/statevec.rs b/crates/feos-core/src/state/statevec.rs index 63d51b3fd..8a498e2db 100644 --- a/crates/feos-core/src/state/statevec.rs +++ b/crates/feos-core/src/state/statevec.rs @@ -2,6 +2,8 @@ use super::Contributions; use super::State; #[cfg(feature = "ndarray")] +use crate::FeosResult; +#[cfg(feature = "ndarray")] use crate::equation_of_state::{Molarweight, Residual, Total}; #[cfg(feature = "ndarray")] use ndarray::{Array1, Array2}; @@ -61,10 +63,15 @@ impl StateVec<'_, E> { Density::from_shape_fn(self.0.len(), |i| self.0[i].density) } - pub fn moles(&self) -> Moles> { - Moles::from_shape_fn((self.0.len(), self.0[0].eos.components()), |(i, j)| { - self.0[i].moles().get(j) - }) + pub fn moles(&self) -> FeosResult>> { + if let Err(e) = self.0[0].moles() { + Err(e) + } else { + Ok(Moles::from_shape_fn( + (self.0.len(), self.0[0].eos.components()), + |(i, j)| self.0[i].moles().unwrap().get(j), + )) + } } pub fn molefracs(&self) -> Array2 { diff --git a/crates/feos/benches/contributions.rs b/crates/feos/benches/contributions.rs index 3e74bf68c..7dc8332c2 100644 --- a/crates/feos/benches/contributions.rs +++ b/crates/feos/benches/contributions.rs @@ -74,7 +74,7 @@ fn pcsaft(c: &mut Criterion) { State::new_npt(&&eos, t, p, &moles, Some(DensityInitialization::Liquid)).unwrap(); let temperature = Dual64::from(state.temperature.into_reduced()).derivative(); let molar_volume = Dual::from(1.0 / state.density.into_reduced()); - let moles = state.moles().to_reduced().map(Dual::from); + let moles = state.moles().unwrap().to_reduced().map(Dual::from); // let state_hd = state.derive1(Derivative::DT); let name1 = comp1.identifier.name.as_deref().unwrap(); let name2 = comp2.identifier.name.as_deref().unwrap(); diff --git a/crates/feos/benches/dft_pore.rs b/crates/feos/benches/dft_pore.rs index 98dcba4e2..4c5b3d7ba 100644 --- a/crates/feos/benches/dft_pore.rs +++ b/crates/feos/benches/dft_pore.rs @@ -67,15 +67,9 @@ fn pcsaft(c: &mut Criterion) { ) .unwrap(); let func = &PcSaftFunctional::new(parameters); - let vle = PhaseEquilibrium::bubble_point( - &func, - 300.0 * KELVIN, - &dvector![0.5, 0.5], - None, - None, - Default::default(), - ) - .unwrap(); + let vle = + PhaseEquilibrium::bubble_point(&func, 300.0 * KELVIN, 0.5, None, None, Default::default()) + .unwrap(); let bulk = vle.liquid(); group.bench_function("butane_pentane_liquid", |b| { b.iter(|| pore.initialize(bulk, None, None).unwrap().solve(None)) diff --git a/crates/feos/benches/dual_numbers.rs b/crates/feos/benches/dual_numbers.rs index c3c1bd07f..eb2eff2e4 100644 --- a/crates/feos/benches/dual_numbers.rs +++ b/crates/feos/benches/dual_numbers.rs @@ -20,10 +20,9 @@ use quantity::*; /// - molefracs (or moles) for equimolar mixture. fn state_pcsaft(n: usize, eos: &PcSaft) -> State<&PcSaft> { let moles = DVector::from_element(n, 1.0 / n as f64) * 10.0 * MOL; - let molefracs = (&moles / moles.sum()).into_value(); - let cp = State::critical_point(&eos, molefracs, None, None, Default::default()).unwrap(); + let cp = State::critical_point(&eos, &moles, None, None, Default::default()).unwrap(); let temperature = 0.8 * cp.temperature; - State::new_nvt(&eos, temperature, cp.volume(), moles).unwrap() + State::new_nvt(&eos, temperature, cp.volume().unwrap(), moles).unwrap() } /// Residual Helmholtz energy given an equation of state and a StateHD. @@ -152,11 +151,10 @@ enum Derivative { /// Creates a [StateHD] cloning temperature, volume and moles. fn derive0(state: &State) -> StateHD { - let total_moles = state.total_moles().into_reduced(); StateHD::new( state.temperature.into_reduced(), - state.volume().into_reduced() / total_moles, - &(state.moles().to_reduced() / total_moles), + state.molar_volume.into_reduced(), + &state.molefracs, ) } diff --git a/crates/feos/benches/dual_numbers_saftvrmie.rs b/crates/feos/benches/dual_numbers_saftvrmie.rs index e7409e307..80f4da09d 100644 --- a/crates/feos/benches/dual_numbers_saftvrmie.rs +++ b/crates/feos/benches/dual_numbers_saftvrmie.rs @@ -20,7 +20,13 @@ fn state_saftvrmie(n: usize, eos: &SaftVRMie) -> State<&SaftVRMie> { let molefracs = DVector::from_element(n, 1.0 / n as f64); let cp = State::critical_point(&eos, &molefracs, None, None, Default::default()).unwrap(); let temperature = 0.8 * cp.temperature; - State::new_nvt(&eos, temperature, cp.volume(), &(molefracs * 10. * MOL)).unwrap() + State::new_nvt( + &eos, + temperature, + cp.volume().unwrap(), + &(molefracs * 10. * MOL), + ) + .unwrap() } /// Residual Helmholtz energy given an equation of state and a StateHD. @@ -100,11 +106,10 @@ enum Derivative { /// Creates a [StateHD] cloning temperature, volume and moles. fn derive0(state: &State) -> StateHD { - let total_moles = state.total_moles().into_reduced(); StateHD::new( state.temperature.into_reduced(), - state.volume().into_reduced() / total_moles, - &(state.moles().to_reduced() / total_moles), + state.molar_volume.into_reduced(), + &state.molefracs, ) } diff --git a/crates/feos/benches/state_creation.rs b/crates/feos/benches/state_creation.rs index 3aa08778e..eb0c07bcd 100644 --- a/crates/feos/benches/state_creation.rs +++ b/crates/feos/benches/state_creation.rs @@ -77,7 +77,7 @@ fn bench_states(c: &mut Criterion, group_name: &str, eos: &E) { eos, crit.temperature, crit.pressure(Contributions::Total) * 0.95, - &crit.moles(), + &crit.molefracs, None, Default::default(), None, diff --git a/crates/feos/src/epcsaft/eos/mod.rs b/crates/feos/src/epcsaft/eos/mod.rs index 2edba6f69..5a0a8c81f 100644 --- a/crates/feos/src/epcsaft/eos/mod.rs +++ b/crates/feos/src/epcsaft/eos/mod.rs @@ -181,7 +181,7 @@ mod tests { let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&&e, t, v, &n).unwrap(); - let p_ig = s.total_moles() * RGAS * t / v; + let p_ig = s.total_moles().unwrap() * 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::Residual), @@ -197,7 +197,7 @@ mod tests { let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&&e, t, v, &n).unwrap(); - let p_ig = s.total_moles() * RGAS * t / v; + let p_ig = s.total_moles().unwrap() * 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::Residual), diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index 752e7c6f6..37a885248 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -394,35 +394,37 @@ mod tests { use quantity::{BAR, KELVIN, METER, PASCAL, RGAS}; #[test] - fn ideal_gas_pressure() { + fn ideal_gas_pressure() -> FeosResult<()> { let e = &propane_parameters(); let t = 200.0 * KELVIN; let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&e, t, v, n).unwrap(); - let p_ig = s.total_moles() * RGAS * t / v; + 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::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); + Ok(()) } #[test] - fn ideal_gas_heat_capacity_joback() { + fn ideal_gas_heat_capacity_joback() -> FeosResult<()> { let e = &propane_parameters(); let t = 200.0 * KELVIN; let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&e, t, v, n).unwrap(); - let p_ig = s.total_moles() * RGAS * t / v; + 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::Residual), s.pressure(Contributions::Total), epsilon = 1e-10 ); + Ok(()) } #[test] @@ -599,7 +601,7 @@ mod tests_parameter_fit { use feos_core::{Contributions, PropertiesAD, ReferenceSystem, SolverOptions}; use feos_core::{FeosResult, ParametersAD, PhaseEquilibrium, State}; use nalgebra::{U1, U3, U8, vector}; - use num_dual::{DualStruct, DualVec, partial}; + use num_dual::{Dual64, DualStruct, DualVec, partial}; use quantity::{BAR, KELVIN, LITER, MOL, PASCAL}; fn pcsaft_non_assoc() -> PcSaftPure { @@ -787,9 +789,7 @@ mod tests_parameter_fit { let h = params[i] * 1e-7; params[i] += h; let pcsaft_h = PcSaftPure(params); - let rho_h = - State::new_npt(&pcsaft_h, temperature, pressure, vector![1.0], Some(Liquid))? - .density; + let rho_h = State::new_npt(&pcsaft_h, temperature, pressure, (), Some(Liquid))?.density; let drho_h = (rho_h.convert_into(MOL / LITER) - rho) / h; let drho = grad[i]; println!( @@ -823,7 +823,7 @@ mod tests_parameter_fit { let p_h = PhaseEquilibrium::bubble_point( &pcsaft_h, temperature, - &x, + x, None, None, Default::default(), @@ -846,7 +846,7 @@ mod tests_parameter_fit { let (pcsaft, _) = pcsaft_binary()?; let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); let temperature = 500.0 * KELVIN; - let y = vector![0.5, 0.5]; + let y = 0.5; let p = pcsaft_ad.dew_point_pressure(temperature, None, y)?; let p = p.convert_into(BAR); let (p, [[grad]]) = (p.re, p.eps.unwrap_generic(U1, U1).data.0); @@ -858,16 +858,10 @@ mod tests_parameter_fit { let h = 1e-7; kij += h; let pcsaft_h = PcSaftBinary::new(params, kij); - let p_h = PhaseEquilibrium::dew_point( - &pcsaft_h, - temperature, - &y, - None, - None, - Default::default(), - )? - .vapor() - .pressure(Contributions::Total); + let p_h = + PhaseEquilibrium::dew_point(&pcsaft_h, temperature, y, None, None, Default::default())? + .vapor() + .pressure(Contributions::Total); let dp_h = (p_h.convert_into(BAR) - p) / h; println!( "k_ij: {:11.5} {:11.5} {:.3e}", @@ -885,11 +879,11 @@ mod tests_parameter_fit { let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); let pressure = Pressure::from_reduced(DualVec::from(45. * BAR.into_reduced())); let t_init = Temperature::from_reduced(DualVec::from(500.0)); - let x = vector![0.5, 0.5].map(DualVec::from); + let x = DualVec::from(0.5); let t = PhaseEquilibrium::bubble_point( &pcsaft_ad, pressure, - &x, + x, Some(t_init), None, Default::default(), @@ -909,7 +903,7 @@ mod tests_parameter_fit { let t_h = PhaseEquilibrium::bubble_point( &pcsaft_h, pressure.re(), - &x.map(|x| x.re()), + x.re(), Some(t_init.re()), None, Default::default(), @@ -933,11 +927,11 @@ mod tests_parameter_fit { let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); let pressure = Pressure::from_reduced(DualVec::from(45. * BAR.into_reduced())); let t_init = Temperature::from_reduced(DualVec::from(500.0)); - let x = vector![0.5, 0.5].map(DualVec::from); + let x = DualVec::from(0.5); let t = PhaseEquilibrium::dew_point( &pcsaft_ad, pressure, - &x, + x, Some(t_init), None, Default::default(), @@ -957,7 +951,7 @@ mod tests_parameter_fit { let t_h = PhaseEquilibrium::dew_point( &pcsaft_h, pressure.re(), - &x.map(|x| x.re()), + x.re(), Some(t_init.re()), None, Default::default(), @@ -980,10 +974,10 @@ mod tests_parameter_fit { let (pcsaft, _) = pcsaft_binary()?; let pcsaft_ad = pcsaft; let mut temperature = 500.0 * KELVIN; - let x = vector![0.5, 0.5]; + let x = 0.5; let (p, grad) = first_derivative( partial( - |t, x| { + |t, &x: &Dual64| { let eos = pcsaft_ad.lift(); PhaseEquilibrium::bubble_point(&eos, t, x, None, None, Default::default()) .unwrap() @@ -1003,7 +997,7 @@ mod tests_parameter_fit { let p_h = PhaseEquilibrium::bubble_point( &pcsaft_ad, temperature, - &x, + x, None, None, Default::default(), @@ -1017,7 +1011,7 @@ mod tests_parameter_fit { grad, ((dp_h - grad).convert_into(grad)).abs() ); - assert_relative_eq!(grad, dp_h, max_relative = 1e-7); + assert_relative_eq!(grad, dp_h, max_relative = 2e-7); Ok(()) } @@ -1027,10 +1021,10 @@ mod tests_parameter_fit { let pcsaft_ad = pcsaft; let mut pressure = 45. * BAR; let t0 = Some(500. * KELVIN); - let x = vector![0.5, 0.5]; + let x = 0.5; let (t, grad) = first_derivative( partial2( - |p, x, &t0| { + |p, &x: &Dual64, &t0| { let eos = pcsaft_ad.lift(); PhaseEquilibrium::bubble_point(&eos, p, x, t0, None, Default::default()) .unwrap() @@ -1049,7 +1043,7 @@ mod tests_parameter_fit { let h = 1e-5 * BAR; pressure += h; let t_h = - PhaseEquilibrium::bubble_point(&pcsaft_ad, pressure, &x, t0, None, Default::default())? + PhaseEquilibrium::bubble_point(&pcsaft_ad, pressure, x, t0, None, Default::default())? .vapor() .temperature; let dt_h = (t_h - t) / h; @@ -1059,7 +1053,7 @@ mod tests_parameter_fit { grad, ((dt_h - grad).convert_into(grad)).abs() ); - assert_relative_eq!(grad, dt_h, max_relative = 1e-7); + assert_relative_eq!(grad, dt_h, max_relative = 2e-7); Ok(()) } @@ -1069,22 +1063,19 @@ mod tests_parameter_fit { let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); let temperature = 500.0 * KELVIN; let pressure = 44.6 * BAR; - let x = vector![0.5, 0.5]; + let x = 0.5; let vle = PhaseEquilibrium::tp_flash_binary( &pcsaft_ad, Temperature::from_inner(&temperature), Pressure::from_inner(&pressure), - &Moles::from_inner(&(x * MOL)), + DualVec::from(x), SolverOptions { verbosity: feos_core::Verbosity::Iter, - tol: Some(1e-10), + tol: Some(1e-12), ..Default::default() }, )?; - let beta = vle - .vapor() - .total_moles - .convert_into(vle.vapor().total_moles + vle.liquid().total_moles); + let beta = vle.vapor_phase_fraction(); let (beta, [[grad]]) = (beta.re, beta.eps.unwrap_generic(U1, U1).data.0); println!("{beta:.5}"); @@ -1098,16 +1089,13 @@ mod tests_parameter_fit { &pcsaft_h, temperature, pressure, - &(x * MOL), + x, SolverOptions { - tol: Some(1e-10), + tol: Some(1e-12), ..Default::default() }, )?; - let beta_h = vle - .vapor() - .total_moles - .convert_into(vle.vapor().total_moles + vle.liquid().total_moles); + let beta_h = vle.vapor_phase_fraction(); let dbeta_h = (beta_h - beta) / h; println!( "k_ij: {:11.5} {:11.5} {:.3e}", diff --git a/crates/feos/src/pets/eos/mod.rs b/crates/feos/src/pets/eos/mod.rs index e26b94960..5d9b9a666 100644 --- a/crates/feos/src/pets/eos/mod.rs +++ b/crates/feos/src/pets/eos/mod.rs @@ -152,7 +152,7 @@ mod tests { let v = 1e-3 * METER.powi::<3>(); let n = dvector![1.0] * MOL; let s = State::new_nvt(&e, t, v, &n).unwrap(); - let p_ig = s.total_moles() * RGAS * t / v; + let p_ig = s.total_moles().unwrap() * 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::Residual), diff --git a/crates/feos/src/uvtheory/eos/mod.rs b/crates/feos/src/uvtheory/eos/mod.rs index 8a660db81..1957af6e8 100644 --- a/crates/feos/src/uvtheory/eos/mod.rs +++ b/crates/feos/src/uvtheory/eos/mod.rs @@ -246,9 +246,7 @@ mod test { // EoS let eos_wca = &UVTheory::new(parameters); let state_wca = State::new_nvt(&eos_wca, t_x, volume, &moles).unwrap(); - let a_wca = (state_wca.residual_helmholtz_energy() - / (RGAS * t_x * state_wca.total_moles())) - .into_value(); + let a_wca = (state_wca.residual_molar_helmholtz_energy() / (RGAS * t_x)).into_value(); assert_relative_eq!(a_wca, -0.597791038364405, max_relative = 1e-5); Ok(()) diff --git a/crates/feos/tests/pcsaft/stability_analysis.rs b/crates/feos/tests/pcsaft/stability_analysis.rs index bf31ee24d..dc1cf71d0 100644 --- a/crates/feos/tests/pcsaft/stability_analysis.rs +++ b/crates/feos/tests/pcsaft/stability_analysis.rs @@ -1,7 +1,6 @@ use feos::pcsaft::{PcSaft, PcSaftParameters}; use feos_core::parameter::IdentifierOption; use feos_core::{DensityInitialization, PhaseEquilibrium, SolverOptions, State}; -use nalgebra::dvector; use quantity::*; use std::error::Error; @@ -18,7 +17,7 @@ fn test_stability_analysis() -> Result<(), Box> { &&mix, 300.0 * KELVIN, 1.0 * BAR, - &(dvector![0.5, 0.5] * MOL), + 0.5, Some(DensityInitialization::Liquid), )?; let options = SolverOptions { @@ -38,7 +37,7 @@ fn test_stability_analysis() -> Result<(), Box> { let vle = PhaseEquilibrium::bubble_point( &&mix, 300.0 * KELVIN, - &dvector![0.5, 0.5], + 0.5, Some(6.0 * BAR), None, (options, options), diff --git a/crates/feos/tests/pcsaft/state_creation_mixture.rs b/crates/feos/tests/pcsaft/state_creation_mixture.rs index 46e0442b8..f74ed559b 100644 --- a/crates/feos/tests/pcsaft/state_creation_mixture.rs +++ b/crates/feos/tests/pcsaft/state_creation_mixture.rs @@ -56,7 +56,7 @@ fn volume_temperature_molefracs() -> Result<(), Box> { let moles = MOL; let x = dvector![0.3, 0.7]; let state = State::new_nvt(&&saft, temperature, volume, (x, moles))?; - assert_relative_eq!(state.volume(), volume, max_relative = 1e-10); + assert_relative_eq!(state.volume()?, volume, max_relative = 1e-10); Ok(()) } diff --git a/crates/feos/tests/pcsaft/state_creation_pure.rs b/crates/feos/tests/pcsaft/state_creation_pure.rs index cda35b75f..81ef57168 100644 --- a/crates/feos/tests/pcsaft/state_creation_pure.rs +++ b/crates/feos/tests/pcsaft/state_creation_pure.rs @@ -29,7 +29,7 @@ fn temperature_volume() -> FeosResult<()> { let volume = 1.5e-3 * METER.powi::<3>(); let moles = MOL; let state = State::new_nvt(&&saft, temperature, volume, moles)?; - assert_relative_eq!(state.volume(), volume, max_relative = 1e-10); + assert_relative_eq!(state.volume()?, volume, max_relative = 1e-10); Ok(()) } @@ -50,8 +50,8 @@ fn temperature_total_moles_volume() -> FeosResult<()> { let total_moles = MOL; let volume = METER.powi::<3>(); let state = State::new_nvt(&&saft, temperature, volume, total_moles)?; - assert_relative_eq!(state.volume(), volume, max_relative = 1e-10); - assert_relative_eq!(state.total_moles(), total_moles, max_relative = 1e-10); + assert_relative_eq!(state.volume()?, volume, max_relative = 1e-10); + assert_relative_eq!(state.total_moles()?, total_moles, max_relative = 1e-10); Ok(()) } @@ -63,8 +63,8 @@ fn temperature_total_moles_density() -> FeosResult<()> { let density = MOL / METER.powi::<3>(); let state = State::new_pure(&&saft, temperature, density)?.set_total_moles(total_moles); assert_relative_eq!(state.density, density, max_relative = 1e-10); - assert_relative_eq!(state.total_moles(), total_moles, max_relative = 1e-10); - assert_relative_eq!(state.volume(), total_moles / density, max_relative = 1e-10); + assert_relative_eq!(state.total_moles()?, total_moles, max_relative = 1e-10); + assert_relative_eq!(state.volume()?, total_moles / density, max_relative = 1e-10); Ok(()) } @@ -158,7 +158,7 @@ fn density_internal_energy() -> FeosResult<()> { let molar_internal_energy = state.molar_internal_energy(Contributions::Total); let state_nvu = State::new_nvu( &&eos, - state.volume(), + state.volume()?, molar_internal_energy, total_moles, None, @@ -203,8 +203,8 @@ fn pressure_enthalpy_total_moles_vapor() -> FeosResult<()> { let state = State::new_nvt( &&eos, state.temperature, - state.volume(), - state.total_moles(), + state.volume()?, + state.total_moles()?, )?; assert_relative_eq!( state.molar_enthalpy(Contributions::Total), @@ -266,7 +266,7 @@ fn temperature_entropy_vapor() -> FeosResult<()> { &&eos, temperature, state.molar_entropy(Contributions::Total), - state.moles(), + state.moles()?, None, )?; assert_relative_eq!( diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index 75958d7ca..3f775a2da 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -76,7 +76,7 @@ pub fn vapor_pressure_derivatives<'py>( #[pyfunction] pub fn boiling_temperature_derivatives<'py>( model: PyEquationOfStateAD, - parameter_names: Bound<'py, PyAny>, + parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, input: PyReadonlyArray2, ) -> GradResult<'py> { diff --git a/py-feos/src/eos/mod.rs b/py-feos/src/eos/mod.rs index cd50de42f..0bc7f1a00 100644 --- a/py-feos/src/eos/mod.rs +++ b/py-feos/src/eos/mod.rs @@ -210,7 +210,6 @@ pub enum Compositions { TotalMoles(Moles), Molefracs(DVector), Moles(Moles>), - PartialDensity(Density>), } impl Composition for Compositions { @@ -224,15 +223,6 @@ impl Composition for Compositions { Self::TotalMoles(total_moles) => total_moles.into_molefracs(eos), Self::Molefracs(molefracs) => molefracs.into_molefracs(eos), Self::Moles(moles) => moles.into_molefracs(eos), - Self::PartialDensity(partial_density) => partial_density.into_molefracs(eos), - } - } - - fn density(&self) -> Option> { - if let Self::PartialDensity(partial_density) = self { - partial_density.density() - } else { - None } } } @@ -255,8 +245,6 @@ impl TryFrom>> for Compositions { Ok(Compositions::Moles(n)) } else if let Ok(n) = composition.extract::() { Ok(Compositions::TotalMoles(n)) - } else if let Ok(rho) = composition.extract::>>() { - Ok(Compositions::PartialDensity(rho)) } else { Err(PyErr::new::(format!( "failed to parse value '{composition}' as composition." diff --git a/py-feos/src/phase_equilibria.rs b/py-feos/src/phase_equilibria.rs index 38d0d2d12..c6c857506 100644 --- a/py-feos/src/phase_equilibria.rs +++ b/py-feos/src/phase_equilibria.rs @@ -1,6 +1,6 @@ use crate::{ PyVerbosity, - eos::{PyEquationOfState, parse_molefracs}, + eos::{Compositions, PyEquationOfState, parse_molefracs}, error::PyFeosError, ideal_gas::IdealGasModel, residual::ResidualModel, @@ -108,8 +108,8 @@ impl PyPhaseEquilibrium { /// The system temperature. /// pressure : SINumber /// The system pressure. - /// feed : SIArray1 - /// Feed composition (units of amount of substance). + /// feed : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float] + /// Feed composition. /// initial_state : PhaseEquilibrium, optional /// A phase equilibrium used as initial guess. /// Can speed up convergence. @@ -138,7 +138,7 @@ impl PyPhaseEquilibrium { eos: &PyEquationOfState, temperature: Temperature, pressure: Pressure, - feed: Moles>, + feed: &Bound<'_, PyAny>, initial_state: Option<&PyPhaseEquilibrium>, max_iter: Option, tol: Option, @@ -150,7 +150,7 @@ impl PyPhaseEquilibrium { &eos.0, temperature, pressure, - &feed, + Compositions::try_from(Some(feed))?, initial_state.map(|s| &s.0), (max_iter, tol, verbosity.map(|v| v.into())).into(), non_volatile_components, @@ -168,7 +168,7 @@ impl PyPhaseEquilibrium { /// The equation of state. /// temperature_or_pressure : SINumber /// The system temperature_or_pressure. - /// liquid_molefracs : numpy.ndarray + /// liquid_molefracs : float | numpy.ndarray[float] | SIArray1 | list[float] /// The mole fraction of the liquid phase. /// tp_init : SINumber, optional /// The system pressure/temperature used as starting @@ -196,12 +196,12 @@ impl PyPhaseEquilibrium { )] #[pyo3(signature = (eos, temperature_or_pressure, liquid_molefracs, tp_init=None, vapor_molefracs=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None))] #[expect(clippy::too_many_arguments)] - pub(crate) fn bubble_point<'py>( + pub(crate) fn bubble_point( eos: &PyEquationOfState, temperature_or_pressure: &Bound<'_, PyAny>, - liquid_molefracs: PyReadonlyArray1<'py, f64>, + liquid_molefracs: &Bound<'_, PyAny>, tp_init: Option<&Bound<'_, PyAny>>, - vapor_molefracs: Option>, + vapor_molefracs: Option>, max_iter_inner: Option, max_iter_outer: Option, tol_inner: Option, @@ -214,7 +214,7 @@ impl PyPhaseEquilibrium { PhaseEquilibrium::bubble_point( &eos.0, t, - &parse_molefracs(Some(liquid_molefracs)).unwrap(), + Compositions::try_from(Some(liquid_molefracs))?, tp_init.map(|p| p.extract()).transpose()?, x.as_ref(), ( @@ -229,7 +229,7 @@ impl PyPhaseEquilibrium { PhaseEquilibrium::bubble_point( &eos.0, p, - &parse_molefracs(Some(liquid_molefracs)).unwrap(), + Compositions::try_from(Some(liquid_molefracs))?, tp_init.map(|p| p.extract()).transpose()?, x.as_ref(), ( @@ -256,7 +256,7 @@ impl PyPhaseEquilibrium { /// The equation of state. /// temperature_or_pressure : SINumber /// The system temperature or pressure. - /// vapor_molefracs : numpy.ndarray + /// vapor_molefracs : float | numpy.ndarray[float] | SIArray1 | list[float] /// The mole fraction of the vapor phase. /// tp_init : SINumber, optional /// The system pressure/temperature used as starting @@ -284,12 +284,12 @@ impl PyPhaseEquilibrium { )] #[pyo3(signature = (eos, temperature_or_pressure, vapor_molefracs, tp_init=None, liquid_molefracs=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None))] #[expect(clippy::too_many_arguments)] - pub(crate) fn dew_point<'py>( + pub(crate) fn dew_point( eos: &PyEquationOfState, temperature_or_pressure: &Bound<'_, PyAny>, - vapor_molefracs: PyReadonlyArray1<'py, f64>, + vapor_molefracs: &Bound<'_, PyAny>, tp_init: Option<&Bound<'_, PyAny>>, - liquid_molefracs: Option>, + liquid_molefracs: Option>, max_iter_inner: Option, max_iter_outer: Option, tol_inner: Option, @@ -302,7 +302,7 @@ impl PyPhaseEquilibrium { PhaseEquilibrium::dew_point( &eos.0, t, - &parse_molefracs(Some(vapor_molefracs)).unwrap(), + Compositions::try_from(Some(vapor_molefracs))?, tp_init.map(|p| p.extract()).transpose()?, x.as_ref(), ( @@ -317,7 +317,7 @@ impl PyPhaseEquilibrium { PhaseEquilibrium::dew_point( &eos.0, p, - &parse_molefracs(Some(vapor_molefracs)).unwrap(), + Compositions::try_from(Some(vapor_molefracs))?, tp_init.map(|p| p.extract()).transpose()?, x.as_ref(), ( @@ -335,43 +335,6 @@ impl PyPhaseEquilibrium { } } - // /// Creates a new PhaseEquilibrium that contains two states at the - // /// specified temperature, pressure and moles. - // /// - // /// The constructor can be used in custom phase equilibrium solvers or, - // /// e.g., to generate initial guesses for an actual VLE solver. - // /// In general, the two states generated are NOT in an equilibrium. - // /// - // /// Parameters - // /// ---------- - // /// eos : EquationOfState - // /// The equation of state. - // /// temperature : SINumber - // /// The system temperature. - // /// pressure : SINumber - // /// The system pressure. - // /// vapor_moles : SIArray1 - // /// Amount of substance of the vapor phase. - // /// liquid_moles : SIArray1 - // /// Amount of substance of the liquid phase. - // /// - // /// Returns - // /// ------- - // /// PhaseEquilibrium - // #[staticmethod] - // pub(crate) fn new_npt( - // eos: &PyEquationOfState, - // temperature: Temperature, - // pressure: Pressure, - // vapor_moles: Moles>, - // liquid_moles: Moles>, - // ) -> PyResult { - // Ok(Self( - // PhaseEquilibrium::new_xpt(&eos.0, temperature, pressure, &vapor_moles, &liquid_moles) - // .map_err(PyFeosError::from)?, - // )) - // } - #[getter] fn get_vapor(&self) -> PyState { PyState(self.0.vapor().clone()) @@ -804,8 +767,8 @@ impl PyPhaseDiagram { /// ---------- /// eos: Eos /// The equation of state. - /// molefracs: np.ndarray[float] - /// The composition of the liquid phase. + /// composition : float | numpy.ndarray[float] | SIArray1 | list[float] + /// Composition of the mixture. /// min_temperature: SINumber /// The lower limit for the temperature. /// npoints: int @@ -830,13 +793,13 @@ impl PyPhaseDiagram { /// PhaseDiagram #[staticmethod] #[pyo3( - text_signature = "(eos, molefracs, min_temperature, npoints, critical_temperature=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None)" + text_signature = "(eos, composition, min_temperature, npoints, critical_temperature=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None)" )] - #[pyo3(signature = (eos, molefracs, min_temperature, npoints, critical_temperature=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None))] + #[pyo3(signature = (eos, composition, min_temperature, npoints, critical_temperature=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None))] #[expect(clippy::too_many_arguments)] pub(crate) fn bubble_point_line<'py>( eos: &PyEquationOfState, - molefracs: PyReadonlyArray1<'py, f64>, + composition: &Bound<'py, PyAny>, min_temperature: Temperature, npoints: usize, critical_temperature: Option, @@ -848,7 +811,7 @@ impl PyPhaseDiagram { ) -> PyResult { let dia = PhaseDiagram::bubble_point_line( &eos.0, - &parse_molefracs(Some(molefracs)).unwrap(), + Compositions::try_from(Some(composition))?, min_temperature, npoints, critical_temperature, @@ -871,8 +834,8 @@ impl PyPhaseDiagram { /// ---------- /// eos: Eos /// The equation of state. - /// molefracs: np.ndarray[float] - /// The composition of the vapor phase. + /// composition : float | numpy.ndarray[float] | SIArray1 | list[float] + /// Composition of the mixture. /// min_temperature: SINumber /// The lower limit for the temperature. /// npoints: int @@ -897,13 +860,13 @@ impl PyPhaseDiagram { /// PhaseDiagram #[staticmethod] #[pyo3( - text_signature = "(eos, molefracs, min_temperature, npoints, critical_temperature=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None)" + text_signature = "(eos, composition, min_temperature, npoints, critical_temperature=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None)" )] - #[pyo3(signature = (eos, molefracs, min_temperature, npoints, critical_temperature=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None))] + #[pyo3(signature = (eos, composition, min_temperature, npoints, critical_temperature=None, max_iter_inner=None, max_iter_outer=None, tol_inner=None, tol_outer=None, verbosity=None))] #[expect(clippy::too_many_arguments)] pub(crate) fn dew_point_line<'py>( eos: &PyEquationOfState, - molefracs: PyReadonlyArray1<'py, f64>, + composition: &Bound<'py, PyAny>, min_temperature: Temperature, npoints: usize, critical_temperature: Option, @@ -915,7 +878,7 @@ impl PyPhaseDiagram { ) -> PyResult { let dia = PhaseDiagram::dew_point_line( &eos.0, - &parse_molefracs(Some(molefracs)).unwrap(), + Compositions::try_from(Some(composition))?, min_temperature, npoints, critical_temperature, @@ -934,8 +897,8 @@ impl PyPhaseDiagram { /// ---------- /// eos: Eos /// The equation of state. - /// molefracs: np.ndarray[float] - /// The composition of the mixture. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float] + /// Composition of the mixture. /// min_temperature: SINumber /// The lower limit for the temperature. /// npoints: int @@ -956,13 +919,13 @@ impl PyPhaseDiagram { /// PhaseDiagram #[staticmethod] #[pyo3( - text_signature = "(eos, molefracs, min_temperature, npoints, critical_temperature=None, max_iter=None, tol=None, verbosity=None)" + text_signature = "(eos, composition, min_temperature, npoints, critical_temperature=None, max_iter=None, tol=None, verbosity=None)" )] - #[pyo3(signature = (eos, molefracs, min_temperature, npoints, critical_temperature=None, max_iter=None, tol=None, verbosity=None))] + #[pyo3(signature = (eos, composition, min_temperature, npoints, critical_temperature=None, max_iter=None, tol=None, verbosity=None))] #[expect(clippy::too_many_arguments)] pub(crate) fn spinodal<'py>( eos: &PyEquationOfState, - molefracs: PyReadonlyArray1<'py, f64>, + composition: &Bound<'py, PyAny>, min_temperature: Temperature, npoints: usize, critical_temperature: Option, @@ -972,7 +935,7 @@ impl PyPhaseDiagram { ) -> PyResult { let dia = PhaseDiagram::spinodal( &eos.0, - &parse_molefracs(Some(molefracs)).unwrap(), + Compositions::try_from(Some(composition))?, min_temperature, npoints, critical_temperature, diff --git a/py-feos/src/state.rs b/py-feos/src/state.rs index 6d5ff09f8..761055a3b 100644 --- a/py-feos/src/state.rs +++ b/py-feos/src/state.rs @@ -110,7 +110,7 @@ impl PyState { eos: &PyEquationOfState, temperature: Option, volume: Option, - density: Option, + mut density: Option, composition: Option<&Bound<'py, PyAny>>, pressure: Option, molar_enthalpy: Option, @@ -119,7 +119,15 @@ impl PyState { density_initialization: Option<&Bound<'py, PyAny>>, initial_temperature: Option, ) -> PyResult { - let composition = Compositions::try_from(composition)?; + // partial density is supported here as a special case + let composition = if let Some(composition) = composition + && let Ok(rho) = composition.extract::>>() + { + density = Some(rho.sum()); + Compositions::Molefracs(rho.convert_into(density.unwrap())) + } else { + Compositions::try_from(composition)? + }; let density_init = if let Some(di) = density_initialization { if let Ok(d) = di.extract::().as_deref() { match d { @@ -203,9 +211,8 @@ impl PyState { /// ---------- /// eos: EquationOfState /// The equation of state to use. - /// molefracs: np.ndarray[float], optional - /// Molar composition. - /// Only optional for a pure component. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// Composition of the mixture. /// initial_temperature: SINumber, optional /// The initial temperature. /// max_iter : int, optional @@ -325,9 +332,8 @@ impl PyState { /// The equation of state to use. /// temperature: SINumber /// The temperature. - /// molefracs: np.ndarray[float], optional - /// Molar composition. - /// Only optional for a pure component. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// Composition of the mixture. /// max_iter : int, optional /// The maximum number of iterations. /// tol: float, optional @@ -834,8 +840,11 @@ impl PyState { /// ------- /// SINumber #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn entropy(&self, contributions: PyContributions) -> Entropy { - self.0.entropy(contributions.into()) + fn entropy(&self, contributions: PyContributions) -> PyResult { + self.0 + .entropy(contributions.into()) + .map_err(PyFeosError::from) + .map_err(PyErr::from) } /// Return derivative of molar entropy with respect to temperature. @@ -891,8 +900,11 @@ impl PyState { /// ------- /// SINumber #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn enthalpy(&self, contributions: PyContributions) -> Energy { - self.0.enthalpy(contributions.into()) + fn enthalpy(&self, contributions: PyContributions) -> PyResult { + self.0 + .enthalpy(contributions.into()) + .map_err(PyFeosError::from) + .map_err(PyErr::from) } /// Return molar enthalpy. @@ -932,8 +944,11 @@ impl PyState { /// ------- /// SINumber #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn helmholtz_energy(&self, contributions: PyContributions) -> Energy { - self.0.helmholtz_energy(contributions.into()) + fn helmholtz_energy(&self, contributions: PyContributions) -> PyResult { + self.0 + .helmholtz_energy(contributions.into()) + .map_err(PyFeosError::from) + .map_err(PyErr::from) } /// Return molar Helmholtz energy. @@ -973,8 +988,11 @@ impl PyState { /// ------- /// SINumber #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn gibbs_energy(&self, contributions: PyContributions) -> Energy { - self.0.gibbs_energy(contributions.into()) + fn gibbs_energy(&self, contributions: PyContributions) -> PyResult { + self.0 + .gibbs_energy(contributions.into()) + .map_err(PyFeosError::from) + .map_err(PyErr::from) } /// Return molar Gibbs energy. @@ -1005,8 +1023,11 @@ impl PyState { /// ------- /// SINumber #[pyo3(signature = (contributions=PyContributions::Total), text_signature = "($self, contributions)")] - fn internal_energy(&self, contributions: PyContributions) -> Energy { - self.0.internal_energy(contributions.into()) + fn internal_energy(&self, contributions: PyContributions) -> PyResult { + self.0 + .internal_energy(contributions.into()) + .map_err(PyFeosError::from) + .map_err(PyErr::from) } /// Return molar internal energy. @@ -1111,8 +1132,11 @@ impl PyState { /// Returns /// ------- /// SIArray1 - fn mass(&self) -> Mass> { - self.0.mass() + fn mass(&self) -> PyResult>> { + self.0 + .mass() + .map_err(PyFeosError::from) + .map_err(PyErr::from) } /// Returns system's total mass. @@ -1120,8 +1144,11 @@ impl PyState { /// Returns /// ------- /// SINumber - fn total_mass(&self) -> Mass { - self.0.total_mass() + fn total_mass(&self) -> PyResult { + self.0 + .total_mass() + .map_err(PyFeosError::from) + .map_err(PyErr::from) } /// Returns system's mass density. @@ -1346,8 +1373,11 @@ impl PyState { } #[getter] - fn get_total_moles(&self) -> Moles { - self.0.total_moles() + fn get_total_moles(&self) -> PyResult { + self.0 + .total_moles() + .map_err(PyFeosError::from) + .map_err(PyErr::from) } #[getter] @@ -1356,8 +1386,11 @@ impl PyState { } #[getter] - fn get_volume(&self) -> Volume { - self.0.volume() + fn get_volume(&self) -> PyResult { + self.0 + .volume() + .map_err(PyFeosError::from) + .map_err(PyErr::from) } #[getter] @@ -1366,8 +1399,11 @@ impl PyState { } #[getter] - fn get_moles(&self) -> Moles> { - self.0.moles() + fn get_moles(&self) -> PyResult>> { + self.0 + .moles() + .map_err(PyFeosError::from) + .map_err(PyErr::from) } #[getter] @@ -1541,8 +1577,11 @@ impl PyStateVec { } #[getter] - fn get_moles(&self) -> Moles> { - StateVec::from(self).moles() + fn get_moles(&self) -> PyResult>> { + StateVec::from(self) + .moles() + .map_err(PyFeosError::from) + .map_err(PyErr::from) } #[getter] From daa26a755567c2b0afc5fe1524ac24815cb39a0f Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:28:56 +0200 Subject: [PATCH 6/6] Implement ph and ps flashes for binary mixtures (#338) --- CHANGELOG.md | 5 +- crates/feos-core/src/equation_of_state/mod.rs | 115 +++-- crates/feos-core/src/lib.rs | 4 +- crates/feos-core/src/phase_equilibria/mod.rs | 42 +- .../src/phase_equilibria/px_flashes.rs | 465 ++++++++++++++++++ crates/feos-core/src/state/composition.rs | 31 +- crates/feos-core/src/state/mod.rs | 66 +-- crates/feos-core/src/state/properties.rs | 18 +- crates/feos-derive/src/ideal_gas.rs | 2 +- crates/feos-dft/src/profile/properties.rs | 2 +- crates/feos/src/ideal_gas/joback.rs | 34 +- crates/feos/src/multiparameter/mod.rs | 4 +- crates/feos/tests/pcsaft/mod.rs | 1 + crates/feos/tests/pcsaft/px_flashes.rs | 138 ++++++ crates/feos/tests/pcsaft/tp_flash.rs | 14 +- docs/recipes/index.md | 1 + .../recipes_phase_equilibrium_flash.ipynb | 149 ++++++ py-feos/src/phase_equilibria.rs | 152 ++++++ 18 files changed, 1110 insertions(+), 133 deletions(-) create mode 100644 crates/feos-core/src/phase_equilibria/px_flashes.rs create mode 100644 crates/feos/tests/pcsaft/px_flashes.rs create mode 100644 docs/recipes/recipes_phase_equilibrium_flash.ipynb diff --git a/CHANGELOG.md b/CHANGELOG.md index 513a7cc1e..3d69100a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,15 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rewrote `PhaseEquilibrium::pure_p` to mirror `pure_t` and enabled automatic differentiation. [#337](https://github.com/feos-org/feos/pull/337) - Added `boiling_temperature` to the list of properties for parallel evaluations of gradients. [#337](https://github.com/feos-org/feos/pull/337) - Added the `Composition` trait to allow more flexibility in the creation of states and phase equilibria. [#330](https://github.com/feos-org/feos/pull/330) +- Added `PhaseEquilibrium::ph_flash` and `PhaseEquilibrium::ps_flash`. [#338](https://github.com/feos-org/feos/pull/338) +- Added getters for `vapor_phase_fraction`, `molar_enthalpy`, `molar_entropy`, `total_moles`, `enthalpy`, and `entropy` to `PhaseEquilibrium`. [#338](https://github.com/feos-org/feos/pull/338) ### Changed - Removed any assumptions about the total number of moles in a `State` or `PhaseEquilibrium`. Evaluating extensive properties now returns a `Result`. [#330](https://github.com/feos-org/feos/pull/330) +- Redesigned the `IdealGas` trait and added `IdealGasAD` in analogy to `ResidualDyn` and `Residual`. [#330](https://github.com/feos-org/feos/pull/330) ### Removed - Removed the `StateBuilder` struct, because it is mostly obsolete with the addition of the `Composition` trait. [#330](https://github.com/feos-org/feos/pull/330) ### Packaging -- Updated `quantity` dependency to 0.13 and removed the `typenum` dependency. [#323](https://github.com/feos-org/feos/pull/323) +- Updated `quantity` dependency to 0.13 and removed the `typenum` dependency. [#328](https://github.com/feos-org/feos/pull/328) ## [Unreleased] ### Added diff --git a/crates/feos-core/src/equation_of_state/mod.rs b/crates/feos-core/src/equation_of_state/mod.rs index dc849068f..14e45c07a 100644 --- a/crates/feos-core/src/equation_of_state/mod.rs +++ b/crates/feos-core/src/equation_of_state/mod.rs @@ -1,7 +1,7 @@ use crate::ReferenceSystem; use crate::state::StateHD; use nalgebra::{ - Const, DVector, DefaultAllocator, Dim, Dyn, OVector, SVector, U1, allocator::Allocator, + Const, DVector, DefaultAllocator, Dim, Dyn, OVector, SVector, allocator::Allocator, }; use num_dual::DualNum; use quantity::{Dimensionless, MolarEnergy, MolarVolume, Temperature}; @@ -114,11 +114,28 @@ impl, D>, D: DualNum + Copy, const N: usize> } /// Ideal gas Helmholtz energy contribution. -pub trait IdealGas { +pub trait IdealGas { /// Implementation of an ideal gas model in terms of the /// logarithm of the cubic thermal de Broglie wavelength /// in units ln(A³) for each component in the system. - fn ln_lambda3 + Copy>(&self, temperature: D2) -> D2; + fn ln_lambda3 + Copy>(&self, temperature: D) -> D; + + /// The name of the ideal gas model. + fn ideal_gas_model(&self) -> &'static str; +} + +/// Ideal gas Helmholtz energy contribution with automatic differentiation with +/// respect to parameters. +pub trait IdealGasAD: Clone { + type Real: IdealGasAD; + type Lifted + Copy>: IdealGasAD; + fn re(&self) -> Self::Real; + fn lift + Copy>(&self) -> Self::Lifted; + + /// Implementation of an ideal gas model in terms of the + /// logarithm of the cubic thermal de Broglie wavelength + /// in units ln(A³) for each component in the system. + fn ln_lambda3(&self, temperature: D) -> D; /// The name of the ideal gas model. fn ideal_gas_model(&self) -> &'static str; @@ -129,45 +146,44 @@ pub trait Total + Copy = f64>: Residual where DefaultAllocator: Allocator, { - type IdealGas: IdealGas; + type RealTotal: Total; + type LiftedTotal + Copy>: Total; + fn re_total(&self) -> Self::RealTotal; + fn lift_total + Copy>(&self) -> Self::LiftedTotal; fn ideal_gas_model(&self) -> &'static str; - fn ideal_gas(&self) -> impl Iterator; - - fn ln_lambda3 + Copy>(&self, temperature: D2) -> OVector { - OVector::from_iterator_generic( - N::from_usize(self.components()), - U1, - self.ideal_gas().map(|i| i.ln_lambda3(temperature)), - ) - } + fn ln_lambda3(&self, temperature: D) -> OVector; - fn ideal_gas_molar_helmholtz_energy + Copy>( + fn ideal_gas_molar_helmholtz_energy( &self, - temperature: D2, - molar_volume: D2, - molefracs: &OVector, - ) -> D2 { + temperature: D, + molar_volume: D, + molefracs: &OVector, + ) -> D { let partial_density = molefracs / molar_volume; - let mut res = D2::from(0.0); - for (i, &r) in self.ideal_gas().zip(partial_density.iter()) { + let mut res = D::from(0.0); + for (&l, &r) in self + .ln_lambda3(temperature) + .iter() + .zip(partial_density.iter()) + { let ln_rho_m1 = if r.re() == 0.0 { - D2::from(0.0) + D::from(0.0) } else { r.ln() - 1.0 }; - res += r * (i.ln_lambda3(temperature) + ln_rho_m1) + res += r * (l + ln_rho_m1) } res * molar_volume * temperature } - fn ideal_gas_helmholtz_energy + Copy>( + fn ideal_gas_helmholtz_energy( &self, - temperature: Temperature, - volume: MolarVolume, - moles: &OVector, - ) -> MolarEnergy { + temperature: Temperature, + volume: MolarVolume, + moles: &OVector, + ) -> MolarEnergy { let total_moles = moles.sum(); let molefracs = moles / total_moles; let molar_volume = volume.into_reduced() / total_moles; @@ -180,32 +196,59 @@ where } impl< - I: IdealGas + Clone + 'static, + I: IdealGas + 'static, C: Deref, R>> + Clone, R: ResidualDyn + 'static, -> Total for C + D: DualNum + Copy, +> Total for C { - type IdealGas = I; + type RealTotal = Self; + type LiftedTotal + Copy> = Self; + fn re_total(&self) -> Self::RealTotal { + self.clone() + } + fn lift_total + Copy>(&self) -> Self::LiftedTotal { + self.clone() + } fn ideal_gas_model(&self) -> &'static str { self.ideal_gas[0].ideal_gas_model() } - fn ideal_gas(&self) -> impl Iterator { - self.ideal_gas.iter() + fn ln_lambda3(&self, temperature: D) -> DVector { + DVector::from_vec( + self.ideal_gas + .iter() + .map(|i| i.ln_lambda3(temperature)) + .collect(), + ) } } -impl + Clone, R: Residual, D>, D: DualNum + Copy, const N: usize> +impl, R: Residual, D>, D: DualNum + Copy, const N: usize> Total, D> for EquationOfState<[I; N], R> { - type IdealGas = I; + type RealTotal = EquationOfState<[I::Real; N], R::Real>; + type LiftedTotal + Copy> = + EquationOfState<[I::Lifted; N], R::Lifted>; + fn re_total(&self) -> Self::RealTotal { + EquationOfState::new( + self.ideal_gas.each_ref().map(|i| i.re()), + self.residual.re(), + ) + } + fn lift_total + Copy>(&self) -> Self::LiftedTotal { + EquationOfState::new( + self.ideal_gas.each_ref().map(|i| i.lift()), + self.residual.lift(), + ) + } fn ideal_gas_model(&self) -> &'static str { self.ideal_gas[0].ideal_gas_model() } - fn ideal_gas(&self) -> impl Iterator { - self.ideal_gas.iter() + fn ln_lambda3(&self, temperature: D) -> SVector { + SVector::from(self.ideal_gas.each_ref().map(|i| i.ln_lambda3(temperature))) } } diff --git a/crates/feos-core/src/lib.rs b/crates/feos-core/src/lib.rs index 48b8306f3..cc280cf0a 100644 --- a/crates/feos-core/src/lib.rs +++ b/crates/feos-core/src/lib.rs @@ -34,8 +34,8 @@ mod phase_equilibria; mod state; pub use ad::{ParametersAD, PropertiesAD}; pub use equation_of_state::{ - EntropyScaling, EquationOfState, IdealGas, Molarweight, NoResidual, Residual, ResidualDyn, - Subset, Total, + EntropyScaling, EquationOfState, IdealGas, IdealGasAD, Molarweight, NoResidual, Residual, + ResidualDyn, Subset, Total, }; pub use errors::{FeosError, FeosResult}; #[cfg(feature = "ndarray")] diff --git a/crates/feos-core/src/phase_equilibria/mod.rs b/crates/feos-core/src/phase_equilibria/mod.rs index 461a1a901..5ed15a767 100644 --- a/crates/feos-core/src/phase_equilibria/mod.rs +++ b/crates/feos-core/src/phase_equilibria/mod.rs @@ -10,7 +10,15 @@ use quantity::{Dimensionless, Energy, Entropy, MolarEnergy, MolarEntropy, Moles} use std::fmt; use std::fmt::Write; +// with empty lines to not mess up the order in the documentation +mod vle_pure; + mod bubble_dew; + +mod tp_flash; + +mod px_flashes; + #[cfg(feature = "ndarray")] mod phase_diagram_binary; #[cfg(feature = "ndarray")] @@ -18,8 +26,7 @@ mod phase_diagram_pure; #[cfg(feature = "ndarray")] mod phase_envelope; mod stability_analysis; -mod tp_flash; -mod vle_pure; + pub use bubble_dew::TemperatureOrPressure; #[cfg(feature = "ndarray")] pub use phase_diagram_binary::PhaseDiagramHetero; @@ -33,22 +40,25 @@ pub use phase_diagram_pure::PhaseDiagram; /// /// ## Contents /// +/// + [Pure component phase equilibria](#pure-component-phase-equilibria) /// + [Bubble and dew point calculations](#bubble-and-dew-point-calculations) -/// + [Heteroazeotropes](#heteroazeotropes) /// + [Flash calculations](#flash-calculations) -/// + [Pure component phase equilibria](#pure-component-phase-equilibria) +/// + [Heteroazeotropes](#heteroazeotropes) /// + [Utility functions](#utility-functions) #[derive(Debug, Clone)] pub struct PhaseEquilibrium + Copy = f64> where DefaultAllocator: Allocator, { - states: [State; P], + pub states: [State; P], pub phase_fractions: [D; P], total_moles: Option>, } -impl fmt::Display for PhaseEquilibrium { +impl, N: Dim, const P: usize> fmt::Display for PhaseEquilibrium +where + DefaultAllocator: Allocator, +{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (i, s) in self.states.iter().enumerate() { writeln!(f, "phase {i}: {s}")?; @@ -125,12 +135,12 @@ impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium, { - pub(super) fn single_phase(state: State) -> Self { + pub fn single_phase(state: State) -> Self { let total_moles = state.total_moles; Self::with_vapor_phase_fraction(state.clone(), state, D::from(1.0), total_moles) } - pub(super) fn two_phase(vapor: State, liquid: State) -> Self { + pub fn two_phase(vapor: State, liquid: State) -> Self { let (beta, total_moles) = if let (Some(nv), Some(nl)) = (vapor.total_moles, liquid.total_moles) { (nv.convert_into(nl + nv), Some(nl + nv)) @@ -140,7 +150,7 @@ where Self::with_vapor_phase_fraction(vapor, liquid, beta, total_moles) } - pub(super) fn with_vapor_phase_fraction( + pub fn with_vapor_phase_fraction( vapor: State, liquid: State, vapor_phase_fraction: D, @@ -158,11 +168,7 @@ impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium, { - pub(super) fn new( - vapor: State, - liquid1: State, - liquid2: State, - ) -> Self { + pub fn new(vapor: State, liquid1: State, liquid2: State) -> Self { Self { states: [vapor, liquid1, liquid2], phase_fractions: [D::from(1.0), D::from(0.0), D::from(0.0)], @@ -171,7 +177,7 @@ where } } -impl, N: Gradients, const P: usize, D: DualNum + Copy> +impl, N: Gradients, const P: usize, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator, @@ -179,7 +185,13 @@ where pub fn total_moles(&self) -> FeosResult> { self.total_moles.ok_or(FeosError::IntensiveState) } +} +impl, N: Gradients, const P: usize, D: DualNum + Copy> + PhaseEquilibrium +where + DefaultAllocator: Allocator, +{ pub fn molar_enthalpy(&self) -> MolarEnergy { self.states .iter() diff --git a/crates/feos-core/src/phase_equilibria/px_flashes.rs b/crates/feos-core/src/phase_equilibria/px_flashes.rs new file mode 100644 index 000000000..aadeffb76 --- /dev/null +++ b/crates/feos-core/src/phase_equilibria/px_flashes.rs @@ -0,0 +1,465 @@ +#![expect(clippy::toplevel_ref_arg)] +use super::PhaseEquilibrium; +use crate::errors::FeosResult; +use crate::state::State; +use crate::{Composition, FeosError, ReferenceSystem, SolverOptions, Total, Verbosity}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, Dim, DimAdd, OVector, U1, U2, U3, stack, vector}; +use num_dual::linalg::LU; +use num_dual::{ + Dual, Dual64, DualNum, DualStruct, Gradients, first_derivative, implicit_derivative_sp, partial, +}; +use quantity::{Density, MolarEnergy, MolarEntropy, Pressure, Quantity, SIUnit, Temperature}; + +const MAX_ITER_PX: usize = 20; +const TOL_PX: f64 = 1e-11; + +type PXVars = >::Output; +type TPVars = >::Output; + +impl, N: Gradients + DimAdd + DimAdd, D: DualNum + Copy> + PhaseEquilibrium +where + DefaultAllocator: Allocator + + Allocator + + Allocator> + + Allocator> + + Allocator, PXVars> + + Allocator> + + Allocator> + + Allocator, TPVars>, + PXVars: Gradients, + TPVars: Gradients, +{ + /// Perform a ph-flash calculation. An initial temperature is required + /// and the system needs to be in the two-phase region at that initial + /// temperature. + /// + /// based on Michelsen's work [State function based flash specifications](https://doi.org/10.1016/S0378-3812(99)00092-8) + pub fn ph_flash>( + eos: &E, + pressure: Pressure, + molar_enthalpy: MolarEnergy, + feed: X, + initial_temperature: Temperature, + options: SolverOptions, + ) -> FeosResult { + PhaseEquilibrium::px_flash( + eos, + pressure, + molar_enthalpy, + feed, + initial_temperature, + options, + ) + } + + /// Perform a ps-flash calculation. An initial temperature is required + /// and the system needs to be in the two-phase region at that initial + /// temperature. + /// + /// based on Michelsen's work [State function based flash specifications](https://doi.org/10.1016/S0378-3812(99)00092-8) + pub fn ps_flash>( + eos: &E, + pressure: Pressure, + molar_entropy: MolarEntropy, + feed: X, + initial_temperature: Temperature, + options: SolverOptions, + ) -> FeosResult { + PhaseEquilibrium::px_flash( + eos, + pressure, + molar_entropy, + feed, + initial_temperature, + options, + ) + } + + // Generic implementation of ph and ps flashes. + fn px_flash, U: PXFlash>( + eos: &E, + pressure: Pressure, + specification: Quantity, + feed: X, + initial_temperature: Temperature, + options: SolverOptions, + ) -> FeosResult + where + Quantity: ReferenceSystem, + Quantity: ReferenceSystem, + { + let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PX, TOL_PX); + let (molefracs, total_moles) = feed.into_molefracs(eos)?; + + // initialize with a tp flash + let eos_f64 = eos.re_total(); + let vle = PhaseEquilibrium::tp_flash( + &eos_f64, + initial_temperature, + pressure.re(), + molefracs.map(|x| x.re()), + None, + Default::default(), + None, + )?; + + // extract specifications + let p = pressure.into_reduced().re(); + let hs = specification.into_reduced().re(); + let z = molefracs.map(|x| x.re()); + let specs = (p, hs, z.clone()); + + // extract variables + let t = initial_temperature.into_reduced(); + let beta = vle.vapor_phase_fraction(); + let rho_v = vle.vapor().density.into_reduced(); + let rho_l = vle.liquid().partial_density().into_reduced(); + let mut vars = stack![rho_l; vector![t, beta, rho_v]]; + let mut old_res = None; + + log_iter!( + verbosity, + " iter | method | temperature | residual | phase I mole fractions | phase II mole fractions " + ); + log_iter!(verbosity, "{:-<102}", ""); + log_iter!( + verbosity, + " {:4} | | {:9.5} | | {:10.8?} | {:10.8?}", + 0, + Temperature::from_reduced(t), + (&rho_l / rho_l.sum() + (&z - &rho_l / rho_l.sum()) / beta).as_slice(), + (&rho_l / rho_l.sum()).as_slice(), + ); + + // iterate + for k in 0..max_iter { + // always try a Newton step first + let (grad, new_vars) = U::newton_step(&eos_f64, &vars, &specs)?; + let new_res = grad.norm(); + let (method, res) = if let Some(r) = old_res + && r < new_res + { + // if the residual is not reduced, reject the step and do a tp-flash instead + vars = U::tp_step(&eos_f64, &vars, &specs)?; + ("Tp-flash", None) + } else { + vars = new_vars; + ("Newton", Some(new_res)) + }; + + if let Verbosity::Iter = verbosity { + let (t, _, _, _, x, y) = unpack_variables(&z, &vars); + log_iter!( + verbosity, + " {:4} | {:^8} | {:9.5} | {} | {:10.8?} | {:10.8?}", + k + 1, + method, + Temperature::from_reduced(t), + res.map_or(String::from(" "), |r| format!("{r:14.8e}")), + y.as_slice(), + x.as_slice(), + ); + } + + if let Some(res) = res + && res < tol + { + log_result!( + verbosity, + "px flash: calculation converged in {} step(s)\n", + k + 1 + ); + + // implicit differentiation + let specs = ( + pressure.into_reduced(), + specification.into_reduced(), + molefracs.clone(), + ); + let vars = implicit_derivative_sp( + |variables, specifications| { + U::state_function(&eos.lift_total(), variables, specifications) + }, + vars, + &specs, + ); + let (t, beta, rho_l, rho_v, x, y) = unpack_variables(&molefracs, &vars); + + // store results in PhaseEquilibrium + let liquid = State::new( + eos, + Temperature::from_reduced(t), + Density::from_reduced(rho_l), + x, + )?; + let vapor = State::new( + eos, + Temperature::from_reduced(t), + Density::from_reduced(rho_v), + y, + )?; + return Ok(PhaseEquilibrium::with_vapor_phase_fraction( + vapor, + liquid, + beta, + total_moles, + )); + } + old_res = res; + } + Err(FeosError::NotConverged("px flash".to_owned())) + } +} + +fn unpack_variables + Copy, N: Dim + DimAdd>( + molefracs: &OVector, + variables: &OVector>, +) -> (D, D, D, D, OVector, OVector) +where + DefaultAllocator: Allocator + Allocator>, +{ + let n = molefracs.len(); + let rho_i_l = variables.rows_generic(0, N::from_usize(n)).clone_owned(); + let [[t, beta, rho_v]] = variables.rows_generic(n, U3).clone_owned().data.0; + let rho_l = rho_i_l.sum(); + let x = rho_i_l / rho_l; + let y = &x + (molefracs - &x) / beta; + (t, beta, rho_l, rho_v, x, y) +} + +fn unpack_tp_variables + Copy, N: Dim + DimAdd>( + molefracs: &OVector, + variables: &OVector>, +) -> (D, D, D, OVector, OVector) +where + DefaultAllocator: Allocator + Allocator>, +{ + let n = molefracs.len(); + let rho_i_l = variables.rows_generic(0, N::from_usize(n)).clone_owned(); + let [[beta, rho_v]] = variables.rows_generic(n, U2).clone_owned().data.0; + let rho_l = rho_i_l.sum(); + let x = rho_i_l / rho_l; + let y = &x + (molefracs - &x) / beta; + (beta, rho_l, rho_v, x, y) +} + +trait PXFlash: Sized + Copy { + // potential function for which the flash solution is a saddle point. + fn state_function, N: Dim + DimAdd, D: DualNum + Copy>( + eos: &E, + variables: OVector>, + args: &(D, D, OVector), + ) -> D + where + DefaultAllocator: Allocator + Allocator>; + + fn evaluate_property, N: Gradients, D: DualNum + Copy>( + vle: &PhaseEquilibrium, + ) -> Quantity + where + DefaultAllocator: Allocator; + + // the potential function for a tp-flash specification (Q = A + V*p_spec) + fn tp_state_function, N: Dim + DimAdd, D: DualNum + Copy>( + eos: &E, + variables: OVector>, + &(t, p, ref z): &(D, D, OVector), + ) -> D + where + DefaultAllocator: Allocator + Allocator>, + { + let (beta, rho_l, rho_v, x, y) = unpack_tp_variables(z, &variables); + let potential = |molefracs, rho: D, t| { + let v = rho.recip(); + let a_res = eos.residual_helmholtz_energy(t, v, &molefracs); + let a_ig = eos.ideal_gas_molar_helmholtz_energy(t, v, &molefracs); + a_res + a_ig + v * p + }; + potential(y, rho_v, t) * beta + potential(x, rho_l, t) * (-beta + 1.0) + } + + // An undamped Newton step for the gradients of the potential function. + // Because the ps and ph flashes are saddle points rather then extrema, + // the value of the potential can not be used as convergence criterion. + #[expect(clippy::type_complexity)] + fn newton_step, N: Dim + DimAdd, D: DualNum + Copy>( + eos: &E, + variables: &OVector>, + specifications: &(D, D, OVector), + ) -> FeosResult<(OVector>, OVector>)> + where + DefaultAllocator: Allocator + Allocator> + Allocator, PXVars>, + PXVars: Gradients, + { + let (_, grad, hess) = PXVars::::hessian( + |variables, specifications| { + Self::state_function(&eos.lift_total(), variables, specifications) + }, + variables, + specifications, + ); + let dx = LU::new(hess)?.solve(&grad); + Ok((grad, variables - &dx)) + } + + // A much slower but more robust step that calculates the implicit + // derivative of the temperature only (which is well behaved + // according to Michelsen) and then calculates all other variables + // from a tp-flash. + fn tp_step, N: Gradients + DimAdd + DimAdd>( + eos: &E, + variables: &OVector>, + &(p, hs_spec, ref z): &(f64, f64, OVector), + ) -> FeosResult>> + where + Quantity: ReferenceSystem, + DefaultAllocator: Allocator + + Allocator + + Allocator> + + Allocator> + + Allocator> + + Allocator, TPVars>, + TPVars: Gradients, + { + let (mut t, beta, rho_l, rho_v, x, y) = unpack_variables(z, variables); + let rho_i_l = rho_l * x; + let (hs, dhs) = first_derivative( + partial( + |t: Dual<_, _>, args: &(_, OVector<_, _>)| { + let &(p, ref z) = args; + let args = (t, p, z.clone_owned()); + + // implicit differentiation of the tp stationarity condition + // to obtain the derivative of the other variables w.r.t. t + let tp_vars = implicit_derivative_sp( + |variables, args| { + Self::tp_state_function(&eos.lift_total().lift_total(), variables, args) + }, + stack![rho_i_l; vector![beta, rho_v]], + &args, + ); + let (beta, rho_l, rho_v, x, y) = unpack_tp_variables(z, &tp_vars); + + // Evaluation of the enthalpy/entropy including the derivatives. + let liquid = State::new( + &eos.lift_total(), + Temperature::from_reduced(t), + Density::from_reduced(rho_l), + x, + )?; + let vapor = State::new( + &eos.lift_total(), + Temperature::from_reduced(t), + Density::from_reduced(rho_v), + y, + )?; + Ok::<_, FeosError>( + Self::evaluate_property(&PhaseEquilibrium::with_vapor_phase_fraction( + vapor, liquid, beta, None, + )) + .into_reduced(), + ) + }, + &(p, z.clone_owned()), + ), + t, + )?; + + // Newton step for the temperature + t -= (hs - hs_spec) / dhs; + + // pack variables into PhaseEquilibrium for initial values + let liquid = State::new_density( + eos, + Temperature::from_reduced(t), + Density::from_reduced(rho_i_l), + )?; + let vapor = State::new( + eos, + Temperature::from_reduced(t), + Density::from_reduced(rho_v), + y, + )?; + let vle = PhaseEquilibrium::with_vapor_phase_fraction(vapor, liquid, beta, None); + + // tp-flash for all other variables + let vle = PhaseEquilibrium::tp_flash( + eos, + Temperature::from_reduced(t), + Pressure::from_reduced(p), + z, + Some(&vle), + Default::default(), + None, + )?; + let beta = vle.vapor_phase_fraction(); + let rho_v = vle.vapor().density.into_reduced(); + let rho_l = vle.liquid().partial_density().into_reduced(); + Ok(stack![rho_l; vector![t, beta, rho_v]]) + } +} + +impl PXFlash for SIUnit<-2, 2, 1, 0, 0, -1, 0> { + // the potential function for a ph-flash specification (Q = (A + V*p_spec - H_spec) / T) + fn state_function, N: Dim + DimAdd, D: DualNum + Copy>( + eos: &E, + variables: OVector>, + &(p, h, ref z): &(D, D, OVector), + ) -> D + where + DefaultAllocator: Allocator + Allocator>, + { + let (t, beta, rho_l, rho_v, x, y) = unpack_variables(z, &variables); + let potential = |molefracs, rho: D, t| { + let v = rho.recip(); + let a_res = eos.residual_helmholtz_energy(t, v, &molefracs); + let a_ig = eos.ideal_gas_molar_helmholtz_energy(t, v, &molefracs); + (a_res + a_ig + v * p - h) / t + }; + potential(y, rho_v, t) * beta + potential(x, rho_l, t) * (-beta + 1.0) + } + + fn evaluate_property, N: Gradients, D: DualNum + Copy>( + vle: &PhaseEquilibrium, + ) -> Quantity + where + DefaultAllocator: Allocator, + { + vle.molar_enthalpy() + } +} + +impl PXFlash for SIUnit<-2, 2, 1, 0, -1, -1, 0> { + // the potential function for a ps-flash specification (Q = A + T*S_spec + V*p_spec) + fn state_function, N: Dim + DimAdd, D: DualNum + Copy>( + eos: &E, + variables: OVector>, + &(p, s, ref z): &(D, D, OVector), + ) -> D + where + DefaultAllocator: Allocator + Allocator>, + { + let (t, beta, rho_l, rho_v, x, y) = unpack_variables(z, &variables); + let potential = |molefracs, rho: D, t| { + let v = rho.recip(); + let a_res = eos.residual_helmholtz_energy(t, v, &molefracs); + let a_ig = eos.ideal_gas_molar_helmholtz_energy(t, v, &molefracs); + // Division by t.re() is done to ensure that the state function has the same + // units (and in conclusion same order of magnitude) as the ph state function. + // This allows using the same toelrances for both methods. + (a_res + a_ig + t * s + v * p) / t.re() + }; + potential(y, rho_v, t) * beta + potential(x, rho_l, t) * (-beta + 1.0) + } + + fn evaluate_property, N: Gradients, D: DualNum + Copy>( + vle: &PhaseEquilibrium, + ) -> Quantity + where + DefaultAllocator: Allocator, + { + vle.molar_entropy() + } +} diff --git a/crates/feos-core/src/state/composition.rs b/crates/feos-core/src/state/composition.rs index f6a190e33..973af8ffb 100644 --- a/crates/feos-core/src/state/composition.rs +++ b/crates/feos-core/src/state/composition.rs @@ -1,4 +1,3 @@ -use super::State; use crate::equation_of_state::Residual; use crate::{FeosError, FeosResult}; use nalgebra::allocator::Allocator; @@ -6,10 +5,27 @@ use nalgebra::{DefaultAllocator, Dim, Dyn, OVector, U1, U2, dvector, vector}; use num_dual::{DualNum, DualStruct}; use quantity::Moles; +/// Trait to generalize over different input types for the composition of +/// a state. +/// +/// The trait is implemented for the following data types: +/// +/// |components|input|total_moles?|comment| +/// |:-:|-|-|-| +/// |1|`()`|-|| +/// |1|`Moles`|✅| +/// |2|`f64`|-| +/// |N|`OVector`|-| +/// |N|`&OVector`|-| +/// |N|`OVector`|-|`Dyn` only| +/// |N|`&OVector`|-|`Dyn` only| +/// |N|`Moles>`|✅| +/// |N|`&Moles>`|✅| pub trait Composition + Copy, N: Dim> where DefaultAllocator: Allocator, { + /// Convert the composition into molefracs and total moles if possible. #[expect(clippy::type_complexity)] fn into_molefracs>( self, @@ -42,19 +58,6 @@ where } } -// copy the composition from a given state -impl + Copy, N: Dim> Composition for &State -where - DefaultAllocator: Allocator, -{ - fn into_molefracs>( - self, - _: &E1, - ) -> FeosResult<(OVector, Option>)> { - Ok(((self.molefracs.clone()), self.total_moles)) - } -} - // a pure component needs no specification impl + Copy> Composition for () { fn into_molefracs>( diff --git a/crates/feos-core/src/state/mod.rs b/crates/feos-core/src/state/mod.rs index 716468aa8..daa7a7ff4 100644 --- a/crates/feos-core/src/state/mod.rs +++ b/crates/feos-core/src/state/mod.rs @@ -306,17 +306,9 @@ where /// 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. - /// - /// 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. + /// When the state cannot be created using the combination of inputs (is over- or underdetermined). pub fn build>( eos: &E, temperature: Temperature, @@ -417,18 +409,9 @@ where { /// 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. + /// When the state cannot be created using the combination of inputs (is over- or underdetermined). #[expect(clippy::too_many_arguments)] pub fn build_full + Clone>( eos: &E, @@ -461,28 +444,33 @@ where match state { Some(state) => Ok(state), None => { - // Check if new state can be created using molar_enthalpy and temperature - if let (Some(p), Some(h)) = (pressure, molar_enthalpy) { - return State::new_nph(eos, p, h, composition, density_initialization, ti); - } - if let (Some(p), Some(s)) = (pressure, molar_entropy) { - return State::new_nps(eos, p, s, composition, density_initialization, ti); - } - if let (Some(t), Some(h)) = (temperature, molar_enthalpy) { - return State::new_nth(eos, t, h, composition, density_initialization); - } - if let (Some(t), Some(s)) = (temperature, molar_entropy) { - return State::new_nts(eos, t, s, composition, density_initialization); - } - if let (Some(u), Some(v)) = (molar_internal_energy, volume) { - let (molefracs, total_moles) = composition.into_molefracs(eos)?; - if let Some(n) = total_moles { - return State::new_nvu(eos, v, u, (molefracs, n), ti); + match ( + temperature, + pressure, + volume, + molar_enthalpy, + molar_entropy, + molar_internal_energy, + ) { + (Some(t), None, None, Some(h), None, None) => { + State::new_nth(eos, t, h, composition, density_initialization) + } + (Some(t), None, None, None, Some(s), None) => { + State::new_nts(eos, t, s, composition, density_initialization) + } + (None, Some(p), None, Some(h), None, None) => { + State::new_nph(eos, p, h, composition, density_initialization, ti) + } + (None, Some(p), None, None, Some(s), None) => { + State::new_nps(eos, p, s, composition, density_initialization, ti) + } + (None, None, Some(v), None, None, Some(u)) => { + State::new_nvu(eos, v, u, composition, ti) } + _ => Err(FeosError::UndeterminedState(String::from( + "Missing input parameters.", + ))), } - Err(FeosError::UndeterminedState(String::from( - "Missing input parameters.", - ))) } } } diff --git a/crates/feos-core/src/state/properties.rs b/crates/feos-core/src/state/properties.rs index fa0f53b4a..132bad936 100644 --- a/crates/feos-core/src/state/properties.rs +++ b/crates/feos-core/src/state/properties.rs @@ -20,7 +20,9 @@ where let ideal_gas = || { quantity::ad::gradient_copy( partial2( - |n: Dimensionless<_>, &t, &v| self.eos.ideal_gas_helmholtz_energy(t, v, &n), + |n: Dimensionless<_>, &t, &v| { + self.eos.lift_total().ideal_gas_helmholtz_energy(t, v, &n) + }, &self.temperature, &self.molar_volume, ), @@ -38,7 +40,7 @@ where quantity::ad::partial_hessian_copy( partial( |(n, t): (Dimensionless<_>, _), &v| { - self.eos.ideal_gas_helmholtz_energy(t, v, &n) + self.eos.lift_total().ideal_gas_helmholtz_energy(t, v, &n) }, &self.molar_volume, ), @@ -89,7 +91,7 @@ where let ideal_gas = || { -quantity::ad::first_derivative( partial2( - |t, &v, n| self.eos.ideal_gas_helmholtz_energy(t, v, n), + |t, &v, n| self.eos.lift_total().ideal_gas_helmholtz_energy(t, v, n), &self.molar_volume, &self.molefracs, ), @@ -115,7 +117,7 @@ where let ideal_gas = || { -quantity::ad::second_derivative( partial2( - |t, &v, n| self.eos.ideal_gas_helmholtz_energy(t, v, n), + |t, &v, n| self.eos.lift_total().ideal_gas_helmholtz_energy(t, v, n), &self.molar_volume, &self.molefracs, ), @@ -135,7 +137,7 @@ where let ideal_gas = || { -quantity::ad::third_derivative( partial2( - |t, &v, n| self.eos.ideal_gas_helmholtz_energy(t, v, n), + |t, &v, n| self.eos.lift_total().ideal_gas_helmholtz_energy(t, v, n), &self.molar_volume, &self.molefracs, ), @@ -176,7 +178,7 @@ where let ideal_gas = || { quantity::ad::zeroth_derivative( partial2( - |t, &v, n| self.eos.ideal_gas_helmholtz_energy(t, v, n), + |t, &v, n| self.eos.lift_total().ideal_gas_helmholtz_energy(t, v, n), &self.molar_volume, &self.molefracs, ), @@ -253,7 +255,9 @@ where if let Contributions::IdealGas | Contributions::Total = contributions { res.push(( self.eos.ideal_gas_model(), - self.eos.ideal_gas_molar_helmholtz_energy(t, v, &x), + self.eos + .lift_total() + .ideal_gas_molar_helmholtz_energy(t, v, &x), )); } if let Contributions::Residual | Contributions::Total = contributions { diff --git a/crates/feos-derive/src/ideal_gas.rs b/crates/feos-derive/src/ideal_gas.rs index 06196e82c..f9b5eb671 100644 --- a/crates/feos-derive/src/ideal_gas.rs +++ b/crates/feos-derive/src/ideal_gas.rs @@ -42,7 +42,7 @@ fn impl_ideal_gas( }); quote! { impl IdealGas for IdealGasModel { - fn ln_lambda3 + Copy>(&self, temperature: D) -> D { + fn ln_lambda3 + Copy>(&self, temperature: D) -> D { match self { #(#ln_lambda3,)* } diff --git a/crates/feos-dft/src/profile/properties.rs b/crates/feos-dft/src/profile/properties.rs index 6095fb172..d542b27da 100644 --- a/crates/feos-dft/src/profile/properties.rs +++ b/crates/feos-dft/src/profile/properties.rs @@ -184,7 +184,7 @@ where temperature: Dual64, density: &Array, ) -> Array { - let lambda = self.bulk.eos.ln_lambda3(temperature); + let lambda = self.bulk.eos.lift_total().ln_lambda3(temperature); let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); for (i, rhoi) in density.outer_iter().enumerate() { phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi); diff --git a/crates/feos/src/ideal_gas/joback.rs b/crates/feos/src/ideal_gas/joback.rs index 8f2279d5b..e30c630b2 100644 --- a/crates/feos/src/ideal_gas/joback.rs +++ b/crates/feos/src/ideal_gas/joback.rs @@ -1,12 +1,13 @@ //! Implementation of the ideal gas heat capacity (de Broglie wavelength) //! of [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). use feos_core::parameter::{FromSegments, Parameters}; -use feos_core::{FeosResult, IdealGas, ReferenceSystem}; +use feos_core::{FeosResult, IdealGas, IdealGasAD, ReferenceSystem}; use nalgebra::DVector; use num_dual::*; use quantity::{MolarEntropy, Temperature}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::ops::Mul; /// Coefficients used in the Joback model. /// @@ -96,9 +97,9 @@ impl Joback { } } -impl + Copy> IdealGas for Joback { - fn ln_lambda3 + Copy>(&self, temperature: D2) -> D2 { - let [a, b, c, d, e] = self.0.each_ref().map(D2::from_inner); +impl + Copy> Joback { + fn ln_lambda3 + Copy + Mul>(&self, temperature: D2) -> D2 { + let [a, b, c, d, e] = self.0; let t = temperature; let t2 = t * t; let t4 = t2 * t2; @@ -115,6 +116,31 @@ impl + Copy> IdealGas for Joback { + (t / T0).ln() * a; (h - t * s) / (t * RGAS) + f } +} + +impl IdealGas for Joback { + fn ln_lambda3 + Copy>(&self, temperature: D) -> D { + self.ln_lambda3(temperature) + } + + fn ideal_gas_model(&self) -> &'static str { + "Ideal gas (Joback)" + } +} + +impl + Copy> IdealGasAD for Joback { + type Real = Joback; + type Lifted + Copy> = Joback; + fn re(&self) -> Self::Real { + Joback(self.0.each_ref().map(D::re)) + } + fn lift + Copy>(&self) -> Self::Lifted { + Joback(self.0.each_ref().map(D2::from_inner)) + } + + fn ln_lambda3(&self, temperature: D) -> D { + self.ln_lambda3(temperature) + } fn ideal_gas_model(&self) -> &'static str { "Ideal gas (Joback)" diff --git a/crates/feos/src/multiparameter/mod.rs b/crates/feos/src/multiparameter/mod.rs index b4a6c4f9f..fe30093ac 100644 --- a/crates/feos/src/multiparameter/mod.rs +++ b/crates/feos/src/multiparameter/mod.rs @@ -118,10 +118,10 @@ impl Subset for MultiParameter { } impl IdealGas for MultiParameterIdealGas { - fn ln_lambda3 + Copy>(&self, temperature: D2) -> D2 { + fn ln_lambda3 + Copy>(&self, temperature: D) -> D { let tau = temperature.recip() * self.tc; // bit of a hack to convert from phi^0 into ln Lambda^3 - let delta = D2::from(E / (6.02214076e-7 * self.rhoc)); + let delta = D::from(E / (6.02214076e-7 * self.rhoc)); self.terms.iter().map(|r| r.evaluate(delta, tau)).sum() } diff --git a/crates/feos/tests/pcsaft/mod.rs b/crates/feos/tests/pcsaft/mod.rs index 6193aca74..732e6c0c5 100644 --- a/crates/feos/tests/pcsaft/mod.rs +++ b/crates/feos/tests/pcsaft/mod.rs @@ -1,6 +1,7 @@ mod critical_point; mod dft; mod properties; +mod px_flashes; mod stability_analysis; mod state_creation_mixture; mod state_creation_pure; diff --git a/crates/feos/tests/pcsaft/px_flashes.rs b/crates/feos/tests/pcsaft/px_flashes.rs new file mode 100644 index 000000000..4cfb179b6 --- /dev/null +++ b/crates/feos/tests/pcsaft/px_flashes.rs @@ -0,0 +1,138 @@ +use approx::assert_relative_eq; +use feos::ideal_gas::Joback; +use feos::pcsaft::PcSaftBinary; +use feos_core::{ + Contributions, EquationOfState, FeosResult, IdealGasAD, ParametersAD, PhaseEquilibrium, + ReferenceSystem, SolverOptions, Verbosity, +}; +use nalgebra::U1; +use num_dual::{DualStruct, DualVec}; +use quantity::*; + +#[test] +fn test_ph_flash() -> FeosResult<()> { + let params = [ + [1.5, 3.4, 180.0, 2.2, 0.03, 2500., 2.0, 1.0], + [4.5, 3.6, 250.0, 1.2, 0.015, 1500., 1.0, 2.0], + ]; + let kij = 0.15; + let pcsaft = PcSaftBinary::new(params, kij); + let joback = [ + Joback([380., 0.0, 0.0, 0.0, 0.0]), + Joback([210., 0.0, 0.0, 0.0, 0.0]), + ]; + let eos = EquationOfState::new(joback.clone(), pcsaft); + let p = 50.0 * BAR; + let t0 = Some(500.0 * KELVIN); + let x = 0.3; + let dew = PhaseEquilibrium::dew_point(&eos, p, x, t0, None, Default::default())?; + let bubble = PhaseEquilibrium::bubble_point(&eos, p, x, t0, None, Default::default())?; + let h = 0.2 * dew.molar_enthalpy() + 0.8 * bubble.molar_enthalpy(); + let t0 = 0.8 * dew.vapor().temperature + 0.2 * bubble.vapor().temperature; + let options = SolverOptions { + verbosity: Verbosity::Iter, + ..Default::default() + }; + let vle = PhaseEquilibrium::ph_flash(&eos, p, h, x, t0, options)?; + println!("{vle}"); + println!("{h}\n{}", vle.molar_enthalpy()); + assert_relative_eq!(h, vle.molar_enthalpy(), max_relative = 1e-10); + + let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); + let joback_ad = joback.each_ref().map(|j| j.lift()); + let eos_ad = EquationOfState::new(joback_ad, pcsaft_ad); + let vle_ad = PhaseEquilibrium::ph_flash( + &eos_ad, + Pressure::from_inner(&p), + MolarEnergy::from_inner(&h), + DualVec::from_inner(&x), + t0, + Default::default(), + )?; + let [[dt]] = vle_ad + .vapor() + .temperature + .into_reduced() + .eps + .unwrap_generic(U1, U1) + .data + .0; + println!("{dt}"); + + let dkij = 1e-7; + let pcsaft_h = PcSaftBinary::new(params, kij + dkij); + let eos_h = EquationOfState::new(joback.clone(), pcsaft_h); + let vle_h = PhaseEquilibrium::ph_flash(&eos_h, p, h, x, t0, Default::default())?; + let dt_h = (vle_h.vapor().temperature - vle.vapor().temperature).into_reduced() / dkij; + println!("{dt_h}"); + assert_relative_eq!(dt, dt_h, max_relative = 1e-4); + + Ok(()) +} + +#[test] +fn test_ps_flash() -> FeosResult<()> { + let params = [ + [1.5, 3.4, 180.0, 2.2, 0.03, 2500., 2.0, 1.0], + [4.5, 3.6, 250.0, 1.2, 0.015, 1500., 1.0, 2.0], + ]; + let kij = 0.15; + let pcsaft = PcSaftBinary::new(params, kij); + let joback = [ + Joback([380., 0.0, 0.0, 0.0, 0.0]), + Joback([210., 0.0, 0.0, 0.0, 0.0]), + ]; + let eos = EquationOfState::new(joback.clone(), pcsaft); + let p = 50.0 * BAR; + println!( + "{}", + PhaseEquilibrium::bubble_point(&eos, 500.0 * KELVIN, 0.5, None, None, Default::default())? + .vapor() + .pressure(Contributions::Total) + ); + let t0 = Some(500.0 * KELVIN); + let x = 0.3; + let dew = PhaseEquilibrium::dew_point(&eos, p, x, t0, None, Default::default())?; + let bubble = PhaseEquilibrium::bubble_point(&eos, p, x, t0, None, Default::default())?; + let s = 0.2 * dew.molar_entropy() + 0.8 * bubble.molar_entropy(); + let t0 = 0.8 * dew.vapor().temperature + 0.2 * bubble.vapor().temperature; + let options = SolverOptions { + verbosity: Verbosity::Iter, + ..Default::default() + }; + let vle = PhaseEquilibrium::ps_flash(&eos, p, s, x, t0, options)?; + println!("{vle}"); + println!("{s}\n{}", vle.molar_entropy()); + assert_relative_eq!(s, vle.molar_entropy(), max_relative = 1e-10); + + let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); + let joback_ad = joback.each_ref().map(|j| j.lift()); + let eos_ad = EquationOfState::new(joback_ad, pcsaft_ad); + let vle_ad = PhaseEquilibrium::ps_flash( + &eos_ad, + Pressure::from_inner(&p), + MolarEntropy::from_inner(&s), + DualVec::from_inner(&x), + t0, + Default::default(), + )?; + let [[dt]] = vle_ad + .vapor() + .temperature + .into_reduced() + .eps + .unwrap_generic(U1, U1) + .data + .0; + println!("{dt}"); + + let dkij = 1e-7; + let pcsaft_h = PcSaftBinary::new(params, kij + dkij); + let eos_h = EquationOfState::new(joback.clone(), pcsaft_h); + let vle_h = PhaseEquilibrium::ps_flash(&eos_h, p, s, x, t0, Default::default())?; + let dt_h = (vle_h.vapor().temperature - vle.vapor().temperature).into_reduced() / dkij; + println!("{dt_h}"); + assert_relative_eq!(dt, dt_h, max_relative = 1e-4); + + Ok(()) +} diff --git a/crates/feos/tests/pcsaft/tp_flash.rs b/crates/feos/tests/pcsaft/tp_flash.rs index b31a14998..0f28e0c2e 100644 --- a/crates/feos/tests/pcsaft/tp_flash.rs +++ b/crates/feos/tests/pcsaft/tp_flash.rs @@ -32,15 +32,7 @@ fn test_tp_flash() -> FeosResult<()> { println!("{p_propane} {p_butane} {x1} {y1} {z1}"); let mix = PcSaft::new(read_params(vec!["propane", "butane"])?); let options = SolverOptions::new().max_iter(100).tol(1e-12); - let vle = PhaseEquilibrium::tp_flash( - &&mix, - t, - p, - &(dvector![z1, 1.0 - z1] * MOL), - None, - options, - None, - )?; + let vle = PhaseEquilibrium::tp_flash(&&mix, t, p, z1, None, options, None)?; println!( "x1: {}, y1: {}", vle.liquid().molefracs[0], @@ -85,7 +77,7 @@ fn test_tp_flash_zero_component() -> FeosResult<()> { &&eos_full, 300.0 * KELVIN, 1.2 * BAR, - &(dvector![0.0, 0.5, 0.5] * MOL), + dvector![0.0, 0.5, 0.5], None, options, None, @@ -94,7 +86,7 @@ fn test_tp_flash_zero_component() -> FeosResult<()> { &&eos_binary, 300.0 * KELVIN, 1.2 * BAR, - &(dvector![0.5, 0.5] * MOL), + dvector![0.5, 0.5], None, options, None, diff --git a/docs/recipes/index.md b/docs/recipes/index.md index 5a7a95086..65b3a340a 100644 --- a/docs/recipes/index.md +++ b/docs/recipes/index.md @@ -12,6 +12,7 @@ If you are looking for tutorials with explanations, see the [tutorials](/tutoria recipes_critical_point_pure recipes_p_sat_t_boil recipes_phase_equilibrium_pure + recipes_phase_equilibrium_flash recipes_phase_diagram_pure recipes_automatic_differentiation ``` diff --git a/docs/recipes/recipes_phase_equilibrium_flash.ipynb b/docs/recipes/recipes_phase_equilibrium_flash.ipynb new file mode 100644 index 000000000..60158b47f --- /dev/null +++ b/docs/recipes/recipes_phase_equilibrium_flash.ipynb @@ -0,0 +1,149 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "8bec74cc", + "metadata": {}, + "outputs": [], + "source": [ + "import si_units as si\n", + "import feos\n", + "\n", + "parameters = feos.Parameters.from_json(\n", + " substances=['methanol', '1-propanol'], \n", + " pure_path='../../parameters/pcsaft/gross2002.json'\n", + ")\n", + "ideal_gas_parameters = feos.Parameters.from_json(\n", + " substances=['methanol', '1-propanol'], \n", + " pure_path='../../parameters/ideal_gas/poling2000.json'\n", + ")\n", + "eos = feos.EquationOfState.pcsaft(parameters).dippr(ideal_gas_parameters)" + ] + }, + { + "cell_type": "markdown", + "id": "0ace1cfd", + "metadata": {}, + "source": [ + "## Tp-flash" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e11fa945", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " dew point pressure: 0.6356 bar\n", + "bubble point pressure: 0.9245 bar\n" + ] + }, + { + "data": { + "text/markdown": [ + "||temperature|density|molefracs|\n", + "|-|-|-|-|\n", + "|phase 1|350.00000 K|28.75751 mol/m³|[0.59847, 0.40153]|\n", + "|phase 2|350.00000 K|14.29164 kmol/m³|[0.28954, 0.71046]|\n" + ], + "text/plain": [ + "phase 0: T = 350.00000 K, ρ = 28.75751 mol/m³, x = [0.59847, 0.40153]\n", + "phase 1: T = 350.00000 K, ρ = 14.29164 kmol/m³, x = [0.28954, 0.71046]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x1 = 0.4\n", + "temperature = 350*si.KELVIN\n", + "\n", + "p_bubble = feos.PhaseEquilibrium.bubble_point(eos, temperature, x1).liquid.pressure()\n", + "p_dew = feos.PhaseEquilibrium.dew_point(eos, temperature, x1).vapor.pressure()\n", + "print(f\"bubble point pressure: {p_bubble/si.BAR:.4} bar\")\n", + "print(f\" dew point pressure: {p_dew/si.BAR:.4} bar\")\n", + "\n", + "feos.PhaseEquilibrium.tp_flash(eos, temperature, 0.8*si.BAR, x1)" + ] + }, + { + "cell_type": "markdown", + "id": "73c81e37", + "metadata": {}, + "source": [ + "## ph-flash" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3acf88f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bubble point\ttemperature: 352.06 K\tenthalpy: -36.5900 kJ/mol\n", + " dew point\ttemperature: 361.09 K\tenthalpy: 3.9800 kJ/mol\n" + ] + }, + { + "data": { + "text/markdown": [ + "||temperature|density|molefracs|\n", + "|-|-|-|-|\n", + "|phase 1|360.47941 K|34.58586 mol/m³|[0.42362, 0.57638]|\n", + "|phase 2|360.47941 K|13.28059 kmol/m³|[0.17482, 0.82518]|\n" + ], + "text/plain": [ + "phase 0: T = 360.47941 K, ρ = 34.58586 mol/m³, x = [0.42362, 0.57638]\n", + "phase 1: T = 360.47941 K, ρ = 13.28059 kmol/m³, x = [0.17482, 0.82518]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x1 = 0.4\n", + "pressure = si.BAR\n", + "bubble = feos.PhaseEquilibrium.bubble_point(eos, pressure, x1, 300*si.KELVIN).liquid\n", + "dew = feos.PhaseEquilibrium.dew_point(eos, pressure, x1, 300*si.KELVIN).vapor\n", + "print(f\"bubble point\\ttemperature: {bubble.temperature/si.KELVIN:.2f} K\\tenthalpy: {bubble.molar_enthalpy()/(si.KILO*si.JOULE/si.MOL):8.4f} kJ/mol\")\n", + "print(f\" dew point\\ttemperature: {dew.temperature/si.KELVIN:.2f} K\\tenthalpy: {dew.molar_enthalpy()/(si.KILO*si.JOULE/si.MOL):8.4f} kJ/mol\")\n", + "\n", + "feos.PhaseEquilibrium.ph_flash(eos, pressure, 0*si.JOULE/si.MOL, x1, 356*si.KELVIN)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "feos_devel", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/py-feos/src/phase_equilibria.rs b/py-feos/src/phase_equilibria.rs index c6c857506..feaa38930 100644 --- a/py-feos/src/phase_equilibria.rs +++ b/py-feos/src/phase_equilibria.rs @@ -159,6 +159,128 @@ impl PyPhaseEquilibrium { )) } + /// Create a liquid and vapor state in equilibrium + /// for given pressure, enthalpy and feed composition. + /// + /// Can also be used to calculate liquid liquid phase separation. + /// + /// Parameters + /// ---------- + /// eos : EquationOfState + /// The equation of state. + /// pressure : SINumber + /// The system pressure. + /// molar_enthalpy : SINumber + /// The molar enthalpy of the system. + /// feed : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float] + /// Feed composition. + /// initial_temperature : SINumber + /// The system temperature. + /// max_iter : int, optional + /// The maximum number of iterations. + /// tol: float, optional + /// The solution tolerance. + /// verbosity : Verbosity, optional + /// The verbosity. + /// + /// Returns + /// ------- + /// PhaseEquilibrium + /// + /// Raises + /// ------ + /// RuntimeError + /// When pressure iteration fails or no phase equilibrium is found. + #[staticmethod] + #[pyo3( + text_signature = "(eos, pressure, molar_enthalpy, feed, initial_temperature, max_iter=None, tol=None, verbosity=None)" + )] + #[pyo3(signature = (eos, pressure, molar_enthalpy, feed, initial_temperature, max_iter=None, tol=None, verbosity=None))] + #[expect(clippy::too_many_arguments)] + pub(crate) fn ph_flash( + eos: &PyEquationOfState, + pressure: Pressure, + molar_enthalpy: MolarEnergy, + feed: &Bound<'_, PyAny>, + initial_temperature: Temperature, + max_iter: Option, + tol: Option, + verbosity: Option, + ) -> PyResult { + Ok(Self( + PhaseEquilibrium::ph_flash( + &eos.0, + pressure, + molar_enthalpy, + Compositions::try_from(Some(feed))?, + initial_temperature, + (max_iter, tol, verbosity.map(|v| v.into())).into(), + ) + .map_err(PyFeosError::from)?, + )) + } + + /// Create a liquid and vapor state in equilibrium + /// for given pressure, entropy and feed composition. + /// + /// Can also be used to calculate liquid liquid phase separation. + /// + /// Parameters + /// ---------- + /// eos : EquationOfState + /// The equation of state. + /// pressure : SINumber + /// The system pressure. + /// molar_entropy : SINumber + /// The molar entropy of the system. + /// feed : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float] + /// Feed composition. + /// initial_temperature : SINumber + /// The system temperature. + /// max_iter : int, optional + /// The maximum number of iterations. + /// tol: float, optional + /// The solution tolerance. + /// verbosity : Verbosity, optional + /// The verbosity. + /// + /// Returns + /// ------- + /// PhaseEquilibrium + /// + /// Raises + /// ------ + /// RuntimeError + /// When pressure iteration fails or no phase equilibrium is found. + #[staticmethod] + #[pyo3( + text_signature = "(eos, pressure, molar_entropy, feed, initial_temperature, max_iter=None, tol=None, verbosity=None)" + )] + #[pyo3(signature = (eos, pressure, molar_entropy, feed, initial_temperature, max_iter=None, tol=None, verbosity=None))] + #[expect(clippy::too_many_arguments)] + pub(crate) fn ps_flash( + eos: &PyEquationOfState, + pressure: Pressure, + molar_entropy: MolarEntropy, + feed: &Bound<'_, PyAny>, + initial_temperature: Temperature, + max_iter: Option, + tol: Option, + verbosity: Option, + ) -> PyResult { + Ok(Self( + PhaseEquilibrium::ps_flash( + &eos.0, + pressure, + molar_entropy, + Compositions::try_from(Some(feed))?, + initial_temperature, + (max_iter, tol, verbosity.map(|v| v.into())).into(), + ) + .map_err(PyFeosError::from)?, + )) + } + /// Compute a phase equilibrium for given temperature /// or pressure and liquid mole fractions. /// @@ -345,6 +467,36 @@ impl PyPhaseEquilibrium { PyState(self.0.liquid().clone()) } + #[getter] + fn get_vapor_phase_fraction(&self) -> f64 { + self.0.vapor_phase_fraction() + } + + #[getter] + fn get_total_moles(&self) -> PyResult { + Ok(self.0.total_moles().map_err(PyFeosError::from)?) + } + + #[getter] + fn get_molar_enthalpy(&self) -> MolarEnergy { + self.0.molar_enthalpy() + } + + #[getter] + fn get_enthalpy(&self) -> PyResult { + Ok(self.0.enthalpy().map_err(PyFeosError::from)?) + } + + #[getter] + fn get_molar_entropy(&self) -> MolarEntropy { + self.0.molar_entropy() + } + + #[getter] + fn get_entropy(&self) -> PyResult { + Ok(self.0.entropy().map_err(PyFeosError::from)?) + } + /// Calculate the pure component vapor-liquid equilibria for all /// components in the system. ///