From 158a721310171666b86ed3736f294a2086717016 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Tue, 19 May 2026 00:45:50 +0200 Subject: [PATCH 01/15] Added module for property calculations --- crates/feos-core/Cargo.toml | 6 +- crates/feos-core/src/ad/mod.rs | 479 ++++------------ .../parameter_optimization/dataset/binary.rs | 254 +++++++++ .../ad/parameter_optimization/dataset/mod.rs | 210 +++++++ .../ad/parameter_optimization/dataset/pure.rs | 343 +++++++++++ .../src/ad/parameter_optimization/mod.rs | 11 + .../src/ad/properties/boiling_temperature.rs | 70 +++ .../ad/properties/bubble_point_pressure.rs | 122 ++++ .../src/ad/properties/dew_point_pressure.rs | 121 ++++ .../ad/properties/enthalpy_of_vaporization.rs | 87 +++ .../properties/equilibrium_liquid_density.rs | 64 +++ .../src/ad/properties/liquid_density.rs | 81 +++ crates/feos-core/src/ad/properties/mod.rs | 43 ++ .../residual_isobaric_heat_capacity.rs | 85 +++ .../src/ad/properties/vapor_pressure.rs | 81 +++ crates/feos-core/src/errors.rs | 1 - crates/feos-core/src/lib.rs | 4 +- .../src/phase_equilibria/bubble_dew.rs | 4 - crates/feos-core/src/phase_equilibria/mod.rs | 5 - crates/feos-core/src/state/statevec.rs | 9 - crates/feos/Cargo.toml | 6 + crates/feos/benches/README.md | 9 +- crates/feos/benches/dual_static_vs_dynamic.rs | 290 ++++++++++ crates/feos/src/gc_pcsaft/dft/mod.rs | 5 +- crates/feos/src/pcsaft/dft/mod.rs | 3 +- crates/feos/src/pcsaft/eos/mod.rs | 63 +- crates/feos/src/pcsaft/eos/pcsaft_binary.rs | 66 ++- crates/feos/src/pcsaft/eos/pcsaft_pure.rs | 50 +- crates/feos/tests/pcsaft/px_flashes.rs | 10 +- py-feos/Cargo.toml | 2 +- py-feos/src/ad/dataset.rs | 537 ++++++++++++++++++ py-feos/src/ad/mod.rs | 65 ++- py-feos/src/lib.rs | 18 +- 33 files changed, 2734 insertions(+), 470 deletions(-) create mode 100644 crates/feos-core/src/ad/parameter_optimization/dataset/binary.rs create mode 100644 crates/feos-core/src/ad/parameter_optimization/dataset/mod.rs create mode 100644 crates/feos-core/src/ad/parameter_optimization/dataset/pure.rs create mode 100644 crates/feos-core/src/ad/parameter_optimization/mod.rs create mode 100644 crates/feos-core/src/ad/properties/boiling_temperature.rs create mode 100644 crates/feos-core/src/ad/properties/bubble_point_pressure.rs create mode 100644 crates/feos-core/src/ad/properties/dew_point_pressure.rs create mode 100644 crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs create mode 100644 crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs create mode 100644 crates/feos-core/src/ad/properties/liquid_density.rs create mode 100644 crates/feos-core/src/ad/properties/mod.rs create mode 100644 crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs create mode 100644 crates/feos-core/src/ad/properties/vapor_pressure.rs create mode 100644 crates/feos/benches/dual_static_vs_dynamic.rs create mode 100644 py-feos/src/ad/dataset.rs diff --git a/crates/feos-core/Cargo.toml b/crates/feos-core/Cargo.toml index f33cd1255..aae01bc3d 100644 --- a/crates/feos-core/Cargo.toml +++ b/crates/feos-core/Cargo.toml @@ -15,9 +15,9 @@ rustdoc-args = ["--html-in-header", "./docs-header.html"] features = ["rayon"] [dependencies] -quantity = { workspace = true, features = ["nalgebra", "num-dual"] } +quantity = { workspace = true, features = ["nalgebra", "ndarray", "num-dual"] } num-dual = { workspace = true } -ndarray = { workspace = true, optional = true } +ndarray = { workspace = true } nalgebra = { workspace = true } num-traits = { workspace = true } thiserror = { workspace = true } @@ -25,6 +25,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["preserve_order"] } indexmap = { workspace = true, features = ["serde"] } rayon = { workspace = true, optional = true } +csv = "1" itertools = { workspace = true } [dev-dependencies] @@ -33,5 +34,4 @@ quantity = { workspace = true, features = ["approx"] } [features] default = [] -ndarray = ["dep:ndarray", "quantity/ndarray"] rayon = ["dep:rayon", "ndarray/rayon"] diff --git a/crates/feos-core/src/ad/mod.rs b/crates/feos-core/src/ad/mod.rs index f48fb153c..0d3e257f0 100644 --- a/crates/feos-core/src/ad/mod.rs +++ b/crates/feos-core/src/ad/mod.rs @@ -1,367 +1,73 @@ -use crate::DensityInitialization::Liquid; -use crate::density_iteration::density_iteration; -use crate::{Composition, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; -use nalgebra::{Const, SVector, U1, U2}; -#[cfg(feature = "rayon")] +pub mod parameter_optimization; +pub mod properties; + +use crate::{FeosResult, Residual}; +use nalgebra::{Const, U1}; use ndarray::{Array1, Array2, ArrayView2, Zip}; -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}; +use num_dual::{Derivative, DualNum, DualSVec}; -type Gradient = DualSVec; +pub(crate) type Gradient = DualSVec; /// A model that can be evaluated with derivatives of its parameters. -pub trait ParametersAD: for<'a> From<&'a [f64]> + Residual> { - /// Return a mutable reference to the parameter named by `index` from the parameter set. - fn index_parameters_mut<'a, const P: usize>( - eos: &'a mut Self::Lifted>, - index: &str, - ) -> &'a mut Gradient

; - - /// Return the parameters with the appropriate derivatives. - fn named_derivatives( - &self, - parameter_names: [&str; P], - ) -> Self::Lifted> { - let mut eos = self.lift::>(); - for (i, p) in parameter_names.into_iter().enumerate() { - Self::index_parameters_mut(&mut eos, p).eps = - Derivative::derivative_generic(Const::

, U1, i) - } - eos - } -} - -/// Properties that can be evaluated with derivatives of model parameters. -pub trait PropertiesAD { - fn vapor_pressure( - &self, - temperature: Temperature, - ) -> FeosResult>> - where - Self: Residual>, - { - let eos_f64 = self.re(); - let (_, [vapor_density, liquid_density]) = - PhaseEquilibrium::pure_t(&eos_f64, temperature, 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 v1 = 1.0 / liquid_density.to_reduced(); - let v2 = 1.0 / vapor_density.to_reduced(); - let t = temperature.into_reduced(); - let (a1, a2) = { - let t = Gradient::from(t); - let v1 = Gradient::from(v1); - let v2 = Gradient::from(v2); - let x = Self::pure_molefracs(); - - let a1 = self.residual_helmholtz_energy(t, v1, &x); - let a2 = self.residual_helmholtz_energy(t, v2, &x); - (a1, a2) - }; - - let p = -(a1 - a2 + t * (v2 / v1).ln()) / (v1 - v2); - 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_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, - ) -> FeosResult<(Pressure>, Density>)> - where - Self: Residual>, - { - let t = Temperature::from_inner(&temperature); - PhaseEquilibrium::pure_t(self, t, None, Default::default()).map(|(p, [_, rho])| (p, rho)) - } - - fn liquid_density( - &self, - temperature: Temperature, - pressure: Pressure, - ) -> FeosResult>> - where - Self: Residual>, - { - let x = Self::pure_molefracs(); - let t = Temperature::from_inner(&temperature); - let p = Pressure::from_inner(&pressure); - density_iteration(self, t, p, &x, Some(Liquid)) - } - - #[cfg(feature = "rayon")] - fn vapor_pressure_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.vapor_pressure(inp[0] * KELVIN) - .map(|p| p.convert_into(PASCAL)) - }, - ) - } - - #[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], - parameters: ArrayView2, - input: ArrayView2, - ) -> (Array1, Array2, Array1) - where - Self: ParametersAD<1>, - { - parallelize::<_, Self, _, _>( - parameter_names, - parameters, - input, - |eos: &Self::Lifted>, inp| { - eos.liquid_density(inp[0] * KELVIN, inp[1] * PASCAL) - .map(|d| d.convert_into(KILO * MOL / (METER * METER * METER))) - }, - ) - } - - #[cfg(feature = "rayon")] - fn equilibrium_liquid_density_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.equilibrium_liquid_density(inp[0] * KELVIN) - .map(|(_, d)| d.convert_into(KILO * MOL / (METER * METER * METER))) - }, - ) - } - - fn bubble_point_pressure>( - &self, - temperature: Temperature, - pressure: Option, - 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, - pressure, - None, - Default::default(), - )?; - - // implicit differentiation is implemented here instead of just calling bubble_point with dual - // numbers, because for the first derivative, we can avoid calculating density derivatives. - let v_l = 1.0 / vle.liquid().density.to_reduced(); - let v_v = 1.0 / vle.vapor().density.to_reduced(); - let y = &vle.vapor().molefracs; - let y: SVector<_, 2> = SVector::from_fn(|i, _| y[i]); - let t = temperature.into_reduced(); - let (a_l, a_v, v_l, v_v) = { - let t = Gradient::from(t); - let v_l = Gradient::from(v_l); - let v_v = Gradient::from(v_v); - let y = y.map(Gradient::from); - let x = liquid_molefracs.map(Gradient::from); - - 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 = [rho_l[0], rho_l[1]]; - 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)) - / (v_v - v_l); - Ok(Pressure::from_reduced(p)) - } - - fn dew_point_pressure>( - &self, - temperature: Temperature, - pressure: Option, - 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, - pressure, - None, - Default::default(), - )?; - - // implicit differentiation is implemented here instead of just calling dew_point with dual - // numbers, because for the first derivative, we can avoid calculating density derivatives. - let v_l = 1.0 / vle.liquid().density.to_reduced(); - let v_v = 1.0 / vle.vapor().density.to_reduced(); - let x = &vle.liquid().molefracs; - let x: SVector<_, 2> = SVector::from_fn(|i, _| x[i]); - let t = temperature.into_reduced(); - let (a_l, a_v, v_l, v_v) = { - let t = Gradient::from(t); - let v_l = Gradient::from(v_l); - let v_v = Gradient::from(v_v); - let x = x.map(Gradient::from); - let y = vapor_molefracs.map(Gradient::from); - - 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 = [rho_l[0], rho_l[1]]; - 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)) - / (v_l - v_v); - Ok(Pressure::from_reduced(p)) +pub trait ParametersAD: Residual> { + /// Build the model by requesting each parameter by name. + /// + /// Call `f(name, differentiable)` for each parameter. The order of calls + /// defines the canonical parameter order. + /// + /// Set `differentiable` to `false` for fixed parameters. + fn build + Copy>( + f: impl FnMut(&'static str, bool) -> D, + ) -> Self::Lifted; + + /// Canonical parameter names in the order defined by [`build`](Self::build). + fn parameter_names() -> Vec<&'static str> { + let mut names = Vec::new(); + let _ = Self::build(|name, _| { + names.push(name); + 0.0 + }); + names } - #[cfg(feature = "rayon")] - fn bubble_point_pressure_parallel( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, - ) -> (Array1, Array2, Array1) - where - Self: ParametersAD<2>, - { - parallelize::<_, Self, _, _>( - parameter_names, - parameters, - input, - |eos: &Self::Lifted>, inp| { - eos.bubble_point_pressure(inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) - .map(|p| p.convert_into(PASCAL)) - }, - ) + /// Parameter names that can be differentiated, in canonical order. + fn differentiable_parameters() -> Vec<&'static str> { + let mut names = Vec::new(); + let _ = Self::build(|name, differentiable| { + if differentiable { + names.push(name); + } + 0.0 + }); + names } - #[cfg(feature = "rayon")] - fn dew_point_pressure_parallel( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, - ) -> (Array1, Array2, Array1) - where - Self: ParametersAD<2>, - { - parallelize::<_, Self, _, _>( - parameter_names, - parameters, - input, - |eos: &Self::Lifted>, inp| { - eos.dew_point_pressure(inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) - .map(|p| p.convert_into(PASCAL)) - }, - ) + /// Construct the model with derivative seeds for the `P` named parameters. + /// + /// - `parameter_values`: all parameter values in the canonical order + /// defined by [`build`](Self::build). + /// - `derivative_names`: names of the parameters to differentiate with + /// respect to. Gradient component `i` corresponds to + /// `derivative_names[i]`. + fn seed_derivatives( + parameter_values: &[f64], + derivative_names: [&str; P], + ) -> Self::Lifted> { + let mut idx = 0; + Self::build(|name, _differentiable| { + let i = idx; + idx += 1; + let mut d = Gradient::

::from(parameter_values[i]); + if let Some(seed_idx) = derivative_names.iter().position(|&n| n == name) { + d.eps = Derivative::derivative_generic(Const::

, U1, seed_idx); + } + d + }) } } -impl PropertiesAD for T {} - -#[cfg(feature = "rayon")] -fn parallelize, const N: usize, const P: usize>( +/// Evaluate a function and its gradients for a batch of parameters and inputs. +pub(crate) fn vectorize_ad, const N: usize, const P: usize>( parameter_names: [String; P], parameters: ArrayView2, input: ArrayView2, @@ -371,25 +77,68 @@ where F: Fn(&E::Lifted>, &[f64]) -> FeosResult> + Sync, { let parameter_names = parameter_names.each_ref().map(|s| s as &str); + + #[cfg(feature = "rayon")] let value_dual = Zip::from(parameters.rows()) .and(input.rows()) .par_map_collect(|par, inp| { let par = par.as_slice().expect("Parameter array is not contiguous!"); let inp = inp.as_slice().expect("Input array is not contiguous!"); - let eos = E::from(par).named_derivatives(parameter_names); + let eos = E::seed_derivatives(par, parameter_names); f(&eos, inp) }); - let status = value_dual.iter().map(|p| p.is_ok()).collect(); - let value_dual: Array1<_> = value_dual.into_iter().flatten().collect(); - let mut value = Array1::zeros(value_dual.len()); - let mut grad = Array2::zeros([value_dual.len(), P]); - Zip::from(grad.rows_mut()) - .and(&mut value) - .and(&value_dual) - .for_each(|mut grad, p, p_dual| { - *p = p_dual.re; - let eps = p_dual.eps.unwrap_generic(Const::

, U1).data.0[0].to_vec(); - grad.assign(&Array1::from(eps)); + + #[cfg(not(feature = "rayon"))] + let value_dual = Zip::from(parameters.rows()) + .and(input.rows()) + .map_collect(|par, inp| { + let par = par.as_slice().expect("Parameter array is not contiguous!"); + let inp = inp.as_slice().expect("Input array is not contiguous!"); + let eos = E::seed_derivatives(par, parameter_names); + f(&eos, inp) }); + + let n = parameters.nrows(); + let status = value_dual.iter().map(|p| p.is_ok()).collect(); + let mut value = Array1::from_elem(n, f64::NAN); + let mut grad = Array2::zeros([n, P]); + for (i, result) in value_dual.into_iter().enumerate() { + if let Ok(p_dual) = result { + value[i] = p_dual.re; + let eps = p_dual.eps.unwrap_generic(Const::

, U1); + for (g, &e) in grad.row_mut(i).iter_mut().zip(eps.data.0[0].iter()) { + *g = e; + } + } + } (value, grad, status) } + +/// Evaluate a function for a batch of inputs using the same parameters for each sample. +pub(crate) fn vectorize(eos: &E, input: ArrayView2, f: F) -> (Array1, Array1) +where + E: Sync, + F: Fn(&E, &[f64]) -> FeosResult + Sync, +{ + #[cfg(feature = "rayon")] + let values = Zip::from(input.rows()).par_map_collect(|inp| { + let inp = inp.as_slice().expect("Input array is not contiguous!"); + f(eos, inp) + }); + + #[cfg(not(feature = "rayon"))] + let values = Zip::from(input.rows()).map_collect(|inp| { + let inp = inp.as_slice().expect("Input array is not contiguous!"); + f(eos, inp) + }); + + let n = input.nrows(); + let status: Array1 = values.iter().map(|r| r.is_ok()).collect(); + let mut value = Array1::from_elem(n, f64::NAN); + for (i, result) in values.into_iter().enumerate() { + if let Ok(v) = result { + value[i] = v; + } + } + (value, status) +} diff --git a/crates/feos-core/src/ad/parameter_optimization/dataset/binary.rs b/crates/feos-core/src/ad/parameter_optimization/dataset/binary.rs new file mode 100644 index 000000000..f20d9c048 --- /dev/null +++ b/crates/feos-core/src/ad/parameter_optimization/dataset/binary.rs @@ -0,0 +1,254 @@ +use std::{io, path::Path}; + +use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; + +use crate::ad::properties::{ + BubblePointRecord, DewPointRecord, bubble_point_pressure_parallel, + bubble_point_pressure_parallel_ad, dew_point_pressure_parallel, dew_point_pressure_parallel_ad, +}; +use crate::{ParametersAD, Residual}; + +use super::{Dataset, DatasetAD, DatasetStorage}; + +/// Expand a list of binary-mixture property entries into: +/// - the [`BinaryProperty`] enum and its metadata + dispatch methods, +/// - typed constructors on [`BinaryDataset`] (one per `constructor:` ident), +/// - the [`BinaryDataset::from_csv`] / [`BinaryDataset::from_reader`] match arms. +macro_rules! binary_properties { + ($( + $variant:ident { + record: $record:ty, + default_name: $default:expr, + input_names: $inputs:expr, + target_name: $target:expr, + ad_fn: $ad_fn:ident, + eval_fn: $eval_fn:ident, + constructor: $ctor:ident, + } + ),* $(,)?) => { + /// Binary-mixture properties supported by the regressor. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum BinaryProperty { + $($variant,)* + } + + impl BinaryProperty { + pub fn default_name(self) -> &'static str { + match self { $(Self::$variant => $default,)* } + } + + pub fn input_names(self) -> &'static [&'static str] { + match self { $(Self::$variant => $inputs,)* } + } + + pub fn target_name(self) -> &'static str { + match self { $(Self::$variant => $target,)* } + } + + fn evaluate_ad, const P: usize>( + self, + names: [String; P], + parameters: ArrayView2, + inputs: ArrayView2, + ) -> (Array1, Array2, Array1) { + match self { + $(Self::$variant => $ad_fn::(names, parameters, inputs),)* + } + } + + fn evaluate(self, eos: &E, inputs: ArrayView2) -> (Array1, Array1) + where + E: Residual + Sync, + { + match self { + $(Self::$variant => $eval_fn(eos, inputs),)* + } + } + } + + impl BinaryDataset { + $( + pub fn $ctor(records: Vec<$record>) -> Self { + Self { + property: BinaryProperty::$variant, + storage: DatasetStorage::from_records(records), + } + } + )* + + pub fn from_csv(property: BinaryProperty, path: &Path) -> Result { + let storage = match property { + $(BinaryProperty::$variant => DatasetStorage::from_csv::<$record>(path)?,)* + }; + Ok(Self { property, storage }) + } + + pub fn from_reader( + property: BinaryProperty, + reader: impl io::Read, + ) -> Result { + let storage = match property { + $(BinaryProperty::$variant => DatasetStorage::from_reader::<$record>(reader)?,)* + }; + Ok(Self { property, storage }) + } + } + }; +} + +binary_properties! { + BubblePointPressure { + record: BubblePointRecord, + default_name: "bubble point pressure", + input_names: &["temperature_k", "liquid_molefrac_1"], + target_name: "bubble_pressure_pa", + ad_fn: bubble_point_pressure_parallel_ad, + eval_fn: bubble_point_pressure_parallel, + constructor: bubble_point_pressure, + }, + DewPointPressure { + record: DewPointRecord, + default_name: "dew point pressure", + input_names: &["temperature_k", "vapor_molefrac_1"], + target_name: "dew_pressure_pa", + ad_fn: dew_point_pressure_parallel_ad, + eval_fn: dew_point_pressure_parallel, + constructor: dew_point_pressure, + }, +} + +/// Binary-mixture dataset: shared data storage plus a property tag. +#[derive(Clone)] +pub struct BinaryDataset { + property: BinaryProperty, + storage: DatasetStorage, +} + +impl BinaryDataset { + pub fn with_name(mut self, name: impl Into) -> Self { + self.storage.set_name(name.into()); + self + } + + pub fn property(&self) -> BinaryProperty { + self.property + } + + pub fn inputs(&self) -> ArrayView2<'_, f64> { + self.storage.inputs() + } + + pub fn target(&self) -> ArrayView1<'_, f64> { + self.storage.target() + } + + pub fn name(&self) -> &str { + self.storage.name().unwrap_or(self.property.default_name()) + } + + pub fn input_names(&self) -> &'static [&'static str] { + self.property.input_names() + } + + pub fn target_name(&self) -> &'static str { + self.property.target_name() + } +} + +impl Dataset for BinaryDataset { + fn inputs(&self) -> ArrayView2<'_, f64> { + self.inputs() + } + + fn target(&self) -> ArrayView1<'_, f64> { + self.target() + } + + fn name(&self) -> &str { + self.name() + } + + fn input_names(&self) -> &'static [&'static str] { + self.input_names() + } + + fn target_name(&self) -> &'static str { + self.target_name() + } + + fn evaluate(&self, model: &E) -> (Array1, Array1) + where + E: Residual + Sync, + { + self.property.evaluate(model, self.inputs()) + } +} + +impl DatasetAD<2> for BinaryDataset { + fn evaluate_ad_const, const P: usize>( + &self, + names: [String; P], + parameters: ArrayView2, + inputs: ArrayView2, + ) -> (Array1, Array2, Array1) { + self.property.evaluate_ad::(names, parameters, inputs) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + fn csv(s: &str) -> Cursor<&[u8]> { + Cursor::new(s.as_bytes()) + } + + #[test] + fn bubble_point_from_reader() { + let data = "\ +temperature_k,liquid_molefrac_1,bubble_pressure_pa +300.0,0.3,500000.0 +320.0,0.5,800000.0 +"; + let ds = + BinaryDataset::from_reader(BinaryProperty::BubblePointPressure, csv(data)).unwrap(); + + assert_eq!(ds.inputs().ncols(), 3); + assert_eq!(ds.inputs().nrows(), 2); + assert_eq!(ds.inputs()[[0, 0]], 300.0); + assert_eq!(ds.inputs()[[0, 1]], 0.3); + assert_eq!(ds.inputs()[[0, 2]], 500000.0); + assert_eq!(ds.target()[0], 500000.0); + assert_eq!(ds.target()[1], 800000.0); + assert_eq!(ds.name(), "bubble point pressure"); + } + + #[test] + fn dew_point_from_reader() { + let data = "\ +temperature_k,vapor_molefrac_1,dew_pressure_pa +310.0,0.7,400000.0 +330.0,0.9,700000.0 +"; + let ds = BinaryDataset::from_reader(BinaryProperty::DewPointPressure, csv(data)).unwrap(); + + assert_eq!(ds.inputs().ncols(), 3); + assert_eq!(ds.inputs()[[0, 0]], 310.0); + assert_eq!(ds.inputs()[[0, 1]], 0.7); + assert_eq!(ds.inputs()[[0, 2]], 400000.0); + assert_eq!(ds.target()[1], 700000.0); + assert_eq!(ds.name(), "dew point pressure"); + } + + #[test] + fn dew_point_pressure_doubles_as_initial_guess() { + let records = vec![DewPointRecord { + temperature_k: 310.0, + vapor_molefrac_1: 0.7, + dew_pressure_pa: 400000.0, + }]; + let ds = BinaryDataset::dew_point_pressure(records); + assert_eq!(ds.inputs()[[0, 2]], ds.target()[0]); + } +} diff --git a/crates/feos-core/src/ad/parameter_optimization/dataset/mod.rs b/crates/feos-core/src/ad/parameter_optimization/dataset/mod.rs new file mode 100644 index 000000000..eccb5993b --- /dev/null +++ b/crates/feos-core/src/ad/parameter_optimization/dataset/mod.rs @@ -0,0 +1,210 @@ +mod binary; +mod pure; + +use std::{io, path::Path, sync::Arc}; + +use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; +use serde::Serialize; +use serde::de::DeserializeOwned; + +use crate::{ParametersAD, Residual}; + +pub use binary::{BinaryDataset, BinaryProperty}; +pub use pure::{PureDataset, PureProperty}; + +/// Per-dataset evaluation result: inputs, experimental target, model +/// prediction, and derived statistics at a given set of parameters. +/// +/// Produced by [`crate::Regressor::evaluate_datasets`]. +/// Fully serializable and convertable into any tabular format (CSV, JSON, DataFrame). +#[derive(Debug, Serialize)] +pub struct DatasetResult { + /// Dataset name (default property name or user-supplied). + pub name: String, + /// Reported independent input column names and their values. + pub inputs: Vec<(&'static str, Vec)>, + /// Name of the target property column. + pub target_name: &'static str, + /// Experimental target values. + pub target: Vec, + /// Values at the given parameters predicted by the model. + /// `NaN` for points where calculations did not converge. + pub predicted: Vec, + /// Whether the calculation converged for each point. + pub converged: Vec, + /// Relative deviation `(predicted − target) / target`. + /// `NaN` for non-converged points. + pub relative_deviation: Vec, +} + +/// Shared numerical data for all datasets. +struct DatasetData { + inputs: Array2, + target: Array1, +} + +/// Shared in-memory representation for all datasets. +/// +/// Cheap to clone: the inputs/target arrays are kept behind an `Arc`, +/// while the user-supplied name is owned per handle. +#[derive(Clone)] +struct DatasetStorage { + data: Arc, + name: Option, +} + +impl DatasetStorage { + fn from_records(records: Vec) -> Self { + let n = records.len(); + let inputs = Array2::from_shape_fn((n, R::N_INPUTS), |(i, j)| records[i].input(j)); + let target = Array1::from_iter(records.iter().map(DatasetRecord::target)); + Self { + data: Arc::new(DatasetData { inputs, target }), + name: None, + } + } + + fn from_csv(path: &Path) -> Result { + let records = csv::Reader::from_path(path)? + .deserialize() + .collect::, _>>()?; + Ok(Self::from_records(records)) + } + + fn from_reader(reader: impl io::Read) -> Result { + let records = csv::Reader::from_reader(reader) + .deserialize() + .collect::, _>>()?; + Ok(Self::from_records(records)) + } + + fn inputs(&self) -> ArrayView2<'_, f64> { + self.data.inputs.view() + } + + fn target(&self) -> ArrayView1<'_, f64> { + self.data.target.view() + } + + fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + fn set_name(&mut self, name: String) { + self.name = Some(name); + } +} + +/// Conversion logic for records that can be collected into a dataset. +pub trait DatasetRecord: DeserializeOwned { + /// Number of columns passed to the property evaluator. + const N_INPUTS: usize; + + /// Value of model input column `column`. + fn input(&self, column: usize) -> f64; + + /// Experimental target value. + fn target(&self) -> f64; +} + +/// Experimental data container with metadata and a non-AD model evaluator. +/// +/// Implementors expose the inputs and targets plus an +/// [`evaluate`](Self::evaluate) method that runs the dataset's property +/// against a single model without computing gradients. Used for model +/// comparison and CLI/Python multi-model workflows. +pub trait Dataset { + /// Inputs for model evaluation, shape `[n_points, k]`. + fn inputs(&self) -> ArrayView2<'_, f64>; + + /// Target values, shape `[n_points]`. + fn target(&self) -> ArrayView1<'_, f64>; + + /// Property name used for logging and diagnostics. + fn name(&self) -> &str; + + /// Names of independent input columns reported in diagnostics. + /// + /// These can be fewer than the number of columns in [`Self::inputs`] when + /// the target is also passed to the model as an initial guess. + fn input_names(&self) -> &'static [&'static str]; + + /// Name of the target property column. + fn target_name(&self) -> &'static str; + + /// Evaluate this dataset's property against a single model. + /// + /// Returns `(predicted, converged)`: + /// - `predicted`: shape `[n_points]`, in SI units; `NaN` where the + /// underlying solver did not converge. + /// - `converged`: shape `[n_points]`. + fn evaluate(&self, model: &E) -> (Array1, Array1) + where + E: Residual + Sync; +} + +/// Maximum number of parameters that can be fitted simultaneously. +/// +/// Enforced upstream at `Regressor::new`. The match arms generated by +/// [`impl_evaluate_ad!`] must cover `1..=MAX_FITTED_PARAMETERS`; bumping +/// this constant means widening that list too. +pub(crate) const MAX_FITTED_PARAMETERS: usize = 14; + +/// Emit the default body of [`DatasetAD::evaluate_ad`]: a runtime-`P` → +/// const-`P` dispatch covering each listed parameter count. +macro_rules! impl_evaluate_ad { + ($($p:literal),+ $(,)?) => { + /// Evaluate the property and its parameter gradients at the given parameters. + /// + /// - `param_names`: names of the `P` parameters being differentiated. + /// - `params`: the full parameter vector; only entries listed in `param_names` are seeded. + /// + /// Returns `(predicted, gradients, converged)`: + /// - `predicted`: shape `[n_points]`, in SI units. + /// - `gradients`: shape `[n_points, P]`. + /// - `converged`: shape `[n_points]`. + fn evaluate_ad>( + &self, + param_names: &[String], + params: &[f64], + ) -> (Array1, Array2, Array1) { + let n = self.inputs().nrows(); + let parameters = Array2::from_shape_fn((n, params.len()), |(_, j)| params[j]); + + fn to_const(names: &[String]) -> [String; P] { + names.to_vec().try_into().expect("parameter count mismatch") + } + + match param_names.len() { + $( + $p => self.evaluate_ad_const::( + to_const(param_names), + parameters.view(), + self.inputs(), + ), + )+ + p => unreachable!( + "Regressor::new rejects fit lists longer than \ + MAX_FITTED_PARAMETERS={MAX_FITTED_PARAMETERS}; got {p}", + ), + } + } + }; +} + +/// Experimental data container that supports parameter-gradient evaluation +/// for models implementing [`ParametersAD`]. +pub trait DatasetAD: Dataset { + /// Evaluate the property and its `P` parameter gradients at compile-time-known `P`. + /// + /// Implementors typically delegate to a property-specific + /// `*_parallel_ad::` function. + fn evaluate_ad_const, const P: usize>( + &self, + names: [String; P], + parameters: ArrayView2, + inputs: ArrayView2, + ) -> (Array1, Array2, Array1); + + impl_evaluate_ad!(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14); +} diff --git a/crates/feos-core/src/ad/parameter_optimization/dataset/pure.rs b/crates/feos-core/src/ad/parameter_optimization/dataset/pure.rs new file mode 100644 index 000000000..3f3a756f2 --- /dev/null +++ b/crates/feos-core/src/ad/parameter_optimization/dataset/pure.rs @@ -0,0 +1,343 @@ +use std::{io, path::Path}; + +use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; + +use crate::ad::properties::{ + EnthalpyOfVaporizationRecord, EquilibriumLiquidDensityRecord, LiquidDensityRecord, + ResidualIsobaricHeatCapacityRecord, VaporPressureRecord, enthalpy_of_vaporization_parallel, + enthalpy_of_vaporization_parallel_ad, equilibrium_liquid_density_parallel, + equilibrium_liquid_density_parallel_ad, liquid_density_parallel, liquid_density_parallel_ad, + residual_isobaric_heat_capacity_parallel, residual_isobaric_heat_capacity_parallel_ad, + vapor_pressure_parallel, vapor_pressure_parallel_ad, +}; +use crate::{ParametersAD, Residual}; + +use super::{Dataset, DatasetAD, DatasetStorage}; + +/// Expand a list of pure-component property entries into: +/// - the [`PureProperty`] enum and its metadata + dispatch methods, +/// - typed constructors on [`PureDataset`] (one per `constructor:` ident), +/// - the [`PureDataset::from_csv`] / [`PureDataset::from_reader`] match arms. +/// +/// Adding a new property means writing the property file (record, `*_ad`, +/// `*_parallel`, `*_parallel_ad`) and adding one entry here. +macro_rules! pure_properties { + ($( + $variant:ident { + record: $record:ty, + default_name: $default:expr, + input_names: $inputs:expr, + target_name: $target:expr, + ad_fn: $ad_fn:ident, + eval_fn: $eval_fn:ident, + constructor: $ctor:ident, + } + ),* $(,)?) => { + /// Pure-component properties. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum PureProperty { + $($variant,)* + } + + impl PureProperty { + pub fn default_name(self) -> &'static str { + match self { $(Self::$variant => $default,)* } + } + + pub fn input_names(self) -> &'static [&'static str] { + match self { $(Self::$variant => $inputs,)* } + } + + pub fn target_name(self) -> &'static str { + match self { $(Self::$variant => $target,)* } + } + + fn evaluate_ad, const P: usize>( + self, + names: [String; P], + parameters: ArrayView2, + inputs: ArrayView2, + ) -> (Array1, Array2, Array1) { + match self { + $(Self::$variant => $ad_fn::(names, parameters, inputs),)* + } + } + + fn evaluate(self, eos: &E, inputs: ArrayView2) -> (Array1, Array1) + where + E: Residual + Sync, + { + match self { + $(Self::$variant => $eval_fn(eos, inputs),)* + } + } + } + + impl PureDataset { + $( + pub fn $ctor(records: Vec<$record>) -> Self { + Self { + property: PureProperty::$variant, + storage: DatasetStorage::from_records(records), + } + } + )* + + pub fn from_csv(property: PureProperty, path: &Path) -> Result { + let storage = match property { + $(PureProperty::$variant => DatasetStorage::from_csv::<$record>(path)?,)* + }; + Ok(Self { property, storage }) + } + + pub fn from_reader( + property: PureProperty, + reader: impl io::Read, + ) -> Result { + let storage = match property { + $(PureProperty::$variant => DatasetStorage::from_reader::<$record>(reader)?,)* + }; + Ok(Self { property, storage }) + } + } + }; +} + +pure_properties! { + VaporPressure { + record: VaporPressureRecord, + default_name: "vapor pressure", + input_names: &["temperature_k"], + target_name: "vapor_pressure_pa", + ad_fn: vapor_pressure_parallel_ad, + eval_fn: vapor_pressure_parallel, + constructor: vapor_pressure, + }, + LiquidDensity { + record: LiquidDensityRecord, + default_name: "liquid density", + input_names: &["temperature_k", "pressure_pa"], + target_name: "liquid_density_kmol_m3", + ad_fn: liquid_density_parallel_ad, + eval_fn: liquid_density_parallel, + constructor: liquid_density, + }, + EquilibriumLiquidDensity { + record: EquilibriumLiquidDensityRecord, + default_name: "equilibrium liquid density", + input_names: &["temperature_k"], + target_name: "liquid_density_kmol_m3", + ad_fn: equilibrium_liquid_density_parallel_ad, + eval_fn: equilibrium_liquid_density_parallel, + constructor: equilibrium_liquid_density, + }, + EnthalpyOfVaporization { + record: EnthalpyOfVaporizationRecord, + default_name: "enthalpy of vaporization", + input_names: &["temperature_k"], + target_name: "dh_vap_j_mol", + ad_fn: enthalpy_of_vaporization_parallel_ad, + eval_fn: enthalpy_of_vaporization_parallel, + constructor: enthalpy_of_vaporization, + }, + ResidualIsobaricHeatCapacity { + record: ResidualIsobaricHeatCapacityRecord, + default_name: "residual isobaric heat capacity", + input_names: &["temperature_k", "pressure_pa"], + target_name: "cp_res_j_molk", + ad_fn: residual_isobaric_heat_capacity_parallel_ad, + eval_fn: residual_isobaric_heat_capacity_parallel, + constructor: residual_isobaric_heat_capacity, + }, +} + +/// Pure-component dataset: shared data storage plus a property tag. +#[derive(Clone)] +pub struct PureDataset { + property: PureProperty, + storage: DatasetStorage, +} + +impl PureDataset { + pub fn with_name(mut self, name: impl Into) -> Self { + self.storage.set_name(name.into()); + self + } + + pub fn property(&self) -> PureProperty { + self.property + } + + pub fn inputs(&self) -> ArrayView2<'_, f64> { + self.storage.inputs() + } + + pub fn target(&self) -> ArrayView1<'_, f64> { + self.storage.target() + } + + pub fn name(&self) -> &str { + self.storage.name().unwrap_or(self.property.default_name()) + } + + pub fn input_names(&self) -> &'static [&'static str] { + self.property.input_names() + } + + pub fn target_name(&self) -> &'static str { + self.property.target_name() + } +} + +impl Dataset for PureDataset { + fn inputs(&self) -> ArrayView2<'_, f64> { + self.inputs() + } + + fn target(&self) -> ArrayView1<'_, f64> { + self.target() + } + + fn name(&self) -> &str { + self.name() + } + + fn input_names(&self) -> &'static [&'static str] { + self.input_names() + } + + fn target_name(&self) -> &'static str { + self.target_name() + } + + fn evaluate(&self, model: &E) -> (Array1, Array1) + where + E: Residual + Sync, + { + self.property.evaluate(model, self.inputs()) + } +} + +impl DatasetAD<1> for PureDataset { + fn evaluate_ad_const, const P: usize>( + &self, + names: [String; P], + parameters: ArrayView2, + inputs: ArrayView2, + ) -> (Array1, Array2, Array1) { + self.property.evaluate_ad::(names, parameters, inputs) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + fn csv(s: &str) -> Cursor<&[u8]> { + Cursor::new(s.as_bytes()) + } + + #[test] + fn vapor_pressure_from_reader() { + let data = "\ +temperature_k,vapor_pressure_pa +300.0,3540.0 +350.0,41682.0 +400.0,245600.0 +"; + let ds = PureDataset::from_reader(PureProperty::VaporPressure, csv(data)).unwrap(); + + assert_eq!(ds.target().len(), 3); + assert_eq!(ds.inputs().nrows(), 3); + assert_eq!(ds.inputs().ncols(), 1); + assert_eq!(ds.inputs()[[0, 0]], 300.0); + assert_eq!(ds.inputs()[[1, 0]], 350.0); + assert_eq!(ds.inputs()[[2, 0]], 400.0); + assert_eq!(ds.target()[0], 3540.0); + assert_eq!(ds.target()[1], 41682.0); + assert_eq!(ds.target()[2], 245600.0); + assert_eq!(ds.name(), "vapor pressure"); + } + + #[test] + fn vapor_pressure_from_records() { + let records = vec![ + VaporPressureRecord { + temperature_k: 300.0, + vapor_pressure_pa: 3540.0, + }, + VaporPressureRecord { + temperature_k: 350.0, + vapor_pressure_pa: 41682.0, + }, + ]; + let ds = PureDataset::vapor_pressure(records); + assert_eq!(ds.inputs()[[0, 0]], 300.0); + assert_eq!(ds.target()[1], 41682.0); + } + + #[test] + fn liquid_density_from_reader() { + let data = "\ +temperature_k,pressure_pa,liquid_density_kmol_m3 +300.0,101325.0,15.2 +320.0,200000.0,14.8 +"; + let ds = PureDataset::from_reader(PureProperty::LiquidDensity, csv(data)).unwrap(); + + assert_eq!(ds.inputs().nrows(), 2); + assert_eq!(ds.inputs().ncols(), 2); + assert_eq!(ds.inputs()[[0, 0]], 300.0); + assert_eq!(ds.inputs()[[0, 1]], 101325.0); + assert_eq!(ds.inputs()[[1, 0]], 320.0); + assert_eq!(ds.inputs()[[1, 1]], 200000.0); + assert_eq!(ds.target()[0], 15.2); + assert_eq!(ds.target()[1], 14.8); + assert_eq!(ds.name(), "liquid density"); + } + + #[test] + fn liquid_density_from_records() { + let records = vec![LiquidDensityRecord { + temperature_k: 300.0, + pressure_pa: 101325.0, + liquid_density_kmol_m3: 15.2, + }]; + let ds = PureDataset::liquid_density(records); + assert_eq!(ds.inputs()[[0, 1]], 101325.0); + assert_eq!(ds.target()[0], 15.2); + } + + #[test] + fn equilibrium_liquid_density_from_reader() { + let data = "\ +temperature_k,liquid_density_kmol_m3 +290.0,15.5 +310.0,14.9 +330.0,14.1 +"; + let ds = + PureDataset::from_reader(PureProperty::EquilibriumLiquidDensity, csv(data)).unwrap(); + + assert_eq!(ds.inputs().ncols(), 1); + assert_eq!(ds.inputs().nrows(), 3); + assert_eq!(ds.inputs()[[2, 0]], 330.0); + assert_eq!(ds.target()[2], 14.1); + assert_eq!(ds.name(), "equilibrium liquid density"); + } + + #[test] + fn missing_column_returns_error() { + let data = "temperature_k\n300.0\n"; + let result = PureDataset::from_reader(PureProperty::VaporPressure, csv(data)); + assert!(result.is_err()); + } + + #[test] + fn wrong_type_returns_error() { + let data = "temperature_k,vapor_pressure_pa\n300.0,not_a_number\n"; + let result = PureDataset::from_reader(PureProperty::VaporPressure, csv(data)); + assert!(result.is_err()); + } +} diff --git a/crates/feos-core/src/ad/parameter_optimization/mod.rs b/crates/feos-core/src/ad/parameter_optimization/mod.rs new file mode 100644 index 000000000..329e075c9 --- /dev/null +++ b/crates/feos-core/src/ad/parameter_optimization/mod.rs @@ -0,0 +1,11 @@ +pub mod dataset; + +pub use crate::ad::properties::{ + BubblePointRecord, DewPointRecord, EnthalpyOfVaporizationRecord, + EquilibriumLiquidDensityRecord, LiquidDensityRecord, ResidualIsobaricHeatCapacityRecord, + VaporPressureRecord, +}; +pub use dataset::{ + BinaryDataset, BinaryProperty, Dataset, DatasetAD, DatasetRecord, DatasetResult, PureDataset, + PureProperty, +}; diff --git a/crates/feos-core/src/ad/properties/boiling_temperature.rs b/crates/feos-core/src/ad/properties/boiling_temperature.rs new file mode 100644 index 000000000..4300fe74e --- /dev/null +++ b/crates/feos-core/src/ad/properties/boiling_temperature.rs @@ -0,0 +1,70 @@ +use crate::ad::Gradient; +use crate::ad::{ParametersAD, vectorize, vectorize_ad}; +use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; +use nalgebra::U1; +use ndarray::{Array1, Array2, ArrayView2}; +use num_dual::{DualNum, first_derivative, partial2}; +use quantity::{KELVIN, PASCAL, Pressure, Temperature}; + +pub fn boiling_temperature_ad>, const P: usize>( + eos: &E, + pressure: Pressure, +) -> FeosResult>> { + let eos_f64 = eos.re(); + let (temperature, [vapor_density, liquid_density]) = + PhaseEquilibrium::pure_p(&eos_f64, pressure, None, Default::default())?; + + 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 = E::pure_molefracs(); + + let residual_entropy = |v| { + let (a, s) = first_derivative( + partial2( + |t, &v, x| eos.lift().residual_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)) +} + +pub fn boiling_temperature(eos: &E, pressure: Pressure) -> FeosResult { + let (t, _) = PhaseEquilibrium::pure_p(eos, pressure, None, Default::default())?; + Ok(t) +} + +pub fn boiling_temperature_parallel( + eos: &E, + input: ArrayView2, +) -> (Array1, Array1) { + vectorize(eos, input, |eos, inp| { + boiling_temperature(eos, inp[0] * PASCAL).map(|t| t.convert_into(KELVIN)) + }) +} + +pub fn boiling_temperature_parallel_ad, const P: usize>( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, +) -> (Array1, Array2, Array1) { + vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { + boiling_temperature_ad(eos, inp[0] * PASCAL).map(|t| t.convert_into(KELVIN)) + }) +} diff --git a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs new file mode 100644 index 000000000..52b0b669f --- /dev/null +++ b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs @@ -0,0 +1,122 @@ +use crate::Contributions; +use crate::ad::Gradient; +use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::{ParametersAD, vectorize, vectorize_ad}; +use crate::{Composition, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; +use nalgebra::{SVector, U2}; +use ndarray::{Array1, Array2, ArrayView2}; +use quantity::{KELVIN, PASCAL, Pressure, Temperature}; +use serde::{Deserialize, Serialize}; + +/// The pressure column doubles as the initial guess passed to the VLE solver. +#[derive(Deserialize, Serialize)] +pub struct BubblePointRecord { + pub temperature_k: f64, + pub liquid_molefrac_1: f64, + pub bubble_pressure_pa: f64, +} + +impl DatasetRecord for BubblePointRecord { + const N_INPUTS: usize = 3; + + fn input(&self, column: usize) -> f64 { + match column { + 0 => self.temperature_k, + 1 => self.liquid_molefrac_1, + 2 => self.bubble_pressure_pa, + _ => unreachable!("invalid bubble point input column"), + } + } + + fn target(&self) -> f64 { + self.bubble_pressure_pa + } +} + +pub fn bubble_point_pressure_ad< + E: Residual>, + const P: usize, + X: Composition, +>( + eos: &E, + temperature: Temperature, + pressure: Option, + liquid_molefracs: X, +) -> FeosResult>> { + let eos_f64 = eos.re(); + let (liquid_molefracs, _) = liquid_molefracs.into_molefracs(&eos_f64)?; + let vle = PhaseEquilibrium::bubble_point( + &eos_f64, + temperature, + liquid_molefracs, + pressure, + None, + Default::default(), + )?; + + let v_l = 1.0 / vle.liquid().density.to_reduced(); + let v_v = 1.0 / vle.vapor().density.to_reduced(); + let y = &vle.vapor().molefracs; + let y: SVector<_, 2> = SVector::from_fn(|i, _| y[i]); + let t = temperature.into_reduced(); + let (a_l, a_v, v_l, v_v) = { + let t = Gradient::from(t); + let v_l = Gradient::from(v_l); + let v_v = Gradient::from(v_v); + let y = y.map(Gradient::from); + let x = liquid_molefracs.map(Gradient::from); + + let a_v = eos.residual_helmholtz_energy(t, v_v, &y); + let (p_l, mu_res_l, dp_l, dmu_l) = eos.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 = [rho_l[0], rho_l[1]]; + 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)) + / (v_v - v_l); + Ok(Pressure::from_reduced(p)) +} + +pub fn bubble_point_pressure( + eos: &E, + temperature: Temperature, + pressure_init: Option, + liquid_molefrac_1: f64, +) -> FeosResult { + let vle = PhaseEquilibrium::bubble_point( + eos, + temperature, + liquid_molefrac_1, + pressure_init, + None, + Default::default(), + )?; + Ok(vle.vapor().pressure(Contributions::Total)) +} + +pub fn bubble_point_pressure_parallel( + eos: &E, + input: ArrayView2, +) -> (Array1, Array1) { + vectorize(eos, input, |eos, inp| { + bubble_point_pressure(eos, inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) + .map(|p| p.convert_into(PASCAL)) + }) +} + +pub fn bubble_point_pressure_parallel_ad, const P: usize>( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, +) -> (Array1, Array2, Array1) { + vectorize_ad::<_, T, 2, P>(parameter_names, parameters, input, |eos, inp| { + bubble_point_pressure_ad(eos, inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) + .map(|p| p.convert_into(PASCAL)) + }) +} diff --git a/crates/feos-core/src/ad/properties/dew_point_pressure.rs b/crates/feos-core/src/ad/properties/dew_point_pressure.rs new file mode 100644 index 000000000..a6312d292 --- /dev/null +++ b/crates/feos-core/src/ad/properties/dew_point_pressure.rs @@ -0,0 +1,121 @@ +use crate::ad::Gradient; +use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::{ParametersAD, vectorize, vectorize_ad}; +use crate::{Composition, Contributions, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; +use nalgebra::{SVector, U2}; +use ndarray::{Array1, Array2, ArrayView2}; +use quantity::{KELVIN, PASCAL, Pressure, Temperature}; +use serde::{Deserialize, Serialize}; + +/// The pressure column doubles as the initial guess passed to the VLE solver. +#[derive(Deserialize, Serialize)] +pub struct DewPointRecord { + pub temperature_k: f64, + pub vapor_molefrac_1: f64, + pub dew_pressure_pa: f64, +} + +impl DatasetRecord for DewPointRecord { + const N_INPUTS: usize = 3; + + fn input(&self, column: usize) -> f64 { + match column { + 0 => self.temperature_k, + 1 => self.vapor_molefrac_1, + 2 => self.dew_pressure_pa, + _ => unreachable!("invalid dew point input column"), + } + } + + fn target(&self) -> f64 { + self.dew_pressure_pa + } +} + +pub fn dew_point_pressure_ad< + E: Residual>, + const P: usize, + X: Composition, +>( + eos: &E, + temperature: Temperature, + pressure: Option, + vapor_molefracs: X, +) -> FeosResult>> { + let eos_f64 = eos.re(); + let (vapor_molefracs, _) = vapor_molefracs.into_molefracs(&eos_f64)?; + let vle = PhaseEquilibrium::dew_point( + &eos_f64, + temperature, + vapor_molefracs, + pressure, + None, + Default::default(), + )?; + + let v_l = 1.0 / vle.liquid().density.to_reduced(); + let v_v = 1.0 / vle.vapor().density.to_reduced(); + let x = &vle.liquid().molefracs; + let x: SVector<_, 2> = SVector::from_fn(|i, _| x[i]); + let t = temperature.into_reduced(); + let (a_l, a_v, v_l, v_v) = { + let t = Gradient::from(t); + let v_l = Gradient::from(v_l); + let v_v = Gradient::from(v_v); + let x = x.map(Gradient::from); + let y = vapor_molefracs.map(Gradient::from); + + let a_l = eos.residual_helmholtz_energy(t, v_l, &x); + let (p_v, mu_res_v, dp_v, dmu_v) = eos.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 = [rho_l[0], rho_l[1]]; + 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)) + / (v_l - v_v); + Ok(Pressure::from_reduced(p)) +} + +pub fn dew_point_pressure( + eos: &E, + temperature: Temperature, + pressure_init: Option, + vapor_molefrac_1: f64, +) -> FeosResult { + let vle = PhaseEquilibrium::dew_point( + eos, + temperature, + vapor_molefrac_1, + pressure_init, + None, + Default::default(), + )?; + Ok(vle.vapor().pressure(Contributions::Total)) +} + +pub fn dew_point_pressure_parallel( + eos: &E, + input: ArrayView2, +) -> (Array1, Array1) { + vectorize(eos, input, |eos, inp| { + dew_point_pressure(eos, inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) + .map(|p| p.convert_into(PASCAL)) + }) +} + +pub fn dew_point_pressure_parallel_ad, const P: usize>( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, +) -> (Array1, Array2, Array1) { + vectorize_ad::<_, T, 2, P>(parameter_names, parameters, input, |eos, inp| { + dew_point_pressure_ad(eos, inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) + .map(|p| p.convert_into(PASCAL)) + }) +} diff --git a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs new file mode 100644 index 000000000..34977bb17 --- /dev/null +++ b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs @@ -0,0 +1,87 @@ +use crate::ad::Gradient; +use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::{ParametersAD, vectorize, vectorize_ad}; +use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; +use nalgebra::U1; +use ndarray::{Array1, Array2, ArrayView2}; +use num_dual::{DualNum, DualStruct, first_derivative, partial2}; +use quantity::{JOULE, KELVIN, MOL, MolarEnergy, Temperature}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct EnthalpyOfVaporizationRecord { + pub temperature_k: f64, + pub dh_vap_j_mol: f64, +} + +impl DatasetRecord for EnthalpyOfVaporizationRecord { + const N_INPUTS: usize = 1; + + fn input(&self, _column: usize) -> f64 { + self.temperature_k + } + + fn target(&self) -> f64 { + self.dh_vap_j_mol + } +} + +pub fn enthalpy_of_vaporization_ad>, const P: usize>( + eos: &E, + temperature: Temperature, +) -> FeosResult>> { + let t = Temperature::from_inner(&temperature); + let (_, [vapor_density, liquid_density]) = + PhaseEquilibrium::pure_t(eos, t, None, Default::default())?; + + let v1 = liquid_density.into_reduced().recip(); + let v2 = vapor_density.into_reduced().recip(); + let x = E::pure_molefracs(); + let t = t.into_reduced(); + let residual_entropy = |v| { + let (_a, s) = first_derivative( + partial2( + |t, &v, x| eos.lift().residual_helmholtz_energy(t, v, x), + &v, + &x, + ), + t, + ); + -s + }; + + let s1 = residual_entropy(v1); + let s2 = residual_entropy(v2); + + let dh = t * ((v2 / v1).ln() + s2 - s1); + Ok(MolarEnergy::from_reduced(dh)) +} + +pub fn enthalpy_of_vaporization( + eos: &E, + temperature: Temperature, +) -> FeosResult { + let vle = PhaseEquilibrium::pure(eos, temperature, None, Default::default())?; + let h_v = vle.vapor().residual_molar_enthalpy(); + let h_l = vle.liquid().residual_molar_enthalpy(); + Ok(h_v - h_l) +} + +pub fn enthalpy_of_vaporization_parallel( + eos: &E, + input: ArrayView2, +) -> (Array1, Array1) { + vectorize(eos, input, |eos, inp| { + enthalpy_of_vaporization(eos, inp[0] * KELVIN).map(|dh| dh.convert_into(JOULE / MOL)) + }) +} + +pub fn enthalpy_of_vaporization_parallel_ad, const P: usize>( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, +) -> (Array1, Array2, Array1) { + vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { + enthalpy_of_vaporization_ad(eos, inp[0] * KELVIN).map(|dh| dh.convert_into(JOULE / MOL)) + }) +} diff --git a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs new file mode 100644 index 000000000..49b258784 --- /dev/null +++ b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs @@ -0,0 +1,64 @@ +use crate::ad::Gradient; +use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::{ParametersAD, vectorize, vectorize_ad}; +use crate::{FeosResult, PhaseEquilibrium, Residual}; +use nalgebra::U1; +use ndarray::{Array1, Array2, ArrayView2}; +use num_dual::DualStruct; +use quantity::{Density, KELVIN, KILO, METER, MOL, Pressure, Temperature}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct EquilibriumLiquidDensityRecord { + pub temperature_k: f64, + pub liquid_density_kmol_m3: f64, +} + +impl DatasetRecord for EquilibriumLiquidDensityRecord { + const N_INPUTS: usize = 1; + + fn input(&self, _column: usize) -> f64 { + self.temperature_k + } + + fn target(&self) -> f64 { + self.liquid_density_kmol_m3 + } +} + +pub fn equilibrium_liquid_density_ad>, const P: usize>( + eos: &E, + temperature: Temperature, +) -> FeosResult<(Pressure>, Density>)> { + let t = Temperature::from_inner(&temperature); + PhaseEquilibrium::pure_t(eos, t, None, Default::default()).map(|(p, [_, rho])| (p, rho)) +} + +pub fn equilibrium_liquid_density( + eos: &E, + temperature: Temperature, +) -> FeosResult { + let (_, [_, rho]) = PhaseEquilibrium::pure_t(eos, temperature, None, Default::default())?; + Ok(rho) +} + +pub fn equilibrium_liquid_density_parallel( + eos: &E, + input: ArrayView2, +) -> (Array1, Array1) { + vectorize(eos, input, |eos, inp| { + equilibrium_liquid_density(eos, inp[0] * KELVIN) + .map(|d| d.convert_into(KILO * MOL / (METER * METER * METER))) + }) +} + +pub fn equilibrium_liquid_density_parallel_ad, const P: usize>( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, +) -> (Array1, Array2, Array1) { + vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { + equilibrium_liquid_density_ad(eos, inp[0] * KELVIN) + .map(|(_, d)| d.convert_into(KILO * MOL / (METER * METER * METER))) + }) +} diff --git a/crates/feos-core/src/ad/properties/liquid_density.rs b/crates/feos-core/src/ad/properties/liquid_density.rs new file mode 100644 index 000000000..4777f3f78 --- /dev/null +++ b/crates/feos-core/src/ad/properties/liquid_density.rs @@ -0,0 +1,81 @@ +use crate::DensityInitialization::Liquid; +use crate::ad::Gradient; +use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::{ParametersAD, vectorize, vectorize_ad}; +use crate::density_iteration::density_iteration; +use crate::{FeosResult, ReferenceSystem, Residual, State}; +use nalgebra::U1; +use ndarray::{Array1, Array2, ArrayView2}; +use num_dual::DualStruct; +use quantity::{Density, KELVIN, KILO, METER, MOL, Moles, PASCAL, Pressure, Temperature}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct LiquidDensityRecord { + pub temperature_k: f64, + pub pressure_pa: f64, + pub liquid_density_kmol_m3: f64, +} + +impl DatasetRecord for LiquidDensityRecord { + const N_INPUTS: usize = 2; + + fn input(&self, column: usize) -> f64 { + match column { + 0 => self.temperature_k, + 1 => self.pressure_pa, + _ => unreachable!("invalid liquid density input column"), + } + } + + fn target(&self) -> f64 { + self.liquid_density_kmol_m3 + } +} + +pub fn liquid_density_ad>, const P: usize>( + eos: &E, + temperature: Temperature, + pressure: Pressure, +) -> FeosResult>> { + let x = E::pure_molefracs(); + let t = Temperature::from_inner(&temperature); + let p = Pressure::from_inner(&pressure); + density_iteration(eos, t, p, &x, Some(Liquid)) +} + +pub fn liquid_density( + eos: &E, + temperature: Temperature, + pressure: Pressure, +) -> FeosResult { + let state = State::new_npt( + eos, + temperature, + pressure, + &Moles::from_reduced(nalgebra::DVector::from_element(eos.components(), 1.0)), + Some(Liquid), + )?; + Ok(state.density) +} + +pub fn liquid_density_parallel( + eos: &E, + input: ArrayView2, +) -> (Array1, Array1) { + vectorize(eos, input, |eos, inp| { + liquid_density(eos, inp[0] * KELVIN, inp[1] * PASCAL) + .map(|d| d.convert_into(KILO * MOL / (METER * METER * METER))) + }) +} + +pub fn liquid_density_parallel_ad, const P: usize>( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, +) -> (Array1, Array2, Array1) { + vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { + liquid_density_ad(eos, inp[0] * KELVIN, inp[1] * PASCAL) + .map(|d| d.convert_into(KILO * MOL / (METER * METER * METER))) + }) +} diff --git a/crates/feos-core/src/ad/properties/mod.rs b/crates/feos-core/src/ad/properties/mod.rs new file mode 100644 index 000000000..aff5e28de --- /dev/null +++ b/crates/feos-core/src/ad/properties/mod.rs @@ -0,0 +1,43 @@ +pub mod boiling_temperature; +pub mod bubble_point_pressure; +pub mod dew_point_pressure; +pub mod enthalpy_of_vaporization; +pub mod equilibrium_liquid_density; +pub mod liquid_density; +pub mod residual_isobaric_heat_capacity; +pub mod vapor_pressure; + +pub use boiling_temperature::{boiling_temperature, boiling_temperature_ad}; +pub use bubble_point_pressure::{ + BubblePointRecord, bubble_point_pressure, bubble_point_pressure_ad, +}; +pub use dew_point_pressure::{DewPointRecord, dew_point_pressure, dew_point_pressure_ad}; +pub use enthalpy_of_vaporization::{ + EnthalpyOfVaporizationRecord, enthalpy_of_vaporization, enthalpy_of_vaporization_ad, +}; +pub use equilibrium_liquid_density::{ + EquilibriumLiquidDensityRecord, equilibrium_liquid_density, equilibrium_liquid_density_ad, +}; +pub use liquid_density::{LiquidDensityRecord, liquid_density, liquid_density_ad}; +pub use residual_isobaric_heat_capacity::{ + ResidualIsobaricHeatCapacityRecord, residual_isobaric_heat_capacity, + residual_isobaric_heat_capacity_ad, +}; +pub use vapor_pressure::{VaporPressureRecord, vapor_pressure, vapor_pressure_ad}; + +pub use boiling_temperature::{boiling_temperature_parallel, boiling_temperature_parallel_ad}; +pub use bubble_point_pressure::{ + bubble_point_pressure_parallel, bubble_point_pressure_parallel_ad, +}; +pub use dew_point_pressure::{dew_point_pressure_parallel, dew_point_pressure_parallel_ad}; +pub use enthalpy_of_vaporization::{ + enthalpy_of_vaporization_parallel, enthalpy_of_vaporization_parallel_ad, +}; +pub use equilibrium_liquid_density::{ + equilibrium_liquid_density_parallel, equilibrium_liquid_density_parallel_ad, +}; +pub use liquid_density::{liquid_density_parallel, liquid_density_parallel_ad}; +pub use residual_isobaric_heat_capacity::{ + residual_isobaric_heat_capacity_parallel, residual_isobaric_heat_capacity_parallel_ad, +}; +pub use vapor_pressure::{vapor_pressure_parallel, vapor_pressure_parallel_ad}; diff --git a/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs new file mode 100644 index 000000000..e740ff694 --- /dev/null +++ b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs @@ -0,0 +1,85 @@ +use crate::DensityInitialization::Liquid; +use crate::ad::Gradient; +use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::{ParametersAD, vectorize, vectorize_ad}; +use crate::density_iteration::density_iteration; +use crate::{FeosResult, ReferenceSystem, Residual, State}; +use nalgebra::U1; +use ndarray::{Array1, Array2, ArrayView2}; +use num_dual::DualStruct; +use quantity::{JOULE, KELVIN, MOL, MolarEntropy, Moles, PASCAL, Pressure, Temperature}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct ResidualIsobaricHeatCapacityRecord { + pub temperature_k: f64, + pub pressure_pa: f64, + pub cp_res_j_molk: f64, +} + +impl DatasetRecord for ResidualIsobaricHeatCapacityRecord { + const N_INPUTS: usize = 2; + + fn input(&self, column: usize) -> f64 { + match column { + 0 => self.temperature_k, + 1 => self.pressure_pa, + _ => unreachable!("invalid residual isobaric heat capacity input column"), + } + } + + fn target(&self) -> f64 { + self.cp_res_j_molk + } +} + +/// Residual isobaric molar heat capacity of the liquid phase at the given +/// temperature and pressure. +pub fn residual_isobaric_heat_capacity_ad>, const P: usize>( + eos: &E, + temperature: Temperature, + pressure: Pressure, +) -> FeosResult>> { + let x = E::pure_molefracs(); + let t = Temperature::from_inner(&temperature); + let p = Pressure::from_inner(&pressure); + let density = density_iteration(eos, t, p, &x, Some(Liquid))?; + let state = State::new_pure(eos, t, density)?; + Ok(state.residual_molar_isobaric_heat_capacity()) +} + +pub fn residual_isobaric_heat_capacity( + eos: &E, + temperature: Temperature, + pressure: Pressure, +) -> FeosResult { + let state = State::new_npt( + eos, + temperature, + pressure, + &Moles::from_reduced(nalgebra::DVector::from_element(eos.components(), 1.0)), + Some(Liquid), + )?; + Ok(state.residual_molar_isobaric_heat_capacity()) +} + +pub fn residual_isobaric_heat_capacity_parallel( + eos: &E, + input: ArrayView2, +) -> (Array1, Array1) { + vectorize(eos, input, |eos, inp| { + residual_isobaric_heat_capacity(eos, inp[0] * KELVIN, inp[1] * PASCAL) + .map(|cp| cp.convert_into(JOULE / (MOL * KELVIN))) + }) +} + +pub fn residual_isobaric_heat_capacity_parallel_ad, const P: usize>( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, +) -> (Array1, Array2, Array1) { + vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { + residual_isobaric_heat_capacity_ad(eos, inp[0] * KELVIN, inp[1] * PASCAL) + .map(|cp| cp.convert_into(JOULE / (MOL * KELVIN))) + }) +} diff --git a/crates/feos-core/src/ad/properties/vapor_pressure.rs b/crates/feos-core/src/ad/properties/vapor_pressure.rs new file mode 100644 index 000000000..5c97d30b9 --- /dev/null +++ b/crates/feos-core/src/ad/properties/vapor_pressure.rs @@ -0,0 +1,81 @@ +use crate::ad::Gradient; +use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::{ParametersAD, vectorize, vectorize_ad}; +use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; +use nalgebra::U1; +use ndarray::{Array1, Array2, ArrayView2}; +use quantity::{KELVIN, PASCAL}; +use quantity::{Pressure, Temperature}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct VaporPressureRecord { + pub temperature_k: f64, + pub vapor_pressure_pa: f64, +} + +impl DatasetRecord for VaporPressureRecord { + const N_INPUTS: usize = 1; + + fn input(&self, _column: usize) -> f64 { + self.temperature_k + } + + fn target(&self) -> f64 { + self.vapor_pressure_pa + } +} + +pub fn vapor_pressure_ad>, const P: usize>( + eos: &E, + temperature: Temperature, +) -> FeosResult>> { + let eos_f64 = eos.re(); + let (_, [vapor_density, liquid_density]) = + PhaseEquilibrium::pure_t(&eos_f64, temperature, 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 v1 = 1.0 / liquid_density.to_reduced(); + let v2 = 1.0 / vapor_density.to_reduced(); + let t = temperature.into_reduced(); + let (a1, a2) = { + let t = Gradient::from(t); + let v1 = Gradient::from(v1); + let v2 = Gradient::from(v2); + let x = E::pure_molefracs(); + + let a1 = eos.residual_helmholtz_energy(t, v1, &x); + let a2 = eos.residual_helmholtz_energy(t, v2, &x); + (a1, a2) + }; + + let p = -(a1 - a2 + t * (v2 / v1).ln()) / (v1 - v2); + Ok(Pressure::from_reduced(p)) +} + +/// Non-AD vapor pressure for a single-component model. +pub fn vapor_pressure(eos: &E, temperature: Temperature) -> FeosResult { + let (p, _) = PhaseEquilibrium::pure_t(eos, temperature, None, Default::default())?; + Ok(p) +} + +/// Non-AD batched evaluation over input rows. Single shared model. +pub fn vapor_pressure_parallel( + eos: &E, + input: ArrayView2, +) -> (Array1, Array1) { + vectorize(eos, input, |eos, inp| { + vapor_pressure(eos, inp[0] * KELVIN).map(|p| p.convert_into(PASCAL)) + }) +} + +pub fn vapor_pressure_parallel_ad, const P: usize>( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, +) -> (Array1, Array2, Array1) { + vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { + vapor_pressure_ad(eos, inp[0] * KELVIN).map(|p| p.convert_into(PASCAL)) + }) +} diff --git a/crates/feos-core/src/errors.rs b/crates/feos-core/src/errors.rs index eb7ea4147..e904e4576 100644 --- a/crates/feos-core/src/errors.rs +++ b/crates/feos-core/src/errors.rs @@ -63,7 +63,6 @@ pub enum FeosError { #[cfg(feature = "rayon")] #[error(transparent)] RayonError(#[from] rayon::ThreadPoolBuildError), - #[cfg(feature = "ndarray")] #[error(transparent)] ShapeError(#[from] ndarray::ShapeError), } diff --git a/crates/feos-core/src/lib.rs b/crates/feos-core/src/lib.rs index cc280cf0a..773e7fa7e 100644 --- a/crates/feos-core/src/lib.rs +++ b/crates/feos-core/src/lib.rs @@ -32,13 +32,13 @@ mod errors; pub mod parameter; mod phase_equilibria; mod state; -pub use ad::{ParametersAD, PropertiesAD}; +pub use ad::parameter_optimization; +pub use ad::{ParametersAD, properties}; pub use equation_of_state::{ EntropyScaling, EquationOfState, IdealGas, IdealGasAD, Molarweight, NoResidual, Residual, ResidualDyn, Subset, Total, }; pub use errors::{FeosError, FeosResult}; -#[cfg(feature = "ndarray")] pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero}; pub use phase_equilibria::{PhaseEquilibrium, TemperatureOrPressure}; pub use state::{Composition, Contributions, DensityInitialization, State, StateHD, StateVec}; diff --git a/crates/feos-core/src/phase_equilibria/bubble_dew.rs b/crates/feos-core/src/phase_equilibria/bubble_dew.rs index 05dd42849..8f99c729d 100644 --- a/crates/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/crates/feos-core/src/phase_equilibria/bubble_dew.rs @@ -7,7 +7,6 @@ use crate::state::{ use crate::{Composition, ReferenceSystem, Residual, SolverOptions, State, Verbosity}; use nalgebra::allocator::Allocator; use nalgebra::{DMatrix, DVector, DefaultAllocator, Dim, Dyn, OVector, U1}; -#[cfg(feature = "ndarray")] use ndarray::Array1; use num_dual::linalg::LU; use num_dual::{DualNum, DualStruct, Gradients}; @@ -40,7 +39,6 @@ pub trait TemperatureOrPressure + Copy = f64>: Copy { where DefaultAllocator: Allocator; - #[cfg(feature = "ndarray")] fn linspace( &self, start: Self::Other, @@ -75,7 +73,6 @@ impl + Copy> TemperatureOrPressure for Temperature { state.pressure(Contributions::Total) } - #[cfg(feature = "ndarray")] fn linspace( &self, start: Pressure, @@ -120,7 +117,6 @@ impl + Copy> TemperatureOrPressure state.temperature } - #[cfg(feature = "ndarray")] fn linspace( &self, start: Temperature, diff --git a/crates/feos-core/src/phase_equilibria/mod.rs b/crates/feos-core/src/phase_equilibria/mod.rs index 5ed15a767..6caeca968 100644 --- a/crates/feos-core/src/phase_equilibria/mod.rs +++ b/crates/feos-core/src/phase_equilibria/mod.rs @@ -19,18 +19,13 @@ mod tp_flash; mod px_flashes; -#[cfg(feature = "ndarray")] mod phase_diagram_binary; -#[cfg(feature = "ndarray")] mod phase_diagram_pure; -#[cfg(feature = "ndarray")] mod phase_envelope; mod stability_analysis; pub use bubble_dew::TemperatureOrPressure; -#[cfg(feature = "ndarray")] pub use phase_diagram_binary::PhaseDiagramHetero; -#[cfg(feature = "ndarray")] pub use phase_diagram_pure::PhaseDiagram; /// A thermodynamic equilibrium state. diff --git a/crates/feos-core/src/state/statevec.rs b/crates/feos-core/src/state/statevec.rs index 8a498e2db..34b41f875 100644 --- a/crates/feos-core/src/state/statevec.rs +++ b/crates/feos-core/src/state/statevec.rs @@ -1,14 +1,9 @@ -#[cfg(feature = "ndarray")] 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}; -#[cfg(feature = "ndarray")] use quantity::{ Density, MassDensity, MolarEnergy, MolarEntropy, Moles, Pressure, SpecificEnergy, SpecificEntropy, Temperature, @@ -43,7 +38,6 @@ impl<'a, E> Deref for StateVec<'a, E> { } } -#[cfg(feature = "ndarray")] impl StateVec<'_, E> { pub fn temperature(&self) -> Temperature> { Temperature::from_shape_fn(self.0.len(), |i| self.0[i].temperature) @@ -81,7 +75,6 @@ impl StateVec<'_, E> { } } -#[cfg(feature = "ndarray")] impl StateVec<'_, E> { pub fn mass_density(&self) -> MassDensity> { MassDensity::from_shape_fn(self.0.len(), |i| self.0[i].mass_density()) @@ -94,7 +87,6 @@ impl StateVec<'_, E> { } } -#[cfg(feature = "ndarray")] impl StateVec<'_, E> { pub fn molar_enthalpy(&self, contributions: Contributions) -> MolarEnergy> { MolarEnergy::from_shape_fn(self.0.len(), |i| self.0[i].molar_enthalpy(contributions)) @@ -105,7 +97,6 @@ impl StateVec<'_, E> { } } -#[cfg(feature = "ndarray")] impl StateVec<'_, E> { pub fn specific_enthalpy(&self, contributions: Contributions) -> SpecificEnergy> { SpecificEnergy::from_shape_fn(self.0.len(), |i| self.0[i].specific_enthalpy(contributions)) diff --git a/crates/feos/Cargo.toml b/crates/feos/Cargo.toml index 28e6e6109..147baaf9f 100644 --- a/crates/feos/Cargo.toml +++ b/crates/feos/Cargo.toml @@ -35,6 +35,7 @@ feos-dft = { workspace = true, optional = true } approx = { workspace = true } quantity = { workspace = true, features = ["approx"] } criterion = { workspace = true } +mimalloc = "0.1" [features] default = [] @@ -76,6 +77,11 @@ name = "dual_numbers" harness = false required-features = ["pcsaft"] +[[bench]] +name = "dual_static_vs_dynamic" +harness = false +required-features = ["pcsaft"] + [[bench]] name = "dual_numbers_saftvrmie" required-features = ["saftvrmie"] diff --git a/crates/feos/benches/README.md b/crates/feos/benches/README.md index b1921cd1a..4d0f3b756 100644 --- a/crates/feos/benches/README.md +++ b/crates/feos/benches/README.md @@ -7,11 +7,18 @@ For example, to run the benchmarks in `dual_numbers`, which uses PC-SAFT, use ``` cargo bench --profile=release-lto --bench=dual_numbers -``` +``` + +The static-vs-dynamic vector dual benchmark uses mimalloc. Run it with LTO via + +``` +cargo bench --profile=release-lto --bench=dual_static_vs_dynamic --features=pcsaft +``` |Name|Description| |--|--| |`dual_numbers`|Helmholtz energy function evaluated using `StateHD` with different dual number types using the PC-SAFT equation of state.| +|`dual_static_vs_dynamic`|PC-SAFT-like pure-component Helmholtz energy expression evaluated with static (`DualSVec64

`) and dynamic (`DualDVec64`) vector dual numbers.| |`dual_numbers_saftvrmie`|Helmholtz energy function evaluated using `StateHD` with different dual number types using the SAFT-VR-Mie equation of state.| |`state_properties`|Properties of `State`. Including state creation using the natural variables of the Helmholtz energy (no density iteration).| |`state_creation`|Different constructors of `State` and `PhaseEquilibrium` including critical point calculations. For pure substances and mixtures.| diff --git a/crates/feos/benches/dual_static_vs_dynamic.rs b/crates/feos/benches/dual_static_vs_dynamic.rs new file mode 100644 index 000000000..0f0a75c8a --- /dev/null +++ b/crates/feos/benches/dual_static_vs_dynamic.rs @@ -0,0 +1,290 @@ +//! Compare statically sized and dynamically sized vector dual numbers for a +//! PC-SAFT-like Helmholtz energy density evaluation. +//! +//! This intentionally copies the optimized pure-component PC-SAFT expressions +//! instead of calling the production implementation: the production `Residual` +//! stack requires `D: Copy`, while dynamic dual vectors (`DualDVec64`) are not +//! `Copy`. The copied expression is close enough to expose the arithmetic and +//! allocation cost of realistic PC-SAFT parameter derivatives. + +use criterion::{Criterion, criterion_group, criterion_main}; +use num_dual::{DualDVec64, DualNum, DualSVec64}; +use std::f64::consts::{FRAC_PI_6, PI}; +use std::hint::black_box; + +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +const PI_SQ_43: f64 = 4.0 / 3.0 * PI * PI; + +// PC-SAFT parameters for an associating, polar pure component-ish model: +// m, sigma, epsilon_k, mu, kappa_ab, epsilon_k_ab, na, nb +const PARAMS: [f64; 8] = [1.5, 3.4, 180.0, 2.2, 0.03, 2500.0, 2.0, 1.0]; +const TEMPERATURE: f64 = 300.0; +const DENSITY: f64 = 0.01; + +// Dispersion coefficients copied from pcsaft/eos/dispersion.rs. +const A0: [f64; 7] = [ + 0.91056314451539, + 0.63612814494991, + 2.68613478913903, + -26.5473624914884, + 97.7592087835073, + -159.591540865600, + 91.2977740839123, +]; +const A1: [f64; 7] = [ + -0.30840169182720, + 0.18605311591713, + -2.50300472586548, + 21.4197936296668, + -65.2558853303492, + 83.3186804808856, + -33.7469229297323, +]; +const A2: [f64; 7] = [ + -0.09061483509767, + 0.45278428063920, + 0.59627007280101, + -1.72418291311787, + -4.13021125311661, + 13.7766318697211, + -8.67284703679646, +]; +const B0: [f64; 7] = [ + 0.72409469413165, + 2.23827918609380, + -4.00258494846342, + -21.00357681484648, + 26.8556413626615, + 206.5513384066188, + -355.60235612207947, +]; +const B1: [f64; 7] = [ + -0.57554980753450, + 0.69950955214436, + 3.89256733895307, + -17.21547164777212, + 192.6722644652495, + -161.8264616487648, + -165.2076934555607, +]; +const B2: [f64; 7] = [ + 0.09768831158356, + -0.25575749816100, + -9.15585615297321, + 20.64207597439724, + -38.80443005206285, + 93.6267740770146, + -29.66690558514725, +]; + +// Dipole coefficients copied from pcsaft/eos/polar.rs. +const AD: [[f64; 3]; 5] = [ + [0.30435038064, 0.95346405973, -1.16100802773], + [-0.13585877707, -1.83963831920, 4.52586067320], + [1.44933285154, 2.01311801180, 0.97512223853], + [0.35569769252, -7.37249576667, -12.2810377713], + [-2.06533084541, 8.23741345333, 5.93975747420], +]; +const BD: [[f64; 3]; 5] = [ + [0.21879385627, -0.58731641193, 3.48695755800], + [-1.18964307357, 1.24891317047, -14.9159739347], + [1.16268885692, -0.50852797392, 15.3720218600], + [0.0; 3], + [0.0; 3], +]; +const CD: [[f64; 3]; 4] = [ + [-0.06467735252, -0.95208758351, -0.62609792333], + [0.19758818347, 2.99242575222, 1.29246858189], + [-0.80875619458, -2.38026356489, 1.65427830900], + [0.69028490492, -0.27012609786, -3.43967436378], +]; + +fn helmholtz_energy_density_non_assoc( + m: D, + sigma: D, + epsilon_k: D, + mu: D, + temperature: D, + density: D, +) -> (D, [D; 2]) +where + D: DualNum + Clone, +{ + // temperature dependent segment diameter + let diameter = + sigma.clone() * (D::one() - (epsilon_k.clone() * -3.0 / temperature.clone()).exp() * 0.12); + + let eta = m.clone() * density.clone() * diameter.clone().powi(3) * FRAC_PI_6; + let eta2 = eta.clone() * eta.clone(); + let eta3 = eta2.clone() * eta.clone(); + let eta_m1 = (D::one() - eta.clone()).recip(); + let eta_m2 = eta_m1.clone() * eta_m1.clone(); + let etas = [ + D::one(), + eta.clone(), + eta2.clone(), + eta3.clone(), + eta2.clone() * eta2.clone(), + eta2.clone() * eta3.clone(), + eta3.clone() * eta3.clone(), + ]; + + // hard sphere + let hs = + m.clone() * density.clone() * (eta.clone() * 4.0 - eta2.clone() * 3.0) * eta_m2.clone(); + + // hard chain + let g = (D::one() - eta.clone() * 0.5) * eta_m1.clone() * eta_m2.clone(); + let hc = -(density.clone() * (m.clone() - 1.0) * g.ln()); + + // dispersion + let e = epsilon_k.clone() / temperature.clone(); + let s3 = sigma.clone().powi(3); + let mut i1 = D::zero(); + let mut i2 = D::zero(); + let m1 = (m.clone() - 1.0) / m.clone(); + let m2 = (m.clone() - 2.0) / m.clone(); + for i in 0..7 { + i1 += (m1.clone() * (m2.clone() * A2[i] + A1[i]) + A0[i]) * etas[i].clone(); + i2 += (m1.clone() * (m2.clone() * B2[i] + B1[i]) + B0[i]) * etas[i].clone(); + } + let c1 = + (m.clone() * (eta.clone() * 8.0 - eta2.clone() * 2.0) * eta_m2.clone() * eta_m2.clone() + + 1.0 + - (m.clone() - 1.0) + * (eta.clone() * 20.0 - eta2.clone() * 27.0 + eta3.clone() * 12.0 + - eta2.clone() * eta2.clone() * 2.0) + / ((eta.clone() - 1.0) * (eta.clone() - 2.0)).powi(2)) + .recip(); + let i = i1 * 2.0 + c1 * i2 * m.clone() * e.clone(); + let disp = + -(density.clone() * density.clone() * m.clone().powi(2) * e.clone() * s3.clone() * i * PI); + + // dipoles + let mu2 = mu.clone().powi(2) / (m.clone() * temperature * 1.380649e-4); + let m_dipole = if m.re() > 2.0 { + D::from(2.0) + } else { + m.clone() + }; + let m1 = (m_dipole.clone() - 1.0) / m_dipole.clone(); + let m2 = m1.clone() * (m_dipole.clone() - 2.0) / m_dipole; + let mut j1 = D::zero(); + let mut j2 = D::zero(); + for i in 0..5 { + let a = m2.clone() * AD[i][2] + m1.clone() * AD[i][1] + AD[i][0]; + let b = m2.clone() * BD[i][2] + m1.clone() * BD[i][1] + BD[i][0]; + j1 += (a + b * e.clone()) * etas[i].clone(); + if i < 4 { + j2 += (m2.clone() * CD[i][2] + m1.clone() * CD[i][1] + CD[i][0]) * etas[i].clone(); + } + } + + // mu is factored out of these expressions to deal with the case where mu=0 + let phi2 = -(density.clone() * density.clone() * j1 / s3.clone() * PI); + let phi3 = -(density.clone() * density.clone() * density * j2 / s3 * PI_SQ_43); + let dipole = phi2.clone() * phi2.clone() * mu2.clone() * mu2.clone() / (phi2 - phi3 * mu2); + + (hs + hc + disp + dipole, [eta, eta_m1]) +} + +fn helmholtz_energy_density(parameters: &[D; 8], temperature: D, density: D) -> D +where + D: DualNum + Clone, +{ + let [m, sigma, epsilon_k, mu, kappa_ab, epsilon_k_ab, na, nb] = + parameters.each_ref().map(Clone::clone); + let (non_assoc, [eta, eta_m1]) = helmholtz_energy_density_non_assoc( + m, + sigma.clone(), + epsilon_k, + mu, + temperature.clone(), + density.clone(), + ); + + // association + let delta_assoc = ((epsilon_k_ab / temperature).exp() - 1.0) * sigma.powi(3) * kappa_ab; + let k = eta * eta_m1.clone(); + let delta = (k.clone() * (k * 0.5 + 1.5) + 1.0) * eta_m1 * delta_assoc; + let rhoa = na * density.clone(); + let rhob = nb * density; + let aux = (rhoa.clone() - rhob.clone()) * delta.clone() + 1.0; + let sqrt = (aux.clone() * aux + rhob.clone() * delta.clone() * 4.0).sqrt(); + let xa = (sqrt.clone() + 1.0 + (rhob.clone() - rhoa.clone()) * delta.clone()).recip() * 2.0; + let xb = (sqrt + 1.0 - (rhob.clone() - rhoa.clone()) * delta).recip() * 2.0; + let assoc = + rhoa * (xa.clone().ln() - xa * 0.5 + 0.5) + rhob * (xb.clone().ln() - xb * 0.5 + 0.5); + + non_assoc + assoc +} + +fn static_parameters(params: [f64; 8]) -> [DualSVec64

; 8] { + std::array::from_fn(|i| { + let x = DualSVec64::

::from_re(params[i]); + if i < P { x.derivative(i) } else { x } + }) +} + +fn dynamic_parameters(params: [f64; 8], p: usize) -> [DualDVec64; 8] { + std::array::from_fn(|i| { + let x = DualDVec64::from_re(params[i]); + if i < p { x.derivative(p, i) } else { x } + }) +} + +fn eval_static(params: [f64; 8], temperature: f64, density: f64) -> DualSVec64

{ + helmholtz_energy_density( + &static_parameters::

(params), + DualSVec64::

::from_re(temperature), + DualSVec64::

::from_re(density), + ) +} + +fn eval_dynamic(params: [f64; 8], temperature: f64, density: f64, p: usize) -> DualDVec64 { + helmholtz_energy_density( + &dynamic_parameters(params, p), + DualDVec64::from_re(temperature), + DualDVec64::from_re(density), + ) +} + +fn bench_pair( + group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, +) { + group.bench_function(format!("static_p{P}"), |b| { + b.iter(|| { + black_box(eval_static::

( + black_box(PARAMS), + black_box(TEMPERATURE), + black_box(DENSITY), + )) + }) + }); + group.bench_function(format!("dynamic_p{P}"), |b| { + b.iter(|| { + black_box(eval_dynamic( + black_box(PARAMS), + black_box(TEMPERATURE), + black_box(DENSITY), + P, + )) + }) + }); +} + +fn static_vs_dynamic_pcsaft(c: &mut Criterion) { + let mut group = c.benchmark_group("dual_static_vs_dynamic_pcsaft_helmholtz"); + bench_pair::<1>(&mut group); + bench_pair::<2>(&mut group); + bench_pair::<3>(&mut group); + bench_pair::<4>(&mut group); + bench_pair::<6>(&mut group); + bench_pair::<8>(&mut group); + group.finish(); +} + +criterion_group!(benches, static_vs_dynamic_pcsaft); +criterion_main!(benches); diff --git a/crates/feos/src/gc_pcsaft/dft/mod.rs b/crates/feos/src/gc_pcsaft/dft/mod.rs index 5005fa315..18ba4c793 100644 --- a/crates/feos/src/gc_pcsaft/dft/mod.rs +++ b/crates/feos/src/gc_pcsaft/dft/mod.rs @@ -1,6 +1,6 @@ use super::eos::GcPcSaftOptions; use super::record::GcPcSaftAssociationRecord; -use crate::association::{Association, YuWuAssociationFunctional, AssociationStrength}; +use crate::association::{Association, AssociationStrength, YuWuAssociationFunctional}; use crate::gc_pcsaft::GcPcSaftParameters; use crate::hard_sphere::{FMTContribution, FMTVersion, HardSphereProperties, MonomerShape}; use feos_core::{FeosResult, Molarweight, ResidualDyn, StateHD, Subset}; @@ -108,7 +108,8 @@ impl HelmholtzEnergyFunctionalDyn for GcPcSaftFunctional { fn contributions<'a>(&'a self) -> impl Iterator> { let mut contributions = Vec::with_capacity(4); - let assoc = YuWuAssociationFunctional::new(&self.params, &self.parameters, self.association); + let assoc = + YuWuAssociationFunctional::new(&self.params, &self.parameters, self.association); // Hard sphere contribution let hs = FMTContribution::new(&self.params, self.fmt_version); diff --git a/crates/feos/src/pcsaft/dft/mod.rs b/crates/feos/src/pcsaft/dft/mod.rs index a7f6b4566..b3afa5435 100644 --- a/crates/feos/src/pcsaft/dft/mod.rs +++ b/crates/feos/src/pcsaft/dft/mod.rs @@ -103,7 +103,8 @@ impl HelmholtzEnergyFunctionalDyn for PcSaftFunctional { fn contributions<'a>(&'a self) -> impl Iterator> { let mut contributions = Vec::with_capacity(4); - let assoc = YuWuAssociationFunctional::new(&self.params, &self.parameters, self.association); + let assoc = + YuWuAssociationFunctional::new(&self.params, &self.parameters, self.association); if matches!( self.fmt_version, diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index 37a885248..39de98c58 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -598,12 +598,25 @@ mod tests_parameter_fit { use super::*; use approx::assert_relative_eq; use feos_core::DensityInitialization::Liquid; - use feos_core::{Contributions, PropertiesAD, ReferenceSystem, SolverOptions}; + use feos_core::properties::{ + boiling_temperature_ad, bubble_point_pressure_ad, dew_point_pressure_ad, + equilibrium_liquid_density_ad, liquid_density_ad, vapor_pressure_ad, + }; + use feos_core::{Contributions, ReferenceSystem, SolverOptions}; use feos_core::{FeosResult, ParametersAD, PhaseEquilibrium, State}; use nalgebra::{U1, U3, U8, vector}; use num_dual::{Dual64, DualStruct, DualVec, partial}; use quantity::{BAR, KELVIN, LITER, MOL, PASCAL}; + fn flat_binary_params(b: &PcSaftBinary) -> Vec { + b.0.0 + .iter() + .flatten() + .copied() + .chain(std::iter::once(b.0.1)) + .collect() + } + fn pcsaft_non_assoc() -> PcSaftPure { let m = 1.5; let sigma = 3.4; @@ -626,9 +639,9 @@ mod tests_parameter_fit { "nb", ]; let (pcsaft, _) = pcsaft()?; - let pcsaft_ad = pcsaft.named_derivatives(pcsaft_params); + let pcsaft_ad = PcSaftPure::::seed_derivatives(&pcsaft.0, pcsaft_params); let temperature = 250.0 * KELVIN; - let p = pcsaft_ad.vapor_pressure(temperature)?; + let p = vapor_pressure_ad(&pcsaft_ad, temperature)?; let p = p.convert_into(PASCAL); let (p, grad) = (p.re, p.eps.unwrap_generic(U8, U1)); @@ -658,9 +671,10 @@ mod tests_parameter_fit { #[test] fn test_vapor_pressure_derivatives_fit() -> FeosResult<()> { let pcsaft = pcsaft_non_assoc(); - let pcsaft_ad = pcsaft.named_derivatives(["m", "sigma", "epsilon_k"]); + let pcsaft_ad = + PcSaftPure::::seed_derivatives(&pcsaft.0, ["m", "sigma", "epsilon_k"]); let temperature = 150.0 * KELVIN; - let p = pcsaft_ad.vapor_pressure(temperature)?; + let p = vapor_pressure_ad(&pcsaft_ad, temperature)?; let p = p.convert_into(PASCAL); let (p, grad) = (p.re, p.eps.unwrap_generic(U3, U1)); @@ -690,9 +704,10 @@ mod tests_parameter_fit { #[test] fn test_boiling_temperature_derivatives_fit() -> FeosResult<()> { let pcsaft = pcsaft_non_assoc(); - let pcsaft_ad = pcsaft.named_derivatives(["m", "sigma", "epsilon_k"]); + let pcsaft_ad = + PcSaftPure::::seed_derivatives(&pcsaft.0, ["m", "sigma", "epsilon_k"]); let pressure = BAR; - let t = pcsaft_ad.boiling_temperature(pressure)?; + let t = boiling_temperature_ad(&pcsaft_ad, pressure)?; let t = t.convert_into(KELVIN); let (t, grad) = (t.re, t.eps.unwrap_generic(U3, U1)); @@ -734,9 +749,10 @@ mod tests_parameter_fit { #[test] fn test_equilibrium_liquid_density_derivatives_fit() -> FeosResult<()> { let pcsaft = pcsaft_non_assoc(); - let pcsaft_ad = pcsaft.named_derivatives(["m", "sigma", "epsilon_k"]); + let pcsaft_ad = + PcSaftPure::::seed_derivatives(&pcsaft.0, ["m", "sigma", "epsilon_k"]); let temperature = 150.0 * KELVIN; - let (p, rho) = pcsaft_ad.equilibrium_liquid_density(temperature)?; + let (p, rho) = equilibrium_liquid_density_ad(&pcsaft_ad, temperature)?; let p = p.convert_into(PASCAL); let rho = rho.convert_into(MOL / LITER); let (p, p_grad) = (p.re, p.eps.unwrap_generic(U3, U1)); @@ -774,10 +790,11 @@ mod tests_parameter_fit { #[test] fn test_liquid_density_derivatives_fit() -> FeosResult<()> { let pcsaft = pcsaft_non_assoc(); - let pcsaft_ad = pcsaft.named_derivatives(["m", "sigma", "epsilon_k"]); + let pcsaft_ad = + PcSaftPure::::seed_derivatives(&pcsaft.0, ["m", "sigma", "epsilon_k"]); let temperature = 150.0 * KELVIN; let pressure = BAR; - let rho = pcsaft_ad.liquid_density(temperature, pressure)?; + let rho = liquid_density_ad(&pcsaft_ad, temperature, pressure)?; let rho = rho.convert_into(MOL / LITER); let (rho, grad) = (rho.re, rho.eps.unwrap_generic(U3, U1)); @@ -806,10 +823,11 @@ mod tests_parameter_fit { #[test] fn test_bubble_point_pressure() -> FeosResult<()> { let (pcsaft, _) = pcsaft_binary()?; - let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); + let pcsaft_ad = + PcSaftBinary::::seed_derivatives(&flat_binary_params(&pcsaft), ["k_ij"]); let temperature = 500.0 * KELVIN; let x = vector![0.5, 0.5]; - let p = pcsaft_ad.bubble_point_pressure(temperature, None, x)?; + let p = bubble_point_pressure_ad(&pcsaft_ad, temperature, None, x)?; let p = p.convert_into(BAR); let (p, [[grad]]) = (p.re, p.eps.unwrap_generic(U1, U1).data.0); @@ -844,10 +862,11 @@ mod tests_parameter_fit { #[test] fn test_dew_point_pressure() -> FeosResult<()> { let (pcsaft, _) = pcsaft_binary()?; - let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); + let pcsaft_ad = + PcSaftBinary::::seed_derivatives(&flat_binary_params(&pcsaft), ["k_ij"]); let temperature = 500.0 * KELVIN; let y = 0.5; - let p = pcsaft_ad.dew_point_pressure(temperature, None, y)?; + let p = dew_point_pressure_ad(&pcsaft_ad, temperature, None, y)?; let p = p.convert_into(BAR); let (p, [[grad]]) = (p.re, p.eps.unwrap_generic(U1, U1).data.0); @@ -876,7 +895,8 @@ mod tests_parameter_fit { #[test] fn test_bubble_point_temperature() -> FeosResult<()> { let (pcsaft, _) = pcsaft_binary()?; - let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); + let pcsaft_ad = + PcSaftBinary::::seed_derivatives(&flat_binary_params(&pcsaft), ["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 = DualVec::from(0.5); @@ -924,7 +944,8 @@ mod tests_parameter_fit { #[test] fn test_dew_point_temperature() -> FeosResult<()> { let (pcsaft, _) = pcsaft_binary()?; - let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); + let pcsaft_ad = + PcSaftBinary::::seed_derivatives(&flat_binary_params(&pcsaft), ["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 = DualVec::from(0.5); @@ -1060,7 +1081,11 @@ mod tests_parameter_fit { #[test] fn test_tp_flash() -> FeosResult<()> { let (pcsaft, _) = pcsaft_binary()?; - let pcsaft_ad = pcsaft.named_derivatives(["k_ij"]); + let (params, mut kij) = pcsaft.0; + let mut flat_params: Vec = params[0].to_vec(); + flat_params.extend_from_slice(¶ms[1]); + flat_params.push(kij); + let pcsaft_ad = PcSaftBinary::::seed_derivatives(&flat_params, ["k_ij"]); let temperature = 500.0 * KELVIN; let pressure = 44.6 * BAR; let x = 0.5; @@ -1080,8 +1105,6 @@ mod tests_parameter_fit { 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); diff --git a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs index 71b15ae88..f6c83258b 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs @@ -2,7 +2,7 @@ use super::dispersion::{A0, A1, A2, B0, B1, B2}; use super::polar::{AD, BD, CD}; use feos_core::{ParametersAD, Residual, StateHD}; use nalgebra::{SVector, U2}; -use num_dual::{DualNum, DualSVec64, DualVec, jacobian}; +use num_dual::{DualNum, DualVec, jacobian}; use std::f64::consts::{FRAC_PI_6, PI}; const PI_SQ_43: f64 = 4.0 / 3.0 * PI * PI; @@ -38,26 +38,58 @@ impl + Copy, const N: usize> From<&[f64]> for PcSaftBinary } impl ParametersAD<2> for PcSaftBinary { - fn index_parameters_mut<'a, const P: usize>( - eos: &'a mut Self::Lifted>, - index: &str, - ) -> &'a mut DualSVec64

{ - match index { - "k_ij" => &mut eos.0.1, - _ => panic!("{index} is not a valid binary PC-SAFT parameter!"), - } + fn build + Copy>( + mut f: impl FnMut(&'static str, bool) -> D, + ) -> PcSaftBinary { + PcSaftBinary::new( + [ + [ + f("m1", true), + f("sigma1", true), + f("epsilon_k1", true), + f("mu1", true), + ], + [ + f("m2", true), + f("sigma2", true), + f("epsilon_k2", true), + f("mu2", true), + ], + ], + f("k_ij", true), + ) } } impl ParametersAD<2> for PcSaftBinary { - fn index_parameters_mut<'a, const P: usize>( - eos: &'a mut Self::Lifted>, - index: &str, - ) -> &'a mut DualSVec64

{ - match index { - "k_ij" => &mut eos.0.1, - _ => panic!("{index} is not a valid binary PC-SAFT parameter!"), - } + fn build + Copy>( + mut f: impl FnMut(&'static str, bool) -> D, + ) -> PcSaftBinary { + PcSaftBinary::new( + [ + [ + f("m1", true), + f("sigma1", true), + f("epsilon_k1", true), + f("mu1", true), + f("kappa_ab1", true), + f("epsilon_k_ab1", true), + f("na1", false), + f("nb1", false), + ], + [ + f("m2", true), + f("sigma2", true), + f("epsilon_k2", true), + f("mu2", true), + f("kappa_ab2", true), + f("epsilon_k_ab2", true), + f("na2", false), + f("nb2", false), + ], + ], + f("k_ij", true), + ) } } diff --git a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs index f9ea2983f..d6ad49bac 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs @@ -2,7 +2,7 @@ use super::dispersion::{A0, A1, A2, B0, B1, B2}; use super::polar::{AD, BD, CD}; use feos_core::{ParametersAD, Residual, StateHD}; use nalgebra::{SVector, U1}; -use num_dual::{DualNum, DualSVec64}; +use num_dual::DualNum; use std::f64::consts::{FRAC_PI_6, PI}; const PI_SQ_43: f64 = 4.0 / 3.0 * PI * PI; @@ -193,36 +193,32 @@ impl + Copy, const N: usize> From<&[f64]> for PcSaftPure { } impl ParametersAD<1> for PcSaftPure { - fn index_parameters_mut<'a, const P: usize>( - eos: &'a mut Self::Lifted>, - index: &str, - ) -> &'a mut DualSVec64

{ - match index { - "m" => &mut eos.0[0], - "sigma" => &mut eos.0[1], - "epsilon_k" => &mut eos.0[2], - "mu" => &mut eos.0[3], - _ => panic!("{index} is not a valid PC-SAFT parameter!"), - } + fn build + Copy>( + mut f: impl FnMut(&'static str, bool) -> D, + ) -> PcSaftPure { + PcSaftPure([ + f("m", true), + f("sigma", true), + f("epsilon_k", true), + f("mu", true), + ]) } } impl ParametersAD<1> for PcSaftPure { - fn index_parameters_mut<'a, const P: usize>( - eos: &'a mut Self::Lifted>, - index: &str, - ) -> &'a mut DualSVec64

{ - match index { - "m" => &mut eos.0[0], - "sigma" => &mut eos.0[1], - "epsilon_k" => &mut eos.0[2], - "mu" => &mut eos.0[3], - "kappa_ab" => &mut eos.0[4], - "epsilon_k_ab" => &mut eos.0[5], - "na" => &mut eos.0[6], - "nb" => &mut eos.0[7], - _ => panic!("{index} is not a valid PC-SAFT parameter!"), - } + fn build + Copy>( + mut f: impl FnMut(&'static str, bool) -> D, + ) -> PcSaftPure { + PcSaftPure([ + f("m", true), + f("sigma", true), + f("epsilon_k", true), + f("mu", true), + f("kappa_ab", true), + f("epsilon_k_ab", true), + f("na", false), + f("nb", false), + ]) } } diff --git a/crates/feos/tests/pcsaft/px_flashes.rs b/crates/feos/tests/pcsaft/px_flashes.rs index 4cfb179b6..0f5e53bac 100644 --- a/crates/feos/tests/pcsaft/px_flashes.rs +++ b/crates/feos/tests/pcsaft/px_flashes.rs @@ -38,7 +38,10 @@ fn test_ph_flash() -> FeosResult<()> { 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 mut flat_params: Vec = params[0].to_vec(); + flat_params.extend_from_slice(¶ms[1]); + flat_params.push(kij); + let pcsaft_ad = PcSaftBinary::::seed_derivatives(&flat_params, ["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( @@ -105,7 +108,10 @@ fn test_ps_flash() -> FeosResult<()> { 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 mut flat_params: Vec = params[0].to_vec(); + flat_params.extend_from_slice(¶ms[1]); + flat_params.push(kij); + let pcsaft_ad = PcSaftBinary::::seed_derivatives(&flat_params, ["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( diff --git a/py-feos/Cargo.toml b/py-feos/Cargo.toml index c06369b72..3395514f6 100644 --- a/py-feos/Cargo.toml +++ b/py-feos/Cargo.toml @@ -38,7 +38,7 @@ itertools = { workspace = true } paste = { workspace = true } feos = { workspace = true } -feos-core = { workspace = true, features = ["ndarray"] } +feos-core = { workspace = true } feos-derive = { workspace = true } feos-dft = { workspace = true, optional = true } diff --git a/py-feos/src/ad/dataset.rs b/py-feos/src/ad/dataset.rs new file mode 100644 index 000000000..9c8a20148 --- /dev/null +++ b/py-feos/src/ad/dataset.rs @@ -0,0 +1,537 @@ +use feos_core::parameter_optimization::{ + BinaryDataset, BinaryProperty, BubblePointRecord, Dataset, DewPointRecord, + EnthalpyOfVaporizationRecord, EquilibriumLiquidDensityRecord, LiquidDensityRecord, PureDataset, + PureProperty, ResidualIsobaricHeatCapacityRecord, VaporPressureRecord, +}; +use ndarray::{Array2, ArrayView1}; +use numpy::{PyArray1, PyArray2, PyReadonlyArray1, ToPyArray}; +use pyo3::exceptions::{PyTypeError, PyValueError}; +use pyo3::prelude::*; + +use crate::eos::PyEquationOfState; + +/// Run a `Dataset::evaluate` against a single model or a list of models. +/// +/// If `models` extracts as a single `PyEquationOfState`, returns the +/// `(predicted, converged)` arrays as 1D. If it extracts as a sequence of +/// models, returns them stacked as 2D arrays with shape `[n_points, n_models]`. +fn evaluate_models<'py, D: Dataset>( + py: Python<'py>, + dataset: &D, + models: &Bound<'py, PyAny>, +) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { + if let Ok(model) = models.extract::>() { + let (pred, ok) = dataset.evaluate(&model.0); + return Ok((pred.to_pyarray(py).into_any(), ok.to_pyarray(py).into_any())); + } + + let model_refs: Vec> = models.extract().map_err(|_| { + PyTypeError::new_err( + "expected an EquationOfState or a sequence of EquationOfState instances", + ) + })?; + + let n_points = dataset.target().len(); + let n_models = model_refs.len(); + let mut pred = Array2::::from_elem((n_points, n_models), f64::NAN); + let mut ok = Array2::::from_elem((n_points, n_models), false); + for (j, model) in model_refs.iter().enumerate() { + let (p, c) = dataset.evaluate(&model.0); + pred.column_mut(j).assign(&p); + ok.column_mut(j).assign(&c); + } + Ok((pred.to_pyarray(py).into_any(), ok.to_pyarray(py).into_any())) +} + +fn ensure_same_len(arrays: &[(&str, usize)]) -> PyResult { + let Some((_, n)) = arrays.first() else { + return Ok(0); + }; + if arrays.iter().any(|(_, len)| len != n) { + let names = arrays + .iter() + .map(|(name, _)| *name) + .collect::>() + .join(", "); + return Err(PyValueError::new_err(format!( + "all arrays must have the same length: {names}" + ))); + } + Ok(*n) +} + +fn collect_records_2( + a: (&str, ArrayView1<'_, f64>), + b: (&str, ArrayView1<'_, f64>), + f: impl Fn(f64, f64) -> R, +) -> PyResult> { + ensure_same_len(&[(a.0, a.1.len()), (b.0, b.1.len())])?; + Ok(a.1.iter().zip(b.1.iter()).map(|(&a, &b)| f(a, b)).collect()) +} + +fn collect_records_3( + a: (&str, ArrayView1<'_, f64>), + b: (&str, ArrayView1<'_, f64>), + c: (&str, ArrayView1<'_, f64>), + f: impl Fn(f64, f64, f64) -> R, +) -> PyResult> { + ensure_same_len(&[(a.0, a.1.len()), (b.0, b.1.len()), (c.0, c.1.len())])?; + Ok(a.1 + .iter() + .zip(b.1.iter()) + .zip(c.1.iter()) + .map(|((&a, &b), &c)| f(a, b, c)) + .collect()) +} + +fn parse_pure_property(property: &str) -> PyResult { + match property { + "vapor_pressure" => Ok(PureProperty::VaporPressure), + "liquid_density" => Ok(PureProperty::LiquidDensity), + "equilibrium_liquid_density" => Ok(PureProperty::EquilibriumLiquidDensity), + "enthalpy_of_vaporization" => Ok(PureProperty::EnthalpyOfVaporization), + "residual_isobaric_heat_capacity" => Ok(PureProperty::ResidualIsobaricHeatCapacity), + _ => Err(PyValueError::new_err(format!( + "unknown pure property '{property}'; valid: \ + 'vapor_pressure', 'liquid_density', 'equilibrium_liquid_density', \ + 'enthalpy_of_vaporization', 'residual_isobaric_heat_capacity'" + ))), + } +} + +fn parse_binary_property(property: &str) -> PyResult { + match property { + "bubble_point_pressure" | "bubble_point" => Ok(BinaryProperty::BubblePointPressure), + "dew_point_pressure" | "dew_point" => Ok(BinaryProperty::DewPointPressure), + _ => Err(PyValueError::new_err(format!( + "unknown binary property '{property}'; valid: \ + 'bubble_point_pressure', 'dew_point_pressure'" + ))), + } +} + +#[pyclass(name = "PureDataset")] +pub struct PyPureDataset { + pub(crate) inner: PureDataset, +} + +#[pymethods] +impl PyPureDataset { + /// Load pure-component data from CSV. + /// + /// Args: + /// path (str): Path to the CSV file. + /// property (str): Property identifier. Valid values are + /// ``"vapor_pressure"``, ``"liquid_density"``, + /// ``"equilibrium_liquid_density"``, ``"enthalpy_of_vaporization"``, + /// and ``"residual_isobaric_heat_capacity"``. + /// name (str, optional): Dataset name used in regressor diagnostics. + #[staticmethod] + #[pyo3(signature = (path, property, name=None))] + pub fn from_csv(path: &str, property: &str, name: Option<&str>) -> PyResult { + Self::from_csv_for_property(parse_pure_property(property)?, path, name) + } + + /// Load vapor pressure data from CSV. + /// + /// CSV columns: ``temperature_k, vapor_pressure_pa``. + #[staticmethod] + #[pyo3(signature = (path, name=None))] + pub fn vapor_pressure_from_csv(path: &str, name: Option<&str>) -> PyResult { + Self::from_csv_for_property(PureProperty::VaporPressure, path, name) + } + + /// Construct vapor pressure data from numpy arrays. + #[staticmethod] + #[pyo3(signature = (temperature_k, vapor_pressure_pa, name=None))] + pub fn vapor_pressure( + temperature_k: PyReadonlyArray1, + vapor_pressure_pa: PyReadonlyArray1, + name: Option<&str>, + ) -> PyResult { + let temperature_k = temperature_k.as_array(); + let vapor_pressure_pa = vapor_pressure_pa.as_array(); + let records = collect_records_2( + ("temperature_k", temperature_k), + ("vapor_pressure_pa", vapor_pressure_pa), + |temperature_k, vapor_pressure_pa| VaporPressureRecord { + temperature_k, + vapor_pressure_pa, + }, + )?; + Ok(Self::with_optional_name( + PureDataset::vapor_pressure(records), + name, + )) + } + + /// Load liquid density data from CSV. + /// + /// CSV columns: ``temperature_k, pressure_pa, liquid_density_kmol_m3``. + #[staticmethod] + #[pyo3(signature = (path, name=None))] + pub fn liquid_density_from_csv(path: &str, name: Option<&str>) -> PyResult { + Self::from_csv_for_property(PureProperty::LiquidDensity, path, name) + } + + /// Construct liquid density data from numpy arrays. + #[staticmethod] + #[pyo3(signature = (temperature_k, pressure_pa, liquid_density_kmol_m3, name=None))] + pub fn liquid_density( + temperature_k: PyReadonlyArray1, + pressure_pa: PyReadonlyArray1, + liquid_density_kmol_m3: PyReadonlyArray1, + name: Option<&str>, + ) -> PyResult { + let temperature_k = temperature_k.as_array(); + let pressure_pa = pressure_pa.as_array(); + let liquid_density_kmol_m3 = liquid_density_kmol_m3.as_array(); + let records = collect_records_3( + ("temperature_k", temperature_k), + ("pressure_pa", pressure_pa), + ("liquid_density_kmol_m3", liquid_density_kmol_m3), + |temperature_k, pressure_pa, liquid_density_kmol_m3| LiquidDensityRecord { + temperature_k, + pressure_pa, + liquid_density_kmol_m3, + }, + )?; + Ok(Self::with_optional_name( + PureDataset::liquid_density(records), + name, + )) + } + + /// Load saturated liquid density data from CSV. + /// + /// CSV columns: ``temperature_k, liquid_density_kmol_m3``. + #[staticmethod] + #[pyo3(signature = (path, name=None))] + pub fn equilibrium_liquid_density_from_csv(path: &str, name: Option<&str>) -> PyResult { + Self::from_csv_for_property(PureProperty::EquilibriumLiquidDensity, path, name) + } + + /// Construct saturated liquid density data from numpy arrays. + #[staticmethod] + #[pyo3(signature = (temperature_k, liquid_density_kmol_m3, name=None))] + pub fn equilibrium_liquid_density( + temperature_k: PyReadonlyArray1, + liquid_density_kmol_m3: PyReadonlyArray1, + name: Option<&str>, + ) -> PyResult { + let temperature_k = temperature_k.as_array(); + let liquid_density_kmol_m3 = liquid_density_kmol_m3.as_array(); + let records = collect_records_2( + ("temperature_k", temperature_k), + ("liquid_density_kmol_m3", liquid_density_kmol_m3), + |temperature_k, liquid_density_kmol_m3| EquilibriumLiquidDensityRecord { + temperature_k, + liquid_density_kmol_m3, + }, + )?; + Ok(Self::with_optional_name( + PureDataset::equilibrium_liquid_density(records), + name, + )) + } + + /// Load enthalpy of vaporization data from CSV. + /// + /// CSV columns: ``temperature_k, dh_vap_j_mol``. + #[staticmethod] + #[pyo3(signature = (path, name=None))] + pub fn enthalpy_of_vaporization_from_csv(path: &str, name: Option<&str>) -> PyResult { + Self::from_csv_for_property(PureProperty::EnthalpyOfVaporization, path, name) + } + + /// Construct enthalpy of vaporization data from numpy arrays. + #[staticmethod] + #[pyo3(signature = (temperature_k, dh_vap_j_mol, name=None))] + pub fn enthalpy_of_vaporization( + temperature_k: PyReadonlyArray1, + dh_vap_j_mol: PyReadonlyArray1, + name: Option<&str>, + ) -> PyResult { + let temperature_k = temperature_k.as_array(); + let dh_vap_j_mol = dh_vap_j_mol.as_array(); + let records = collect_records_2( + ("temperature_k", temperature_k), + ("dh_vap_j_mol", dh_vap_j_mol), + |temperature_k, dh_vap_j_mol| EnthalpyOfVaporizationRecord { + temperature_k, + dh_vap_j_mol, + }, + )?; + Ok(Self::with_optional_name( + PureDataset::enthalpy_of_vaporization(records), + name, + )) + } + + /// Load residual isobaric heat capacity data from CSV. + /// + /// CSV columns: ``temperature_k, pressure_pa, cp_res_j_molk``. + #[staticmethod] + #[pyo3(signature = (path, name=None))] + pub fn residual_isobaric_heat_capacity_from_csv( + path: &str, + name: Option<&str>, + ) -> PyResult { + Self::from_csv_for_property(PureProperty::ResidualIsobaricHeatCapacity, path, name) + } + + /// Construct residual isobaric heat capacity data from numpy arrays. + #[staticmethod] + #[pyo3(signature = (temperature_k, pressure_pa, cp_res_j_molk, name=None))] + pub fn residual_isobaric_heat_capacity( + temperature_k: PyReadonlyArray1, + pressure_pa: PyReadonlyArray1, + cp_res_j_molk: PyReadonlyArray1, + name: Option<&str>, + ) -> PyResult { + let temperature_k = temperature_k.as_array(); + let pressure_pa = pressure_pa.as_array(); + let cp_res_j_molk = cp_res_j_molk.as_array(); + let records = collect_records_3( + ("temperature_k", temperature_k), + ("pressure_pa", pressure_pa), + ("cp_res_j_molk", cp_res_j_molk), + |temperature_k, pressure_pa, cp_res_j_molk| ResidualIsobaricHeatCapacityRecord { + temperature_k, + pressure_pa, + cp_res_j_molk, + }, + )?; + Ok(Self::with_optional_name( + PureDataset::residual_isobaric_heat_capacity(records), + name, + )) + } + + /// Property name. + #[getter] + pub fn name(&self) -> &str { + self.inner.name() + } + + /// Number of data points. + pub fn __len__(&self) -> usize { + self.inner.target().len() + } + + /// Target values. + pub fn target<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { + self.inner.target().to_owned().to_pyarray(py) + } + + /// Input values. + pub fn inputs<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2> { + self.inner.inputs().to_owned().to_pyarray(py) + } + + /// Evaluate the dataset's property for one or more models (no gradients). + /// + /// Args: + /// models: A single ``EquationOfState`` or a list of them. Each + /// model must describe a single component (``components() == 1``). + /// + /// Returns: + /// ``(predicted, converged)``. For a single model, both are 1D arrays + /// of length ``n_points``. For a list of ``n_models`` models, both are + /// 2D arrays of shape ``[n_points, n_models]``; column ``k`` + /// corresponds to ``models[k]``. Non-converged points are reported as + /// ``NaN`` in ``predicted`` and ``False`` in ``converged``. + pub fn evaluate<'py>( + &self, + py: Python<'py>, + models: &Bound<'py, PyAny>, + ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { + evaluate_models(py, &self.inner, models) + } + + pub fn __repr__(&self) -> String { + format!( + "PureDataset(property={}, n={})", + self.inner.name(), + self.inner.target().len() + ) + } +} + +impl PyPureDataset { + fn from_csv_for_property( + property: PureProperty, + path: &str, + name: Option<&str>, + ) -> PyResult { + PureDataset::from_csv(property, std::path::Path::new(path)) + .map(|inner| Self::with_optional_name(inner, name)) + .map_err(|e| PyValueError::new_err(e.to_string())) + } + + fn with_optional_name(mut inner: PureDataset, name: Option<&str>) -> Self { + if let Some(n) = name { + inner = inner.with_name(n); + } + Self { inner } + } +} + +#[pyclass(name = "BinaryDataset")] +pub struct PyBinaryDataset { + pub(crate) inner: BinaryDataset, +} + +#[pymethods] +impl PyBinaryDataset { + /// Load binary-mixture data from CSV. + /// + /// Args: + /// path (str): Path to the CSV file. + /// property (str): Property identifier. Valid values are + /// ``"bubble_point_pressure"`` and ``"dew_point_pressure"``. + /// Short aliases ``"bubble_point"`` and ``"dew_point"`` are also accepted. + /// name (str, optional): Dataset name used in regressor diagnostics. + #[staticmethod] + #[pyo3(signature = (path, property, name=None))] + pub fn from_csv(path: &str, property: &str, name: Option<&str>) -> PyResult { + Self::from_csv_for_property(parse_binary_property(property)?, path, name) + } + + /// Load bubble point pressure data from CSV. + /// + /// CSV columns: ``temperature_k, liquid_molefrac_1, bubble_pressure_pa``. + #[staticmethod] + #[pyo3(signature = (path, name=None))] + pub fn bubble_point_pressure_from_csv(path: &str, name: Option<&str>) -> PyResult { + Self::from_csv_for_property(BinaryProperty::BubblePointPressure, path, name) + } + + /// Construct bubble point pressure data from numpy arrays. + #[staticmethod] + #[pyo3(signature = (temperature_k, liquid_molefrac_1, bubble_pressure_pa, name=None))] + pub fn bubble_point_pressure( + temperature_k: PyReadonlyArray1, + liquid_molefrac_1: PyReadonlyArray1, + bubble_pressure_pa: PyReadonlyArray1, + name: Option<&str>, + ) -> PyResult { + let temperature_k = temperature_k.as_array(); + let liquid_molefrac_1 = liquid_molefrac_1.as_array(); + let bubble_pressure_pa = bubble_pressure_pa.as_array(); + let records = collect_records_3( + ("temperature_k", temperature_k), + ("liquid_molefrac_1", liquid_molefrac_1), + ("bubble_pressure_pa", bubble_pressure_pa), + |temperature_k, liquid_molefrac_1, bubble_pressure_pa| BubblePointRecord { + temperature_k, + liquid_molefrac_1, + bubble_pressure_pa, + }, + )?; + Ok(Self::with_optional_name( + BinaryDataset::bubble_point_pressure(records), + name, + )) + } + + /// Load dew point pressure data from CSV. + /// + /// CSV columns: ``temperature_k, vapor_molefrac_1, dew_pressure_pa``. + #[staticmethod] + #[pyo3(signature = (path, name=None))] + pub fn dew_point_pressure_from_csv(path: &str, name: Option<&str>) -> PyResult { + Self::from_csv_for_property(BinaryProperty::DewPointPressure, path, name) + } + + /// Construct dew point pressure data from numpy arrays. + #[staticmethod] + #[pyo3(signature = (temperature_k, vapor_molefrac_1, dew_pressure_pa, name=None))] + pub fn dew_point_pressure( + temperature_k: PyReadonlyArray1, + vapor_molefrac_1: PyReadonlyArray1, + dew_pressure_pa: PyReadonlyArray1, + name: Option<&str>, + ) -> PyResult { + let temperature_k = temperature_k.as_array(); + let vapor_molefrac_1 = vapor_molefrac_1.as_array(); + let dew_pressure_pa = dew_pressure_pa.as_array(); + let records = collect_records_3( + ("temperature_k", temperature_k), + ("vapor_molefrac_1", vapor_molefrac_1), + ("dew_pressure_pa", dew_pressure_pa), + |temperature_k, vapor_molefrac_1, dew_pressure_pa| DewPointRecord { + temperature_k, + vapor_molefrac_1, + dew_pressure_pa, + }, + )?; + Ok(Self::with_optional_name( + BinaryDataset::dew_point_pressure(records), + name, + )) + } + + /// Property name. + #[getter] + pub fn name(&self) -> &str { + self.inner.name() + } + + /// Number of data points. + pub fn __len__(&self) -> usize { + self.inner.target().len() + } + + /// Target values. + pub fn target<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { + self.inner.target().to_owned().to_pyarray(py) + } + + /// Evaluate the dataset's property for one or more models (no gradients). + /// + /// Args: + /// models: A single ``EquationOfState`` or a list of them. Each + /// model must describe a binary system (``components() == 2``). + /// + /// Returns: + /// ``(predicted, converged)``. For a single model, both are 1D arrays + /// of length ``n_points``. For a list of ``n_models`` models, both are + /// 2D arrays of shape ``[n_points, n_models]``; column ``k`` + /// corresponds to ``models[k]``. Non-converged points are reported as + /// ``NaN`` in ``predicted`` and ``False`` in ``converged``. + pub fn evaluate<'py>( + &self, + py: Python<'py>, + models: &Bound<'py, PyAny>, + ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { + evaluate_models(py, &self.inner, models) + } + + pub fn __repr__(&self) -> String { + format!( + "BinaryDataset(property={}, n={})", + self.inner.name(), + self.inner.target().len() + ) + } +} + +impl PyBinaryDataset { + fn from_csv_for_property( + property: BinaryProperty, + path: &str, + name: Option<&str>, + ) -> PyResult { + BinaryDataset::from_csv(property, std::path::Path::new(path)) + .map(|inner| Self::with_optional_name(inner, name)) + .map_err(|e| PyValueError::new_err(e.to_string())) + } + + fn with_optional_name(mut inner: BinaryDataset, name: Option<&str>) -> Self { + if let Some(n) = name { + inner = inner.with_name(n); + } + Self { inner } + } +} diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index 3f775a2da..87bfa3c40 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -1,9 +1,12 @@ use feos::pcsaft::{PcSaftBinary, PcSaftPure}; -use feos_core::{ParametersAD, PropertiesAD}; +use feos_core::ParametersAD; use numpy::{PyArray1, PyArray2, PyReadonlyArray2, ToPyArray}; use paste::paste; use pyo3::prelude::*; +pub mod dataset; +pub use dataset::{PyBinaryDataset, PyPureDataset}; + #[pyclass(name = "EquationOfStateAD", eq, eq_int)] #[derive(Clone, Copy, PartialEq)] pub enum PyEquationOfStateAD { @@ -135,6 +138,60 @@ pub fn equilibrium_liquid_density_derivatives<'py>( _equilibrium_liquid_density_derivatives(model, parameter_names, parameters, input) } +/// Calculate enthalpy of vaporization 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 temperature (in K) for every data point. +/// +/// Returns +/// ------- +/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): +/// The enthalpies of vaporization (in J/mol), gradients, and convergence status. +#[pyfunction] +pub fn enthalpy_of_vaporization_derivatives<'py>( + model: PyEquationOfStateAD, + parameter_names: &Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, +) -> GradResult<'py> { + _enthalpy_of_vaporization_derivatives(model, parameter_names, parameters, input) +} + +/// Calculate residual isobaric molar heat capacities (liquid phase) 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 temperature (in K) and pressure (in Pa) for every data point. +/// +/// Returns +/// ------- +/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): +/// The residual isobaric heat capacities (in J/(mol·K)), gradients, and convergence status. +#[pyfunction] +pub fn residual_isobaric_heat_capacity_derivatives<'py>( + model: PyEquationOfStateAD, + parameter_names: &Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, +) -> GradResult<'py> { + _residual_isobaric_heat_capacity_derivatives(model, parameter_names, parameters, input) +} + /// Calculate bubble point pressures of binary mixtures and derivatives w.r.t. model parameters. /// /// Parameters @@ -231,9 +288,9 @@ macro_rules! impl_evaluate_gradients { let (value, grad, status) = $( if let Ok(p) = parameter_names.extract::<[String; $p]>() { - R::[<$prop _parallel>](p, parameters.as_array(), input.as_array()) + feos_core::properties::[<$prop _parallel_ad>]::(p, parameters.as_array(), input.as_array()) } else)* if let Ok(p) = parameter_names.extract::<[String; $max]>() { - R::[<$prop _parallel>](p, parameters.as_array(), input.as_array()) + feos_core::properties::[<$prop _parallel_ad>]::(p, parameters.as_array(), input.as_array()) } else { panic!("Gradients can only be evaluated for up to {} parameters!", $max) }; @@ -248,7 +305,7 @@ macro_rules! impl_evaluate_gradients { impl_evaluate_gradients!( pure, - [vapor_pressure, boiling_temperature, liquid_density, equilibrium_liquid_density], + [vapor_pressure, boiling_temperature, liquid_density, equilibrium_liquid_density, enthalpy_of_vaporization, residual_isobaric_heat_capacity], {PcSaftNonAssoc: PcSaftPure, PcSaftFull: PcSaftPure} ); diff --git a/py-feos/src/lib.rs b/py-feos/src/lib.rs index 948a24d6b..179fe526f 100644 --- a/py-feos/src/lib.rs +++ b/py-feos/src/lib.rs @@ -179,12 +179,6 @@ fn feos(m: &Bound<'_, PyModule>) -> PyResult<()> { // Equation of state m.add_class::()?; - // // Estimator - // m.add_class::()?; - // m.add_class::()?; - // m.add_class::()?; - // m.add_class::()?; - // AD #[cfg(feature = "ad")] { @@ -195,9 +189,21 @@ fn feos(m: &Bound<'_, PyModule>) -> PyResult<()> { ad::equilibrium_liquid_density_derivatives, m )?)?; + m.add_function(wrap_pyfunction!( + ad::enthalpy_of_vaporization_derivatives, + m + )?)?; + m.add_function(wrap_pyfunction!( + ad::residual_isobaric_heat_capacity_derivatives, + m + )?)?; m.add_function(wrap_pyfunction!(ad::bubble_point_pressure_derivatives, m)?)?; m.add_function(wrap_pyfunction!(ad::dew_point_pressure_derivatives, m)?)?; m.add_class::()?; + + // Datasets + m.add_class::()?; + m.add_class::()?; } #[cfg(feature = "dft")] From 1b0a187b877a0cea4cf833ecee9113611d86ae95 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Tue, 19 May 2026 01:32:26 +0200 Subject: [PATCH 02/15] Added modules for Dataset for pure substances and binary mixtures --- .../dataset/binary.rs | 20 +- crates/feos-core/src/ad/dataset/mod.rs | 170 ++++++++++++++ .../dataset/pure.rs | 31 +-- crates/feos-core/src/ad/mod.rs | 2 +- .../ad/parameter_optimization/dataset/mod.rs | 210 ------------------ .../src/ad/parameter_optimization/mod.rs | 11 - .../ad/properties/bubble_point_pressure.rs | 2 +- .../src/ad/properties/dew_point_pressure.rs | 2 +- .../ad/properties/enthalpy_of_vaporization.rs | 2 +- .../properties/equilibrium_liquid_density.rs | 2 +- .../src/ad/properties/liquid_density.rs | 2 +- crates/feos-core/src/ad/properties/mod.rs | 42 +--- .../residual_isobaric_heat_capacity.rs | 2 +- .../src/ad/properties/vapor_pressure.rs | 2 +- crates/feos-core/src/lib.rs | 3 +- py-feos/src/ad/dataset.rs | 73 +++--- 16 files changed, 241 insertions(+), 335 deletions(-) rename crates/feos-core/src/ad/{parameter_optimization => }/dataset/binary.rs (91%) create mode 100644 crates/feos-core/src/ad/dataset/mod.rs rename crates/feos-core/src/ad/{parameter_optimization => }/dataset/pure.rs (88%) delete mode 100644 crates/feos-core/src/ad/parameter_optimization/dataset/mod.rs delete mode 100644 crates/feos-core/src/ad/parameter_optimization/mod.rs diff --git a/crates/feos-core/src/ad/parameter_optimization/dataset/binary.rs b/crates/feos-core/src/ad/dataset/binary.rs similarity index 91% rename from crates/feos-core/src/ad/parameter_optimization/dataset/binary.rs rename to crates/feos-core/src/ad/dataset/binary.rs index f20d9c048..03e68b7c1 100644 --- a/crates/feos-core/src/ad/parameter_optimization/dataset/binary.rs +++ b/crates/feos-core/src/ad/dataset/binary.rs @@ -2,18 +2,15 @@ use std::{io, path::Path}; use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; -use crate::ad::properties::{ - BubblePointRecord, DewPointRecord, bubble_point_pressure_parallel, - bubble_point_pressure_parallel_ad, dew_point_pressure_parallel, dew_point_pressure_parallel_ad, -}; +use crate::ad::properties::*; use crate::{ParametersAD, Residual}; use super::{Dataset, DatasetAD, DatasetStorage}; /// Expand a list of binary-mixture property entries into: -/// - the [`BinaryProperty`] enum and its metadata + dispatch methods, -/// - typed constructors on [`BinaryDataset`] (one per `constructor:` ident), -/// - the [`BinaryDataset::from_csv`] / [`BinaryDataset::from_reader`] match arms. +/// - the [`BinaryProperty`] enum, metadata and dispatch methods, +/// - constructors, +/// - [`BinaryDataset::from_csv`] and [`BinaryDataset::from_reader`] match arms. macro_rules! binary_properties { ($( $variant:ident { @@ -26,7 +23,7 @@ macro_rules! binary_properties { constructor: $ctor:ident, } ),* $(,)?) => { - /// Binary-mixture properties supported by the regressor. + /// Binary-mixture properties. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BinaryProperty { $($variant,)* @@ -176,11 +173,8 @@ impl Dataset for BinaryDataset { self.target_name() } - fn evaluate(&self, model: &E) -> (Array1, Array1) - where - E: Residual + Sync, - { - self.property.evaluate(model, self.inputs()) + fn evaluate(&self, eos: &E) -> (Array1, Array1) { + self.property.evaluate(eos, self.inputs()) } } diff --git a/crates/feos-core/src/ad/dataset/mod.rs b/crates/feos-core/src/ad/dataset/mod.rs new file mode 100644 index 000000000..ba2a3b192 --- /dev/null +++ b/crates/feos-core/src/ad/dataset/mod.rs @@ -0,0 +1,170 @@ +mod binary; +mod pure; + +use std::{io, path::Path, sync::Arc}; + +use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; +use serde::de::DeserializeOwned; + +use crate::{ParametersAD, Residual}; + +pub use binary::{BinaryDataset, BinaryProperty}; +pub use pure::{PureDataset, PureProperty}; + +/// Shared numerical data for all datasets. +struct DatasetData { + inputs: Array2, + target: Array1, +} + +/// Shared representation for all datasets. +#[derive(Clone)] +struct DatasetStorage { + data: Arc, + name: Option, +} + +impl DatasetStorage { + fn from_records(records: Vec) -> Self { + let n = records.len(); + let inputs = Array2::from_shape_fn((n, R::N_INPUTS), |(i, j)| records[i].input(j)); + let target = Array1::from_iter(records.iter().map(DatasetRecord::target)); + Self { + data: Arc::new(DatasetData { inputs, target }), + name: None, + } + } + + fn from_csv(path: &Path) -> Result { + let records = csv::Reader::from_path(path)? + .deserialize() + .collect::, _>>()?; + Ok(Self::from_records(records)) + } + + fn from_reader(reader: impl io::Read) -> Result { + let records = csv::Reader::from_reader(reader) + .deserialize() + .collect::, _>>()?; + Ok(Self::from_records(records)) + } + + fn inputs(&self) -> ArrayView2<'_, f64> { + self.data.inputs.view() + } + + fn target(&self) -> ArrayView1<'_, f64> { + self.data.target.view() + } + + fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + fn set_name(&mut self, name: String) { + self.name = Some(name); + } +} + +/// A record that can be collected into a dataset. +pub trait DatasetRecord: DeserializeOwned { + /// Number of columns for inputs. + const N_INPUTS: usize; + + /// Value of EoS input column. + fn input(&self, column: usize) -> f64; + + /// Target value. + fn target(&self) -> f64; +} + +/// Dataset that can be evaluated by an equation of state. +pub trait Dataset { + /// Inputs for EoS evaluation, shape `[n_points, k]`. + fn inputs(&self) -> ArrayView2<'_, f64>; + + /// Target values, shape `[n_points]`. + fn target(&self) -> ArrayView1<'_, f64>; + + /// Property name. + /// + /// Used for logging and diagnostics. + fn name(&self) -> &str; + + /// Names of independent input columns. + fn input_names(&self) -> &'static [&'static str]; + + /// Name of the target property. + fn target_name(&self) -> &'static str; + + /// Evaluate this dataset's property with an equation of state. + /// + /// Returns `(predicted, converged)`: + /// - `predicted`: shape `[n_points]`, in SI units; `NaN` where the + /// underlying solver did not converge. + /// - `converged`: shape `[n_points]`. + fn evaluate(&self, eos: &E) -> (Array1, Array1); +} + +/// Build [`GRADIENT_SLOTS`] and [`DatasetAD`] trait from a single list of 'slots'. +/// +/// For each slot in [`GRADIENT_SLOTS`] a compile-time constant `P` variant is generated. +macro_rules! define_dataset_ad { + ($($p:literal),+ $(,)?) => { + /// Compile-time gradient slot supported by [`DatasetAD::evaluate_ad`]. + pub const GRADIENT_SLOTS: &[usize] = &[$($p),+]; + + /// Dataset that supports parameter-gradient evaluation + /// for equations of state implementing [`ParametersAD`]. + pub trait DatasetAD: Dataset { + /// Evaluate the property and its `P` parameter gradients. + fn evaluate_ad_const, const P: usize>( + &self, + names: [String; P], + parameters: ArrayView2, + inputs: ArrayView2, + ) -> (Array1, Array2, Array1); + + /// Evaluate the property and its parameter gradients at the given parameters. + /// + /// - `param_names`: names of the `P` parameters being differentiated. + /// - `params`: the full parameter vector. Only entries listed in `param_names` are seeded. + /// + /// This function dispatches the const-P methods at run-time. + fn evaluate_ad>( + &self, + param_names: &[String], + params: &[f64], + ) -> (Array1, Array2, Array1) { + let n = self.inputs().nrows(); + let parameters = Array2::from_shape_fn((n, params.len()), |(_, j)| params[j]); + + fn to_const(names: &[String]) -> [String; P] { + names.to_vec().try_into().expect("parameter count mismatch") + } + + match param_names.len() { + $( + $p => self.evaluate_ad_const::( + to_const(param_names), + parameters.view(), + self.inputs(), + ), + )+ + p => unreachable!( + "parameter count {p} is not a member of GRADIENT_SLOTS={:?}", + GRADIENT_SLOTS, + ), + } + } + } + }; +} + +// We define the number of slots here. +// +// Note: might be good to investigate whether a smaller list makes sense here. +// LLVM vectorises across entries in DualSVec. We might see no perf. difference +// when using e.g. 3 vs 4 slots (even if only 3 are needed by the user). +// If the number of monomophised variants ever gets problematic, we could reduce it that way. +define_dataset_ad!(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14); diff --git a/crates/feos-core/src/ad/parameter_optimization/dataset/pure.rs b/crates/feos-core/src/ad/dataset/pure.rs similarity index 88% rename from crates/feos-core/src/ad/parameter_optimization/dataset/pure.rs rename to crates/feos-core/src/ad/dataset/pure.rs index 3f3a756f2..6f2688076 100644 --- a/crates/feos-core/src/ad/parameter_optimization/dataset/pure.rs +++ b/crates/feos-core/src/ad/dataset/pure.rs @@ -2,25 +2,19 @@ use std::{io, path::Path}; use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; -use crate::ad::properties::{ - EnthalpyOfVaporizationRecord, EquilibriumLiquidDensityRecord, LiquidDensityRecord, - ResidualIsobaricHeatCapacityRecord, VaporPressureRecord, enthalpy_of_vaporization_parallel, - enthalpy_of_vaporization_parallel_ad, equilibrium_liquid_density_parallel, - equilibrium_liquid_density_parallel_ad, liquid_density_parallel, liquid_density_parallel_ad, - residual_isobaric_heat_capacity_parallel, residual_isobaric_heat_capacity_parallel_ad, - vapor_pressure_parallel, vapor_pressure_parallel_ad, -}; +use crate::ad::properties::*; use crate::{ParametersAD, Residual}; use super::{Dataset, DatasetAD, DatasetStorage}; /// Expand a list of pure-component property entries into: -/// - the [`PureProperty`] enum and its metadata + dispatch methods, -/// - typed constructors on [`PureDataset`] (one per `constructor:` ident), -/// - the [`PureDataset::from_csv`] / [`PureDataset::from_reader`] match arms. +/// - the [`PureProperty`] enum, metadata and dispatch methods, +/// - constructors, +/// - [`PureDataset::from_csv`] and [`PureDataset::from_reader`]. /// -/// Adding a new property means writing the property file (record, `*_ad`, -/// `*_parallel`, `*_parallel_ad`) and adding one entry here. +/// Adding a new property: +/// - write the property file (record, `*_ad`, `*_parallel`, `*_parallel_ad`) +/// - add entry here. macro_rules! pure_properties { ($( $variant:ident { @@ -63,9 +57,7 @@ macro_rules! pure_properties { } } - fn evaluate(self, eos: &E, inputs: ArrayView2) -> (Array1, Array1) - where - E: Residual + Sync, + fn evaluate(self, eos: &E, inputs: ArrayView2) -> (Array1, Array1) { match self { $(Self::$variant => $eval_fn(eos, inputs),)* @@ -210,11 +202,8 @@ impl Dataset for PureDataset { self.target_name() } - fn evaluate(&self, model: &E) -> (Array1, Array1) - where - E: Residual + Sync, - { - self.property.evaluate(model, self.inputs()) + fn evaluate(&self, eos: &E) -> (Array1, Array1) { + self.property.evaluate(eos, self.inputs()) } } diff --git a/crates/feos-core/src/ad/mod.rs b/crates/feos-core/src/ad/mod.rs index 0d3e257f0..f40eab95a 100644 --- a/crates/feos-core/src/ad/mod.rs +++ b/crates/feos-core/src/ad/mod.rs @@ -1,4 +1,4 @@ -pub mod parameter_optimization; +pub mod dataset; pub mod properties; use crate::{FeosResult, Residual}; diff --git a/crates/feos-core/src/ad/parameter_optimization/dataset/mod.rs b/crates/feos-core/src/ad/parameter_optimization/dataset/mod.rs deleted file mode 100644 index eccb5993b..000000000 --- a/crates/feos-core/src/ad/parameter_optimization/dataset/mod.rs +++ /dev/null @@ -1,210 +0,0 @@ -mod binary; -mod pure; - -use std::{io, path::Path, sync::Arc}; - -use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; -use serde::Serialize; -use serde::de::DeserializeOwned; - -use crate::{ParametersAD, Residual}; - -pub use binary::{BinaryDataset, BinaryProperty}; -pub use pure::{PureDataset, PureProperty}; - -/// Per-dataset evaluation result: inputs, experimental target, model -/// prediction, and derived statistics at a given set of parameters. -/// -/// Produced by [`crate::Regressor::evaluate_datasets`]. -/// Fully serializable and convertable into any tabular format (CSV, JSON, DataFrame). -#[derive(Debug, Serialize)] -pub struct DatasetResult { - /// Dataset name (default property name or user-supplied). - pub name: String, - /// Reported independent input column names and their values. - pub inputs: Vec<(&'static str, Vec)>, - /// Name of the target property column. - pub target_name: &'static str, - /// Experimental target values. - pub target: Vec, - /// Values at the given parameters predicted by the model. - /// `NaN` for points where calculations did not converge. - pub predicted: Vec, - /// Whether the calculation converged for each point. - pub converged: Vec, - /// Relative deviation `(predicted − target) / target`. - /// `NaN` for non-converged points. - pub relative_deviation: Vec, -} - -/// Shared numerical data for all datasets. -struct DatasetData { - inputs: Array2, - target: Array1, -} - -/// Shared in-memory representation for all datasets. -/// -/// Cheap to clone: the inputs/target arrays are kept behind an `Arc`, -/// while the user-supplied name is owned per handle. -#[derive(Clone)] -struct DatasetStorage { - data: Arc, - name: Option, -} - -impl DatasetStorage { - fn from_records(records: Vec) -> Self { - let n = records.len(); - let inputs = Array2::from_shape_fn((n, R::N_INPUTS), |(i, j)| records[i].input(j)); - let target = Array1::from_iter(records.iter().map(DatasetRecord::target)); - Self { - data: Arc::new(DatasetData { inputs, target }), - name: None, - } - } - - fn from_csv(path: &Path) -> Result { - let records = csv::Reader::from_path(path)? - .deserialize() - .collect::, _>>()?; - Ok(Self::from_records(records)) - } - - fn from_reader(reader: impl io::Read) -> Result { - let records = csv::Reader::from_reader(reader) - .deserialize() - .collect::, _>>()?; - Ok(Self::from_records(records)) - } - - fn inputs(&self) -> ArrayView2<'_, f64> { - self.data.inputs.view() - } - - fn target(&self) -> ArrayView1<'_, f64> { - self.data.target.view() - } - - fn name(&self) -> Option<&str> { - self.name.as_deref() - } - - fn set_name(&mut self, name: String) { - self.name = Some(name); - } -} - -/// Conversion logic for records that can be collected into a dataset. -pub trait DatasetRecord: DeserializeOwned { - /// Number of columns passed to the property evaluator. - const N_INPUTS: usize; - - /// Value of model input column `column`. - fn input(&self, column: usize) -> f64; - - /// Experimental target value. - fn target(&self) -> f64; -} - -/// Experimental data container with metadata and a non-AD model evaluator. -/// -/// Implementors expose the inputs and targets plus an -/// [`evaluate`](Self::evaluate) method that runs the dataset's property -/// against a single model without computing gradients. Used for model -/// comparison and CLI/Python multi-model workflows. -pub trait Dataset { - /// Inputs for model evaluation, shape `[n_points, k]`. - fn inputs(&self) -> ArrayView2<'_, f64>; - - /// Target values, shape `[n_points]`. - fn target(&self) -> ArrayView1<'_, f64>; - - /// Property name used for logging and diagnostics. - fn name(&self) -> &str; - - /// Names of independent input columns reported in diagnostics. - /// - /// These can be fewer than the number of columns in [`Self::inputs`] when - /// the target is also passed to the model as an initial guess. - fn input_names(&self) -> &'static [&'static str]; - - /// Name of the target property column. - fn target_name(&self) -> &'static str; - - /// Evaluate this dataset's property against a single model. - /// - /// Returns `(predicted, converged)`: - /// - `predicted`: shape `[n_points]`, in SI units; `NaN` where the - /// underlying solver did not converge. - /// - `converged`: shape `[n_points]`. - fn evaluate(&self, model: &E) -> (Array1, Array1) - where - E: Residual + Sync; -} - -/// Maximum number of parameters that can be fitted simultaneously. -/// -/// Enforced upstream at `Regressor::new`. The match arms generated by -/// [`impl_evaluate_ad!`] must cover `1..=MAX_FITTED_PARAMETERS`; bumping -/// this constant means widening that list too. -pub(crate) const MAX_FITTED_PARAMETERS: usize = 14; - -/// Emit the default body of [`DatasetAD::evaluate_ad`]: a runtime-`P` → -/// const-`P` dispatch covering each listed parameter count. -macro_rules! impl_evaluate_ad { - ($($p:literal),+ $(,)?) => { - /// Evaluate the property and its parameter gradients at the given parameters. - /// - /// - `param_names`: names of the `P` parameters being differentiated. - /// - `params`: the full parameter vector; only entries listed in `param_names` are seeded. - /// - /// Returns `(predicted, gradients, converged)`: - /// - `predicted`: shape `[n_points]`, in SI units. - /// - `gradients`: shape `[n_points, P]`. - /// - `converged`: shape `[n_points]`. - fn evaluate_ad>( - &self, - param_names: &[String], - params: &[f64], - ) -> (Array1, Array2, Array1) { - let n = self.inputs().nrows(); - let parameters = Array2::from_shape_fn((n, params.len()), |(_, j)| params[j]); - - fn to_const(names: &[String]) -> [String; P] { - names.to_vec().try_into().expect("parameter count mismatch") - } - - match param_names.len() { - $( - $p => self.evaluate_ad_const::( - to_const(param_names), - parameters.view(), - self.inputs(), - ), - )+ - p => unreachable!( - "Regressor::new rejects fit lists longer than \ - MAX_FITTED_PARAMETERS={MAX_FITTED_PARAMETERS}; got {p}", - ), - } - } - }; -} - -/// Experimental data container that supports parameter-gradient evaluation -/// for models implementing [`ParametersAD`]. -pub trait DatasetAD: Dataset { - /// Evaluate the property and its `P` parameter gradients at compile-time-known `P`. - /// - /// Implementors typically delegate to a property-specific - /// `*_parallel_ad::` function. - fn evaluate_ad_const, const P: usize>( - &self, - names: [String; P], - parameters: ArrayView2, - inputs: ArrayView2, - ) -> (Array1, Array2, Array1); - - impl_evaluate_ad!(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14); -} diff --git a/crates/feos-core/src/ad/parameter_optimization/mod.rs b/crates/feos-core/src/ad/parameter_optimization/mod.rs deleted file mode 100644 index 329e075c9..000000000 --- a/crates/feos-core/src/ad/parameter_optimization/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod dataset; - -pub use crate::ad::properties::{ - BubblePointRecord, DewPointRecord, EnthalpyOfVaporizationRecord, - EquilibriumLiquidDensityRecord, LiquidDensityRecord, ResidualIsobaricHeatCapacityRecord, - VaporPressureRecord, -}; -pub use dataset::{ - BinaryDataset, BinaryProperty, Dataset, DatasetAD, DatasetRecord, DatasetResult, PureDataset, - PureProperty, -}; diff --git a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs index 52b0b669f..cd9f8ff71 100644 --- a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs @@ -1,6 +1,6 @@ use crate::Contributions; use crate::ad::Gradient; -use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{Composition, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::{SVector, U2}; diff --git a/crates/feos-core/src/ad/properties/dew_point_pressure.rs b/crates/feos-core/src/ad/properties/dew_point_pressure.rs index a6312d292..b144ad5a8 100644 --- a/crates/feos-core/src/ad/properties/dew_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/dew_point_pressure.rs @@ -1,5 +1,5 @@ use crate::ad::Gradient; -use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{Composition, Contributions, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::{SVector, U2}; diff --git a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs index 34977bb17..04eedaa3c 100644 --- a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs +++ b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs @@ -1,5 +1,5 @@ use crate::ad::Gradient; -use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::U1; diff --git a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs index 49b258784..7f94bc2a8 100644 --- a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs +++ b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs @@ -1,5 +1,5 @@ use crate::ad::Gradient; -use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{FeosResult, PhaseEquilibrium, Residual}; use nalgebra::U1; diff --git a/crates/feos-core/src/ad/properties/liquid_density.rs b/crates/feos-core/src/ad/properties/liquid_density.rs index 4777f3f78..cb327725b 100644 --- a/crates/feos-core/src/ad/properties/liquid_density.rs +++ b/crates/feos-core/src/ad/properties/liquid_density.rs @@ -1,6 +1,6 @@ use crate::DensityInitialization::Liquid; use crate::ad::Gradient; -use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::density_iteration::density_iteration; use crate::{FeosResult, ReferenceSystem, Residual, State}; diff --git a/crates/feos-core/src/ad/properties/mod.rs b/crates/feos-core/src/ad/properties/mod.rs index aff5e28de..e0e09d796 100644 --- a/crates/feos-core/src/ad/properties/mod.rs +++ b/crates/feos-core/src/ad/properties/mod.rs @@ -7,37 +7,11 @@ pub mod liquid_density; pub mod residual_isobaric_heat_capacity; pub mod vapor_pressure; -pub use boiling_temperature::{boiling_temperature, boiling_temperature_ad}; -pub use bubble_point_pressure::{ - BubblePointRecord, bubble_point_pressure, bubble_point_pressure_ad, -}; -pub use dew_point_pressure::{DewPointRecord, dew_point_pressure, dew_point_pressure_ad}; -pub use enthalpy_of_vaporization::{ - EnthalpyOfVaporizationRecord, enthalpy_of_vaporization, enthalpy_of_vaporization_ad, -}; -pub use equilibrium_liquid_density::{ - EquilibriumLiquidDensityRecord, equilibrium_liquid_density, equilibrium_liquid_density_ad, -}; -pub use liquid_density::{LiquidDensityRecord, liquid_density, liquid_density_ad}; -pub use residual_isobaric_heat_capacity::{ - ResidualIsobaricHeatCapacityRecord, residual_isobaric_heat_capacity, - residual_isobaric_heat_capacity_ad, -}; -pub use vapor_pressure::{VaporPressureRecord, vapor_pressure, vapor_pressure_ad}; - -pub use boiling_temperature::{boiling_temperature_parallel, boiling_temperature_parallel_ad}; -pub use bubble_point_pressure::{ - bubble_point_pressure_parallel, bubble_point_pressure_parallel_ad, -}; -pub use dew_point_pressure::{dew_point_pressure_parallel, dew_point_pressure_parallel_ad}; -pub use enthalpy_of_vaporization::{ - enthalpy_of_vaporization_parallel, enthalpy_of_vaporization_parallel_ad, -}; -pub use equilibrium_liquid_density::{ - equilibrium_liquid_density_parallel, equilibrium_liquid_density_parallel_ad, -}; -pub use liquid_density::{liquid_density_parallel, liquid_density_parallel_ad}; -pub use residual_isobaric_heat_capacity::{ - residual_isobaric_heat_capacity_parallel, residual_isobaric_heat_capacity_parallel_ad, -}; -pub use vapor_pressure::{vapor_pressure_parallel, vapor_pressure_parallel_ad}; +pub use boiling_temperature::*; +pub use bubble_point_pressure::*; +pub use dew_point_pressure::*; +pub use enthalpy_of_vaporization::*; +pub use equilibrium_liquid_density::*; +pub use liquid_density::*; +pub use residual_isobaric_heat_capacity::*; +pub use vapor_pressure::*; diff --git a/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs index e740ff694..98ddf1c1c 100644 --- a/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs +++ b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs @@ -1,6 +1,6 @@ use crate::DensityInitialization::Liquid; use crate::ad::Gradient; -use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::density_iteration::density_iteration; use crate::{FeosResult, ReferenceSystem, Residual, State}; diff --git a/crates/feos-core/src/ad/properties/vapor_pressure.rs b/crates/feos-core/src/ad/properties/vapor_pressure.rs index 5c97d30b9..ba501d11b 100644 --- a/crates/feos-core/src/ad/properties/vapor_pressure.rs +++ b/crates/feos-core/src/ad/properties/vapor_pressure.rs @@ -1,5 +1,5 @@ use crate::ad::Gradient; -use crate::ad::parameter_optimization::dataset::DatasetRecord; +use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::U1; diff --git a/crates/feos-core/src/lib.rs b/crates/feos-core/src/lib.rs index 773e7fa7e..1a65ee25b 100644 --- a/crates/feos-core/src/lib.rs +++ b/crates/feos-core/src/lib.rs @@ -32,8 +32,7 @@ mod errors; pub mod parameter; mod phase_equilibria; mod state; -pub use ad::parameter_optimization; -pub use ad::{ParametersAD, properties}; +pub use ad::{ParametersAD, dataset, properties}; pub use equation_of_state::{ EntropyScaling, EquationOfState, IdealGas, IdealGasAD, Molarweight, NoResidual, Residual, ResidualDyn, Subset, Total, diff --git a/py-feos/src/ad/dataset.rs b/py-feos/src/ad/dataset.rs index 9c8a20148..5c00baf4c 100644 --- a/py-feos/src/ad/dataset.rs +++ b/py-feos/src/ad/dataset.rs @@ -1,7 +1,8 @@ -use feos_core::parameter_optimization::{ - BinaryDataset, BinaryProperty, BubblePointRecord, Dataset, DewPointRecord, - EnthalpyOfVaporizationRecord, EquilibriumLiquidDensityRecord, LiquidDensityRecord, PureDataset, - PureProperty, ResidualIsobaricHeatCapacityRecord, VaporPressureRecord, +use feos_core::dataset::{BinaryDataset, BinaryProperty, Dataset, PureDataset, PureProperty}; +use feos_core::properties::{ + BubblePointRecord, DewPointRecord, EnthalpyOfVaporizationRecord, + EquilibriumLiquidDensityRecord, LiquidDensityRecord, ResidualIsobaricHeatCapacityRecord, + VaporPressureRecord, }; use ndarray::{Array2, ArrayView1}; use numpy::{PyArray1, PyArray2, PyReadonlyArray1, ToPyArray}; @@ -10,33 +11,33 @@ use pyo3::prelude::*; use crate::eos::PyEquationOfState; -/// Run a `Dataset::evaluate` against a single model or a list of models. +/// Run a `Dataset::evaluate` against a single equation of state or a list of them. /// -/// If `models` extracts as a single `PyEquationOfState`, returns the -/// `(predicted, converged)` arrays as 1D. If it extracts as a sequence of -/// models, returns them stacked as 2D arrays with shape `[n_points, n_models]`. -fn evaluate_models<'py, D: Dataset>( +/// If `eos` extracts as a single `PyEquationOfState`, returns the +/// `(predicted, converged)` arrays as 1D. If it extracts as a sequence, +/// returns them stacked as 2D arrays with shape `[n_points, n_eos]`. +fn evaluate_eos<'py, D: Dataset>( py: Python<'py>, dataset: &D, - models: &Bound<'py, PyAny>, + eos: &Bound<'py, PyAny>, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { - if let Ok(model) = models.extract::>() { - let (pred, ok) = dataset.evaluate(&model.0); + if let Ok(e) = eos.extract::>() { + let (pred, ok) = dataset.evaluate(&e.0); return Ok((pred.to_pyarray(py).into_any(), ok.to_pyarray(py).into_any())); } - let model_refs: Vec> = models.extract().map_err(|_| { + let eos_refs: Vec> = eos.extract().map_err(|_| { PyTypeError::new_err( "expected an EquationOfState or a sequence of EquationOfState instances", ) })?; let n_points = dataset.target().len(); - let n_models = model_refs.len(); - let mut pred = Array2::::from_elem((n_points, n_models), f64::NAN); - let mut ok = Array2::::from_elem((n_points, n_models), false); - for (j, model) in model_refs.iter().enumerate() { - let (p, c) = dataset.evaluate(&model.0); + let n_eos = eos_refs.len(); + let mut pred = Array2::::from_elem((n_points, n_eos), f64::NAN); + let mut ok = Array2::::from_elem((n_points, n_eos), false); + for (j, e) in eos_refs.iter().enumerate() { + let (p, c) = dataset.evaluate(&e.0); pred.column_mut(j).assign(&p); ok.column_mut(j).assign(&c); } @@ -329,24 +330,24 @@ impl PyPureDataset { self.inner.inputs().to_owned().to_pyarray(py) } - /// Evaluate the dataset's property for one or more models (no gradients). + /// Evaluate the dataset's property for one or more equations of state (no gradients). /// /// Args: - /// models: A single ``EquationOfState`` or a list of them. Each - /// model must describe a single component (``components() == 1``). + /// eos: A single ``EquationOfState`` or a list of them. Each + /// must describe a single component (``components() == 1``). /// /// Returns: - /// ``(predicted, converged)``. For a single model, both are 1D arrays - /// of length ``n_points``. For a list of ``n_models`` models, both are - /// 2D arrays of shape ``[n_points, n_models]``; column ``k`` - /// corresponds to ``models[k]``. Non-converged points are reported as + /// ``(predicted, converged)``. For a single EoS, both are 1D arrays + /// of length ``n_points``. For a list of ``n_eos`` EoS, both are + /// 2D arrays of shape ``[n_points, n_eos]``; column ``k`` + /// corresponds to ``eos[k]``. Non-converged points are reported as /// ``NaN`` in ``predicted`` and ``False`` in ``converged``. pub fn evaluate<'py>( &self, py: Python<'py>, - models: &Bound<'py, PyAny>, + eos: &Bound<'py, PyAny>, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { - evaluate_models(py, &self.inner, models) + evaluate_eos(py, &self.inner, eos) } pub fn __repr__(&self) -> String { @@ -488,24 +489,24 @@ impl PyBinaryDataset { self.inner.target().to_owned().to_pyarray(py) } - /// Evaluate the dataset's property for one or more models (no gradients). + /// Evaluate the dataset's property for one or more equations of state (no gradients). /// /// Args: - /// models: A single ``EquationOfState`` or a list of them. Each - /// model must describe a binary system (``components() == 2``). + /// eos: A single ``EquationOfState`` or a list of them. Each + /// must describe a binary system (``components() == 2``). /// /// Returns: - /// ``(predicted, converged)``. For a single model, both are 1D arrays - /// of length ``n_points``. For a list of ``n_models`` models, both are - /// 2D arrays of shape ``[n_points, n_models]``; column ``k`` - /// corresponds to ``models[k]``. Non-converged points are reported as + /// ``(predicted, converged)``. For a single EoS, both are 1D arrays + /// of length ``n_points``. For a list of ``n_eos`` EoS, both are + /// 2D arrays of shape ``[n_points, n_eos]``; column ``k`` + /// corresponds to ``eos[k]``. Non-converged points are reported as /// ``NaN`` in ``predicted`` and ``False`` in ``converged``. pub fn evaluate<'py>( &self, py: Python<'py>, - models: &Bound<'py, PyAny>, + eos: &Bound<'py, PyAny>, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { - evaluate_models(py, &self.inner, models) + evaluate_eos(py, &self.inner, eos) } pub fn __repr__(&self) -> String { From 8c7cb4e52424e87edfe8f90827eb8a07ff8583b4 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Tue, 19 May 2026 01:50:57 +0200 Subject: [PATCH 03/15] py-feos exports of Datasets, moved records from props to sets. --- crates/feos-core/src/ad/dataset/binary.rs | 53 ++++++++- crates/feos-core/src/ad/dataset/mod.rs | 4 +- crates/feos-core/src/ad/dataset/pure.rs | 103 +++++++++++++++++- .../ad/properties/bubble_point_pressure.rs | 27 ----- .../src/ad/properties/dew_point_pressure.rs | 27 ----- .../ad/properties/enthalpy_of_vaporization.rs | 20 ---- .../properties/equilibrium_liquid_density.rs | 20 ---- .../src/ad/properties/liquid_density.rs | 25 ----- .../residual_isobaric_heat_capacity.rs | 25 ----- .../src/ad/properties/vapor_pressure.rs | 23 +--- py-feos/src/ad/dataset.rs | 34 ++++-- 11 files changed, 179 insertions(+), 182 deletions(-) diff --git a/crates/feos-core/src/ad/dataset/binary.rs b/crates/feos-core/src/ad/dataset/binary.rs index 03e68b7c1..bad63e246 100644 --- a/crates/feos-core/src/ad/dataset/binary.rs +++ b/crates/feos-core/src/ad/dataset/binary.rs @@ -1,11 +1,62 @@ use std::{io, path::Path}; use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; +use serde::{Deserialize, Serialize}; use crate::ad::properties::*; use crate::{ParametersAD, Residual}; -use super::{Dataset, DatasetAD, DatasetStorage}; +use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage}; + +/// The pressure column doubles as the initial guess passed to the VLE solver. +#[derive(Deserialize, Serialize)] +pub struct BubblePointRecord { + pub temperature_k: f64, + pub liquid_molefrac_1: f64, + pub bubble_pressure_pa: f64, +} + +impl DatasetRecord for BubblePointRecord { + const N_INPUTS: usize = 3; + + fn input(&self, column: usize) -> f64 { + match column { + 0 => self.temperature_k, + 1 => self.liquid_molefrac_1, + 2 => self.bubble_pressure_pa, + _ => unreachable!("invalid bubble point input column"), + } + } + + fn target(&self) -> f64 { + self.bubble_pressure_pa + } +} + +/// The pressure column doubles as the initial guess passed to the VLE solver. +#[derive(Deserialize, Serialize)] +pub struct DewPointRecord { + pub temperature_k: f64, + pub vapor_molefrac_1: f64, + pub dew_pressure_pa: f64, +} + +impl DatasetRecord for DewPointRecord { + const N_INPUTS: usize = 3; + + fn input(&self, column: usize) -> f64 { + match column { + 0 => self.temperature_k, + 1 => self.vapor_molefrac_1, + 2 => self.dew_pressure_pa, + _ => unreachable!("invalid dew point input column"), + } + } + + fn target(&self) -> f64 { + self.dew_pressure_pa + } +} /// Expand a list of binary-mixture property entries into: /// - the [`BinaryProperty`] enum, metadata and dispatch methods, diff --git a/crates/feos-core/src/ad/dataset/mod.rs b/crates/feos-core/src/ad/dataset/mod.rs index ba2a3b192..954ffd863 100644 --- a/crates/feos-core/src/ad/dataset/mod.rs +++ b/crates/feos-core/src/ad/dataset/mod.rs @@ -8,8 +8,8 @@ use serde::de::DeserializeOwned; use crate::{ParametersAD, Residual}; -pub use binary::{BinaryDataset, BinaryProperty}; -pub use pure::{PureDataset, PureProperty}; +pub use binary::*; +pub use pure::*; /// Shared numerical data for all datasets. struct DatasetData { diff --git a/crates/feos-core/src/ad/dataset/pure.rs b/crates/feos-core/src/ad/dataset/pure.rs index 6f2688076..231ada859 100644 --- a/crates/feos-core/src/ad/dataset/pure.rs +++ b/crates/feos-core/src/ad/dataset/pure.rs @@ -1,11 +1,112 @@ use std::{io, path::Path}; use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; +use serde::{Deserialize, Serialize}; use crate::ad::properties::*; use crate::{ParametersAD, Residual}; -use super::{Dataset, DatasetAD, DatasetStorage}; +use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage}; + +#[derive(Deserialize, Serialize)] +pub struct VaporPressureRecord { + pub temperature_k: f64, + pub vapor_pressure_pa: f64, +} + +impl DatasetRecord for VaporPressureRecord { + const N_INPUTS: usize = 1; + + fn input(&self, _column: usize) -> f64 { + self.temperature_k + } + + fn target(&self) -> f64 { + self.vapor_pressure_pa + } +} + +#[derive(Deserialize, Serialize)] +pub struct LiquidDensityRecord { + pub temperature_k: f64, + pub pressure_pa: f64, + pub liquid_density_kmol_m3: f64, +} + +impl DatasetRecord for LiquidDensityRecord { + const N_INPUTS: usize = 2; + + fn input(&self, column: usize) -> f64 { + match column { + 0 => self.temperature_k, + 1 => self.pressure_pa, + _ => unreachable!("invalid liquid density input column"), + } + } + + fn target(&self) -> f64 { + self.liquid_density_kmol_m3 + } +} + +#[derive(Deserialize, Serialize)] +pub struct EquilibriumLiquidDensityRecord { + pub temperature_k: f64, + pub liquid_density_kmol_m3: f64, +} + +impl DatasetRecord for EquilibriumLiquidDensityRecord { + const N_INPUTS: usize = 1; + + fn input(&self, _column: usize) -> f64 { + self.temperature_k + } + + fn target(&self) -> f64 { + self.liquid_density_kmol_m3 + } +} + +#[derive(Deserialize, Serialize)] +pub struct EnthalpyOfVaporizationRecord { + pub temperature_k: f64, + pub dh_vap_j_mol: f64, +} + +impl DatasetRecord for EnthalpyOfVaporizationRecord { + const N_INPUTS: usize = 1; + + fn input(&self, _column: usize) -> f64 { + self.temperature_k + } + + fn target(&self) -> f64 { + self.dh_vap_j_mol + } +} + +#[derive(Deserialize, Serialize)] +pub struct ResidualIsobaricHeatCapacityRecord { + pub temperature_k: f64, + pub pressure_pa: f64, + pub cp_res_j_molk: f64, +} + +impl DatasetRecord for ResidualIsobaricHeatCapacityRecord { + const N_INPUTS: usize = 2; + + fn input(&self, column: usize) -> f64 { + match column { + 0 => self.temperature_k, + 1 => self.pressure_pa, + _ => unreachable!("invalid residual isobaric heat capacity input column"), + } + } + + fn target(&self) -> f64 { + self.cp_res_j_molk + } +} /// Expand a list of pure-component property entries into: /// - the [`PureProperty`] enum, metadata and dispatch methods, diff --git a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs index cd9f8ff71..f0794b07e 100644 --- a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs @@ -1,37 +1,10 @@ use crate::Contributions; use crate::ad::Gradient; -use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{Composition, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::{SVector, U2}; use ndarray::{Array1, Array2, ArrayView2}; use quantity::{KELVIN, PASCAL, Pressure, Temperature}; -use serde::{Deserialize, Serialize}; - -/// The pressure column doubles as the initial guess passed to the VLE solver. -#[derive(Deserialize, Serialize)] -pub struct BubblePointRecord { - pub temperature_k: f64, - pub liquid_molefrac_1: f64, - pub bubble_pressure_pa: f64, -} - -impl DatasetRecord for BubblePointRecord { - const N_INPUTS: usize = 3; - - fn input(&self, column: usize) -> f64 { - match column { - 0 => self.temperature_k, - 1 => self.liquid_molefrac_1, - 2 => self.bubble_pressure_pa, - _ => unreachable!("invalid bubble point input column"), - } - } - - fn target(&self) -> f64 { - self.bubble_pressure_pa - } -} pub fn bubble_point_pressure_ad< E: Residual>, diff --git a/crates/feos-core/src/ad/properties/dew_point_pressure.rs b/crates/feos-core/src/ad/properties/dew_point_pressure.rs index b144ad5a8..333f88799 100644 --- a/crates/feos-core/src/ad/properties/dew_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/dew_point_pressure.rs @@ -1,36 +1,9 @@ use crate::ad::Gradient; -use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{Composition, Contributions, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::{SVector, U2}; use ndarray::{Array1, Array2, ArrayView2}; use quantity::{KELVIN, PASCAL, Pressure, Temperature}; -use serde::{Deserialize, Serialize}; - -/// The pressure column doubles as the initial guess passed to the VLE solver. -#[derive(Deserialize, Serialize)] -pub struct DewPointRecord { - pub temperature_k: f64, - pub vapor_molefrac_1: f64, - pub dew_pressure_pa: f64, -} - -impl DatasetRecord for DewPointRecord { - const N_INPUTS: usize = 3; - - fn input(&self, column: usize) -> f64 { - match column { - 0 => self.temperature_k, - 1 => self.vapor_molefrac_1, - 2 => self.dew_pressure_pa, - _ => unreachable!("invalid dew point input column"), - } - } - - fn target(&self) -> f64 { - self.dew_pressure_pa - } -} pub fn dew_point_pressure_ad< E: Residual>, diff --git a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs index 04eedaa3c..64ffdc369 100644 --- a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs +++ b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs @@ -1,30 +1,10 @@ use crate::ad::Gradient; -use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::U1; use ndarray::{Array1, Array2, ArrayView2}; use num_dual::{DualNum, DualStruct, first_derivative, partial2}; use quantity::{JOULE, KELVIN, MOL, MolarEnergy, Temperature}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize)] -pub struct EnthalpyOfVaporizationRecord { - pub temperature_k: f64, - pub dh_vap_j_mol: f64, -} - -impl DatasetRecord for EnthalpyOfVaporizationRecord { - const N_INPUTS: usize = 1; - - fn input(&self, _column: usize) -> f64 { - self.temperature_k - } - - fn target(&self) -> f64 { - self.dh_vap_j_mol - } -} pub fn enthalpy_of_vaporization_ad>, const P: usize>( eos: &E, diff --git a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs index 7f94bc2a8..9697ea4f3 100644 --- a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs +++ b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs @@ -1,30 +1,10 @@ use crate::ad::Gradient; -use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{FeosResult, PhaseEquilibrium, Residual}; use nalgebra::U1; use ndarray::{Array1, Array2, ArrayView2}; use num_dual::DualStruct; use quantity::{Density, KELVIN, KILO, METER, MOL, Pressure, Temperature}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize)] -pub struct EquilibriumLiquidDensityRecord { - pub temperature_k: f64, - pub liquid_density_kmol_m3: f64, -} - -impl DatasetRecord for EquilibriumLiquidDensityRecord { - const N_INPUTS: usize = 1; - - fn input(&self, _column: usize) -> f64 { - self.temperature_k - } - - fn target(&self) -> f64 { - self.liquid_density_kmol_m3 - } -} pub fn equilibrium_liquid_density_ad>, const P: usize>( eos: &E, diff --git a/crates/feos-core/src/ad/properties/liquid_density.rs b/crates/feos-core/src/ad/properties/liquid_density.rs index cb327725b..fceb9c2c6 100644 --- a/crates/feos-core/src/ad/properties/liquid_density.rs +++ b/crates/feos-core/src/ad/properties/liquid_density.rs @@ -1,6 +1,5 @@ use crate::DensityInitialization::Liquid; use crate::ad::Gradient; -use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::density_iteration::density_iteration; use crate::{FeosResult, ReferenceSystem, Residual, State}; @@ -8,30 +7,6 @@ use nalgebra::U1; use ndarray::{Array1, Array2, ArrayView2}; use num_dual::DualStruct; use quantity::{Density, KELVIN, KILO, METER, MOL, Moles, PASCAL, Pressure, Temperature}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize)] -pub struct LiquidDensityRecord { - pub temperature_k: f64, - pub pressure_pa: f64, - pub liquid_density_kmol_m3: f64, -} - -impl DatasetRecord for LiquidDensityRecord { - const N_INPUTS: usize = 2; - - fn input(&self, column: usize) -> f64 { - match column { - 0 => self.temperature_k, - 1 => self.pressure_pa, - _ => unreachable!("invalid liquid density input column"), - } - } - - fn target(&self) -> f64 { - self.liquid_density_kmol_m3 - } -} pub fn liquid_density_ad>, const P: usize>( eos: &E, diff --git a/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs index 98ddf1c1c..a6c85814d 100644 --- a/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs +++ b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs @@ -1,6 +1,5 @@ use crate::DensityInitialization::Liquid; use crate::ad::Gradient; -use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::density_iteration::density_iteration; use crate::{FeosResult, ReferenceSystem, Residual, State}; @@ -8,30 +7,6 @@ use nalgebra::U1; use ndarray::{Array1, Array2, ArrayView2}; use num_dual::DualStruct; use quantity::{JOULE, KELVIN, MOL, MolarEntropy, Moles, PASCAL, Pressure, Temperature}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize)] -pub struct ResidualIsobaricHeatCapacityRecord { - pub temperature_k: f64, - pub pressure_pa: f64, - pub cp_res_j_molk: f64, -} - -impl DatasetRecord for ResidualIsobaricHeatCapacityRecord { - const N_INPUTS: usize = 2; - - fn input(&self, column: usize) -> f64 { - match column { - 0 => self.temperature_k, - 1 => self.pressure_pa, - _ => unreachable!("invalid residual isobaric heat capacity input column"), - } - } - - fn target(&self) -> f64 { - self.cp_res_j_molk - } -} /// Residual isobaric molar heat capacity of the liquid phase at the given /// temperature and pressure. diff --git a/crates/feos-core/src/ad/properties/vapor_pressure.rs b/crates/feos-core/src/ad/properties/vapor_pressure.rs index ba501d11b..307d79d1b 100644 --- a/crates/feos-core/src/ad/properties/vapor_pressure.rs +++ b/crates/feos-core/src/ad/properties/vapor_pressure.rs @@ -1,30 +1,9 @@ use crate::ad::Gradient; -use crate::ad::dataset::DatasetRecord; use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::U1; use ndarray::{Array1, Array2, ArrayView2}; -use quantity::{KELVIN, PASCAL}; -use quantity::{Pressure, Temperature}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Serialize)] -pub struct VaporPressureRecord { - pub temperature_k: f64, - pub vapor_pressure_pa: f64, -} - -impl DatasetRecord for VaporPressureRecord { - const N_INPUTS: usize = 1; - - fn input(&self, _column: usize) -> f64 { - self.temperature_k - } - - fn target(&self) -> f64 { - self.vapor_pressure_pa - } -} +use quantity::{KELVIN, PASCAL, Pressure, Temperature}; pub fn vapor_pressure_ad>, const P: usize>( eos: &E, diff --git a/py-feos/src/ad/dataset.rs b/py-feos/src/ad/dataset.rs index 5c00baf4c..faf8b5aa0 100644 --- a/py-feos/src/ad/dataset.rs +++ b/py-feos/src/ad/dataset.rs @@ -1,8 +1,7 @@ -use feos_core::dataset::{BinaryDataset, BinaryProperty, Dataset, PureDataset, PureProperty}; -use feos_core::properties::{ - BubblePointRecord, DewPointRecord, EnthalpyOfVaporizationRecord, - EquilibriumLiquidDensityRecord, LiquidDensityRecord, ResidualIsobaricHeatCapacityRecord, - VaporPressureRecord, +use feos_core::dataset::{ + BinaryDataset, BinaryProperty, BubblePointRecord, Dataset, DewPointRecord, + EnthalpyOfVaporizationRecord, EquilibriumLiquidDensityRecord, LiquidDensityRecord, PureDataset, + PureProperty, ResidualIsobaricHeatCapacityRecord, VaporPressureRecord, }; use ndarray::{Array2, ArrayView1}; use numpy::{PyArray1, PyArray2, PyReadonlyArray1, ToPyArray}; @@ -21,11 +20,13 @@ fn evaluate_eos<'py, D: Dataset>( dataset: &D, eos: &Bound<'py, PyAny>, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { + // Evaluate a single EoS. if let Ok(e) = eos.extract::>() { - let (pred, ok) = dataset.evaluate(&e.0); + let (pred, status) = dataset.evaluate(&e.0); return Ok((pred.to_pyarray(py).into_any(), ok.to_pyarray(py).into_any())); } + // Construct a references to multiple EoSs. let eos_refs: Vec> = eos.extract().map_err(|_| { PyTypeError::new_err( "expected an EquationOfState or a sequence of EquationOfState instances", @@ -35,13 +36,19 @@ fn evaluate_eos<'py, D: Dataset>( let n_points = dataset.target().len(); let n_eos = eos_refs.len(); let mut pred = Array2::::from_elem((n_points, n_eos), f64::NAN); - let mut ok = Array2::::from_elem((n_points, n_eos), false); + let mut status = Array2::::from_elem((n_points, n_eos), false); + + // Iterate through models > evaluate dataset > collect results as columns. + // Column index is EoS-index. for (j, e) in eos_refs.iter().enumerate() { let (p, c) = dataset.evaluate(&e.0); pred.column_mut(j).assign(&p); - ok.column_mut(j).assign(&c); + status.column_mut(j).assign(&c); } - Ok((pred.to_pyarray(py).into_any(), ok.to_pyarray(py).into_any())) + Ok(( + pred.to_pyarray(py).into_any(), + status.to_pyarray(py).into_any(), + )) } fn ensure_same_len(arrays: &[(&str, usize)]) -> PyResult { @@ -85,6 +92,9 @@ fn collect_records_3( .collect()) } +// Parser methods: map from str to enum +// Preferred here to declutter enums/structs exported in feos. + fn parse_pure_property(property: &str) -> PyResult { match property { "vapor_pressure" => Ok(PureProperty::VaporPressure), @@ -330,11 +340,11 @@ impl PyPureDataset { self.inner.inputs().to_owned().to_pyarray(py) } - /// Evaluate the dataset's property for one or more equations of state (no gradients). + /// Evaluate the dataset's property for one or more equations of state. /// /// Args: - /// eos: A single ``EquationOfState`` or a list of them. Each - /// must describe a single component (``components() == 1``). + /// eos: A single ``EquationOfState`` or a list of them. + /// Each must describe a single substance. /// /// Returns: /// ``(predicted, converged)``. For a single EoS, both are 1D arrays From 7a5d4444b4e13ddac51cc6a2475c09eae5664dc9 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Tue, 19 May 2026 12:29:49 +0200 Subject: [PATCH 04/15] fixed symbol rename --- py-feos/src/ad/dataset.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/py-feos/src/ad/dataset.rs b/py-feos/src/ad/dataset.rs index faf8b5aa0..3de5dbd55 100644 --- a/py-feos/src/ad/dataset.rs +++ b/py-feos/src/ad/dataset.rs @@ -23,7 +23,10 @@ fn evaluate_eos<'py, D: Dataset>( // Evaluate a single EoS. if let Ok(e) = eos.extract::>() { let (pred, status) = dataset.evaluate(&e.0); - return Ok((pred.to_pyarray(py).into_any(), ok.to_pyarray(py).into_any())); + return Ok(( + pred.to_pyarray(py).into_any(), + status.to_pyarray(py).into_any(), + )); } // Construct a references to multiple EoSs. From 4754b7d6f297e06224740d7ff937cc1d482da029 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 21 May 2026 14:38:57 +0200 Subject: [PATCH 05/15] streamline properties, no changes to datasets --- Cargo.toml | 1 + crates/feos-core/Cargo.toml | 5 +- crates/feos-core/src/ad/dataset/binary.rs | 24 +- crates/feos-core/src/ad/dataset/mod.rs | 11 +- crates/feos-core/src/ad/dataset/pure.rs | 35 +- crates/feos-core/src/ad/mod.rs | 166 +++---- .../src/ad/properties/boiling_temperature.rs | 121 ++--- .../ad/properties/bubble_point_pressure.rs | 166 ++++--- .../src/ad/properties/dew_point_pressure.rs | 166 ++++--- .../ad/properties/enthalpy_of_vaporization.rs | 89 ++-- .../properties/equilibrium_liquid_density.rs | 61 +-- .../src/ad/properties/liquid_density.rs | 75 ++- crates/feos-core/src/ad/properties/mod.rs | 157 ++++++- .../residual_isobaric_heat_capacity.rs | 82 ++-- .../src/ad/properties/vapor_pressure.rs | 106 ++--- crates/feos-core/src/errors.rs | 1 + crates/feos-core/src/lib.rs | 4 +- .../src/phase_equilibria/bubble_dew.rs | 4 + crates/feos-core/src/phase_equilibria/mod.rs | 5 + crates/feos-core/src/state/statevec.rs | 9 + crates/feos/src/pcsaft/eos/mod.rs | 42 +- crates/feos/src/pcsaft/eos/pcsaft_binary.rs | 6 +- crates/feos/src/pcsaft/eos/pcsaft_pure.rs | 6 +- crates/feos/tests/pcsaft/px_flashes.rs | 4 +- py-feos/src/ad/dataset.rs | 2 +- py-feos/src/ad/mod.rs | 441 +++++++++--------- py-feos/src/lib.rs | 18 +- 27 files changed, 923 insertions(+), 884 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index de98c170f..eb351252c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ gauss-quad = "0.2" approx = "0.5" criterion = "0.8" paste = "1.0" +csv = "1.0" feos-core = { version = "0.9", path = "crates/feos-core" } feos-dft = { version = "0.9", path = "crates/feos-dft" } diff --git a/crates/feos-core/Cargo.toml b/crates/feos-core/Cargo.toml index aae01bc3d..4b79f29c5 100644 --- a/crates/feos-core/Cargo.toml +++ b/crates/feos-core/Cargo.toml @@ -17,7 +17,7 @@ features = ["rayon"] [dependencies] quantity = { workspace = true, features = ["nalgebra", "ndarray", "num-dual"] } num-dual = { workspace = true } -ndarray = { workspace = true } +ndarray = { workspace = true, optional = true } nalgebra = { workspace = true } num-traits = { workspace = true } thiserror = { workspace = true } @@ -25,7 +25,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["preserve_order"] } indexmap = { workspace = true, features = ["serde"] } rayon = { workspace = true, optional = true } -csv = "1" +csv = { workspace = true } itertools = { workspace = true } [dev-dependencies] @@ -34,4 +34,5 @@ quantity = { workspace = true, features = ["approx"] } [features] default = [] +ndarray = ["dep:ndarray", "quantity/ndarray"] rayon = ["dep:rayon", "ndarray/rayon"] diff --git a/crates/feos-core/src/ad/dataset/binary.rs b/crates/feos-core/src/ad/dataset/binary.rs index bad63e246..1e4048277 100644 --- a/crates/feos-core/src/ad/dataset/binary.rs +++ b/crates/feos-core/src/ad/dataset/binary.rs @@ -1,12 +1,13 @@ use std::{io, path::Path}; +use nalgebra::U2; use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; use serde::{Deserialize, Serialize}; -use crate::ad::properties::*; -use crate::{ParametersAD, Residual}; +use crate::Residual; +use crate::ad::properties::{BubblePointPressure, DewPointPressure, Property}; -use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage}; +use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage, ParametersAD}; /// The pressure column doubles as the initial guess passed to the VLE solver. #[derive(Deserialize, Serialize)] @@ -66,11 +67,10 @@ macro_rules! binary_properties { ($( $variant:ident { record: $record:ty, + property: $prop:ty, default_name: $default:expr, input_names: $inputs:expr, target_name: $target:expr, - ad_fn: $ad_fn:ident, - eval_fn: $eval_fn:ident, constructor: $ctor:ident, } ),* $(,)?) => { @@ -93,14 +93,14 @@ macro_rules! binary_properties { match self { $(Self::$variant => $target,)* } } - fn evaluate_ad, const P: usize>( + fn evaluate_ad, const P: usize>( self, names: [String; P], parameters: ArrayView2, inputs: ArrayView2, ) -> (Array1, Array2, Array1) { match self { - $(Self::$variant => $ad_fn::(names, parameters, inputs),)* + $(Self::$variant => <$prop>::evaluate_parallel_ad::(names, parameters, inputs),)* } } @@ -109,7 +109,7 @@ macro_rules! binary_properties { E: Residual + Sync, { match self { - $(Self::$variant => $eval_fn(eos, inputs),)* + $(Self::$variant => <$prop>::evaluate_parallel(eos, inputs),)* } } } @@ -147,20 +147,18 @@ macro_rules! binary_properties { binary_properties! { BubblePointPressure { record: BubblePointRecord, + property: BubblePointPressure, default_name: "bubble point pressure", input_names: &["temperature_k", "liquid_molefrac_1"], target_name: "bubble_pressure_pa", - ad_fn: bubble_point_pressure_parallel_ad, - eval_fn: bubble_point_pressure_parallel, constructor: bubble_point_pressure, }, DewPointPressure { record: DewPointRecord, + property: DewPointPressure, default_name: "dew point pressure", input_names: &["temperature_k", "vapor_molefrac_1"], target_name: "dew_pressure_pa", - ad_fn: dew_point_pressure_parallel_ad, - eval_fn: dew_point_pressure_parallel, constructor: dew_point_pressure, }, } @@ -230,7 +228,7 @@ impl Dataset for BinaryDataset { } impl DatasetAD<2> for BinaryDataset { - fn evaluate_ad_const, const P: usize>( + fn evaluate_ad_const, const P: usize>( &self, names: [String; P], parameters: ArrayView2, diff --git a/crates/feos-core/src/ad/dataset/mod.rs b/crates/feos-core/src/ad/dataset/mod.rs index 954ffd863..6ed94f241 100644 --- a/crates/feos-core/src/ad/dataset/mod.rs +++ b/crates/feos-core/src/ad/dataset/mod.rs @@ -3,10 +3,13 @@ mod pure; use std::{io, path::Path, sync::Arc}; +use nalgebra::Const; use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; use serde::de::DeserializeOwned; -use crate::{ParametersAD, Residual}; +use crate::Residual; + +use super::ParametersAD; pub use binary::*; pub use pure::*; @@ -118,7 +121,7 @@ macro_rules! define_dataset_ad { /// for equations of state implementing [`ParametersAD`]. pub trait DatasetAD: Dataset { /// Evaluate the property and its `P` parameter gradients. - fn evaluate_ad_const, const P: usize>( + fn evaluate_ad_const>, const P: usize>( &self, names: [String; P], parameters: ArrayView2, @@ -131,7 +134,7 @@ macro_rules! define_dataset_ad { /// - `params`: the full parameter vector. Only entries listed in `param_names` are seeded. /// /// This function dispatches the const-P methods at run-time. - fn evaluate_ad>( + fn evaluate_ad>>( &self, param_names: &[String], params: &[f64], @@ -148,7 +151,7 @@ macro_rules! define_dataset_ad { $p => self.evaluate_ad_const::( to_const(param_names), parameters.view(), - self.inputs(), + self.inputs().view(), ), )+ p => unreachable!( diff --git a/crates/feos-core/src/ad/dataset/pure.rs b/crates/feos-core/src/ad/dataset/pure.rs index 231ada859..f961351d6 100644 --- a/crates/feos-core/src/ad/dataset/pure.rs +++ b/crates/feos-core/src/ad/dataset/pure.rs @@ -1,12 +1,13 @@ use std::{io, path::Path}; +use nalgebra::U1; use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; use serde::{Deserialize, Serialize}; +use crate::Residual; use crate::ad::properties::*; -use crate::{ParametersAD, Residual}; -use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage}; +use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage, ParametersAD}; #[derive(Deserialize, Serialize)] pub struct VaporPressureRecord { @@ -120,11 +121,10 @@ macro_rules! pure_properties { ($( $variant:ident { record: $record:ty, + property: $prop:ty, default_name: $default:expr, input_names: $inputs:expr, target_name: $target:expr, - ad_fn: $ad_fn:ident, - eval_fn: $eval_fn:ident, constructor: $ctor:ident, } ),* $(,)?) => { @@ -147,21 +147,21 @@ macro_rules! pure_properties { match self { $(Self::$variant => $target,)* } } - fn evaluate_ad, const P: usize>( + fn evaluate_ad, const P: usize>( self, names: [String; P], parameters: ArrayView2, inputs: ArrayView2, ) -> (Array1, Array2, Array1) { match self { - $(Self::$variant => $ad_fn::(names, parameters, inputs),)* + $(Self::$variant => <$prop>::evaluate_parallel_ad::(names, parameters, inputs),)* } } fn evaluate(self, eos: &E, inputs: ArrayView2) -> (Array1, Array1) { match self { - $(Self::$variant => $eval_fn(eos, inputs),)* + $(Self::$variant => <$prop>::evaluate_parallel(eos, inputs.view()),)* } } } @@ -199,53 +199,48 @@ macro_rules! pure_properties { pure_properties! { VaporPressure { record: VaporPressureRecord, + property: VaporPressure, default_name: "vapor pressure", input_names: &["temperature_k"], target_name: "vapor_pressure_pa", - ad_fn: vapor_pressure_parallel_ad, - eval_fn: vapor_pressure_parallel, constructor: vapor_pressure, }, LiquidDensity { record: LiquidDensityRecord, + property: LiquidDensity, default_name: "liquid density", input_names: &["temperature_k", "pressure_pa"], target_name: "liquid_density_kmol_m3", - ad_fn: liquid_density_parallel_ad, - eval_fn: liquid_density_parallel, constructor: liquid_density, }, EquilibriumLiquidDensity { record: EquilibriumLiquidDensityRecord, + property: EquilibriumLiquidDensity, default_name: "equilibrium liquid density", input_names: &["temperature_k"], target_name: "liquid_density_kmol_m3", - ad_fn: equilibrium_liquid_density_parallel_ad, - eval_fn: equilibrium_liquid_density_parallel, constructor: equilibrium_liquid_density, }, EnthalpyOfVaporization { record: EnthalpyOfVaporizationRecord, + property: EnthalpyOfVaporization, default_name: "enthalpy of vaporization", input_names: &["temperature_k"], target_name: "dh_vap_j_mol", - ad_fn: enthalpy_of_vaporization_parallel_ad, - eval_fn: enthalpy_of_vaporization_parallel, constructor: enthalpy_of_vaporization, }, ResidualIsobaricHeatCapacity { record: ResidualIsobaricHeatCapacityRecord, + property: ResidualIsobaricHeatCapacity, default_name: "residual isobaric heat capacity", input_names: &["temperature_k", "pressure_pa"], target_name: "cp_res_j_molk", - ad_fn: residual_isobaric_heat_capacity_parallel_ad, - eval_fn: residual_isobaric_heat_capacity_parallel, constructor: residual_isobaric_heat_capacity, }, } /// Pure-component dataset: shared data storage plus a property tag. -#[derive(Clone)] +// #[derive(Clone)] pub struct PureDataset { property: PureProperty, storage: DatasetStorage, @@ -304,12 +299,12 @@ impl Dataset for PureDataset { } fn evaluate(&self, eos: &E) -> (Array1, Array1) { - self.property.evaluate(eos, self.inputs()) + self.property.evaluate(eos, self.inputs().view()) } } impl DatasetAD<1> for PureDataset { - fn evaluate_ad_const, const P: usize>( + fn evaluate_ad_const, const P: usize>( &self, names: [String; P], parameters: ArrayView2, diff --git a/crates/feos-core/src/ad/mod.rs b/crates/feos-core/src/ad/mod.rs index f40eab95a..16e8d8724 100644 --- a/crates/feos-core/src/ad/mod.rs +++ b/crates/feos-core/src/ad/mod.rs @@ -1,15 +1,21 @@ -pub mod dataset; -pub mod properties; - -use crate::{FeosResult, Residual}; -use nalgebra::{Const, U1}; -use ndarray::{Array1, Array2, ArrayView2, Zip}; +use crate::Residual; +use nalgebra::{Const, DefaultAllocator, Dim, U1, allocator::Allocator}; use num_dual::{Derivative, DualNum, DualSVec}; +#[cfg(feature = "ndarray")] +mod dataset; +mod properties; +#[cfg(feature = "ndarray")] +pub use dataset::*; +pub use properties::*; + pub(crate) type Gradient = DualSVec; /// A model that can be evaluated with derivatives of its parameters. -pub trait ParametersAD: Residual> { +pub trait ParametersAD: Residual +where + DefaultAllocator: Allocator, +{ /// Build the model by requesting each parameter by name. /// /// Call `f(name, differentiable)` for each parameter. The order of calls @@ -59,86 +65,90 @@ pub trait ParametersAD: Residual> { idx += 1; let mut d = Gradient::

::from(parameter_values[i]); if let Some(seed_idx) = derivative_names.iter().position(|&n| n == name) { - d.eps = Derivative::derivative_generic(Const::

, U1, seed_idx); + d.eps = + Derivative::<_, _, Const

, _>::derivative_generic(Const::

, U1, seed_idx); } d }) } } -/// Evaluate a function and its gradients for a batch of parameters and inputs. -pub(crate) fn vectorize_ad, const N: usize, const P: usize>( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, - f: F, -) -> (Array1, Array2, Array1) -where - F: Fn(&E::Lifted>, &[f64]) -> FeosResult> + Sync, -{ - let parameter_names = parameter_names.each_ref().map(|s| s as &str); +// /// Evaluate a function and its gradients for a batch of parameters and inputs. +// #[cfg(feature = "ndarray")] +// pub(crate) fn vectorize_ad, N: Dim, const P: usize>( +// parameter_names: [String; P], +// parameters: ArrayView2, +// input: ArrayView2, +// f: F, +// ) -> (Array1, Array2, Array1) +// where +// DefaultAllocator: Allocator, +// F: Fn(&E::Lifted>, &[f64]) -> FeosResult> + Sync, +// { +// let parameter_names = parameter_names.each_ref().map(|s| s as &str); - #[cfg(feature = "rayon")] - let value_dual = Zip::from(parameters.rows()) - .and(input.rows()) - .par_map_collect(|par, inp| { - let par = par.as_slice().expect("Parameter array is not contiguous!"); - let inp = inp.as_slice().expect("Input array is not contiguous!"); - let eos = E::seed_derivatives(par, parameter_names); - f(&eos, inp) - }); +// #[cfg(feature = "rayon")] +// let value_dual = Zip::from(parameters.rows()) +// .and(input.rows()) +// .par_map_collect(|par, inp| { +// let par = par.as_slice().expect("Parameter array is not contiguous!"); +// let inp = inp.as_slice().expect("Input array is not contiguous!"); +// let eos = E::seed_derivatives(par, parameter_names); +// f(&eos, inp) +// }); - #[cfg(not(feature = "rayon"))] - let value_dual = Zip::from(parameters.rows()) - .and(input.rows()) - .map_collect(|par, inp| { - let par = par.as_slice().expect("Parameter array is not contiguous!"); - let inp = inp.as_slice().expect("Input array is not contiguous!"); - let eos = E::seed_derivatives(par, parameter_names); - f(&eos, inp) - }); +// #[cfg(not(feature = "rayon"))] +// let value_dual = Zip::from(parameters.rows()) +// .and(input.rows()) +// .map_collect(|par, inp| { +// let par = par.as_slice().expect("Parameter array is not contiguous!"); +// let inp = inp.as_slice().expect("Input array is not contiguous!"); +// let eos = E::seed_derivatives(par, parameter_names); +// f(&eos, inp) +// }); - let n = parameters.nrows(); - let status = value_dual.iter().map(|p| p.is_ok()).collect(); - let mut value = Array1::from_elem(n, f64::NAN); - let mut grad = Array2::zeros([n, P]); - for (i, result) in value_dual.into_iter().enumerate() { - if let Ok(p_dual) = result { - value[i] = p_dual.re; - let eps = p_dual.eps.unwrap_generic(Const::

, U1); - for (g, &e) in grad.row_mut(i).iter_mut().zip(eps.data.0[0].iter()) { - *g = e; - } - } - } - (value, grad, status) -} +// let n = parameters.nrows(); +// let status = value_dual.iter().map(|p| p.is_ok()).collect(); +// let mut value = Array1::from_elem(n, f64::NAN); +// let mut grad = Array2::zeros([n, P]); +// for (i, result) in value_dual.into_iter().enumerate() { +// if let Ok(p_dual) = result { +// value[i] = p_dual.re; +// let eps = p_dual.eps.unwrap_generic(Const::

, U1); +// for (g, &e) in grad.row_mut(i).iter_mut().zip(eps.data.0[0].iter()) { +// *g = e; +// } +// } +// } +// (value, grad, status) +// } -/// Evaluate a function for a batch of inputs using the same parameters for each sample. -pub(crate) fn vectorize(eos: &E, input: ArrayView2, f: F) -> (Array1, Array1) -where - E: Sync, - F: Fn(&E, &[f64]) -> FeosResult + Sync, -{ - #[cfg(feature = "rayon")] - let values = Zip::from(input.rows()).par_map_collect(|inp| { - let inp = inp.as_slice().expect("Input array is not contiguous!"); - f(eos, inp) - }); +// /// Evaluate a function for a batch of inputs using the same parameters for each sample. +// #[cfg(feature = "ndarray")] +// pub(crate) fn vectorize(eos: &E, input: ArrayView2, f: F) -> (Array1, Array1) +// where +// E: Sync, +// F: Fn(&E, &[f64]) -> FeosResult + Sync, +// { +// #[cfg(feature = "rayon")] +// let values = Zip::from(input.rows()).par_map_collect(|inp| { +// let inp = inp.as_slice().expect("Input array is not contiguous!"); +// f(eos, inp) +// }); - #[cfg(not(feature = "rayon"))] - let values = Zip::from(input.rows()).map_collect(|inp| { - let inp = inp.as_slice().expect("Input array is not contiguous!"); - f(eos, inp) - }); +// #[cfg(not(feature = "rayon"))] +// let values = Zip::from(input.rows()).map_collect(|inp| { +// let inp = inp.as_slice().expect("Input array is not contiguous!"); +// f(eos, inp) +// }); - let n = input.nrows(); - let status: Array1 = values.iter().map(|r| r.is_ok()).collect(); - let mut value = Array1::from_elem(n, f64::NAN); - for (i, result) in values.into_iter().enumerate() { - if let Ok(v) = result { - value[i] = v; - } - } - (value, status) -} +// let n = input.nrows(); +// let status: Array1 = values.iter().map(|r| r.is_ok()).collect(); +// let mut value = Array1::from_elem(n, f64::NAN); +// for (i, result) in values.into_iter().enumerate() { +// if let Ok(v) = result { +// value[i] = v; +// } +// } +// (value, status) +// } diff --git a/crates/feos-core/src/ad/properties/boiling_temperature.rs b/crates/feos-core/src/ad/properties/boiling_temperature.rs index 4300fe74e..e8943437b 100644 --- a/crates/feos-core/src/ad/properties/boiling_temperature.rs +++ b/crates/feos-core/src/ad/properties/boiling_temperature.rs @@ -1,70 +1,71 @@ +use super::Property; use crate::ad::Gradient; -use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; -use nalgebra::U1; -use ndarray::{Array1, Array2, ArrayView2}; -use num_dual::{DualNum, first_derivative, partial2}; -use quantity::{KELVIN, PASCAL, Pressure, Temperature}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, U1}; +use num_dual::{DualNum, DualStruct, Gradients, first_derivative, partial2}; +use quantity::{_Temperature, KELVIN, PASCAL, Pressure, Temperature}; -pub fn boiling_temperature_ad>, const P: usize>( - eos: &E, - pressure: Pressure, -) -> FeosResult>> { - let eos_f64 = eos.re(); - let (temperature, [vapor_density, liquid_density]) = - PhaseEquilibrium::pure_p(&eos_f64, pressure, None, Default::default())?; +/// Boiling temperature of a pure component as function of pressure. +pub struct BoilingTemperature(pub Pressure); - 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 = E::pure_molefracs(); +impl<'a> From<&'a [f64]> for BoilingTemperature { + fn from(value: &'a [f64]) -> Self { + Self(value[0] * PASCAL) + } +} - let residual_entropy = |v| { - let (a, s) = first_derivative( - partial2( - |t, &v, x| eos.lift().residual_helmholtz_energy(t, v, x), - &v, - &x, - ), - t, - ); - (a, -s) - }; - let (a1, s1) = residual_entropy(v1); - let (a2, s2) = residual_entropy(v2); +impl Property for BoilingTemperature +where + DefaultAllocator: Allocator + Allocator + Allocator, +{ + type Unit = _Temperature; + const REFERENCE: Temperature = KELVIN; - let ln_rho = (v1 / v2).ln(); - (p * (v2 - v1) + (a2 - a1 + t * ln_rho)) / (s2 - s1 - ln_rho) - }; - Ok(Temperature::from_reduced(t)) -} + fn evaluate, D: DualNum + Copy>( + &self, + eos: &E, + ) -> FeosResult> { + let p = Pressure::from_inner(&self.0); + PhaseEquilibrium::pure_p(eos, p, None, Default::default()).map(|(t, _)| t) + } -pub fn boiling_temperature(eos: &E, pressure: Pressure) -> FeosResult { - let (t, _) = PhaseEquilibrium::pure_p(eos, pressure, None, Default::default())?; - Ok(t) -} + fn evaluate_gradient>, const P: usize>( + &self, + eos: &E, + ) -> FeosResult, Self::Unit>> { + let eos_f64 = eos.re(); + let (temperature, [vapor_density, liquid_density]) = + PhaseEquilibrium::pure_p(&eos_f64, self.0, None, Default::default())?; -pub fn boiling_temperature_parallel( - eos: &E, - input: ArrayView2, -) -> (Array1, Array1) { - vectorize(eos, input, |eos, inp| { - boiling_temperature(eos, inp[0] * PASCAL).map(|t| t.convert_into(KELVIN)) - }) -} + let t = temperature.into_reduced(); + let v1 = 1.0 / liquid_density.to_reduced(); + let v2 = 1.0 / vapor_density.to_reduced(); + let p = self.0.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 = E::pure_molefracs(); + + let residual_entropy = |v| { + let (a, s) = first_derivative( + partial2( + |t, &v, x| eos.lift().residual_helmholtz_energy(t, v, x), + &v, + &x, + ), + t, + ); + (a, -s) + }; + let (a1, s1) = residual_entropy(v1); + let (a2, s2) = residual_entropy(v2); -pub fn boiling_temperature_parallel_ad, const P: usize>( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, -) -> (Array1, Array2, Array1) { - vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { - boiling_temperature_ad(eos, inp[0] * PASCAL).map(|t| t.convert_into(KELVIN)) - }) + let ln_rho = (v1 / v2).ln(); + (p * (v2 - v1) + (a2 - a1 + t * ln_rho)) / (s2 - s1 - ln_rho) + }; + Ok(Temperature::from_reduced(t)) + } } diff --git a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs index f0794b07e..7f174fc03 100644 --- a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs @@ -1,95 +1,91 @@ +use super::Property; use crate::Contributions; use crate::ad::Gradient; -use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{Composition, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; -use nalgebra::{SVector, U2}; -use ndarray::{Array1, Array2, ArrayView2}; -use quantity::{KELVIN, PASCAL, Pressure, Temperature}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, U1}; +use num_dual::{DualNum, DualStruct, Gradients}; +use quantity::{_Pressure, KELVIN, PASCAL, Pressure, Temperature}; -pub fn bubble_point_pressure_ad< - E: Residual>, - const P: usize, - X: Composition, ->( - eos: &E, - temperature: Temperature, - pressure: Option, - liquid_molefracs: X, -) -> FeosResult>> { - let eos_f64 = eos.re(); - let (liquid_molefracs, _) = liquid_molefracs.into_molefracs(&eos_f64)?; - let vle = PhaseEquilibrium::bubble_point( - &eos_f64, - temperature, - liquid_molefracs, - pressure, - None, - Default::default(), - )?; +/// Bubble point pressure of a binary mixture as function of temperature and +/// molefracs of the first component. +/// +/// An initial value for the pressure can be passed as optional argument to +/// increase robustness and speed. +pub struct BubblePointPressure(pub Temperature, pub f64, pub Option); - let v_l = 1.0 / vle.liquid().density.to_reduced(); - let v_v = 1.0 / vle.vapor().density.to_reduced(); - let y = &vle.vapor().molefracs; - let y: SVector<_, 2> = SVector::from_fn(|i, _| y[i]); - let t = temperature.into_reduced(); - let (a_l, a_v, v_l, v_v) = { - let t = Gradient::from(t); - let v_l = Gradient::from(v_l); - let v_v = Gradient::from(v_v); - let y = y.map(Gradient::from); - let x = liquid_molefracs.map(Gradient::from); - - let a_v = eos.residual_helmholtz_energy(t, v_v, &y); - let (p_l, mu_res_l, dp_l, dmu_l) = eos.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 = [rho_l[0], rho_l[1]]; - 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)) - / (v_v - v_l); - Ok(Pressure::from_reduced(p)) +impl<'a> From<&'a [f64]> for BubblePointPressure { + fn from(value: &'a [f64]) -> Self { + Self(value[0] * KELVIN, value[1], Some(value[2] * PASCAL)) + } } -pub fn bubble_point_pressure( - eos: &E, - temperature: Temperature, - pressure_init: Option, - liquid_molefrac_1: f64, -) -> FeosResult { - let vle = PhaseEquilibrium::bubble_point( - eos, - temperature, - liquid_molefrac_1, - pressure_init, - None, - Default::default(), - )?; - Ok(vle.vapor().pressure(Contributions::Total)) -} +impl Property for BubblePointPressure +where + DefaultAllocator: Allocator + Allocator + Allocator, + f64: Composition, +{ + type Unit = _Pressure; + const REFERENCE: Pressure = PASCAL; -pub fn bubble_point_pressure_parallel( - eos: &E, - input: ArrayView2, -) -> (Array1, Array1) { - vectorize(eos, input, |eos, inp| { - bubble_point_pressure(eos, inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) - .map(|p| p.convert_into(PASCAL)) - }) -} + fn evaluate, D: DualNum + Copy>( + &self, + eos: &E, + ) -> FeosResult> + where + DefaultAllocator: Allocator + Allocator + Allocator, + { + let t = Temperature::from_inner(&self.0); + let p = Option::from_inner(&self.2); + let (x, _) = self.1.into_molefracs(&eos.re())?; + let x = x.map(D::from); + let vle = PhaseEquilibrium::bubble_point(eos, t, x, p, None, Default::default())?; + Ok(vle.vapor().pressure(Contributions::Total)) + } + + fn evaluate_gradient>, const P: usize>( + &self, + eos: &E, + ) -> FeosResult, Self::Unit>> + where + DefaultAllocator: Allocator + Allocator + Allocator, + { + let eos_f64 = eos.re(); + let (liquid_molefracs, _) = self.1.into_molefracs(&eos_f64)?; + let vle = PhaseEquilibrium::bubble_point( + &eos_f64, + self.0, + &liquid_molefracs, + self.2, + None, + Default::default(), + )?; + + let v_l = 1.0 / vle.liquid().density.to_reduced(); + let v_v = 1.0 / vle.vapor().density.to_reduced(); + let y = &vle.vapor().molefracs; + let t = self.0.into_reduced(); + let (a_l, a_v, v_l, v_v) = { + let t = Gradient::from(t); + let v_l = Gradient::from(v_l); + let v_v = Gradient::from(v_v); + let y = y.map(Gradient::from); + let x = liquid_molefracs.map(Gradient::from); -pub fn bubble_point_pressure_parallel_ad, const P: usize>( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, -) -> (Array1, Array2, Array1) { - vectorize_ad::<_, T, 2, P>(parameter_names, parameters, input, |eos, inp| { - bubble_point_pressure_ad(eos, inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) - .map(|p| p.convert_into(PASCAL)) - }) + let a_v = eos.residual_helmholtz_energy(t, v_v, &y); + let (p_l, mu_res_l, dp_l, dmu_l) = eos.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 = [rho_l[0], rho_l[1]]; + 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)) + / (v_v - v_l); + Ok(Pressure::from_reduced(p)) + } } diff --git a/crates/feos-core/src/ad/properties/dew_point_pressure.rs b/crates/feos-core/src/ad/properties/dew_point_pressure.rs index 333f88799..92e303871 100644 --- a/crates/feos-core/src/ad/properties/dew_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/dew_point_pressure.rs @@ -1,94 +1,90 @@ +use super::Property; use crate::ad::Gradient; -use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{Composition, Contributions, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; -use nalgebra::{SVector, U2}; -use ndarray::{Array1, Array2, ArrayView2}; -use quantity::{KELVIN, PASCAL, Pressure, Temperature}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, U1}; +use num_dual::{DualNum, DualStruct, Gradients}; +use quantity::{_Pressure, KELVIN, PASCAL, Pressure, Temperature}; -pub fn dew_point_pressure_ad< - E: Residual>, - const P: usize, - X: Composition, ->( - eos: &E, - temperature: Temperature, - pressure: Option, - vapor_molefracs: X, -) -> FeosResult>> { - let eos_f64 = eos.re(); - let (vapor_molefracs, _) = vapor_molefracs.into_molefracs(&eos_f64)?; - let vle = PhaseEquilibrium::dew_point( - &eos_f64, - temperature, - vapor_molefracs, - pressure, - None, - Default::default(), - )?; +/// Dew point pressure of a binary mixture as function of temperature and +/// molefracs of the first component. +/// +/// An initial value for the pressure can be passed as optional argument to +/// increase robustness and speed. +pub struct DewPointPressure(pub Temperature, pub f64, pub Option); - let v_l = 1.0 / vle.liquid().density.to_reduced(); - let v_v = 1.0 / vle.vapor().density.to_reduced(); - let x = &vle.liquid().molefracs; - let x: SVector<_, 2> = SVector::from_fn(|i, _| x[i]); - let t = temperature.into_reduced(); - let (a_l, a_v, v_l, v_v) = { - let t = Gradient::from(t); - let v_l = Gradient::from(v_l); - let v_v = Gradient::from(v_v); - let x = x.map(Gradient::from); - let y = vapor_molefracs.map(Gradient::from); - - let a_l = eos.residual_helmholtz_energy(t, v_l, &x); - let (p_v, mu_res_v, dp_v, dmu_v) = eos.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 = [rho_l[0], rho_l[1]]; - 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)) - / (v_l - v_v); - Ok(Pressure::from_reduced(p)) +impl<'a> From<&'a [f64]> for DewPointPressure { + fn from(value: &'a [f64]) -> Self { + Self(value[0] * KELVIN, value[1], Some(value[2] * PASCAL)) + } } -pub fn dew_point_pressure( - eos: &E, - temperature: Temperature, - pressure_init: Option, - vapor_molefrac_1: f64, -) -> FeosResult { - let vle = PhaseEquilibrium::dew_point( - eos, - temperature, - vapor_molefrac_1, - pressure_init, - None, - Default::default(), - )?; - Ok(vle.vapor().pressure(Contributions::Total)) -} +impl Property for DewPointPressure +where + DefaultAllocator: Allocator + Allocator + Allocator, + f64: Composition, +{ + type Unit = _Pressure; + const REFERENCE: Pressure = PASCAL; -pub fn dew_point_pressure_parallel( - eos: &E, - input: ArrayView2, -) -> (Array1, Array1) { - vectorize(eos, input, |eos, inp| { - dew_point_pressure(eos, inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) - .map(|p| p.convert_into(PASCAL)) - }) -} + fn evaluate, D: DualNum + Copy>( + &self, + eos: &E, + ) -> FeosResult> + where + DefaultAllocator: Allocator + Allocator + Allocator, + { + let t = Temperature::from_inner(&self.0); + let p = Option::from_inner(&self.2); + let (y, _) = self.1.into_molefracs(&eos.re())?; + let y = y.map(D::from); + let vle = PhaseEquilibrium::dew_point(eos, t, y, p, None, Default::default())?; + Ok(vle.vapor().pressure(Contributions::Total)) + } + + fn evaluate_gradient>, const P: usize>( + &self, + eos: &E, + ) -> FeosResult, Self::Unit>> + where + DefaultAllocator: Allocator + Allocator + Allocator, + { + let eos_f64 = eos.re(); + let (vapor_molefracs, _) = self.1.into_molefracs(&eos_f64)?; + let vle = PhaseEquilibrium::dew_point( + &eos_f64, + self.0, + &vapor_molefracs, + self.2, + None, + Default::default(), + )?; + + let v_l = 1.0 / vle.liquid().density.to_reduced(); + let v_v = 1.0 / vle.vapor().density.to_reduced(); + let x = &vle.liquid().molefracs; + let t = self.0.into_reduced(); + let (a_l, a_v, v_l, v_v) = { + let t = Gradient::from(t); + let v_l = Gradient::from(v_l); + let v_v = Gradient::from(v_v); + let x = x.map(Gradient::from); + let y = vapor_molefracs.map(Gradient::from); -pub fn dew_point_pressure_parallel_ad, const P: usize>( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, -) -> (Array1, Array2, Array1) { - vectorize_ad::<_, T, 2, P>(parameter_names, parameters, input, |eos, inp| { - dew_point_pressure_ad(eos, inp[0] * KELVIN, Some(inp[2] * PASCAL), inp[1]) - .map(|p| p.convert_into(PASCAL)) - }) + let a_l = eos.residual_helmholtz_energy(t, v_l, &x); + let (p_v, mu_res_v, dp_v, dmu_v) = eos.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 = [rho_l[0], rho_l[1]]; + 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)) + / (v_l - v_v); + Ok(Pressure::from_reduced(p)) + } } diff --git a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs index 64ffdc369..24e5cb367 100644 --- a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs +++ b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs @@ -1,67 +1,34 @@ -use crate::ad::Gradient; -use crate::ad::{ParametersAD, vectorize, vectorize_ad}; -use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; -use nalgebra::U1; -use ndarray::{Array1, Array2, ArrayView2}; -use num_dual::{DualNum, DualStruct, first_derivative, partial2}; -use quantity::{JOULE, KELVIN, MOL, MolarEnergy, Temperature}; +use super::Property; +use crate::{FeosResult, PhaseEquilibrium, Residual}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, U1}; +use num_dual::{DualNum, DualStruct, Gradients}; +use quantity::{_MolarEnergy, KELVIN, MolarEnergy, Temperature}; -pub fn enthalpy_of_vaporization_ad>, const P: usize>( - eos: &E, - temperature: Temperature, -) -> FeosResult>> { - let t = Temperature::from_inner(&temperature); - let (_, [vapor_density, liquid_density]) = - PhaseEquilibrium::pure_t(eos, t, None, Default::default())?; +/// Enthalpy of vaporization of a pure component as function of temperature. +pub struct EnthalpyOfVaporization(pub Temperature); - let v1 = liquid_density.into_reduced().recip(); - let v2 = vapor_density.into_reduced().recip(); - let x = E::pure_molefracs(); - let t = t.into_reduced(); - let residual_entropy = |v| { - let (_a, s) = first_derivative( - partial2( - |t, &v, x| eos.lift().residual_helmholtz_energy(t, v, x), - &v, - &x, - ), - t, - ); - -s - }; - - let s1 = residual_entropy(v1); - let s2 = residual_entropy(v2); - - let dh = t * ((v2 / v1).ln() + s2 - s1); - Ok(MolarEnergy::from_reduced(dh)) +impl<'a> From<&'a [f64]> for EnthalpyOfVaporization { + fn from(value: &'a [f64]) -> Self { + Self(value[0] * KELVIN) + } } -pub fn enthalpy_of_vaporization( - eos: &E, - temperature: Temperature, -) -> FeosResult { - let vle = PhaseEquilibrium::pure(eos, temperature, None, Default::default())?; - let h_v = vle.vapor().residual_molar_enthalpy(); - let h_l = vle.liquid().residual_molar_enthalpy(); - Ok(h_v - h_l) -} - -pub fn enthalpy_of_vaporization_parallel( - eos: &E, - input: ArrayView2, -) -> (Array1, Array1) { - vectorize(eos, input, |eos, inp| { - enthalpy_of_vaporization(eos, inp[0] * KELVIN).map(|dh| dh.convert_into(JOULE / MOL)) - }) -} +impl Property for EnthalpyOfVaporization +where + DefaultAllocator: Allocator + Allocator + Allocator, +{ + type Unit = _MolarEnergy; + const REFERENCE: MolarEnergy = MolarEnergy::new(1.0); -pub fn enthalpy_of_vaporization_parallel_ad, const P: usize>( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, -) -> (Array1, Array2, Array1) { - vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { - enthalpy_of_vaporization_ad(eos, inp[0] * KELVIN).map(|dh| dh.convert_into(JOULE / MOL)) - }) + fn evaluate, D: DualNum + Copy>( + &self, + eos: &E, + ) -> FeosResult> { + let t = Temperature::from_inner(&self.0); + let vle = PhaseEquilibrium::pure(eos, t, None, Default::default())?; + let h_v = vle.vapor().residual_molar_enthalpy(); + let h_l = vle.liquid().residual_molar_enthalpy(); + Ok(h_v - h_l) + } } diff --git a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs index 9697ea4f3..817025789 100644 --- a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs +++ b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs @@ -1,44 +1,31 @@ -use crate::ad::Gradient; -use crate::ad::{ParametersAD, vectorize, vectorize_ad}; +use super::Property; use crate::{FeosResult, PhaseEquilibrium, Residual}; -use nalgebra::U1; -use ndarray::{Array1, Array2, ArrayView2}; -use num_dual::DualStruct; -use quantity::{Density, KELVIN, KILO, METER, MOL, Pressure, Temperature}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, U1}; +use num_dual::{DualNum, DualStruct, Gradients}; +use quantity::{_Density, Density, KELVIN, Temperature}; -pub fn equilibrium_liquid_density_ad>, const P: usize>( - eos: &E, - temperature: Temperature, -) -> FeosResult<(Pressure>, Density>)> { - let t = Temperature::from_inner(&temperature); - PhaseEquilibrium::pure_t(eos, t, None, Default::default()).map(|(p, [_, rho])| (p, rho)) -} +/// Equilibrium liquid density of a pure component as function of temperature. +pub struct EquilibriumLiquidDensity(pub Temperature); -pub fn equilibrium_liquid_density( - eos: &E, - temperature: Temperature, -) -> FeosResult { - let (_, [_, rho]) = PhaseEquilibrium::pure_t(eos, temperature, None, Default::default())?; - Ok(rho) +impl<'a> From<&'a [f64]> for EquilibriumLiquidDensity { + fn from(value: &'a [f64]) -> Self { + Self(value[0] * KELVIN) + } } -pub fn equilibrium_liquid_density_parallel( - eos: &E, - input: ArrayView2, -) -> (Array1, Array1) { - vectorize(eos, input, |eos, inp| { - equilibrium_liquid_density(eos, inp[0] * KELVIN) - .map(|d| d.convert_into(KILO * MOL / (METER * METER * METER))) - }) -} +impl Property for EquilibriumLiquidDensity +where + DefaultAllocator: Allocator + Allocator + Allocator, +{ + type Unit = _Density; + const REFERENCE: Density = Density::new(1000.0); -pub fn equilibrium_liquid_density_parallel_ad, const P: usize>( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, -) -> (Array1, Array2, Array1) { - vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { - equilibrium_liquid_density_ad(eos, inp[0] * KELVIN) - .map(|(_, d)| d.convert_into(KILO * MOL / (METER * METER * METER))) - }) + fn evaluate, D: DualNum + Copy>( + &self, + eos: &E, + ) -> FeosResult> { + let t = Temperature::from_inner(&self.0); + PhaseEquilibrium::pure_t(eos, t, None, Default::default()).map(|(_, [_, r])| r) + } } diff --git a/crates/feos-core/src/ad/properties/liquid_density.rs b/crates/feos-core/src/ad/properties/liquid_density.rs index fceb9c2c6..e0ceffc63 100644 --- a/crates/feos-core/src/ad/properties/liquid_density.rs +++ b/crates/feos-core/src/ad/properties/liquid_density.rs @@ -1,56 +1,35 @@ +use super::Property; use crate::DensityInitialization::Liquid; -use crate::ad::Gradient; -use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::density_iteration::density_iteration; -use crate::{FeosResult, ReferenceSystem, Residual, State}; -use nalgebra::U1; -use ndarray::{Array1, Array2, ArrayView2}; -use num_dual::DualStruct; -use quantity::{Density, KELVIN, KILO, METER, MOL, Moles, PASCAL, Pressure, Temperature}; +use crate::{FeosResult, Residual}; +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, Dim}; +use num_dual::{DualNum, DualStruct}; +use quantity::{_Density, Density, KELVIN, PASCAL, Pressure, Temperature}; -pub fn liquid_density_ad>, const P: usize>( - eos: &E, - temperature: Temperature, - pressure: Pressure, -) -> FeosResult>> { - let x = E::pure_molefracs(); - let t = Temperature::from_inner(&temperature); - let p = Pressure::from_inner(&pressure); - density_iteration(eos, t, p, &x, Some(Liquid)) -} +/// Liquid density of a pure component as function of temperature and pressure. +pub struct LiquidDensity(pub Temperature, pub Pressure); -pub fn liquid_density( - eos: &E, - temperature: Temperature, - pressure: Pressure, -) -> FeosResult { - let state = State::new_npt( - eos, - temperature, - pressure, - &Moles::from_reduced(nalgebra::DVector::from_element(eos.components(), 1.0)), - Some(Liquid), - )?; - Ok(state.density) +impl<'a> From<&'a [f64]> for LiquidDensity { + fn from(value: &'a [f64]) -> Self { + Self(value[0] * KELVIN, value[1] * PASCAL) + } } -pub fn liquid_density_parallel( - eos: &E, - input: ArrayView2, -) -> (Array1, Array1) { - vectorize(eos, input, |eos, inp| { - liquid_density(eos, inp[0] * KELVIN, inp[1] * PASCAL) - .map(|d| d.convert_into(KILO * MOL / (METER * METER * METER))) - }) -} +impl Property for LiquidDensity +where + DefaultAllocator: Allocator, +{ + type Unit = _Density; + const REFERENCE: Density = Density::new(1000.0); -pub fn liquid_density_parallel_ad, const P: usize>( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, -) -> (Array1, Array2, Array1) { - vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { - liquid_density_ad(eos, inp[0] * KELVIN, inp[1] * PASCAL) - .map(|d| d.convert_into(KILO * MOL / (METER * METER * METER))) - }) + fn evaluate, D: DualNum + Copy>( + &self, + eos: &E, + ) -> FeosResult> { + let x = E::pure_molefracs(); + let t = Temperature::from_inner(&self.0); + let p = Pressure::from_inner(&self.1); + density_iteration(eos, t, p, &x, Some(Liquid)) + } } diff --git a/crates/feos-core/src/ad/properties/mod.rs b/crates/feos-core/src/ad/properties/mod.rs index e0e09d796..3120d3ec8 100644 --- a/crates/feos-core/src/ad/properties/mod.rs +++ b/crates/feos-core/src/ad/properties/mod.rs @@ -1,17 +1,140 @@ -pub mod boiling_temperature; -pub mod bubble_point_pressure; -pub mod dew_point_pressure; -pub mod enthalpy_of_vaporization; -pub mod equilibrium_liquid_density; -pub mod liquid_density; -pub mod residual_isobaric_heat_capacity; -pub mod vapor_pressure; - -pub use boiling_temperature::*; -pub use bubble_point_pressure::*; -pub use dew_point_pressure::*; -pub use enthalpy_of_vaporization::*; -pub use equilibrium_liquid_density::*; -pub use liquid_density::*; -pub use residual_isobaric_heat_capacity::*; -pub use vapor_pressure::*; +use super::Gradient; +use crate::{FeosResult, Residual}; +use nalgebra::{DefaultAllocator, Dim, allocator::Allocator}; +#[cfg(feature = "ndarray")] +use ndarray::{Array1, Array2, ArrayView2}; +use num_dual::DualNum; +use quantity::Quantity; + +mod boiling_temperature; +mod bubble_point_pressure; +mod dew_point_pressure; +mod enthalpy_of_vaporization; +mod equilibrium_liquid_density; +mod liquid_density; +mod residual_isobaric_heat_capacity; +mod vapor_pressure; + +pub use boiling_temperature::BoilingTemperature; +pub use bubble_point_pressure::BubblePointPressure; +pub use dew_point_pressure::DewPointPressure; +pub use enthalpy_of_vaporization::EnthalpyOfVaporization; +pub use equilibrium_liquid_density::EquilibriumLiquidDensity; +pub use liquid_density::LiquidDensity; +pub use residual_isobaric_heat_capacity::ResidualIsobaricHeatCapacity; +pub use vapor_pressure::VaporPressure; + +/// Properties that can be rapidly evaluated in parallel together with +/// their gradients with respect to model parameters +pub trait Property: for<'a> From<&'a [f64]> +where + DefaultAllocator: Allocator, +{ + type Unit; + const REFERENCE: Quantity; + + /// Evaluate the property for an arbitrary derivative. + fn evaluate, D: DualNum + Copy>( + &self, + eos: &E, + ) -> FeosResult>; + + /// Evaluate the property for the first derivative w.r.t. model parameters. + /// + /// This can be overridden if there is a more performant implementation than + /// the general implementation in `evaluate`. + fn evaluate_gradient>, const P: usize>( + &self, + eos: &E, + ) -> FeosResult, Self::Unit>> { + self.evaluate(eos) + } + + /// Evaluate the property for all inputs in parallel. + /// + /// Return the property values and the success of the calculations. + #[cfg(feature = "ndarray")] + fn evaluate_parallel + Sync>( + eos: &E, + input: ArrayView2, + ) -> (Array1, Array1) { + #[cfg(feature = "rayon")] + let values = ndarray::Zip::from(input.rows()).par_map_collect(|inp| { + let inp = inp.as_slice().expect("Input array is not contiguous!"); + Self::from(inp) + .evaluate(eos) + .map(|d| d.convert_into(Self::REFERENCE)) + }); + + #[cfg(not(feature = "rayon"))] + let values = ndarray::Zip::from(input.rows()).map_collect(|inp| { + let inp = inp.as_slice().expect("Input array is not contiguous!"); + Self::from(inp) + .evaluate(eos) + .map(|d| d.convert_into(Self::REFERENCE)) + }); + + let n = input.nrows(); + let status: Array1 = values.iter().map(|r| r.is_ok()).collect(); + let mut value = Array1::from_elem(n, f64::NAN); + for (i, result) in values.into_iter().enumerate() { + if let Ok(v) = result { + value[i] = v; + } + } + (value, status) + } + + /// Evaluate the property and its gradients for all inputs in parallel. + /// + /// Return the property values, the gradients, and the success of the calculations. + #[cfg(feature = "ndarray")] + fn evaluate_parallel_ad, const P: usize>( + parameter_names: [String; P], + parameters: ArrayView2, + input: ArrayView2, + ) -> (Array1, Array2, Array1) { + let parameter_names = parameter_names.each_ref().map(|s| s as &str); + + #[cfg(feature = "rayon")] + let value_dual = ndarray::Zip::from(parameters.rows()) + .and(input.rows()) + .par_map_collect(|par, inp| { + let par = par.as_slice().expect("Parameter array is not contiguous!"); + let inp = inp.as_slice().expect("Input array is not contiguous!"); + let eos = E::seed_derivatives(par, parameter_names); + Self::from(inp) + .evaluate_gradient(&eos) + .map(|d| d.convert_into(Self::REFERENCE)) + }); + + #[cfg(not(feature = "rayon"))] + let value_dual = ndarray::Zip::from(parameters.rows()) + .and(input.rows()) + .map_collect(|par, inp| { + let par = par.as_slice().expect("Parameter array is not contiguous!"); + let inp = inp.as_slice().expect("Input array is not contiguous!"); + let eos = E::seed_derivatives(par, parameter_names); + Self::from(inp) + .evaluate_gradient(&eos) + .map(|d| d.convert_into(Self::REFERENCE)) + }); + + let n = parameters.nrows(); + let status = value_dual.iter().map(|p| p.is_ok()).collect(); + let mut value = Array1::from_elem(n, f64::NAN); + let mut grad = Array2::zeros([n, P]); + for (i, result) in value_dual.into_iter().enumerate() { + if let Ok(p_dual) = result { + value[i] = p_dual.re; + let eps = p_dual + .eps + .unwrap_generic(nalgebra::Const::

, nalgebra::U1); + for (g, &e) in grad.row_mut(i).iter_mut().zip(eps.data.0[0].iter()) { + *g = e; + } + } + } + (value, grad, status) + } +} diff --git a/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs index a6c85814d..c7de806ab 100644 --- a/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs +++ b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs @@ -1,60 +1,36 @@ +use super::Property; use crate::DensityInitialization::Liquid; -use crate::ad::Gradient; -use crate::ad::{ParametersAD, vectorize, vectorize_ad}; -use crate::density_iteration::density_iteration; -use crate::{FeosResult, ReferenceSystem, Residual, State}; -use nalgebra::U1; -use ndarray::{Array1, Array2, ArrayView2}; -use num_dual::DualStruct; -use quantity::{JOULE, KELVIN, MOL, MolarEntropy, Moles, PASCAL, Pressure, Temperature}; +use crate::{FeosResult, Residual, State}; +use nalgebra::DefaultAllocator; +use nalgebra::allocator::Allocator; +use num_dual::{DualNum, DualStruct, Gradients}; +use quantity::{_MolarEntropy, KELVIN, MolarEntropy, PASCAL, Pressure, Temperature}; -/// Residual isobaric molar heat capacity of the liquid phase at the given -/// temperature and pressure. -pub fn residual_isobaric_heat_capacity_ad>, const P: usize>( - eos: &E, - temperature: Temperature, - pressure: Pressure, -) -> FeosResult>> { - let x = E::pure_molefracs(); - let t = Temperature::from_inner(&temperature); - let p = Pressure::from_inner(&pressure); - let density = density_iteration(eos, t, p, &x, Some(Liquid))?; - let state = State::new_pure(eos, t, density)?; - Ok(state.residual_molar_isobaric_heat_capacity()) -} +/// Liquid residual isobaric heat capacity of a pure component as function of temperature +/// and pressure. +pub struct ResidualIsobaricHeatCapacity(pub Temperature, pub Pressure); -pub fn residual_isobaric_heat_capacity( - eos: &E, - temperature: Temperature, - pressure: Pressure, -) -> FeosResult { - let state = State::new_npt( - eos, - temperature, - pressure, - &Moles::from_reduced(nalgebra::DVector::from_element(eos.components(), 1.0)), - Some(Liquid), - )?; - Ok(state.residual_molar_isobaric_heat_capacity()) +impl<'a> From<&'a [f64]> for ResidualIsobaricHeatCapacity { + fn from(value: &'a [f64]) -> Self { + Self(value[0] * KELVIN, value[1] * PASCAL) + } } -pub fn residual_isobaric_heat_capacity_parallel( - eos: &E, - input: ArrayView2, -) -> (Array1, Array1) { - vectorize(eos, input, |eos, inp| { - residual_isobaric_heat_capacity(eos, inp[0] * KELVIN, inp[1] * PASCAL) - .map(|cp| cp.convert_into(JOULE / (MOL * KELVIN))) - }) -} +impl Property for ResidualIsobaricHeatCapacity +where + DefaultAllocator: Allocator, +{ + type Unit = _MolarEntropy; + const REFERENCE: MolarEntropy = MolarEntropy::new(1.0); -pub fn residual_isobaric_heat_capacity_parallel_ad, const P: usize>( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, -) -> (Array1, Array2, Array1) { - vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { - residual_isobaric_heat_capacity_ad(eos, inp[0] * KELVIN, inp[1] * PASCAL) - .map(|cp| cp.convert_into(JOULE / (MOL * KELVIN))) - }) + fn evaluate, D: DualNum + Copy>( + &self, + eos: &E, + ) -> FeosResult> { + let x = E::pure_molefracs(); + let t = Temperature::from_inner(&self.0); + let p = Pressure::from_inner(&self.1); + let state = State::new_npt(eos, t, p, x, Some(Liquid))?; + Ok(state.residual_molar_isobaric_heat_capacity()) + } } diff --git a/crates/feos-core/src/ad/properties/vapor_pressure.rs b/crates/feos-core/src/ad/properties/vapor_pressure.rs index 307d79d1b..4f12e4f3c 100644 --- a/crates/feos-core/src/ad/properties/vapor_pressure.rs +++ b/crates/feos-core/src/ad/properties/vapor_pressure.rs @@ -1,60 +1,60 @@ +use super::Property; use crate::ad::Gradient; -use crate::ad::{ParametersAD, vectorize, vectorize_ad}; use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; -use nalgebra::U1; -use ndarray::{Array1, Array2, ArrayView2}; -use quantity::{KELVIN, PASCAL, Pressure, Temperature}; - -pub fn vapor_pressure_ad>, const P: usize>( - eos: &E, - temperature: Temperature, -) -> FeosResult>> { - let eos_f64 = eos.re(); - let (_, [vapor_density, liquid_density]) = - PhaseEquilibrium::pure_t(&eos_f64, temperature, 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 v1 = 1.0 / liquid_density.to_reduced(); - let v2 = 1.0 / vapor_density.to_reduced(); - let t = temperature.into_reduced(); - let (a1, a2) = { - let t = Gradient::from(t); - let v1 = Gradient::from(v1); - let v2 = Gradient::from(v2); - let x = E::pure_molefracs(); - - let a1 = eos.residual_helmholtz_energy(t, v1, &x); - let a2 = eos.residual_helmholtz_energy(t, v2, &x); - (a1, a2) - }; - - let p = -(a1 - a2 + t * (v2 / v1).ln()) / (v1 - v2); - Ok(Pressure::from_reduced(p)) -} +use nalgebra::allocator::Allocator; +use nalgebra::{DefaultAllocator, U1}; +use num_dual::{DualNum, DualStruct, Gradients}; +use quantity::{_Pressure, KELVIN, PASCAL, Pressure, Temperature}; -/// Non-AD vapor pressure for a single-component model. -pub fn vapor_pressure(eos: &E, temperature: Temperature) -> FeosResult { - let (p, _) = PhaseEquilibrium::pure_t(eos, temperature, None, Default::default())?; - Ok(p) -} +/// Vapor pressure of a pure component as function of temperature. +pub struct VaporPressure(pub Temperature); -/// Non-AD batched evaluation over input rows. Single shared model. -pub fn vapor_pressure_parallel( - eos: &E, - input: ArrayView2, -) -> (Array1, Array1) { - vectorize(eos, input, |eos, inp| { - vapor_pressure(eos, inp[0] * KELVIN).map(|p| p.convert_into(PASCAL)) - }) +impl<'a> From<&'a [f64]> for VaporPressure { + fn from(value: &'a [f64]) -> Self { + Self(value[0] * KELVIN) + } } -pub fn vapor_pressure_parallel_ad, const P: usize>( - parameter_names: [String; P], - parameters: ArrayView2, - input: ArrayView2, -) -> (Array1, Array2, Array1) { - vectorize_ad::<_, T, 1, P>(parameter_names, parameters, input, |eos, inp| { - vapor_pressure_ad(eos, inp[0] * KELVIN).map(|p| p.convert_into(PASCAL)) - }) +impl Property for VaporPressure +where + DefaultAllocator: Allocator + Allocator + Allocator, +{ + type Unit = _Pressure; + const REFERENCE: Pressure = PASCAL; + + fn evaluate, D: DualNum + Copy>( + &self, + eos: &E, + ) -> FeosResult> { + let t = Temperature::from_inner(&self.0); + PhaseEquilibrium::pure_t(eos, t, None, Default::default()).map(|(p, _)| p) + } + + fn evaluate_gradient>, const P: usize>( + &self, + eos: &E, + ) -> FeosResult>> { + let eos_f64 = eos.re(); + let (_, [vapor_density, liquid_density]) = + PhaseEquilibrium::pure_t(&eos_f64, self.0, 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 v1 = 1.0 / liquid_density.to_reduced(); + let v2 = 1.0 / vapor_density.to_reduced(); + let t = self.0.into_reduced(); + let (a1, a2) = { + let t = Gradient::from(t); + let v1 = Gradient::from(v1); + let v2 = Gradient::from(v2); + let x = E::pure_molefracs(); + + let a1 = eos.residual_helmholtz_energy(t, v1, &x); + let a2 = eos.residual_helmholtz_energy(t, v2, &x); + (a1, a2) + }; + + let p = -(a1 - a2 + t * (v2 / v1).ln()) / (v1 - v2); + Ok(Pressure::from_reduced(p)) + } } diff --git a/crates/feos-core/src/errors.rs b/crates/feos-core/src/errors.rs index e904e4576..eb7ea4147 100644 --- a/crates/feos-core/src/errors.rs +++ b/crates/feos-core/src/errors.rs @@ -63,6 +63,7 @@ pub enum FeosError { #[cfg(feature = "rayon")] #[error(transparent)] RayonError(#[from] rayon::ThreadPoolBuildError), + #[cfg(feature = "ndarray")] #[error(transparent)] ShapeError(#[from] ndarray::ShapeError), } diff --git a/crates/feos-core/src/lib.rs b/crates/feos-core/src/lib.rs index 1a65ee25b..156894648 100644 --- a/crates/feos-core/src/lib.rs +++ b/crates/feos-core/src/lib.rs @@ -24,7 +24,7 @@ macro_rules! log_result { } } -mod ad; +pub mod ad; pub mod cubic; mod density_iteration; mod equation_of_state; @@ -32,12 +32,12 @@ mod errors; pub mod parameter; mod phase_equilibria; mod state; -pub use ad::{ParametersAD, dataset, properties}; pub use equation_of_state::{ EntropyScaling, EquationOfState, IdealGas, IdealGasAD, Molarweight, NoResidual, Residual, ResidualDyn, Subset, Total, }; pub use errors::{FeosError, FeosResult}; +#[cfg(feature = "ndarray")] pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero}; pub use phase_equilibria::{PhaseEquilibrium, TemperatureOrPressure}; pub use state::{Composition, Contributions, DensityInitialization, State, StateHD, StateVec}; diff --git a/crates/feos-core/src/phase_equilibria/bubble_dew.rs b/crates/feos-core/src/phase_equilibria/bubble_dew.rs index 8f99c729d..05dd42849 100644 --- a/crates/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/crates/feos-core/src/phase_equilibria/bubble_dew.rs @@ -7,6 +7,7 @@ use crate::state::{ use crate::{Composition, ReferenceSystem, Residual, SolverOptions, State, Verbosity}; use nalgebra::allocator::Allocator; use nalgebra::{DMatrix, DVector, DefaultAllocator, Dim, Dyn, OVector, U1}; +#[cfg(feature = "ndarray")] use ndarray::Array1; use num_dual::linalg::LU; use num_dual::{DualNum, DualStruct, Gradients}; @@ -39,6 +40,7 @@ pub trait TemperatureOrPressure + Copy = f64>: Copy { where DefaultAllocator: Allocator; + #[cfg(feature = "ndarray")] fn linspace( &self, start: Self::Other, @@ -73,6 +75,7 @@ impl + Copy> TemperatureOrPressure for Temperature { state.pressure(Contributions::Total) } + #[cfg(feature = "ndarray")] fn linspace( &self, start: Pressure, @@ -117,6 +120,7 @@ impl + Copy> TemperatureOrPressure state.temperature } + #[cfg(feature = "ndarray")] fn linspace( &self, start: Temperature, diff --git a/crates/feos-core/src/phase_equilibria/mod.rs b/crates/feos-core/src/phase_equilibria/mod.rs index 6caeca968..5ed15a767 100644 --- a/crates/feos-core/src/phase_equilibria/mod.rs +++ b/crates/feos-core/src/phase_equilibria/mod.rs @@ -19,13 +19,18 @@ mod tp_flash; mod px_flashes; +#[cfg(feature = "ndarray")] mod phase_diagram_binary; +#[cfg(feature = "ndarray")] mod phase_diagram_pure; +#[cfg(feature = "ndarray")] mod phase_envelope; mod stability_analysis; pub use bubble_dew::TemperatureOrPressure; +#[cfg(feature = "ndarray")] pub use phase_diagram_binary::PhaseDiagramHetero; +#[cfg(feature = "ndarray")] pub use phase_diagram_pure::PhaseDiagram; /// A thermodynamic equilibrium state. diff --git a/crates/feos-core/src/state/statevec.rs b/crates/feos-core/src/state/statevec.rs index 34b41f875..8a498e2db 100644 --- a/crates/feos-core/src/state/statevec.rs +++ b/crates/feos-core/src/state/statevec.rs @@ -1,9 +1,14 @@ +#[cfg(feature = "ndarray")] 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}; +#[cfg(feature = "ndarray")] use quantity::{ Density, MassDensity, MolarEnergy, MolarEntropy, Moles, Pressure, SpecificEnergy, SpecificEntropy, Temperature, @@ -38,6 +43,7 @@ impl<'a, E> Deref for StateVec<'a, E> { } } +#[cfg(feature = "ndarray")] impl StateVec<'_, E> { pub fn temperature(&self) -> Temperature> { Temperature::from_shape_fn(self.0.len(), |i| self.0[i].temperature) @@ -75,6 +81,7 @@ impl StateVec<'_, E> { } } +#[cfg(feature = "ndarray")] impl StateVec<'_, E> { pub fn mass_density(&self) -> MassDensity> { MassDensity::from_shape_fn(self.0.len(), |i| self.0[i].mass_density()) @@ -87,6 +94,7 @@ impl StateVec<'_, E> { } } +#[cfg(feature = "ndarray")] impl StateVec<'_, E> { pub fn molar_enthalpy(&self, contributions: Contributions) -> MolarEnergy> { MolarEnergy::from_shape_fn(self.0.len(), |i| self.0[i].molar_enthalpy(contributions)) @@ -97,6 +105,7 @@ impl StateVec<'_, E> { } } +#[cfg(feature = "ndarray")] impl StateVec<'_, E> { pub fn specific_enthalpy(&self, contributions: Contributions) -> SpecificEnergy> { SpecificEnergy::from_shape_fn(self.0.len(), |i| self.0[i].specific_enthalpy(contributions)) diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index 39de98c58..8d1c03fee 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -598,13 +598,13 @@ mod tests_parameter_fit { use super::*; use approx::assert_relative_eq; use feos_core::DensityInitialization::Liquid; - use feos_core::properties::{ - boiling_temperature_ad, bubble_point_pressure_ad, dew_point_pressure_ad, - equilibrium_liquid_density_ad, liquid_density_ad, vapor_pressure_ad, + use feos_core::ad::{ + BoilingTemperature, BubblePointPressure, DewPointPressure, EquilibriumLiquidDensity, + LiquidDensity, Property, VaporPressure, }; use feos_core::{Contributions, ReferenceSystem, SolverOptions}; - use feos_core::{FeosResult, ParametersAD, PhaseEquilibrium, State}; - use nalgebra::{U1, U3, U8, vector}; + use feos_core::{FeosResult, PhaseEquilibrium, State, ad::ParametersAD}; + use nalgebra::{U1, U3, U8}; use num_dual::{Dual64, DualStruct, DualVec, partial}; use quantity::{BAR, KELVIN, LITER, MOL, PASCAL}; @@ -641,7 +641,7 @@ mod tests_parameter_fit { let (pcsaft, _) = pcsaft()?; let pcsaft_ad = PcSaftPure::::seed_derivatives(&pcsaft.0, pcsaft_params); let temperature = 250.0 * KELVIN; - let p = vapor_pressure_ad(&pcsaft_ad, temperature)?; + let p = VaporPressure(temperature).evaluate(&pcsaft_ad)?; let p = p.convert_into(PASCAL); let (p, grad) = (p.re, p.eps.unwrap_generic(U8, U1)); @@ -674,7 +674,7 @@ mod tests_parameter_fit { let pcsaft_ad = PcSaftPure::::seed_derivatives(&pcsaft.0, ["m", "sigma", "epsilon_k"]); let temperature = 150.0 * KELVIN; - let p = vapor_pressure_ad(&pcsaft_ad, temperature)?; + let p = VaporPressure(temperature).evaluate(&pcsaft_ad)?; let p = p.convert_into(PASCAL); let (p, grad) = (p.re, p.eps.unwrap_generic(U3, U1)); @@ -707,7 +707,7 @@ mod tests_parameter_fit { let pcsaft_ad = PcSaftPure::::seed_derivatives(&pcsaft.0, ["m", "sigma", "epsilon_k"]); let pressure = BAR; - let t = boiling_temperature_ad(&pcsaft_ad, pressure)?; + let t = BoilingTemperature(pressure).evaluate(&pcsaft_ad)?; let t = t.convert_into(KELVIN); let (t, grad) = (t.re, t.eps.unwrap_generic(U3, U1)); @@ -752,14 +752,11 @@ mod tests_parameter_fit { let pcsaft_ad = PcSaftPure::::seed_derivatives(&pcsaft.0, ["m", "sigma", "epsilon_k"]); let temperature = 150.0 * KELVIN; - let (p, rho) = equilibrium_liquid_density_ad(&pcsaft_ad, temperature)?; - let p = p.convert_into(PASCAL); + let rho = EquilibriumLiquidDensity(temperature).evaluate(&pcsaft_ad)?; let rho = rho.convert_into(MOL / LITER); - let (p, p_grad) = (p.re, p.eps.unwrap_generic(U3, U1)); let (rho, rho_grad) = (rho.re, rho.eps.unwrap_generic(U3, U1)); - println!("{p:.5} {rho:.5}"); - println!("{p_grad:.5?}"); + println!("{rho:.5}"); println!("{rho_grad:.5?}"); for (i, par) in ["m", "sigma", "epsilon_k"].into_iter().enumerate() { @@ -767,22 +764,17 @@ mod tests_parameter_fit { let h = params[i] * 1e-7; params[i] += h; let pcsaft_h = PcSaftPure(params); - let (p_h, [_, rho_h]) = + let (_, [_, rho_h]) = PhaseEquilibrium::pure_t(&pcsaft_h, temperature, None, Default::default())?; - let dp_h = (p_h.convert_into(PASCAL) - p) / h; let drho_h = (rho_h.convert_into(MOL / LITER) - rho) / h; - let dp = p_grad[i]; let drho = rho_grad[i]; println!( - "{par:12}: {:11.5} {:11.5} {:.3e} {:11.5} {:11.5} {:.3e}", - dp_h, - dp, - ((dp_h - dp) / dp).abs(), + "{par:12}: {:11.5} {:11.5} {:.3e}", drho_h, drho, ((drho_h - drho) / drho).abs() ); - assert_relative_eq!(dp, dp_h, max_relative = 1e-6); + assert_relative_eq!(drho, drho_h, max_relative = 1e-6); } Ok(()) } @@ -794,7 +786,7 @@ mod tests_parameter_fit { PcSaftPure::::seed_derivatives(&pcsaft.0, ["m", "sigma", "epsilon_k"]); let temperature = 150.0 * KELVIN; let pressure = BAR; - let rho = liquid_density_ad(&pcsaft_ad, temperature, pressure)?; + let rho = LiquidDensity(temperature, pressure).evaluate(&pcsaft_ad)?; let rho = rho.convert_into(MOL / LITER); let (rho, grad) = (rho.re, rho.eps.unwrap_generic(U3, U1)); @@ -826,8 +818,8 @@ mod tests_parameter_fit { let pcsaft_ad = PcSaftBinary::::seed_derivatives(&flat_binary_params(&pcsaft), ["k_ij"]); let temperature = 500.0 * KELVIN; - let x = vector![0.5, 0.5]; - let p = bubble_point_pressure_ad(&pcsaft_ad, temperature, None, x)?; + let x = 0.5; + let p = BubblePointPressure(temperature, x, None).evaluate(&pcsaft_ad)?; let p = p.convert_into(BAR); let (p, [[grad]]) = (p.re, p.eps.unwrap_generic(U1, U1).data.0); @@ -866,7 +858,7 @@ mod tests_parameter_fit { PcSaftBinary::::seed_derivatives(&flat_binary_params(&pcsaft), ["k_ij"]); let temperature = 500.0 * KELVIN; let y = 0.5; - let p = dew_point_pressure_ad(&pcsaft_ad, temperature, None, y)?; + let p = DewPointPressure(temperature, y, None).evaluate(&pcsaft_ad)?; let p = p.convert_into(BAR); let (p, [[grad]]) = (p.re, p.eps.unwrap_generic(U1, U1).data.0); diff --git a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs index f6c83258b..ee850ddc9 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs @@ -1,6 +1,6 @@ use super::dispersion::{A0, A1, A2, B0, B1, B2}; use super::polar::{AD, BD, CD}; -use feos_core::{ParametersAD, Residual, StateHD}; +use feos_core::{Residual, StateHD, ad::ParametersAD}; use nalgebra::{SVector, U2}; use num_dual::{DualNum, DualVec, jacobian}; use std::f64::consts::{FRAC_PI_6, PI}; @@ -37,7 +37,7 @@ impl + Copy, const N: usize> From<&[f64]> for PcSaftBinary } } -impl ParametersAD<2> for PcSaftBinary { +impl ParametersAD for PcSaftBinary { fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D, ) -> PcSaftBinary { @@ -61,7 +61,7 @@ impl ParametersAD<2> for PcSaftBinary { } } -impl ParametersAD<2> for PcSaftBinary { +impl ParametersAD for PcSaftBinary { fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D, ) -> PcSaftBinary { diff --git a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs index d6ad49bac..334fe3416 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs @@ -1,6 +1,6 @@ use super::dispersion::{A0, A1, A2, B0, B1, B2}; use super::polar::{AD, BD, CD}; -use feos_core::{ParametersAD, Residual, StateHD}; +use feos_core::{Residual, StateHD, ad::ParametersAD}; use nalgebra::{SVector, U1}; use num_dual::DualNum; use std::f64::consts::{FRAC_PI_6, PI}; @@ -192,7 +192,7 @@ impl + Copy, const N: usize> From<&[f64]> for PcSaftPure { } } -impl ParametersAD<1> for PcSaftPure { +impl ParametersAD for PcSaftPure { fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D, ) -> PcSaftPure { @@ -205,7 +205,7 @@ impl ParametersAD<1> for PcSaftPure { } } -impl ParametersAD<1> for PcSaftPure { +impl ParametersAD for PcSaftPure { fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D, ) -> PcSaftPure { diff --git a/crates/feos/tests/pcsaft/px_flashes.rs b/crates/feos/tests/pcsaft/px_flashes.rs index 0f5e53bac..3f102a5e5 100644 --- a/crates/feos/tests/pcsaft/px_flashes.rs +++ b/crates/feos/tests/pcsaft/px_flashes.rs @@ -2,8 +2,8 @@ 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, + Contributions, EquationOfState, FeosResult, IdealGasAD, PhaseEquilibrium, ReferenceSystem, + SolverOptions, Verbosity, ad::ParametersAD, }; use nalgebra::U1; use num_dual::{DualStruct, DualVec}; diff --git a/py-feos/src/ad/dataset.rs b/py-feos/src/ad/dataset.rs index 3de5dbd55..27022c58f 100644 --- a/py-feos/src/ad/dataset.rs +++ b/py-feos/src/ad/dataset.rs @@ -1,4 +1,4 @@ -use feos_core::dataset::{ +use feos_core::ad::{ BinaryDataset, BinaryProperty, BubblePointRecord, Dataset, DewPointRecord, EnthalpyOfVaporizationRecord, EquilibriumLiquidDensityRecord, LiquidDensityRecord, PureDataset, PureProperty, ResidualIsobaricHeatCapacityRecord, VaporPressureRecord, diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index 87bfa3c40..c91f6abad 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -1,5 +1,10 @@ use feos::pcsaft::{PcSaftBinary, PcSaftPure}; -use feos_core::ParametersAD; +use feos_core::ad::{ + BoilingTemperature, BubblePointPressure, DewPointPressure, EnthalpyOfVaporization, + EquilibriumLiquidDensity, LiquidDensity, ParametersAD, Property, ResidualIsobaricHeatCapacity, + VaporPressure, +}; +use nalgebra::{U1, U2}; use numpy::{PyArray1, PyArray2, PyReadonlyArray2, ToPyArray}; use paste::paste; use pyo3::prelude::*; @@ -34,216 +39,222 @@ type GradResult<'py> = ( Bound<'py, PyArray1>, ); -/// Calculate vapor pressures 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 temperature (in K) for every data point. -/// -/// Returns -/// ------- -/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The vapor pressures (in Pa), gradients, and convergence status. -#[pyfunction] -pub fn vapor_pressure_derivatives<'py>( - model: PyEquationOfStateAD, - parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, - input: PyReadonlyArray2, -) -> GradResult<'py> { - _vapor_pressure_derivatives(model, parameter_names, parameters, input) -} +#[pyclass(name = "PropertiesAD")] +pub struct PyPropertiesAD; -/// 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) -} +#[pymethods] +impl PyPropertiesAD { + /// Calculate vapor pressures 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 temperature (in K) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The vapor pressures (in Pa), gradients, and convergence status. + #[staticmethod] + pub fn vapor_pressure<'py>( + model: PyEquationOfStateAD, + parameter_names: &Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, + ) -> GradResult<'py> { + _vapor_pressure_derivatives(model, parameter_names, parameters, input) + } -/// Calculate liquid densities 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 temperature (in K) and pressure (in Pa) for every data point. -/// -/// Returns -/// ------- -/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The liquid densities (in kmol/m³), gradients, and convergence status. -#[pyfunction] -pub fn liquid_density_derivatives<'py>( - model: PyEquationOfStateAD, - parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, - input: PyReadonlyArray2, -) -> GradResult<'py> { - _liquid_density_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. + #[staticmethod] + pub fn boiling_temperature<'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 at saturation 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 temperature (in K) for every data point. -/// -/// Returns -/// ------- -/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The liquid densities (in kmol/m³), gradients, and convergence status. -#[pyfunction] -pub fn equilibrium_liquid_density_derivatives<'py>( - model: PyEquationOfStateAD, - parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, - input: PyReadonlyArray2, -) -> GradResult<'py> { - _equilibrium_liquid_density_derivatives(model, parameter_names, parameters, input) -} + /// Calculate liquid densities 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 temperature (in K) and pressure (in Pa) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The liquid densities (in kmol/m³), gradients, and convergence status. + #[staticmethod] + pub fn liquid_density<'py>( + model: PyEquationOfStateAD, + parameter_names: &Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, + ) -> GradResult<'py> { + _liquid_density_derivatives(model, parameter_names, parameters, input) + } -/// Calculate enthalpy of vaporization 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 temperature (in K) for every data point. -/// -/// Returns -/// ------- -/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): -/// The enthalpies of vaporization (in J/mol), gradients, and convergence status. -#[pyfunction] -pub fn enthalpy_of_vaporization_derivatives<'py>( - model: PyEquationOfStateAD, - parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, - input: PyReadonlyArray2, -) -> GradResult<'py> { - _enthalpy_of_vaporization_derivatives(model, parameter_names, parameters, input) -} + /// Calculate liquid densities at saturation 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 temperature (in K) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The liquid densities (in kmol/m³), gradients, and convergence status. + #[staticmethod] + pub fn equilibrium_liquid_density<'py>( + model: PyEquationOfStateAD, + parameter_names: &Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, + ) -> GradResult<'py> { + _equilibrium_liquid_density_derivatives(model, parameter_names, parameters, input) + } -/// Calculate residual isobaric molar heat capacities (liquid phase) 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 temperature (in K) and pressure (in Pa) for every data point. -/// -/// Returns -/// ------- -/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): -/// The residual isobaric heat capacities (in J/(mol·K)), gradients, and convergence status. -#[pyfunction] -pub fn residual_isobaric_heat_capacity_derivatives<'py>( - model: PyEquationOfStateAD, - parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, - input: PyReadonlyArray2, -) -> GradResult<'py> { - _residual_isobaric_heat_capacity_derivatives(model, parameter_names, parameters, input) -} + /// Calculate enthalpy of vaporization 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 temperature (in K) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): + /// The enthalpies of vaporization (in J/mol), gradients, and convergence status. + #[staticmethod] + pub fn enthalpy_of_vaporization<'py>( + model: PyEquationOfStateAD, + parameter_names: &Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, + ) -> GradResult<'py> { + _enthalpy_of_vaporization_derivatives(model, parameter_names, parameters, input) + } -/// Calculate bubble point pressures of binary mixtures 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 temperature (in K), composition of the first component, and an initial guess for the -/// pressure (in Pa) for every data point. -/// -/// Returns -/// ------- -/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The bubble point pressures (in Pa), gradients, and convergence status. -#[pyfunction] -pub fn bubble_point_pressure_derivatives<'py>( - model: PyEquationOfStateAD, - parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, - input: PyReadonlyArray2, -) -> GradResult<'py> { - _bubble_point_pressure_derivatives(model, parameter_names, parameters, input) -} + /// Calculate residual isobaric molar heat capacities (liquid phase) 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 temperature (in K) and pressure (in Pa) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): + /// The residual isobaric heat capacities (in J/(mol·K)), gradients, and convergence status. + #[staticmethod] + pub fn residual_isobaric_heat_capacity<'py>( + model: PyEquationOfStateAD, + parameter_names: &Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, + ) -> GradResult<'py> { + _residual_isobaric_heat_capacity_derivatives(model, parameter_names, parameters, input) + } + + /// Calculate bubble point pressures of binary mixtures 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 temperature (in K), composition of the first component, and an initial guess for the + /// pressure (in Pa) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The bubble point pressures (in Pa), gradients, and convergence status. + #[staticmethod] + pub fn bubble_point_pressure<'py>( + model: PyEquationOfStateAD, + parameter_names: &Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, + ) -> GradResult<'py> { + _bubble_point_pressure_derivatives(model, parameter_names, parameters, input) + } -/// Calculate dew point pressures of binary mixtures 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 temperature (in K), composition of the first component, and an initial guess for the -/// pressure (in Pa) for every data point. -/// -/// Returns -/// ------- -/// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The dew point pressures (in Pa), gradients, and convergence status. -#[pyfunction] -pub fn dew_point_pressure_derivatives<'py>( - model: PyEquationOfStateAD, - parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, - input: PyReadonlyArray2, -) -> GradResult<'py> { - _dew_point_pressure_derivatives(model, parameter_names, parameters, input) + /// Calculate dew point pressures of binary mixtures 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 temperature (in K), composition of the first component, and an initial guess for the + /// pressure (in Pa) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The dew point pressures (in Pa), gradients, and convergence status. + #[staticmethod] + pub fn dew_point_pressure<'py>( + model: PyEquationOfStateAD, + parameter_names: &Bound<'py, PyAny>, + parameters: PyReadonlyArray2, + input: PyReadonlyArray2, + ) -> GradResult<'py> { + _dew_point_pressure_derivatives(model, parameter_names, parameters, input) + } } macro_rules! expand_models { @@ -267,15 +278,14 @@ macro_rules! expand_models { } macro_rules! impl_evaluate_gradients { - (pure, [$($prop:ident),*], $models:tt) => { - $(impl_evaluate_gradients!(1,PyEquationOfStateAD,$prop,$models,0,1,2,3,4,5,max:6);)* + (pure, [$($prop:ident: $prop_type:ty),*], $models:tt) => { + $(impl_evaluate_gradients!(U1,PyEquationOfStateAD,$prop,$prop_type,$models,0,1,2,3,4,5,max:6);)* }; - (binary, [$($prop:ident),*], $models:tt) => { - $(impl_evaluate_gradients!(2,BinaryModels,$prop,$models,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,max:15);)* + (binary, [$($prop:ident: $prop_type:ty),*], $models:tt) => { + $(impl_evaluate_gradients!(U2,BinaryModels,$prop,$prop_type,$models,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,max:15);)* }; - ($n:literal, $enum:ty, $prop:ident, {$($model:ident: $type:ty),*}, $($p:literal,)* max: $max:literal) => { + ($n:ty, $enum:ty, $prop:ident, $prop_type:ty, {$($model:ident: $type:ty),*}, $($p:literal,)* max: $max:literal) => { expand_models!($enum, $prop, $($model: $type),*); - paste!( fn $prop<'py, R: ParametersAD<$n>>( parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, @@ -288,9 +298,9 @@ macro_rules! impl_evaluate_gradients { let (value, grad, status) = $( if let Ok(p) = parameter_names.extract::<[String; $p]>() { - feos_core::properties::[<$prop _parallel_ad>]::(p, parameters.as_array(), input.as_array()) + <$prop_type>::evaluate_parallel_ad::(p, parameters.as_array(), input.as_array()) } else)* if let Ok(p) = parameter_names.extract::<[String; $max]>() { - feos_core::properties::[<$prop _parallel_ad>]::(p, parameters.as_array(), input.as_array()) + <$prop_type>::evaluate_parallel_ad::(p, parameters.as_array(), input.as_array()) } else { panic!("Gradients can only be evaluated for up to {} parameters!", $max) }; @@ -299,18 +309,19 @@ macro_rules! impl_evaluate_gradients { grad.to_pyarray(parameter_names.py()), status.to_pyarray(parameter_names.py()), ) - }); + } }; } +// [vapor_pressure: feos_core::ad::VaporPressure, boiling_temperature: feos_core::ad::BoilingTemperature, liquid_density, equilibrium_liquid_density, enthalpy_of_vaporization, residual_isobaric_heat_capacity], impl_evaluate_gradients!( pure, - [vapor_pressure, boiling_temperature, liquid_density, equilibrium_liquid_density, enthalpy_of_vaporization, residual_isobaric_heat_capacity], + [vapor_pressure: VaporPressure, boiling_temperature: BoilingTemperature, liquid_density: LiquidDensity, equilibrium_liquid_density: EquilibriumLiquidDensity, enthalpy_of_vaporization: EnthalpyOfVaporization, residual_isobaric_heat_capacity: ResidualIsobaricHeatCapacity], {PcSaftNonAssoc: PcSaftPure, PcSaftFull: PcSaftPure} ); impl_evaluate_gradients!( binary, - [bubble_point_pressure, dew_point_pressure], + [bubble_point_pressure: BubblePointPressure, dew_point_pressure: DewPointPressure], {PcSaftNonAssoc: PcSaftBinary, PcSaftFull: PcSaftBinary} ); diff --git a/py-feos/src/lib.rs b/py-feos/src/lib.rs index 179fe526f..50fc51fdb 100644 --- a/py-feos/src/lib.rs +++ b/py-feos/src/lib.rs @@ -182,24 +182,8 @@ fn feos(m: &Bound<'_, PyModule>) -> PyResult<()> { // AD #[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, - m - )?)?; - m.add_function(wrap_pyfunction!( - ad::enthalpy_of_vaporization_derivatives, - m - )?)?; - m.add_function(wrap_pyfunction!( - ad::residual_isobaric_heat_capacity_derivatives, - m - )?)?; - m.add_function(wrap_pyfunction!(ad::bubble_point_pressure_derivatives, m)?)?; - m.add_function(wrap_pyfunction!(ad::dew_point_pressure_derivatives, m)?)?; m.add_class::()?; + m.add_class::()?; // Datasets m.add_class::()?; From 0ff40ef506a512f7bea26b65191d271ccc39991a Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 21 May 2026 14:40:56 +0200 Subject: [PATCH 06/15] streamline properties, no changes to datasets --- crates/feos-core/src/ad/properties/boiling_temperature.rs | 4 ++-- crates/feos-core/src/ad/properties/bubble_point_pressure.rs | 4 ++-- crates/feos-core/src/ad/properties/dew_point_pressure.rs | 4 ++-- .../feos-core/src/ad/properties/enthalpy_of_vaporization.rs | 4 ++-- .../feos-core/src/ad/properties/equilibrium_liquid_density.rs | 4 ++-- crates/feos-core/src/ad/properties/liquid_density.rs | 4 ++-- crates/feos-core/src/ad/properties/mod.rs | 2 +- .../src/ad/properties/residual_isobaric_heat_capacity.rs | 4 ++-- crates/feos-core/src/ad/properties/vapor_pressure.rs | 4 ++-- py-feos/Cargo.toml | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/feos-core/src/ad/properties/boiling_temperature.rs b/crates/feos-core/src/ad/properties/boiling_temperature.rs index e8943437b..c597fb30e 100644 --- a/crates/feos-core/src/ad/properties/boiling_temperature.rs +++ b/crates/feos-core/src/ad/properties/boiling_temperature.rs @@ -1,4 +1,4 @@ -use super::Property; +use super::PropertyAD; use crate::ad::Gradient; use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::allocator::Allocator; @@ -15,7 +15,7 @@ impl<'a> From<&'a [f64]> for BoilingTemperature { } } -impl Property for BoilingTemperature +impl PropertyAD for BoilingTemperature where DefaultAllocator: Allocator + Allocator + Allocator, { diff --git a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs index 7f174fc03..9e838b9f7 100644 --- a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs @@ -1,4 +1,4 @@ -use super::Property; +use super::PropertyAD; use crate::Contributions; use crate::ad::Gradient; use crate::{Composition, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; @@ -20,7 +20,7 @@ impl<'a> From<&'a [f64]> for BubblePointPressure { } } -impl Property for BubblePointPressure +impl PropertyAD for BubblePointPressure where DefaultAllocator: Allocator + Allocator + Allocator, f64: Composition, diff --git a/crates/feos-core/src/ad/properties/dew_point_pressure.rs b/crates/feos-core/src/ad/properties/dew_point_pressure.rs index 92e303871..4ce733484 100644 --- a/crates/feos-core/src/ad/properties/dew_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/dew_point_pressure.rs @@ -1,4 +1,4 @@ -use super::Property; +use super::PropertyAD; use crate::ad::Gradient; use crate::{Composition, Contributions, FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::allocator::Allocator; @@ -19,7 +19,7 @@ impl<'a> From<&'a [f64]> for DewPointPressure { } } -impl Property for DewPointPressure +impl PropertyAD for DewPointPressure where DefaultAllocator: Allocator + Allocator + Allocator, f64: Composition, diff --git a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs index 24e5cb367..ee0d3d3aa 100644 --- a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs +++ b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs @@ -1,4 +1,4 @@ -use super::Property; +use super::PropertyAD; use crate::{FeosResult, PhaseEquilibrium, Residual}; use nalgebra::allocator::Allocator; use nalgebra::{DefaultAllocator, U1}; @@ -14,7 +14,7 @@ impl<'a> From<&'a [f64]> for EnthalpyOfVaporization { } } -impl Property for EnthalpyOfVaporization +impl PropertyAD for EnthalpyOfVaporization where DefaultAllocator: Allocator + Allocator + Allocator, { diff --git a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs index 817025789..85ded0885 100644 --- a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs +++ b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs @@ -1,4 +1,4 @@ -use super::Property; +use super::PropertyAD; use crate::{FeosResult, PhaseEquilibrium, Residual}; use nalgebra::allocator::Allocator; use nalgebra::{DefaultAllocator, U1}; @@ -14,7 +14,7 @@ impl<'a> From<&'a [f64]> for EquilibriumLiquidDensity { } } -impl Property for EquilibriumLiquidDensity +impl PropertyAD for EquilibriumLiquidDensity where DefaultAllocator: Allocator + Allocator + Allocator, { diff --git a/crates/feos-core/src/ad/properties/liquid_density.rs b/crates/feos-core/src/ad/properties/liquid_density.rs index e0ceffc63..f8992c3fe 100644 --- a/crates/feos-core/src/ad/properties/liquid_density.rs +++ b/crates/feos-core/src/ad/properties/liquid_density.rs @@ -1,4 +1,4 @@ -use super::Property; +use super::PropertyAD; use crate::DensityInitialization::Liquid; use crate::density_iteration::density_iteration; use crate::{FeosResult, Residual}; @@ -16,7 +16,7 @@ impl<'a> From<&'a [f64]> for LiquidDensity { } } -impl Property for LiquidDensity +impl PropertyAD for LiquidDensity where DefaultAllocator: Allocator, { diff --git a/crates/feos-core/src/ad/properties/mod.rs b/crates/feos-core/src/ad/properties/mod.rs index 3120d3ec8..37f9325f5 100644 --- a/crates/feos-core/src/ad/properties/mod.rs +++ b/crates/feos-core/src/ad/properties/mod.rs @@ -26,7 +26,7 @@ pub use vapor_pressure::VaporPressure; /// Properties that can be rapidly evaluated in parallel together with /// their gradients with respect to model parameters -pub trait Property: for<'a> From<&'a [f64]> +pub trait PropertyAD: for<'a> From<&'a [f64]> where DefaultAllocator: Allocator, { diff --git a/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs index c7de806ab..b8f962757 100644 --- a/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs +++ b/crates/feos-core/src/ad/properties/residual_isobaric_heat_capacity.rs @@ -1,4 +1,4 @@ -use super::Property; +use super::PropertyAD; use crate::DensityInitialization::Liquid; use crate::{FeosResult, Residual, State}; use nalgebra::DefaultAllocator; @@ -16,7 +16,7 @@ impl<'a> From<&'a [f64]> for ResidualIsobaricHeatCapacity { } } -impl Property for ResidualIsobaricHeatCapacity +impl PropertyAD for ResidualIsobaricHeatCapacity where DefaultAllocator: Allocator, { diff --git a/crates/feos-core/src/ad/properties/vapor_pressure.rs b/crates/feos-core/src/ad/properties/vapor_pressure.rs index 4f12e4f3c..8192d3ff0 100644 --- a/crates/feos-core/src/ad/properties/vapor_pressure.rs +++ b/crates/feos-core/src/ad/properties/vapor_pressure.rs @@ -1,4 +1,4 @@ -use super::Property; +use super::PropertyAD; use crate::ad::Gradient; use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem, Residual}; use nalgebra::allocator::Allocator; @@ -15,7 +15,7 @@ impl<'a> From<&'a [f64]> for VaporPressure { } } -impl Property for VaporPressure +impl PropertyAD for VaporPressure where DefaultAllocator: Allocator + Allocator + Allocator, { diff --git a/py-feos/Cargo.toml b/py-feos/Cargo.toml index 3395514f6..c06369b72 100644 --- a/py-feos/Cargo.toml +++ b/py-feos/Cargo.toml @@ -38,7 +38,7 @@ itertools = { workspace = true } paste = { workspace = true } feos = { workspace = true } -feos-core = { workspace = true } +feos-core = { workspace = true, features = ["ndarray"] } feos-derive = { workspace = true } feos-dft = { workspace = true, optional = true } From 2dfe1bf97e1201bc2af85aa9814dd3e2717b6cd0 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 21 May 2026 14:45:41 +0200 Subject: [PATCH 07/15] fixed symbol rename --- crates/feos-core/src/ad/dataset/binary.rs | 2 +- crates/feos/src/pcsaft/eos/mod.rs | 2 +- py-feos/Cargo.toml | 2 +- py-feos/src/ad/mod.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/feos-core/src/ad/dataset/binary.rs b/crates/feos-core/src/ad/dataset/binary.rs index 1e4048277..9ac846b3d 100644 --- a/crates/feos-core/src/ad/dataset/binary.rs +++ b/crates/feos-core/src/ad/dataset/binary.rs @@ -5,7 +5,7 @@ use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; use serde::{Deserialize, Serialize}; use crate::Residual; -use crate::ad::properties::{BubblePointPressure, DewPointPressure, Property}; +use crate::ad::properties::{BubblePointPressure, DewPointPressure, PropertyAD}; use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage, ParametersAD}; diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index 8d1c03fee..6a68f67f7 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -600,7 +600,7 @@ mod tests_parameter_fit { use feos_core::DensityInitialization::Liquid; use feos_core::ad::{ BoilingTemperature, BubblePointPressure, DewPointPressure, EquilibriumLiquidDensity, - LiquidDensity, Property, VaporPressure, + LiquidDensity, PropertyAD, VaporPressure, }; use feos_core::{Contributions, ReferenceSystem, SolverOptions}; use feos_core::{FeosResult, PhaseEquilibrium, State, ad::ParametersAD}; diff --git a/py-feos/Cargo.toml b/py-feos/Cargo.toml index c06369b72..390f7deea 100644 --- a/py-feos/Cargo.toml +++ b/py-feos/Cargo.toml @@ -43,7 +43,7 @@ feos-derive = { workspace = true } feos-dft = { workspace = true, optional = true } [features] -default = [] +default = ["ad", "pcsaft"] dft = ["feos/dft", "feos-dft", "petgraph", "rayon"] pcsaft = ["feos/pcsaft"] epcsaft = ["feos/epcsaft"] diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index c91f6abad..696085550 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -1,8 +1,8 @@ use feos::pcsaft::{PcSaftBinary, PcSaftPure}; use feos_core::ad::{ BoilingTemperature, BubblePointPressure, DewPointPressure, EnthalpyOfVaporization, - EquilibriumLiquidDensity, LiquidDensity, ParametersAD, Property, ResidualIsobaricHeatCapacity, - VaporPressure, + EquilibriumLiquidDensity, LiquidDensity, ParametersAD, PropertyAD, + ResidualIsobaricHeatCapacity, VaporPressure, }; use nalgebra::{U1, U2}; use numpy::{PyArray1, PyArray2, PyReadonlyArray2, ToPyArray}; From dc7b42a1032b5a5108794c2656c92656469ee68e Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 21 May 2026 14:46:20 +0200 Subject: [PATCH 08/15] Fix default features --- py-feos/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py-feos/Cargo.toml b/py-feos/Cargo.toml index 390f7deea..c06369b72 100644 --- a/py-feos/Cargo.toml +++ b/py-feos/Cargo.toml @@ -43,7 +43,7 @@ feos-derive = { workspace = true } feos-dft = { workspace = true, optional = true } [features] -default = ["ad", "pcsaft"] +default = [] dft = ["feos/dft", "feos-dft", "petgraph", "rayon"] pcsaft = ["feos/pcsaft"] epcsaft = ["feos/epcsaft"] From 61ea3dfee4b81baea3077725ed8c64d1059618e2 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 21 May 2026 15:52:00 +0200 Subject: [PATCH 09/15] Add parallel evaluations of properties without derivatives --- py-feos/src/ad/mod.rs | 203 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 193 insertions(+), 10 deletions(-) diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index 696085550..5ba5548c7 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -1,3 +1,4 @@ +use crate::eos::PyEquationOfState; use feos::pcsaft::{PcSaftBinary, PcSaftPure}; use feos_core::ad::{ BoilingTemperature, BubblePointPressure, DewPointPressure, EnthalpyOfVaporization, @@ -33,17 +34,41 @@ impl From for BinaryModels { } } +type EvalResult<'py> = (Bound<'py, PyArray1>, Bound<'py, PyArray1>); + type GradResult<'py> = ( Bound<'py, PyArray1>, Bound<'py, PyArray2>, Bound<'py, PyArray1>, ); -#[pyclass(name = "PropertiesAD")] +#[pyclass(name = "Properties")] pub struct PyPropertiesAD; #[pymethods] impl PyPropertiesAD { + /// Calculate vapor pressures in parallel. + /// + /// Parameters + /// ---------- + /// eos: EquationOfState + /// The equation of state to use. + /// input: np.ndarray[float] + /// The temperature (in K) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[bool]): The vapor pressures (in Pa), and convergence status. + #[staticmethod] + pub fn vapor_pressure<'py>( + py: Python<'py>, + eos: &PyEquationOfState, + input: PyReadonlyArray2, + ) -> EvalResult<'py> { + let (value, status) = VaporPressure::evaluate_parallel(&eos.0, input.as_array()); + (value.to_pyarray(py), status.to_pyarray(py)) + } + /// Calculate vapor pressures and derivatives w.r.t. model parameters. /// /// Parameters @@ -61,7 +86,7 @@ impl PyPropertiesAD { /// ------- /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The vapor pressures (in Pa), gradients, and convergence status. #[staticmethod] - pub fn vapor_pressure<'py>( + pub fn vapor_pressure_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, @@ -70,6 +95,28 @@ impl PyPropertiesAD { _vapor_pressure_derivatives(model, parameter_names, parameters, input) } + /// Calculate boiling temperatures in parallel. + /// + /// Parameters + /// ---------- + /// eos: EquationOfState + /// The equation of state to use. + /// input: np.ndarray[float] + /// The pressure (in Pa) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[bool]): The boiling temperature (in K), and convergence status. + #[staticmethod] + pub fn boiling_temperature<'py>( + py: Python<'py>, + eos: &PyEquationOfState, + input: PyReadonlyArray2, + ) -> EvalResult<'py> { + let (value, status) = BoilingTemperature::evaluate_parallel(&eos.0, input.as_array()); + (value.to_pyarray(py), status.to_pyarray(py)) + } + /// Calculate boiling temperatures and derivatives w.r.t. model parameters. /// /// Parameters @@ -87,7 +134,7 @@ impl PyPropertiesAD { /// ------- /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The boiling temperature (in K), gradients, and convergence status. #[staticmethod] - pub fn boiling_temperature<'py>( + pub fn boiling_temperature_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, @@ -96,6 +143,28 @@ impl PyPropertiesAD { _boiling_temperature_derivatives(model, parameter_names, parameters, input) } + /// Calculate liquid densities in parallel. + /// + /// Parameters + /// ---------- + /// eos: EquationOfState + /// The equation of state to use. + /// input: np.ndarray[float] + /// The temperature (in K) and pressure (in Pa) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[bool]): The liquid densities (in kmol/m³), and convergence status. + #[staticmethod] + pub fn liquid_density<'py>( + py: Python<'py>, + eos: &PyEquationOfState, + input: PyReadonlyArray2, + ) -> EvalResult<'py> { + let (value, status) = LiquidDensity::evaluate_parallel(&eos.0, input.as_array()); + (value.to_pyarray(py), status.to_pyarray(py)) + } + /// Calculate liquid densities and derivatives w.r.t. model parameters. /// /// Parameters @@ -113,7 +182,7 @@ impl PyPropertiesAD { /// ------- /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The liquid densities (in kmol/m³), gradients, and convergence status. #[staticmethod] - pub fn liquid_density<'py>( + pub fn liquid_density_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, @@ -122,6 +191,28 @@ impl PyPropertiesAD { _liquid_density_derivatives(model, parameter_names, parameters, input) } + /// Calculate liquid densities at saturation in parallel. + /// + /// Parameters + /// ---------- + /// eos: EquationOfState + /// The equation of state to use. + /// input: np.ndarray[float] + /// The temperature (in K) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[bool]): The liquid densities (in kmol/m³), and convergence status. + #[staticmethod] + pub fn equilibrium_liquid_density<'py>( + py: Python<'py>, + eos: &PyEquationOfState, + input: PyReadonlyArray2, + ) -> EvalResult<'py> { + let (value, status) = EquilibriumLiquidDensity::evaluate_parallel(&eos.0, input.as_array()); + (value.to_pyarray(py), status.to_pyarray(py)) + } + /// Calculate liquid densities at saturation and derivatives w.r.t. model parameters. /// /// Parameters @@ -139,7 +230,7 @@ impl PyPropertiesAD { /// ------- /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The liquid densities (in kmol/m³), gradients, and convergence status. #[staticmethod] - pub fn equilibrium_liquid_density<'py>( + pub fn equilibrium_liquid_density_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, @@ -148,6 +239,29 @@ impl PyPropertiesAD { _equilibrium_liquid_density_derivatives(model, parameter_names, parameters, input) } + /// Calculate enthalpy of vaporization in parallel. + /// + /// Parameters + /// ---------- + /// eos: EquationOfState + /// The equation of state to use. + /// input: np.ndarray[float] + /// The temperature (in K) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[bool]): + /// The enthalpies of vaporization (in J/mol), and convergence status. + #[staticmethod] + pub fn enthalpy_of_vaporization<'py>( + py: Python<'py>, + eos: &PyEquationOfState, + input: PyReadonlyArray2, + ) -> EvalResult<'py> { + let (value, status) = EnthalpyOfVaporization::evaluate_parallel(&eos.0, input.as_array()); + (value.to_pyarray(py), status.to_pyarray(py)) + } + /// Calculate enthalpy of vaporization and derivatives w.r.t. model parameters. /// /// Parameters @@ -166,7 +280,7 @@ impl PyPropertiesAD { /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): /// The enthalpies of vaporization (in J/mol), gradients, and convergence status. #[staticmethod] - pub fn enthalpy_of_vaporization<'py>( + pub fn enthalpy_of_vaporization_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, @@ -175,6 +289,30 @@ impl PyPropertiesAD { _enthalpy_of_vaporization_derivatives(model, parameter_names, parameters, input) } + /// Calculate residual isobaric molar heat capacities (liquid phase) in parallel. + /// + /// Parameters + /// ---------- + /// eos: EquationOfState + /// The equation of state to use. + /// input: np.ndarray[float] + /// The temperature (in K) and pressure (in Pa) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[bool]): + /// The residual isobaric heat capacities (in J/(mol·K)), and convergence status. + #[staticmethod] + pub fn residual_isobaric_heat_capacity<'py>( + py: Python<'py>, + eos: &PyEquationOfState, + input: PyReadonlyArray2, + ) -> EvalResult<'py> { + let (value, status) = + ResidualIsobaricHeatCapacity::evaluate_parallel(&eos.0, input.as_array()); + (value.to_pyarray(py), status.to_pyarray(py)) + } + /// Calculate residual isobaric molar heat capacities (liquid phase) and derivatives w.r.t. model parameters. /// /// Parameters @@ -193,7 +331,7 @@ impl PyPropertiesAD { /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): /// The residual isobaric heat capacities (in J/(mol·K)), gradients, and convergence status. #[staticmethod] - pub fn residual_isobaric_heat_capacity<'py>( + pub fn residual_isobaric_heat_capacity_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, @@ -202,6 +340,29 @@ impl PyPropertiesAD { _residual_isobaric_heat_capacity_derivatives(model, parameter_names, parameters, input) } + /// Calculate bubble point pressures of binary mixtures in parallel. + /// + /// Parameters + /// ---------- + /// eos: EquationOfState + /// The equation of state to use. + /// input: np.ndarray[float] + /// The temperature (in K), composition of the first component, and an initial guess for the + /// pressure (in Pa) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[bool]): The bubble point pressures (in Pa), and convergence status. + #[staticmethod] + pub fn bubble_point_pressure<'py>( + py: Python<'py>, + eos: &PyEquationOfState, + input: PyReadonlyArray2, + ) -> EvalResult<'py> { + let (value, status) = BubblePointPressure::evaluate_parallel(&eos.0, input.as_array()); + (value.to_pyarray(py), status.to_pyarray(py)) + } + /// Calculate bubble point pressures of binary mixtures and derivatives w.r.t. model parameters. /// /// Parameters @@ -220,7 +381,7 @@ impl PyPropertiesAD { /// ------- /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The bubble point pressures (in Pa), gradients, and convergence status. #[staticmethod] - pub fn bubble_point_pressure<'py>( + pub fn bubble_point_pressure_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, @@ -229,6 +390,29 @@ impl PyPropertiesAD { _bubble_point_pressure_derivatives(model, parameter_names, parameters, input) } + /// Calculate dew point pressures of binary mixtures in parallel. + /// + /// Parameters + /// ---------- + /// eos: EquationOfState + /// The equation of state to use. + /// input: np.ndarray[float] + /// The temperature (in K), composition of the first component, and an initial guess for the + /// pressure (in Pa) for every data point. + /// + /// Returns + /// ------- + /// (np.ndarray[float], np.ndarray[bool]): The dew point pressures (in Pa), and convergence status. + #[staticmethod] + pub fn dew_point_pressure<'py>( + py: Python<'py>, + eos: &PyEquationOfState, + input: PyReadonlyArray2, + ) -> EvalResult<'py> { + let (value, status) = DewPointPressure::evaluate_parallel(&eos.0, input.as_array()); + (value.to_pyarray(py), status.to_pyarray(py)) + } + /// Calculate dew point pressures of binary mixtures and derivatives w.r.t. model parameters. /// /// Parameters @@ -247,7 +431,7 @@ impl PyPropertiesAD { /// ------- /// (np.ndarray[float], np.ndarray[float], np.ndarray[bool]): The dew point pressures (in Pa), gradients, and convergence status. #[staticmethod] - pub fn dew_point_pressure<'py>( + pub fn dew_point_pressure_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, parameters: PyReadonlyArray2, @@ -313,7 +497,6 @@ macro_rules! impl_evaluate_gradients { }; } -// [vapor_pressure: feos_core::ad::VaporPressure, boiling_temperature: feos_core::ad::BoilingTemperature, liquid_density, equilibrium_liquid_density, enthalpy_of_vaporization, residual_isobaric_heat_capacity], impl_evaluate_gradients!( pure, [vapor_pressure: VaporPressure, boiling_temperature: BoilingTemperature, liquid_density: LiquidDensity, equilibrium_liquid_density: EquilibriumLiquidDensity, enthalpy_of_vaporization: EnthalpyOfVaporization, residual_isobaric_heat_capacity: ResidualIsobaricHeatCapacity], From 1e580c041610e364d1b4a2cb152a6e59417c3a7c Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 21 May 2026 17:40:36 +0200 Subject: [PATCH 10/15] add evaluate function with constant parameters for parameter fits --- crates/feos-core/src/ad/dataset/binary.rs | 17 ++++--- crates/feos-core/src/ad/dataset/mod.rs | 19 ++++---- crates/feos-core/src/ad/dataset/pure.rs | 16 ++++--- crates/feos-core/src/ad/properties/mod.rs | 51 ++++++++++++++++++++- crates/feos-core/src/density_iteration.rs | 5 +-- crates/feos-derive/src/dft.rs | 2 +- py-feos/src/ad/mod.rs | 54 ++++++++++++++--------- py-feos/src/lib.rs | 2 +- py-feos/src/user_defined.rs | 26 ++++++++--- 9 files changed, 141 insertions(+), 51 deletions(-) diff --git a/crates/feos-core/src/ad/dataset/binary.rs b/crates/feos-core/src/ad/dataset/binary.rs index 9ac846b3d..2bf797f6e 100644 --- a/crates/feos-core/src/ad/dataset/binary.rs +++ b/crates/feos-core/src/ad/dataset/binary.rs @@ -5,6 +5,7 @@ use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; use serde::{Deserialize, Serialize}; use crate::Residual; +use crate::ad::Gradient; use crate::ad::properties::{BubblePointPressure, DewPointPressure, PropertyAD}; use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage, ParametersAD}; @@ -96,11 +97,14 @@ macro_rules! binary_properties { fn evaluate_ad, const P: usize>( self, names: [String; P], - parameters: ArrayView2, + parameters: &[f64], inputs: ArrayView2, - ) -> (Array1, Array2, Array1) { + ) -> (Array1, Array2, Array1) + where + T::Lifted>: Sync, + { match self { - $(Self::$variant => <$prop>::evaluate_parallel_ad::(names, parameters, inputs),)* + $(Self::$variant => <$prop>::evaluate_parallel_derivatives::(names, parameters, inputs),)* } } @@ -231,9 +235,12 @@ impl DatasetAD<2> for BinaryDataset { fn evaluate_ad_const, const P: usize>( &self, names: [String; P], - parameters: ArrayView2, + parameters: &[f64], inputs: ArrayView2, - ) -> (Array1, Array2, Array1) { + ) -> (Array1, Array2, Array1) + where + T::Lifted>: Sync, + { self.property.evaluate_ad::(names, parameters, inputs) } } diff --git a/crates/feos-core/src/ad/dataset/mod.rs b/crates/feos-core/src/ad/dataset/mod.rs index 6ed94f241..7be48a6fb 100644 --- a/crates/feos-core/src/ad/dataset/mod.rs +++ b/crates/feos-core/src/ad/dataset/mod.rs @@ -8,6 +8,7 @@ use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; use serde::de::DeserializeOwned; use crate::Residual; +use crate::ad::Gradient; use super::ParametersAD; @@ -124,9 +125,11 @@ macro_rules! define_dataset_ad { fn evaluate_ad_const>, const P: usize>( &self, names: [String; P], - parameters: ArrayView2, + parameters: &[f64], inputs: ArrayView2, - ) -> (Array1, Array2, Array1); + ) -> (Array1, Array2, Array1) + where + T::Lifted>: Sync; /// Evaluate the property and its parameter gradients at the given parameters. /// @@ -137,11 +140,11 @@ macro_rules! define_dataset_ad { fn evaluate_ad>>( &self, param_names: &[String], - params: &[f64], - ) -> (Array1, Array2, Array1) { - let n = self.inputs().nrows(); - let parameters = Array2::from_shape_fn((n, params.len()), |(_, j)| params[j]); - + parameters: &[f64], + ) -> (Array1, Array2, Array1) + where + $(T::Lifted>: Sync,)* + { fn to_const(names: &[String]) -> [String; P] { names.to_vec().try_into().expect("parameter count mismatch") } @@ -150,7 +153,7 @@ macro_rules! define_dataset_ad { $( $p => self.evaluate_ad_const::( to_const(param_names), - parameters.view(), + parameters, self.inputs().view(), ), )+ diff --git a/crates/feos-core/src/ad/dataset/pure.rs b/crates/feos-core/src/ad/dataset/pure.rs index f961351d6..4a7e74aa8 100644 --- a/crates/feos-core/src/ad/dataset/pure.rs +++ b/crates/feos-core/src/ad/dataset/pure.rs @@ -5,6 +5,7 @@ use ndarray::{Array1, Array2, ArrayView1, ArrayView2}; use serde::{Deserialize, Serialize}; use crate::Residual; +use crate::ad::Gradient; use crate::ad::properties::*; use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage, ParametersAD}; @@ -150,11 +151,13 @@ macro_rules! pure_properties { fn evaluate_ad, const P: usize>( self, names: [String; P], - parameters: ArrayView2, + parameters: &[f64], inputs: ArrayView2, - ) -> (Array1, Array2, Array1) { + ) -> (Array1, Array2, Array1) + where T::Lifted>: Sync + { match self { - $(Self::$variant => <$prop>::evaluate_parallel_ad::(names, parameters, inputs),)* + $(Self::$variant => <$prop>::evaluate_parallel_derivatives::(names, parameters, inputs),)* } } @@ -307,9 +310,12 @@ impl DatasetAD<1> for PureDataset { fn evaluate_ad_const, const P: usize>( &self, names: [String; P], - parameters: ArrayView2, + parameters: &[f64], inputs: ArrayView2, - ) -> (Array1, Array2, Array1) { + ) -> (Array1, Array2, Array1) + where + T::Lifted>: Sync, + { self.property.evaluate_ad::(names, parameters, inputs) } } diff --git a/crates/feos-core/src/ad/properties/mod.rs b/crates/feos-core/src/ad/properties/mod.rs index 37f9325f5..660716e1d 100644 --- a/crates/feos-core/src/ad/properties/mod.rs +++ b/crates/feos-core/src/ad/properties/mod.rs @@ -89,7 +89,56 @@ where /// /// Return the property values, the gradients, and the success of the calculations. #[cfg(feature = "ndarray")] - fn evaluate_parallel_ad, const P: usize>( + fn evaluate_parallel_derivatives, const P: usize>( + parameter_names: [String; P], + parameters: &[f64], + input: ArrayView2, + ) -> (Array1, Array2, Array1) + where + E::Lifted>: Sync, + { + let parameter_names = parameter_names.each_ref().map(|s| s as &str); + let eos = E::seed_derivatives(parameters, parameter_names); + + #[cfg(feature = "rayon")] + let value_dual = ndarray::Zip::from(input.rows()).par_map_collect(|inp| { + let inp = inp.as_slice().expect("Input array is not contiguous!"); + Self::from(inp) + .evaluate_gradient(&eos) + .map(|d| d.convert_into(Self::REFERENCE)) + }); + + #[cfg(not(feature = "rayon"))] + let value_dual = ndarray::Zip::from(input.rows()).map_collect(|inp| { + let inp = inp.as_slice().expect("Input array is not contiguous!"); + Self::from(inp) + .evaluate_gradient(&eos) + .map(|d| d.convert_into(Self::REFERENCE)) + }); + + let n = input.nrows(); + let status = value_dual.iter().map(|p| p.is_ok()).collect(); + let mut value = Array1::from_elem(n, f64::NAN); + let mut grad = Array2::zeros([n, P]); + for (i, result) in value_dual.into_iter().enumerate() { + if let Ok(p_dual) = result { + value[i] = p_dual.re; + let eps = p_dual + .eps + .unwrap_generic(nalgebra::Const::

, nalgebra::U1); + for (g, &e) in grad.row_mut(i).iter_mut().zip(eps.data.0[0].iter()) { + *g = e; + } + } + } + (value, grad, status) + } + + /// Evaluate the property and its gradients for all inputs and parameters in parallel. + /// + /// Return the property values, the gradients, and the success of the calculations. + #[cfg(feature = "ndarray")] + fn evaluate_parallel_derivatives_params, const P: usize>( parameter_names: [String; P], parameters: ArrayView2, input: ArrayView2, diff --git a/crates/feos-core/src/density_iteration.rs b/crates/feos-core/src/density_iteration.rs index aaee68ec1..e27ca0fba 100644 --- a/crates/feos-core/src/density_iteration.rs +++ b/crates/feos-core/src/density_iteration.rs @@ -85,10 +85,7 @@ where let t = Dual::from_re(temperature); let x = molefracs.map(Dual::from); let (a_res, da_res) = first_derivative( - |molar_volume| { - eos.lift() - .residual_helmholtz_energy(t, molar_volume, &x) - }, + |molar_volume| eos.lift().residual_helmholtz_energy(t, molar_volume, &x), molar_volume, ); a_res - da_res * molar_volume + temperature * density.ln() diff --git a/crates/feos-derive/src/dft.rs b/crates/feos-derive/src/dft.rs index a9ceb4f6a..d7dcd8e63 100644 --- a/crates/feos-derive/src/dft.rs +++ b/crates/feos-derive/src/dft.rs @@ -1,4 +1,4 @@ -use crate::{implement, OPT_IMPLS}; +use crate::{OPT_IMPLS, implement}; use quote::quote; use syn::DeriveInput; diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index 5ba5548c7..a0ce36b59 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -6,7 +6,7 @@ use feos_core::ad::{ ResidualIsobaricHeatCapacity, VaporPressure, }; use nalgebra::{U1, U2}; -use numpy::{PyArray1, PyArray2, PyReadonlyArray2, ToPyArray}; +use numpy::{PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray2, ToPyArray}; use paste::paste; use pyo3::prelude::*; @@ -42,11 +42,11 @@ type GradResult<'py> = ( Bound<'py, PyArray1>, ); -#[pyclass(name = "Properties")] -pub struct PyPropertiesAD; +#[pyclass(name = "Property")] +pub struct PyPropertyAD; #[pymethods] -impl PyPropertiesAD { +impl PyPropertyAD { /// Calculate vapor pressures in parallel. /// /// Parameters @@ -89,7 +89,7 @@ impl PyPropertiesAD { pub fn vapor_pressure_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> GradResult<'py> { _vapor_pressure_derivatives(model, parameter_names, parameters, input) @@ -137,7 +137,7 @@ impl PyPropertiesAD { pub fn boiling_temperature_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> GradResult<'py> { _boiling_temperature_derivatives(model, parameter_names, parameters, input) @@ -185,7 +185,7 @@ impl PyPropertiesAD { pub fn liquid_density_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> GradResult<'py> { _liquid_density_derivatives(model, parameter_names, parameters, input) @@ -233,7 +233,7 @@ impl PyPropertiesAD { pub fn equilibrium_liquid_density_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> GradResult<'py> { _equilibrium_liquid_density_derivatives(model, parameter_names, parameters, input) @@ -283,7 +283,7 @@ impl PyPropertiesAD { pub fn enthalpy_of_vaporization_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> GradResult<'py> { _enthalpy_of_vaporization_derivatives(model, parameter_names, parameters, input) @@ -334,7 +334,7 @@ impl PyPropertiesAD { pub fn residual_isobaric_heat_capacity_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> GradResult<'py> { _residual_isobaric_heat_capacity_derivatives(model, parameter_names, parameters, input) @@ -384,7 +384,7 @@ impl PyPropertiesAD { pub fn bubble_point_pressure_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> GradResult<'py> { _bubble_point_pressure_derivatives(model, parameter_names, parameters, input) @@ -434,7 +434,7 @@ impl PyPropertiesAD { pub fn dew_point_pressure_derivatives<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> GradResult<'py> { _dew_point_pressure_derivatives(model, parameter_names, parameters, input) @@ -448,7 +448,7 @@ macro_rules! expand_models { fn [<_ $prop _derivatives>]<'py>( model: PyEquationOfStateAD, parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> GradResult<'py> { match <$enum>::from(model) { @@ -472,7 +472,7 @@ macro_rules! impl_evaluate_gradients { expand_models!($enum, $prop, $($model: $type),*); fn $prop<'py, R: ParametersAD<$n>>( parameter_names: &Bound<'py, PyAny>, - parameters: PyReadonlyArray2, + parameters: &Bound<'py, PyAny>, input: PyReadonlyArray2, ) -> ( Bound<'py, PyArray1>, @@ -480,13 +480,27 @@ macro_rules! impl_evaluate_gradients { Bound<'py, PyArray1>, ) { let (value, grad, status) = - $( - if let Ok(p) = parameter_names.extract::<[String; $p]>() { - <$prop_type>::evaluate_parallel_ad::(p, parameters.as_array(), input.as_array()) - } else)* if let Ok(p) = parameter_names.extract::<[String; $max]>() { - <$prop_type>::evaluate_parallel_ad::(p, parameters.as_array(), input.as_array()) + if let Ok(pars) = parameters.extract::>() { + let pars = pars.as_slice().expect("Parameter array is not contiguous!"); + $( + if let Ok(p) = parameter_names.extract::<[String; $p]>() { + <$prop_type>::evaluate_parallel_derivatives::(p, pars, input.as_array()) + } else)* if let Ok(p) = parameter_names.extract::<[String; $max]>() { + <$prop_type>::evaluate_parallel_derivatives::(p, pars, input.as_array()) + } else { + panic!("Gradients can only be evaluated for up to {} parameters!", $max) + } + } else if let Ok(pars) = parameters.extract::>() { + $( + if let Ok(p) = parameter_names.extract::<[String; $p]>() { + <$prop_type>::evaluate_parallel_derivatives_params::(p, pars.as_array(), input.as_array()) + } else)* if let Ok(p) = parameter_names.extract::<[String; $max]>() { + <$prop_type>::evaluate_parallel_derivatives_params::(p, pars.as_array(), input.as_array()) + } else { + panic!("Gradients can only be evaluated for up to {} parameters!", $max) + } } else { - panic!("Gradients can only be evaluated for up to {} parameters!", $max) + panic!("Argument `parameters` needs to be a 1D or 2D array!") }; ( value.to_pyarray(parameter_names.py()), diff --git a/py-feos/src/lib.rs b/py-feos/src/lib.rs index 50fc51fdb..017dbb4a6 100644 --- a/py-feos/src/lib.rs +++ b/py-feos/src/lib.rs @@ -183,7 +183,7 @@ fn feos(m: &Bound<'_, PyModule>) -> PyResult<()> { #[cfg(feature = "ad")] { m.add_class::()?; - m.add_class::()?; + m.add_class::()?; // Datasets m.add_class::()?; diff --git a/py-feos/src/user_defined.rs b/py-feos/src/user_defined.rs index 38bbe38ca..641238f0b 100644 --- a/py-feos/src/user_defined.rs +++ b/py-feos/src/user_defined.rs @@ -14,7 +14,10 @@ impl PyIdealGas { pub fn new(obj: Bound<'_, PyAny>) -> PyResult { let attr = obj.hasattr("ln_lambda3")?; if !attr { - panic!("{}", "Python Class has to have a method 'ln_lambda3' with signature:\n\tdef ln_lambda3(self, temperature: HD) -> HD\nwhere 'HD' has to be any (hyper-) dual number.") + panic!( + "{}", + "Python Class has to have a method 'ln_lambda3' with signature:\n\tdef ln_lambda3(self, temperature: HD) -> HD\nwhere 'HD' has to be any (hyper-) dual number." + ) } Ok(Self(obj.unbind())) } @@ -57,23 +60,34 @@ impl PyResidual { pub fn new(obj: Bound<'_, PyAny>) -> PyResult { let attr = obj.hasattr("components")?; if !attr { - panic!("Python Class has to have a method 'components' with signature:\n\tdef signature(self) -> int") + panic!( + "Python Class has to have a method 'components' with signature:\n\tdef signature(self) -> int" + ) } let attr = obj.hasattr("subset")?; if !attr { - panic!("Python Class has to have a method 'subset' with signature:\n\tdef subset(self, component_list: List[int]) -> Self") + panic!( + "Python Class has to have a method 'subset' with signature:\n\tdef subset(self, component_list: List[int]) -> Self" + ) } let attr = obj.hasattr("molar_weight")?; if !attr { - panic!("Python Class has to have a method 'molar_weight' with signature:\n\tdef molar_weight(self) -> SIArray1\nwhere the size of the returned array has to be 'components'.") + panic!( + "Python Class has to have a method 'molar_weight' with signature:\n\tdef molar_weight(self) -> SIArray1\nwhere the size of the returned array has to be 'components'." + ) } let attr = obj.hasattr("max_density")?; if !attr { - panic!("Python Class has to have a method 'max_density' with signature:\n\tdef max_density(self, moles: numpy.ndarray[float]) -> float\nwhere the size of the input array has to be 'components'.") + panic!( + "Python Class has to have a method 'max_density' with signature:\n\tdef max_density(self, moles: numpy.ndarray[float]) -> float\nwhere the size of the input array has to be 'components'." + ) } let attr = obj.hasattr("helmholtz_energy")?; if !attr { - panic!("{}", "Python Class has to have a method 'helmholtz_energy' with signature:\n\tdef helmholtz_energy(self, state: StateHD) -> HD\nwhere 'HD' has to be any of {{float, Dual64, HyperDual64, HyperDualDual64, Dual3Dual64, Dual3_64}}.") + panic!( + "{}", + "Python Class has to have a method 'helmholtz_energy' with signature:\n\tdef helmholtz_energy(self, state: StateHD) -> HD\nwhere 'HD' has to be any of {{float, Dual64, HyperDual64, HyperDualDual64, Dual3Dual64, Dual3_64}}." + ) } Ok(Self(obj.unbind())) } From 2a4307b595c5cf64b2327e1e53e6940f33f8a4f8 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 21 May 2026 17:47:47 +0200 Subject: [PATCH 11/15] cleanup --- Cargo.toml | 1 + crates/feos-core/Cargo.toml | 2 +- crates/feos-core/src/ad/mod.rs | 80 ---------------------------------- crates/feos/Cargo.toml | 2 +- 4 files changed, 3 insertions(+), 82 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eb351252c..e9a0bf3e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ approx = "0.5" criterion = "0.8" paste = "1.0" csv = "1.0" +mimalloc = "0.1" feos-core = { version = "0.9", path = "crates/feos-core" } feos-dft = { version = "0.9", path = "crates/feos-dft" } diff --git a/crates/feos-core/Cargo.toml b/crates/feos-core/Cargo.toml index 4b79f29c5..20e516cc3 100644 --- a/crates/feos-core/Cargo.toml +++ b/crates/feos-core/Cargo.toml @@ -15,7 +15,7 @@ rustdoc-args = ["--html-in-header", "./docs-header.html"] features = ["rayon"] [dependencies] -quantity = { workspace = true, features = ["nalgebra", "ndarray", "num-dual"] } +quantity = { workspace = true, features = ["nalgebra", "num-dual"] } num-dual = { workspace = true } ndarray = { workspace = true, optional = true } nalgebra = { workspace = true } diff --git a/crates/feos-core/src/ad/mod.rs b/crates/feos-core/src/ad/mod.rs index 16e8d8724..b8d94ab6c 100644 --- a/crates/feos-core/src/ad/mod.rs +++ b/crates/feos-core/src/ad/mod.rs @@ -72,83 +72,3 @@ where }) } } - -// /// Evaluate a function and its gradients for a batch of parameters and inputs. -// #[cfg(feature = "ndarray")] -// pub(crate) fn vectorize_ad, N: Dim, const P: usize>( -// parameter_names: [String; P], -// parameters: ArrayView2, -// input: ArrayView2, -// f: F, -// ) -> (Array1, Array2, Array1) -// where -// DefaultAllocator: Allocator, -// F: Fn(&E::Lifted>, &[f64]) -> FeosResult> + Sync, -// { -// let parameter_names = parameter_names.each_ref().map(|s| s as &str); - -// #[cfg(feature = "rayon")] -// let value_dual = Zip::from(parameters.rows()) -// .and(input.rows()) -// .par_map_collect(|par, inp| { -// let par = par.as_slice().expect("Parameter array is not contiguous!"); -// let inp = inp.as_slice().expect("Input array is not contiguous!"); -// let eos = E::seed_derivatives(par, parameter_names); -// f(&eos, inp) -// }); - -// #[cfg(not(feature = "rayon"))] -// let value_dual = Zip::from(parameters.rows()) -// .and(input.rows()) -// .map_collect(|par, inp| { -// let par = par.as_slice().expect("Parameter array is not contiguous!"); -// let inp = inp.as_slice().expect("Input array is not contiguous!"); -// let eos = E::seed_derivatives(par, parameter_names); -// f(&eos, inp) -// }); - -// let n = parameters.nrows(); -// let status = value_dual.iter().map(|p| p.is_ok()).collect(); -// let mut value = Array1::from_elem(n, f64::NAN); -// let mut grad = Array2::zeros([n, P]); -// for (i, result) in value_dual.into_iter().enumerate() { -// if let Ok(p_dual) = result { -// value[i] = p_dual.re; -// let eps = p_dual.eps.unwrap_generic(Const::

, U1); -// for (g, &e) in grad.row_mut(i).iter_mut().zip(eps.data.0[0].iter()) { -// *g = e; -// } -// } -// } -// (value, grad, status) -// } - -// /// Evaluate a function for a batch of inputs using the same parameters for each sample. -// #[cfg(feature = "ndarray")] -// pub(crate) fn vectorize(eos: &E, input: ArrayView2, f: F) -> (Array1, Array1) -// where -// E: Sync, -// F: Fn(&E, &[f64]) -> FeosResult + Sync, -// { -// #[cfg(feature = "rayon")] -// let values = Zip::from(input.rows()).par_map_collect(|inp| { -// let inp = inp.as_slice().expect("Input array is not contiguous!"); -// f(eos, inp) -// }); - -// #[cfg(not(feature = "rayon"))] -// let values = Zip::from(input.rows()).map_collect(|inp| { -// let inp = inp.as_slice().expect("Input array is not contiguous!"); -// f(eos, inp) -// }); - -// let n = input.nrows(); -// let status: Array1 = values.iter().map(|r| r.is_ok()).collect(); -// let mut value = Array1::from_elem(n, f64::NAN); -// for (i, result) in values.into_iter().enumerate() { -// if let Ok(v) = result { -// value[i] = v; -// } -// } -// (value, status) -// } diff --git a/crates/feos/Cargo.toml b/crates/feos/Cargo.toml index 147baaf9f..92ebb8e5f 100644 --- a/crates/feos/Cargo.toml +++ b/crates/feos/Cargo.toml @@ -35,7 +35,7 @@ feos-dft = { workspace = true, optional = true } approx = { workspace = true } quantity = { workspace = true, features = ["approx"] } criterion = { workspace = true } -mimalloc = "0.1" +mimalloc = { workspace = true } [features] default = [] From 9d52c74877154de28767773a773cf8c7e83c102b Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 21 May 2026 18:06:45 +0200 Subject: [PATCH 12/15] Fix Sync trait bound --- py-feos/src/ad/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index a0ce36b59..71eac67a0 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -6,6 +6,7 @@ use feos_core::ad::{ ResidualIsobaricHeatCapacity, VaporPressure, }; use nalgebra::{U1, U2}; +use num_dual::DualSVec; use numpy::{PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray2, ToPyArray}; use paste::paste; use pyo3::prelude::*; @@ -478,7 +479,11 @@ macro_rules! impl_evaluate_gradients { Bound<'py, PyArray1>, Bound<'py, PyArray2>, Bound<'py, PyArray1>, - ) { + ) + where + $(R::Lifted>: Sync,)* + R::Lifted>: Sync + { let (value, grad, status) = if let Ok(pars) = parameters.extract::>() { let pars = pars.as_slice().expect("Parameter array is not contiguous!"); From 1db9e45e1fb105883ff286a4fefe0d6a2aedbb83 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Thu, 21 May 2026 22:15:34 +0200 Subject: [PATCH 13/15] updated changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d69100a7..85400f656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,16 +12,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) +- Added `PropertyAD` trait in `feos_core::ad` with one struct per property for uniform evaluation with or without parameter derivatives, including parallel variants. [#358](https://github.com/feos-org/feos/pull/358) +- Added `feos_core::ad::dataset` module with `PureDataset` and `BinaryDataset` types, constructible from records, CSV files, or readers, for use in parameter fits. [#358](https://github.com/feos-org/feos/pull/358) +- Exposed `Property`, `PureDataset`, and `BinaryDataset` in `py-feos`. [#358](https://github.com/feos-org/feos/pull/358) ### 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) +- Replaced the `PropertiesAD` blanket-impl trait with per-property `PropertyAD` types. [#358](https://github.com/feos-org/feos/pull/358) +- Replaced `ParametersAD::named_derivatives` with a `build` constructor and `seed_derivatives(&values, names)`. [#358](https://github.com/feos-org/feos/pull/358) +- Removed the per-property `*_derivatives` free functions in Python in favour of static methods on the new `Property` class. [#358](https://github.com/feos-org/feos/pull/358) ### 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. [#328](https://github.com/feos-org/feos/pull/328) +- Added `csv` as a `feos-core` dependency for the new dataset module. [#358](https://github.com/feos-org/feos/pull/358) ## [Unreleased] ### Added From e17ad716d4f8161e47fb0ca766df0c8ae469b244 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Thu, 21 May 2026 22:19:30 +0200 Subject: [PATCH 14/15] removed unrelated benchmark and mimalloc --- Cargo.toml | 1 - crates/feos/Cargo.toml | 6 - crates/feos/benches/dual_static_vs_dynamic.rs | 290 ------------------ 3 files changed, 297 deletions(-) delete mode 100644 crates/feos/benches/dual_static_vs_dynamic.rs diff --git a/Cargo.toml b/Cargo.toml index e9a0bf3e5..eb351252c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ approx = "0.5" criterion = "0.8" paste = "1.0" csv = "1.0" -mimalloc = "0.1" feos-core = { version = "0.9", path = "crates/feos-core" } feos-dft = { version = "0.9", path = "crates/feos-dft" } diff --git a/crates/feos/Cargo.toml b/crates/feos/Cargo.toml index 92ebb8e5f..28e6e6109 100644 --- a/crates/feos/Cargo.toml +++ b/crates/feos/Cargo.toml @@ -35,7 +35,6 @@ feos-dft = { workspace = true, optional = true } approx = { workspace = true } quantity = { workspace = true, features = ["approx"] } criterion = { workspace = true } -mimalloc = { workspace = true } [features] default = [] @@ -77,11 +76,6 @@ name = "dual_numbers" harness = false required-features = ["pcsaft"] -[[bench]] -name = "dual_static_vs_dynamic" -harness = false -required-features = ["pcsaft"] - [[bench]] name = "dual_numbers_saftvrmie" required-features = ["saftvrmie"] diff --git a/crates/feos/benches/dual_static_vs_dynamic.rs b/crates/feos/benches/dual_static_vs_dynamic.rs deleted file mode 100644 index 0f0a75c8a..000000000 --- a/crates/feos/benches/dual_static_vs_dynamic.rs +++ /dev/null @@ -1,290 +0,0 @@ -//! Compare statically sized and dynamically sized vector dual numbers for a -//! PC-SAFT-like Helmholtz energy density evaluation. -//! -//! This intentionally copies the optimized pure-component PC-SAFT expressions -//! instead of calling the production implementation: the production `Residual` -//! stack requires `D: Copy`, while dynamic dual vectors (`DualDVec64`) are not -//! `Copy`. The copied expression is close enough to expose the arithmetic and -//! allocation cost of realistic PC-SAFT parameter derivatives. - -use criterion::{Criterion, criterion_group, criterion_main}; -use num_dual::{DualDVec64, DualNum, DualSVec64}; -use std::f64::consts::{FRAC_PI_6, PI}; -use std::hint::black_box; - -#[global_allocator] -static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; - -const PI_SQ_43: f64 = 4.0 / 3.0 * PI * PI; - -// PC-SAFT parameters for an associating, polar pure component-ish model: -// m, sigma, epsilon_k, mu, kappa_ab, epsilon_k_ab, na, nb -const PARAMS: [f64; 8] = [1.5, 3.4, 180.0, 2.2, 0.03, 2500.0, 2.0, 1.0]; -const TEMPERATURE: f64 = 300.0; -const DENSITY: f64 = 0.01; - -// Dispersion coefficients copied from pcsaft/eos/dispersion.rs. -const A0: [f64; 7] = [ - 0.91056314451539, - 0.63612814494991, - 2.68613478913903, - -26.5473624914884, - 97.7592087835073, - -159.591540865600, - 91.2977740839123, -]; -const A1: [f64; 7] = [ - -0.30840169182720, - 0.18605311591713, - -2.50300472586548, - 21.4197936296668, - -65.2558853303492, - 83.3186804808856, - -33.7469229297323, -]; -const A2: [f64; 7] = [ - -0.09061483509767, - 0.45278428063920, - 0.59627007280101, - -1.72418291311787, - -4.13021125311661, - 13.7766318697211, - -8.67284703679646, -]; -const B0: [f64; 7] = [ - 0.72409469413165, - 2.23827918609380, - -4.00258494846342, - -21.00357681484648, - 26.8556413626615, - 206.5513384066188, - -355.60235612207947, -]; -const B1: [f64; 7] = [ - -0.57554980753450, - 0.69950955214436, - 3.89256733895307, - -17.21547164777212, - 192.6722644652495, - -161.8264616487648, - -165.2076934555607, -]; -const B2: [f64; 7] = [ - 0.09768831158356, - -0.25575749816100, - -9.15585615297321, - 20.64207597439724, - -38.80443005206285, - 93.6267740770146, - -29.66690558514725, -]; - -// Dipole coefficients copied from pcsaft/eos/polar.rs. -const AD: [[f64; 3]; 5] = [ - [0.30435038064, 0.95346405973, -1.16100802773], - [-0.13585877707, -1.83963831920, 4.52586067320], - [1.44933285154, 2.01311801180, 0.97512223853], - [0.35569769252, -7.37249576667, -12.2810377713], - [-2.06533084541, 8.23741345333, 5.93975747420], -]; -const BD: [[f64; 3]; 5] = [ - [0.21879385627, -0.58731641193, 3.48695755800], - [-1.18964307357, 1.24891317047, -14.9159739347], - [1.16268885692, -0.50852797392, 15.3720218600], - [0.0; 3], - [0.0; 3], -]; -const CD: [[f64; 3]; 4] = [ - [-0.06467735252, -0.95208758351, -0.62609792333], - [0.19758818347, 2.99242575222, 1.29246858189], - [-0.80875619458, -2.38026356489, 1.65427830900], - [0.69028490492, -0.27012609786, -3.43967436378], -]; - -fn helmholtz_energy_density_non_assoc( - m: D, - sigma: D, - epsilon_k: D, - mu: D, - temperature: D, - density: D, -) -> (D, [D; 2]) -where - D: DualNum + Clone, -{ - // temperature dependent segment diameter - let diameter = - sigma.clone() * (D::one() - (epsilon_k.clone() * -3.0 / temperature.clone()).exp() * 0.12); - - let eta = m.clone() * density.clone() * diameter.clone().powi(3) * FRAC_PI_6; - let eta2 = eta.clone() * eta.clone(); - let eta3 = eta2.clone() * eta.clone(); - let eta_m1 = (D::one() - eta.clone()).recip(); - let eta_m2 = eta_m1.clone() * eta_m1.clone(); - let etas = [ - D::one(), - eta.clone(), - eta2.clone(), - eta3.clone(), - eta2.clone() * eta2.clone(), - eta2.clone() * eta3.clone(), - eta3.clone() * eta3.clone(), - ]; - - // hard sphere - let hs = - m.clone() * density.clone() * (eta.clone() * 4.0 - eta2.clone() * 3.0) * eta_m2.clone(); - - // hard chain - let g = (D::one() - eta.clone() * 0.5) * eta_m1.clone() * eta_m2.clone(); - let hc = -(density.clone() * (m.clone() - 1.0) * g.ln()); - - // dispersion - let e = epsilon_k.clone() / temperature.clone(); - let s3 = sigma.clone().powi(3); - let mut i1 = D::zero(); - let mut i2 = D::zero(); - let m1 = (m.clone() - 1.0) / m.clone(); - let m2 = (m.clone() - 2.0) / m.clone(); - for i in 0..7 { - i1 += (m1.clone() * (m2.clone() * A2[i] + A1[i]) + A0[i]) * etas[i].clone(); - i2 += (m1.clone() * (m2.clone() * B2[i] + B1[i]) + B0[i]) * etas[i].clone(); - } - let c1 = - (m.clone() * (eta.clone() * 8.0 - eta2.clone() * 2.0) * eta_m2.clone() * eta_m2.clone() - + 1.0 - - (m.clone() - 1.0) - * (eta.clone() * 20.0 - eta2.clone() * 27.0 + eta3.clone() * 12.0 - - eta2.clone() * eta2.clone() * 2.0) - / ((eta.clone() - 1.0) * (eta.clone() - 2.0)).powi(2)) - .recip(); - let i = i1 * 2.0 + c1 * i2 * m.clone() * e.clone(); - let disp = - -(density.clone() * density.clone() * m.clone().powi(2) * e.clone() * s3.clone() * i * PI); - - // dipoles - let mu2 = mu.clone().powi(2) / (m.clone() * temperature * 1.380649e-4); - let m_dipole = if m.re() > 2.0 { - D::from(2.0) - } else { - m.clone() - }; - let m1 = (m_dipole.clone() - 1.0) / m_dipole.clone(); - let m2 = m1.clone() * (m_dipole.clone() - 2.0) / m_dipole; - let mut j1 = D::zero(); - let mut j2 = D::zero(); - for i in 0..5 { - let a = m2.clone() * AD[i][2] + m1.clone() * AD[i][1] + AD[i][0]; - let b = m2.clone() * BD[i][2] + m1.clone() * BD[i][1] + BD[i][0]; - j1 += (a + b * e.clone()) * etas[i].clone(); - if i < 4 { - j2 += (m2.clone() * CD[i][2] + m1.clone() * CD[i][1] + CD[i][0]) * etas[i].clone(); - } - } - - // mu is factored out of these expressions to deal with the case where mu=0 - let phi2 = -(density.clone() * density.clone() * j1 / s3.clone() * PI); - let phi3 = -(density.clone() * density.clone() * density * j2 / s3 * PI_SQ_43); - let dipole = phi2.clone() * phi2.clone() * mu2.clone() * mu2.clone() / (phi2 - phi3 * mu2); - - (hs + hc + disp + dipole, [eta, eta_m1]) -} - -fn helmholtz_energy_density(parameters: &[D; 8], temperature: D, density: D) -> D -where - D: DualNum + Clone, -{ - let [m, sigma, epsilon_k, mu, kappa_ab, epsilon_k_ab, na, nb] = - parameters.each_ref().map(Clone::clone); - let (non_assoc, [eta, eta_m1]) = helmholtz_energy_density_non_assoc( - m, - sigma.clone(), - epsilon_k, - mu, - temperature.clone(), - density.clone(), - ); - - // association - let delta_assoc = ((epsilon_k_ab / temperature).exp() - 1.0) * sigma.powi(3) * kappa_ab; - let k = eta * eta_m1.clone(); - let delta = (k.clone() * (k * 0.5 + 1.5) + 1.0) * eta_m1 * delta_assoc; - let rhoa = na * density.clone(); - let rhob = nb * density; - let aux = (rhoa.clone() - rhob.clone()) * delta.clone() + 1.0; - let sqrt = (aux.clone() * aux + rhob.clone() * delta.clone() * 4.0).sqrt(); - let xa = (sqrt.clone() + 1.0 + (rhob.clone() - rhoa.clone()) * delta.clone()).recip() * 2.0; - let xb = (sqrt + 1.0 - (rhob.clone() - rhoa.clone()) * delta).recip() * 2.0; - let assoc = - rhoa * (xa.clone().ln() - xa * 0.5 + 0.5) + rhob * (xb.clone().ln() - xb * 0.5 + 0.5); - - non_assoc + assoc -} - -fn static_parameters(params: [f64; 8]) -> [DualSVec64

; 8] { - std::array::from_fn(|i| { - let x = DualSVec64::

::from_re(params[i]); - if i < P { x.derivative(i) } else { x } - }) -} - -fn dynamic_parameters(params: [f64; 8], p: usize) -> [DualDVec64; 8] { - std::array::from_fn(|i| { - let x = DualDVec64::from_re(params[i]); - if i < p { x.derivative(p, i) } else { x } - }) -} - -fn eval_static(params: [f64; 8], temperature: f64, density: f64) -> DualSVec64

{ - helmholtz_energy_density( - &static_parameters::

(params), - DualSVec64::

::from_re(temperature), - DualSVec64::

::from_re(density), - ) -} - -fn eval_dynamic(params: [f64; 8], temperature: f64, density: f64, p: usize) -> DualDVec64 { - helmholtz_energy_density( - &dynamic_parameters(params, p), - DualDVec64::from_re(temperature), - DualDVec64::from_re(density), - ) -} - -fn bench_pair( - group: &mut criterion::BenchmarkGroup<'_, criterion::measurement::WallTime>, -) { - group.bench_function(format!("static_p{P}"), |b| { - b.iter(|| { - black_box(eval_static::

( - black_box(PARAMS), - black_box(TEMPERATURE), - black_box(DENSITY), - )) - }) - }); - group.bench_function(format!("dynamic_p{P}"), |b| { - b.iter(|| { - black_box(eval_dynamic( - black_box(PARAMS), - black_box(TEMPERATURE), - black_box(DENSITY), - P, - )) - }) - }); -} - -fn static_vs_dynamic_pcsaft(c: &mut Criterion) { - let mut group = c.benchmark_group("dual_static_vs_dynamic_pcsaft_helmholtz"); - bench_pair::<1>(&mut group); - bench_pair::<2>(&mut group); - bench_pair::<3>(&mut group); - bench_pair::<4>(&mut group); - bench_pair::<6>(&mut group); - bench_pair::<8>(&mut group); - group.finish(); -} - -criterion_group!(benches, static_vs_dynamic_pcsaft); -criterion_main!(benches); From 8489885294c26c4353aeb5fb2fcaee80e5175fe7 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Thu, 21 May 2026 22:36:11 +0200 Subject: [PATCH 15/15] removed array conversions necessary for old version of ParameterAD trait for pure and binary PC-SAFT impl --- crates/feos/src/pcsaft/eos/pcsaft_binary.rs | 18 ------------------ crates/feos/src/pcsaft/eos/pcsaft_pure.rs | 9 --------- 2 files changed, 27 deletions(-) diff --git a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs index ee850ddc9..160b3bd27 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs @@ -19,24 +19,6 @@ impl PcSaftBinary { } } -impl + Copy, const N: usize> From<&[f64]> for PcSaftBinary { - fn from(parameters: &[f64]) -> Self { - if parameters.len() != 2 * N + 1 { - panic!( - "This version of PC-SAFT requires exactly {} parameters!", - 2 * N + 1 - ) - } - let (Ok(p1), Ok(p2)): (Result<[f64; N], _>, Result<[f64; N], _>) = - (parameters[..N].try_into(), parameters[N..2 * N].try_into()) - else { - unreachable!() - }; - let kij = D::from(parameters[2 * N]); - Self::new([p1.map(D::from), p2.map(D::from)], kij) - } -} - impl ParametersAD for PcSaftBinary { fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D, diff --git a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs index 334fe3416..46155f4cf 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs @@ -183,15 +183,6 @@ impl + Copy> Residual for PcSaftPure { } } -impl + Copy, const N: usize> From<&[f64]> for PcSaftPure { - fn from(parameters: &[f64]) -> Self { - let Ok(parameters): Result<[f64; N], _> = parameters.try_into() else { - panic!("This version of PC-SAFT requires exactly {N} parameters!") - }; - Self(parameters.map(D::from)) - } -} - impl ParametersAD for PcSaftPure { fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D,