From 897a369be6868ab776631c09d820e5e9ee79825f Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:16:46 +0200 Subject: [PATCH 1/6] Clean the implementation of temperature_or_pressure arguments somewhat (#369) --- CHANGELOG.md | 4 + .../src/phase_equilibria/bubble_dew.rs | 206 ++++++++---------- .../phase_equilibria/phase_diagram_binary.rs | 65 +++--- crates/feos/src/pcsaft/eos/mod.rs | 4 +- 4 files changed, 132 insertions(+), 147 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a05e9188..adc0b1945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Breaking] +### Changed +- Changed data type of initial temperatures or pressure for phase equilibrium calculations (`TemperatureOrPressure::Other`) from `D` to `f64`. [#369](https://github.com/feos-org/feos/pull/369) + ## [Unreleased] ## [0.10.0] - 2026-07-15 diff --git a/crates/feos-core/src/phase_equilibria/bubble_dew.rs b/crates/feos-core/src/phase_equilibria/bubble_dew.rs index f956d4d7d..6d2c9333d 100644 --- a/crates/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/crates/feos-core/src/phase_equilibria/bubble_dew.rs @@ -31,10 +31,7 @@ pub trait TemperatureOrPressure + Copy = f64>: Copy { fn temperature(&self) -> Option>; fn pressure(&self) -> Option>; - fn temperature_pressure( - &self, - tp_init: Option, - ) -> (Option>, Option>, bool); + fn specification(&self, tp_init: Option) -> TemperatureOrPressureSpecification; fn from_state, N: Gradients>(state: &State) -> Self::Other where @@ -50,7 +47,7 @@ pub trait TemperatureOrPressure + Copy = f64>: Copy { } impl + Copy> TemperatureOrPressure for Temperature { - type Other = Pressure; + type Other = Pressure; const IDENTIFIER: &'static str = "temperature"; fn temperature(&self) -> Option> { @@ -61,30 +58,27 @@ impl + Copy> TemperatureOrPressure for Temperature { None } - fn temperature_pressure( - &self, - tp_init: Option, - ) -> (Option>, Option>, bool) { - (Some(*self), tp_init, true) + fn specification(&self, tp_init: Option) -> TemperatureOrPressureSpecification { + TemperatureOrPressureSpecification::Temperature(*self, tp_init) } fn from_state, N: Gradients>(state: &State) -> Self::Other where DefaultAllocator: Allocator, { - state.pressure(Contributions::Total) + state.pressure(Contributions::Total).re() } #[cfg(feature = "ndarray")] fn linspace( &self, - start: Pressure, - end: Pressure, + start: Pressure, + end: Pressure, n: usize, ) -> (Temperature>, Pressure>) { ( Temperature::linspace(self.re(), self.re(), n), - Pressure::linspace(start.re(), end.re(), n), + Pressure::linspace(start, end, n), ) } } @@ -95,7 +89,7 @@ impl + Copy> TemperatureOrPressure for Temperature { impl + Copy> TemperatureOrPressure for Quantity> { - type Other = Temperature; + type Other = Temperature; const IDENTIFIER: &'static str = "pressure"; fn temperature(&self) -> Option> { @@ -106,34 +100,43 @@ impl + Copy> TemperatureOrPressure Some(*self) } - fn temperature_pressure( - &self, - tp_init: Option, - ) -> (Option>, Option>, bool) { - (tp_init, Some(*self), false) + fn specification(&self, tp_init: Option) -> TemperatureOrPressureSpecification { + TemperatureOrPressureSpecification::Pressure(*self, tp_init) } fn from_state, N: Dim>(state: &State) -> Self::Other where DefaultAllocator: Allocator, { - state.temperature + state.temperature.re() } #[cfg(feature = "ndarray")] fn linspace( &self, - start: Temperature, - end: Temperature, + start: Temperature, + end: Temperature, n: usize, ) -> (Temperature>, Pressure>) { ( - Temperature::linspace(start.re(), end.re(), n), + Temperature::linspace(start, end, n), Pressure::linspace(self.re(), self.re(), n), ) } } +/// Specification and initial value for a phase equilibrium calculation. +pub enum TemperatureOrPressureSpecification { + Temperature(Temperature, Option), + Pressure(Pressure, Option), +} + +impl TemperatureOrPressureSpecification { + fn is_temperature(&self) -> bool { + matches!(self, Self::Temperature(_, _)) + } +} + /// # Bubble and dew point calculations impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium where @@ -197,134 +200,113 @@ where } Ok(vle) } else { - let (temperature, pressure, iterate_p) = - temperature_or_pressure.temperature_pressure(tp_init); Self::bubble_dew_point_tp( eos, - temperature, - pressure, + temperature_or_pressure, + tp_init, vapor_molefracs, liquid_molefracs, bubble, - iterate_p, options, ) } } - #[expect(clippy::too_many_arguments)] - fn bubble_dew_point_tp>( + fn bubble_dew_point_tp, X: Composition>( eos: &E, - temperature: Option>, - pressure: Option>, + temperature_or_pressure: TP, + tp_init: Option, composition: X, molefracs_init: Option<&OVector>, bubble: bool, - iterate_p: bool, options: (SolverOptions, SolverOptions), ) -> FeosResult { let eos_re = eos.re(); - let mut temperature_re = temperature.map(|t| t.re()); - let mut pressure_re = pressure.map(|p| p.re()); + let iterate_p = temperature_or_pressure + .specification(tp_init) + .is_temperature(); let (molefracs_spec, total_moles) = composition.into_molefracs(eos)?; let molefracs_spec_re = molefracs_spec.map(|x| x.re()); - let (v1, rho2) = if iterate_p { - // temperature is specified - let temperature_re = temperature_re.as_mut().ok_or(FeosError::Error( - "Temperature information is expected for bubble/dew calculation.".to_string(), - ))?; - - // First use given initial pressure if applicable - if let Some(p) = pressure_re.as_mut() { - PhaseEquilibrium::iterate_bubble_dew( - &eos_re, - temperature_re, - p, - &molefracs_spec_re, - molefracs_init, - bubble, - iterate_p, - options, - )? - } else { - // Next try to initialize with an ideal gas assumption - let x2 = PhaseEquilibrium::starting_pressure_ideal_gas( - &eos_re, - *temperature_re, - &molefracs_spec_re, - bubble, - ) - .and_then(|(p, x)| { - let p = pressure_re.insert(p); - PhaseEquilibrium::iterate_bubble_dew( + let (mut t, mut p, v1, rho2) = match temperature_or_pressure.specification(tp_init) { + TemperatureOrPressureSpecification::Temperature(t, mut p) => { + // First use given initial pressure if applicable + let (p, v1, rho2) = if let Some(p) = p.as_mut() { + let (v1, rho2) = PhaseEquilibrium::iterate_bubble_dew( &eos_re, - temperature_re, + &mut t.re(), p, &molefracs_spec_re, - molefracs_init.or(Some(&x)), + molefracs_init, bubble, iterate_p, options, - ) - }); - - // Finally use the spinodal to initialize the calculation - x2.or_else(|_| { - PhaseEquilibrium::starting_pressure_spinodal( + )?; + (*p, v1, rho2) + } else { + let x2 = PhaseEquilibrium::starting_pressure_ideal_gas( &eos_re, - *temperature_re, + t.re(), &molefracs_spec_re, + bubble, ) - .and_then(|p| { - let p = pressure_re.insert(p); - PhaseEquilibrium::iterate_bubble_dew( + .and_then(|(mut p, x)| { + let (v1, rho2) = PhaseEquilibrium::iterate_bubble_dew( &eos_re, - temperature_re, - p, + &mut t.re(), + &mut p, &molefracs_spec_re, - molefracs_init, + molefracs_init.or(Some(&x)), bubble, iterate_p, options, + )?; + Ok((p, v1, rho2)) + }); + + // Finally use the spinodal to initialize the calculation + x2.or_else(|_| { + PhaseEquilibrium::starting_pressure_spinodal( + &eos_re, + t.re(), + &molefracs_spec_re, ) - }) - })? + .and_then(|mut p| { + let (v1, rho2) = PhaseEquilibrium::iterate_bubble_dew( + &eos_re, + &mut t.re(), + &mut p, + &molefracs_spec_re, + molefracs_init, + bubble, + iterate_p, + options, + )?; + Ok((p, v1, rho2)) + }) + })? + }; + (t.into_reduced(), D::from(p.into_reduced()), v1, rho2) } - } else { - // pressure is specified - let pressure_re = pressure_re.as_mut().ok_or(FeosError::Error( - "Pressure information is expected for bubble/dew calculation.".to_string(), - ))?; - - let temperature_re = temperature_re - .as_mut() + TemperatureOrPressureSpecification::Pressure(p, mut t) => { + let mut pressure_re = p.re(); + let t = t.as_mut() .ok_or(FeosError::Error( "An initial temperature is required for the calculation of bubble/dew points at given pressure.".to_string()))?; - PhaseEquilibrium::iterate_bubble_dew( - &eos.re(), - temperature_re, - pressure_re, - &molefracs_spec_re, - molefracs_init, - bubble, - iterate_p, - options, - )? + let (v1, rho2) = PhaseEquilibrium::iterate_bubble_dew( + &eos.re(), + t, + &mut pressure_re, + &molefracs_spec_re, + molefracs_init, + bubble, + iterate_p, + options, + )?; + (D::from(t.into_reduced()), p.into_reduced(), v1, rho2) + } }; // implicit differentiation - // unwraps here are safe - let (mut t, mut p) = if iterate_p { - ( - temperature.unwrap().into_reduced(), - D::from(pressure_re.unwrap().into_reduced()), - ) - } else { - ( - D::from(temperature_re.unwrap().into_reduced()), - pressure.unwrap().into_reduced(), - ) - }; let mut molar_volume = D::from(v1); let mut rho2 = rho2.map(D::from); for _ in 0..D::NDERIV { diff --git a/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs b/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs index a3b23cbcc..f33b6b75d 100644 --- a/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs +++ b/crates/feos-core/src/phase_equilibria/phase_diagram_binary.rs @@ -1,6 +1,7 @@ use super::bubble_dew::TemperatureOrPressure; use super::{PhaseDiagram, PhaseEquilibrium}; use crate::errors::{FeosError, FeosResult}; +use crate::phase_equilibria::bubble_dew::TemperatureOrPressureSpecification; use crate::state::{Contributions, DensityInitialization::Vapor, State}; use crate::{ReferenceSystem, Residual, SolverOptions, Subset}; use nalgebra::{DVector, dvector, matrix, stack, vector}; @@ -503,31 +504,27 @@ impl PhaseEquilibrium { options: SolverOptions, bubble_dew_options: (SolverOptions, SolverOptions), ) -> FeosResult { - let (temperature, pressure, iterate_p) = - temperature_or_pressure.temperature_pressure(tp_init); - if iterate_p { - PhaseEquilibrium::heteroazeotrope_t( - eos, - temperature.ok_or(FeosError::Error( - "Temperature information is expected for heteroazeotrope calculation." - .to_string(), - ))?, - x_init, - pressure, - options, - bubble_dew_options, - ) - } else { - PhaseEquilibrium::heteroazeotrope_p( - eos, - pressure.ok_or(FeosError::Error( - "Pressure information is expected for heteroazeotrope calculation.".to_string(), - ))?, - x_init, - temperature, - options, - bubble_dew_options, - ) + match temperature_or_pressure.specification(tp_init) { + TemperatureOrPressureSpecification::Temperature(temperature, pressure) => { + PhaseEquilibrium::heteroazeotrope_t( + eos, + temperature, + x_init, + pressure, + options, + bubble_dew_options, + ) + } + TemperatureOrPressureSpecification::Pressure(pressure, temperature) => { + PhaseEquilibrium::heteroazeotrope_p( + eos, + pressure, + x_init, + temperature, + options, + bubble_dew_options, + ) + } } } @@ -826,12 +823,14 @@ impl PhaseEquilibrium { let x0 = -ln_alpha1 / (ln_alpha2 - ln_alpha1); // solve for the azeotropic composition and return the corresponding VLE state - let (temperature, pressure, iterate_t) = temperature_or_pressure.temperature_pressure(None); - (if iterate_t { - Self::iterate_azeotrope_t(eos, temperature.unwrap(), x0, 10, 1e-10) - } else { - let t_init = vle1.liquid().temperature.min(vle2.liquid().temperature); - Self::iterate_azeotrope_p(eos, pressure.unwrap(), x0, t_init, 10, 1e-10) + (match temperature_or_pressure.specification(None) { + TemperatureOrPressureSpecification::Temperature(temperature, _) => { + Self::iterate_azeotrope_t(eos, temperature, x0, 10, 1e-10) + } + TemperatureOrPressureSpecification::Pressure(pressure, _) => { + let t_init = vle1.liquid().temperature.min(vle2.liquid().temperature); + Self::iterate_azeotrope_p(eos, pressure, x0, t_init, 10, 1e-10) + } }) .map(Some) } @@ -849,7 +848,7 @@ impl PhaseEquilibrium { PhaseEquilibrium::bubble_point( &eos.lift(), t, - &dvector![x, -x + 1.0], + dvector![x, -x + 1.0], None, None, Default::default(), @@ -884,7 +883,7 @@ impl PhaseEquilibrium { PhaseEquilibrium::bubble_point( &eos.lift(), p, - &dvector![x, -x + 1.0], + dvector![x, -x + 1.0], Some(t_init), None, Default::default(), diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index 6a68f67f7..aa8e05d0c 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -890,7 +890,7 @@ mod tests_parameter_fit { 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 t_init = Temperature::from_reduced(500.0); let x = DualVec::from(0.5); let t = PhaseEquilibrium::bubble_point( &pcsaft_ad, @@ -939,7 +939,7 @@ mod tests_parameter_fit { 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 t_init = Temperature::from_reduced(500.0); let x = DualVec::from(0.5); let t = PhaseEquilibrium::dew_point( &pcsaft_ad, From c29bbd20fd7a9396b955b74c0d7a5de220133630 Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:30:48 +0200 Subject: [PATCH 2/6] Implement DFT at fixed moles (#371) --- CHANGELOG.md | 7 ++ crates/feos-dft/src/adsorption/mod.rs | 44 ++++++--- crates/feos-dft/src/adsorption/pore.rs | 57 +++++++++--- crates/feos-dft/src/adsorption/pore2d.rs | 19 ++-- crates/feos-dft/src/adsorption/pore3d.rs | 21 +++-- crates/feos-dft/src/interface/mod.rs | 11 +-- crates/feos-dft/src/lib.rs | 2 +- crates/feos-dft/src/profile/mod.rs | 108 ++++++++-------------- crates/feos-dft/src/profile/properties.rs | 4 +- crates/feos-dft/src/solver.rs | 34 +++---- crates/feos/benches/dft_pore.rs | 36 ++++++-- crates/feos/tests/gc_pcsaft/dft.rs | 4 +- py-feos/src/dft/adsorption/mod.rs | 4 +- py-feos/src/dft/adsorption/pore.rs | 56 +++++++++-- py-feos/src/dft/mod.rs | 25 +---- py-feos/src/lib.rs | 1 + 16 files changed, 253 insertions(+), 180 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adc0b1945..05c507355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Breaking] +### Added +- Added `PoreSpecification` enum to specify the state of the fluid in the pore (currently chemical potential or moles). [#371](https://github.com/feos-org/feos/pull/371) + ### Changed - Changed data type of initial temperatures or pressure for phase equilibrium calculations (`TemperatureOrPressure::Other`) from `D` to `f64`. [#369](https://github.com/feos-org/feos/pull/369) +- Reworked DFT solution algorithms slightly for the cases in which additional specifications are given. [#371](https://github.com/feos-org/feos/pull/371) + +### Removed +- Removed the `DFTSpecification` trait in favor of only using the `DFTSpecification` enum (renamed from `DFTSpecifications`). [#371](https://github.com/feos-org/feos/pull/371) ## [Unreleased] diff --git a/crates/feos-dft/src/adsorption/mod.rs b/crates/feos-dft/src/adsorption/mod.rs index bf9ddabe0..463989f67 100644 --- a/crates/feos-dft/src/adsorption/mod.rs +++ b/crates/feos-dft/src/adsorption/mod.rs @@ -17,7 +17,7 @@ mod fea_potential; mod pore; mod pore2d; pub use external_potential::{ExternalPotential, FluidParameters}; -pub use pore::{HenryCoefficient, Pore1D, PoreProfile, PoreProfile1D, PoreSpecification}; +pub use pore::{HenryCoefficient, Pore, Pore1D, PoreProfile, PoreProfile1D, PoreSpecification}; pub use pore2d::{Pore2D, PoreProfile2D}; #[cfg(feature = "rayon")] @@ -54,7 +54,7 @@ where } /// Calculate an adsorption isotherm (starting at low pressure) - pub fn adsorption_isotherm, X: Composition + Clone>( + pub fn adsorption_isotherm, X: Composition + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, @@ -74,7 +74,7 @@ where } /// Calculate an desorption isotherm (starting at high pressure) - pub fn desorption_isotherm, X: Composition + Clone>( + pub fn desorption_isotherm, X: Composition + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, @@ -99,7 +99,7 @@ where } /// Calculate an equilibrium isotherm - pub fn equilibrium_isotherm, X: Composition + Clone>( + pub fn equilibrium_isotherm, X: Composition + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, @@ -182,7 +182,7 @@ where } } - fn isotherm, X: Composition + Clone>( + fn isotherm, X: Composition + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, @@ -210,7 +210,10 @@ where _ => unreachable!(), }; } - let profile = pore.initialize(&bulk, None, None)?.solve(solver)?.profile; + let profile = pore + .initialize(&bulk, None, None, PoreSpecification::ChemicalPotential)? + .solve(solver)? + .profile; let external_potential = Some(&profile.external_potential); let mut old_density = Some(&profile.density); @@ -229,8 +232,18 @@ where .clone(); } - let p = pore.initialize(&bulk, old_density, external_potential)?; - let p2 = pore.initialize(&bulk, None, external_potential)?; + let p = pore.initialize( + &bulk, + old_density, + external_potential, + PoreSpecification::ChemicalPotential, + )?; + let p2 = pore.initialize( + &bulk, + None, + external_potential, + PoreSpecification::ChemicalPotential, + )?; profiles.push(p.solve(solver).or_else(|_| p2.solve(solver))); old_density = if let Some(Ok(l)) = profiles.last() { @@ -245,7 +258,7 @@ where /// Calculate the phase transition from an empty to a filled pore. #[expect(clippy::too_many_arguments)] - pub fn phase_equilibrium, X: Composition + Clone>( + pub fn phase_equilibrium, X: Composition + Clone>( functional: &F, temperature: Temperature, p_min: Pressure, @@ -261,8 +274,17 @@ where let bulk_init = State::new_npt(functional, temperature, p_max, x.clone(), Some(Liquid))?; let liquid_bulk = State::new_npt(functional, temperature, p_max, x.clone(), Some(Vapor))?; - let mut vapor = pore.initialize(&vapor_bulk, None, None)?.solve(solver)?; - let mut liquid = pore.initialize(&bulk_init, None, None)?.solve(solver)?; + let mut vapor = pore + .initialize( + &vapor_bulk, + None, + None, + PoreSpecification::ChemicalPotential, + )? + .solve(solver)?; + let mut liquid = pore + .initialize(&bulk_init, None, None, PoreSpecification::ChemicalPotential)? + .solve(solver)?; // calculate initial value for bulk density let n_dp_drho_v = (vapor.profile.moles() * vapor_bulk.dp_drho(Contributions::Total)).sum(); diff --git a/crates/feos-dft/src/adsorption/pore.rs b/crates/feos-dft/src/adsorption/pore.rs index ddf2f7479..5db29b1f7 100644 --- a/crates/feos-dft/src/adsorption/pore.rs +++ b/crates/feos-dft/src/adsorption/pore.rs @@ -1,4 +1,3 @@ -use crate::WeightFunctionInfo; use crate::adsorption::{ExternalPotential, FluidParameters}; use crate::convolver::ConvolverFFT; use crate::functional::{HelmholtzEnergyFunctional, HelmholtzEnergyFunctionalDyn, MoleculeShape}; @@ -6,6 +5,7 @@ use crate::functional_contribution::FunctionalContribution; use crate::geometry::{Axis, Geometry, Grid}; use crate::profile::{DFTProfile, MAX_POTENTIAL}; use crate::solver::DFTSolver; +use crate::{DFTSpecification, WeightFunctionInfo}; use feos_core::{Contributions, FeosResult, ReferenceSystem, ResidualDyn, State, StateHD}; use nalgebra::{DVector, dvector}; use ndarray::prelude::*; @@ -13,8 +13,8 @@ use ndarray::{Axis as Axis_nd, RemoveAxis}; use num_dual::linalg::LU; use num_dual::{Dual64, DualNum}; use quantity::{ - _Moles, _Pressure, Density, Dimensionless, Energy, KELVIN, Length, MolarEnergy, Quantity, RGAS, - Temperature, Volume, + _Moles, _Pressure, Density, Dimensionless, Energy, KELVIN, Length, MolarEnergy, Moles, + Quantity, RGAS, Temperature, Volume, }; use rustdct::DctNum; use std::ops::Sub; @@ -52,14 +52,25 @@ impl Pore1D { } } +/// Different ways that the thermodynamic state of the fluid in the pore +/// can be specified. +#[derive(Clone)] +pub enum PoreSpecification { + /// Specify the chemical potential (via the bulk state). + ChemicalPotential, + /// Specify the amount of moles of every component. + Moles(Moles>), +} + /// Trait for the generic implementation of adsorption applications. -pub trait PoreSpecification { +pub trait Pore { /// Initialize a new single pore. fn initialize( &self, bulk: &State, density: Option<&Density>>, external_potential: Option<&Array>, + specification: PoreSpecification, ) -> FeosResult>; /// Return the pore volume using Helium at 298 K as reference. @@ -68,7 +79,7 @@ pub trait PoreSpecification { D::Larger: Dimension, { let bulk = State::new_pure(&&Helium, 298.0 * KELVIN, Density::from_reduced(1.0))?; - let pore = self.initialize(&bulk, None, None)?; + let pore = self.initialize(&bulk, None, None, PoreSpecification::ChemicalPotential)?; let pot = Dimensionless::from_reduced( pore.profile .external_potential @@ -96,6 +107,27 @@ where D::Smaller: Dimension, ::Larger: Dimension, { + pub fn new( + grid: Grid, + bulk: &State, + external_potential: Option>, + density: Option<&Density>>, + specification: PoreSpecification, + ) -> Self { + let mut profile = DFTProfile::new(grid, bulk, external_potential, density, Some(1)); + + // fix the number of particles + if let PoreSpecification::Moles(moles) = specification { + profile.specification = DFTSpecification::Moles(moles.to_reduced()) + } + + Self { + profile, + grand_potential: None, + interfacial_tension: None, + } + } + pub fn solve_inplace(&mut self, solver: Option<&DFTSolver>, debug: bool) -> FeosResult<()> { // Solve the profile self.profile.solve(solver, debug)?; @@ -180,12 +212,13 @@ where } } -impl PoreSpecification for Pore1D { +impl Pore for Pore1D { fn initialize( &self, bulk: &State, density: Option<&Density>>, external_potential: Option<&Array2>, + specification: PoreSpecification, ) -> FeosResult> { let dft: &F = &bulk.eos; let n_grid = self.n_grid.unwrap_or(DEFAULT_GRID_POINTS); @@ -223,11 +256,13 @@ impl PoreSpecification for Pore1D { // initialize grid let grid = Grid::new_1d(axis); - Ok(PoreProfile { - profile: DFTProfile::new(grid, bulk, Some(external_potential), density, Some(1)), - grand_potential: None, - interfacial_tension: None, - }) + Ok(PoreProfile::new( + grid, + bulk, + Some(external_potential), + density, + specification, + )) } } diff --git a/crates/feos-dft/src/adsorption/pore2d.rs b/crates/feos-dft/src/adsorption/pore2d.rs index 4c5dad7e8..f7290c6e3 100644 --- a/crates/feos-dft/src/adsorption/pore2d.rs +++ b/crates/feos-dft/src/adsorption/pore2d.rs @@ -1,5 +1,5 @@ -use super::{FluidParameters, PoreProfile, PoreSpecification}; -use crate::{Axis, DFTProfile, Grid, HelmholtzEnergyFunctional}; +use super::{FluidParameters, Pore, PoreProfile}; +use crate::{Axis, Grid, HelmholtzEnergyFunctional, adsorption::pore::PoreSpecification}; use feos_core::{FeosResult, State}; use ndarray::{Array3, Ix2}; use quantity::{Angle, Density, Length}; @@ -22,22 +22,25 @@ impl Pore2D { } } -impl PoreSpecification for Pore2D { +impl Pore for Pore2D { fn initialize( &self, bulk: &State, density: Option<&Density>>, external_potential: Option<&Array3>, + specification: PoreSpecification, ) -> FeosResult> { // generate grid let x = Axis::new_cartesian(self.n_grid[0], self.system_size[0], None); let y = Axis::new_cartesian(self.n_grid[1], self.system_size[1], None); let grid = Grid::Periodical2(x, y, self.angle); - Ok(PoreProfile { - profile: DFTProfile::new(grid, bulk, external_potential.cloned(), density, Some(1)), - grand_potential: None, - interfacial_tension: None, - }) + Ok(PoreProfile::new( + grid, + bulk, + external_potential.cloned(), + density, + specification, + )) } } diff --git a/crates/feos-dft/src/adsorption/pore3d.rs b/crates/feos-dft/src/adsorption/pore3d.rs index 51743f9eb..bfbfd38df 100644 --- a/crates/feos-dft/src/adsorption/pore3d.rs +++ b/crates/feos-dft/src/adsorption/pore3d.rs @@ -1,8 +1,8 @@ -use super::pore::{PoreProfile, PoreSpecification}; -use crate::adsorption::FluidParameters; +use super::pore::{PoreProfile, Pore}; +use crate::adsorption::{FluidParameters, PoreSpecification}; use crate::functional::HelmholtzEnergyFunctional; use crate::geometry::{Axis, Grid}; -use crate::profile::{CUTOFF_RADIUS, DFTProfile, MAX_POTENTIAL}; +use crate::profile::{CUTOFF_RADIUS, MAX_POTENTIAL}; use feos_core::{FeosError, FeosResult, ReferenceSystem, State}; use ndarray::Zip; use ndarray::prelude::*; @@ -48,12 +48,13 @@ impl Pore3D { /// Density profile and properties of a 3D confined system. pub type PoreProfile3D = PoreProfile; -impl PoreSpecification for Pore3D { +impl Pore for Pore3D { fn initialize( &self, bulk: &State, density: Option<&Density>>, external_potential: Option<&Array4>, + specification: PoreSpecification, ) -> FeosResult> { let dft: &F = &bulk.eos; @@ -94,11 +95,13 @@ impl PoreSpecification for Pore3D { )?; let grid = Grid::Periodical3(x, y, z, self.angles.unwrap_or([90.0 * DEGREES; 3])); - Ok(PoreProfile { - profile: DFTProfile::new(grid, bulk, Some(external_potential), density, Some(1)), - grand_potential: None, - interfacial_tension: None, - }) + Ok(PoreProfile::new( + grid, + bulk, + Some(external_potential), + density, + specification, + )) } } diff --git a/crates/feos-dft/src/interface/mod.rs b/crates/feos-dft/src/interface/mod.rs index 1e9f02b99..da1ec8881 100644 --- a/crates/feos-dft/src/interface/mod.rs +++ b/crates/feos-dft/src/interface/mod.rs @@ -2,12 +2,11 @@ use crate::functional::HelmholtzEnergyFunctional; use crate::geometry::{Axis, Grid}; use crate::pdgt::PdgtFunctionalProperties; -use crate::profile::{DFTProfile, DFTSpecifications}; +use crate::profile::DFTProfile; use crate::solver::DFTSolver; use feos_core::{Contributions, FeosError, FeosResult, PhaseEquilibrium, ReferenceSystem}; use ndarray::{Array1, Array2, Axis as Axis_nd, Ix1, s}; use quantity::{Area, Density, Length, Moles, SurfaceTension, Temperature}; -use std::sync::Arc; mod surface_tension_diagram; pub use surface_tension_diagram::SurfaceTensionDiagram; @@ -95,9 +94,7 @@ impl PlanarInterface { // specify specification if fix_equimolar_surface { - profile.profile.specification = Arc::new(DFTSpecifications::total_moles_from_profile( - &profile.profile, - )); + profile.profile.fix_total_moles(); } profile @@ -145,9 +142,7 @@ impl PlanarInterface { // specify specification if fix_equimolar_surface { - profile.profile.specification = Arc::new(DFTSpecifications::total_moles_from_profile( - &profile.profile, - )); + profile.profile.fix_total_moles(); } Ok(profile) diff --git a/crates/feos-dft/src/lib.rs b/crates/feos-dft/src/lib.rs index eea9f5bb4..d9e93f99a 100644 --- a/crates/feos-dft/src/lib.rs +++ b/crates/feos-dft/src/lib.rs @@ -19,6 +19,6 @@ pub use functional::{HelmholtzEnergyFunctional, HelmholtzEnergyFunctionalDyn, Mo pub use functional_contribution::FunctionalContribution; pub use geometry::{Axis, Geometry, Grid}; pub use pdgt::PdgtFunctionalProperties; -pub use profile::{DFTProfile, DFTSpecification, DFTSpecifications}; +pub use profile::{DFTProfile, DFTSpecification}; pub use solver::{DFTSolver, DFTSolverLog}; pub use weight_functions::{WeightFunction, WeightFunctionInfo, WeightFunctionShape}; diff --git a/crates/feos-dft/src/profile/mod.rs b/crates/feos-dft/src/profile/mod.rs index 941a3818b..26b1d0d94 100644 --- a/crates/feos-dft/src/profile/mod.rs +++ b/crates/feos-dft/src/profile/mod.rs @@ -22,19 +22,10 @@ pub(crate) const CUTOFF_RADIUS: f64 = 14.0; /// General specifications for the chemical potential in a DFT calculation. /// /// In the most basic case, the chemical potential is specified in a DFT calculation, -/// for more general systems, this trait provides the possibility to declare additional +/// for more general systems, this enum provides the possibility to declare additional /// equations for the calculation of the chemical potential during the iteration. -pub trait DFTSpecification: Send + Sync { - fn calculate_bulk_density( - &self, - profile: &DFTProfile, - bulk_density: &Array1, - z: &Array1, - ) -> FeosResult>; -} - -/// Common specifications for the grand potentials in a DFT calculation. -pub enum DFTSpecifications { +#[derive(Clone)] +pub enum DFTSpecification { /// DFT with specified chemical potential. ChemicalPotential, /// DFT with specified number of particles. @@ -42,57 +33,21 @@ pub enum DFTSpecifications { /// The solution is still a grand canonical density profile, but the chemical /// potentials are iterated together with the density profile to obtain a result /// with the specified number of particles. - Moles { moles: Array1 }, + Moles(Array1), /// DFT with specified total number of moles. - TotalMoles { total_moles: f64 }, -} - -impl DFTSpecifications { - /// Calculate the number of particles from the profile. - /// - /// Call this after initializing the density profile to keep the number of - /// particles constant in systems, where the number itself is difficult to obtain. - pub fn moles_from_profile( - profile: &DFTProfile, - ) -> Self - where - D::Larger: Dimension, - { - let rho = profile.density.to_reduced(); - Self::Moles { - moles: profile.integrate_reduced_comp(&rho), - } - } - - /// Calculate the number of particles from the profile. - /// - /// Call this after initializing the density profile to keep the total number of - /// particles constant in systems, e.g. to fix the equimolar dividing surface. - pub fn total_moles_from_profile( - profile: &DFTProfile, - ) -> Self - where - D::Larger: Dimension, - { - let rho = profile.density.to_reduced(); - let moles = profile.integrate_reduced_comp(&rho).sum(); - Self::TotalMoles { total_moles: moles } - } + TotalMoles(f64), } -impl DFTSpecification for DFTSpecifications { +impl DFTSpecification { fn calculate_bulk_density( &self, - _profile: &DFTProfile, bulk_density: &Array1, z: &Array1, ) -> FeosResult> { Ok(match self { Self::ChemicalPotential => bulk_density.clone(), - Self::Moles { moles } => moles / z, - Self::TotalMoles { total_moles } => { - bulk_density * *total_moles / (bulk_density * z).sum() - } + Self::Moles(moles) => moles / z, + Self::TotalMoles(total_moles) => bulk_density * *total_moles / (bulk_density * z).sum(), }) } } @@ -104,7 +59,7 @@ pub struct DFTProfile { pub convolver: Arc>, pub temperature: Temperature, pub density: Density>, - pub specification: Arc>, + pub specification: DFTSpecification, pub external_potential: Array, pub bulk: State, pub solver_log: Option, @@ -246,13 +201,28 @@ where convolver, temperature: bulk.temperature, density, - specification: Arc::new(DFTSpecifications::ChemicalPotential), + specification: DFTSpecification::ChemicalPotential, external_potential, bulk: bulk.clone(), solver_log: None, lanczos, } } + + /// Set a constraint to fix the number of particles of every component based on + /// the current density profile. + pub fn fix_moles(&mut self) { + let moles = self.integrate_reduced_comp(&self.density.to_reduced()); + self.specification = DFTSpecification::Moles(moles); + } + + /// Set a constraint to fix the total number of particles based on + /// the current density profile. + pub fn fix_total_moles(&mut self) { + let rho = self.density.to_reduced(); + let moles = self.integrate_reduced_comp(&rho).sum(); + self.specification = DFTSpecification::TotalMoles(moles); + } } impl DFTProfile @@ -374,7 +344,7 @@ where // Read from profile let density = self.density.to_reduced(); let partial_density = self.bulk.partial_density().into_reduced(); - let bulk_density = self + let mut bulk_density = self .bulk .eos .component_index() @@ -383,7 +353,7 @@ where .collect(); let (res, res_bulk, res_norm, _, _) = - self.euler_lagrange_equation(&density, &bulk_density, log)?; + self.euler_lagrange_equation(&density, &mut bulk_density, log)?; Ok((res, res_bulk, res_norm)) } @@ -391,7 +361,7 @@ where pub(crate) fn euler_lagrange_equation( &self, density: &Array, - bulk_density: &Array1, + bulk_density: &mut Array1, log: bool, ) -> FeosResult<( Array, @@ -436,6 +406,14 @@ where .bond_integrals(temperature, &exp_dfdrho, self.convolver.as_ref()); let mut rho_projected = &exp_dfdrho * bonds; + // calculate bulk density based on the given specification + let res_bulk = &*bulk_density + - self.specification.calculate_bulk_density( + bulk_density, + &self.integrate_reduced_comp(&rho_projected), + )?; + *bulk_density -= &res_bulk; + // multiply bulk density rho_projected .outer_iter_mut() @@ -457,18 +435,9 @@ where .filter(|&(_, &p)| p + f64::EPSILON >= MAX_POTENTIAL) .for_each(|(r, _)| *r = 0.0); - // additional residuals for the calculation of the bulk densities - let z = self.integrate_reduced_comp(&rho_projected); - let res_bulk = bulk_density - - self - .specification - .calculate_bulk_density(self, bulk_density, &z)?; - // calculate the norm of the residual - let res_norm = ((density - &rho_projected).mapv(|x| x * x).sum() - + res_bulk.mapv(|x| x * x).sum()) - .sqrt() - / ((res.len() + res_bulk.len()) as f64).sqrt(); + let res_norm = + (density - &rho_projected).mapv(|x| x * x).sum().sqrt() / (res.len() as f64).sqrt(); if res_norm.is_finite() { Ok((res, res_bulk, res_norm, exp_dfdrho, rho_projected)) @@ -501,7 +470,6 @@ where .enumerate() .for_each(|(i, r)| partial_density.set(component_index[i], Density::from_reduced(r))); self.bulk = State::new_density(&self.bulk.eos, self.bulk.temperature, partial_density)?; - Ok(()) } } diff --git a/crates/feos-dft/src/profile/properties.rs b/crates/feos-dft/src/profile/properties.rs index d542b27da..2fe6a92a9 100644 --- a/crates/feos-dft/src/profile/properties.rs +++ b/crates/feos-dft/src/profile/properties.rs @@ -299,7 +299,7 @@ where fn density_derivative(&self, lhs: &Array) -> FeosResult> { let rho = self.density.to_reduced(); let partial_density = self.bulk.partial_density().into_reduced(); - let rho_bulk = self + let mut rho_bulk = self .bulk .eos .component_index() @@ -308,7 +308,7 @@ where .collect(); let second_partial_derivatives = self.second_partial_derivatives(&rho)?; - let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &rho_bulk, false)?; + let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &mut rho_bulk, false)?; let rhs = |x: &_| { let delta_functional_derivative = diff --git a/crates/feos-dft/src/solver.rs b/crates/feos-dft/src/solver.rs index ed915d91b..69d2e2757 100644 --- a/crates/feos-dft/src/solver.rs +++ b/crates/feos-dft/src/solver.rs @@ -266,8 +266,8 @@ where for k in 0..picard.max_iter { // calculate residual - let (res, res_bulk, res_norm, _, _) = - self.euler_lagrange_equation(&*rho, &*rho_bulk, picard.log)?; + let (res, _, res_norm, _, _) = + self.euler_lagrange_equation(&*rho, rho_bulk, picard.log)?; log.add_residual(solver, k, res_norm); // check for convergence @@ -284,10 +284,8 @@ where // update solution if picard.log { *rho *= &(&res * damping_coefficient).mapv(f64::exp); - *rho_bulk *= &(&res_bulk * damping_coefficient).mapv(f64::exp); } else { *rho += &(&res * damping_coefficient); - *rho_bulk += &(&res_bulk * damping_coefficient); } } Ok((false, picard.max_iter)) @@ -297,7 +295,7 @@ where &self, rho: &Array, delta_rho: &Array, - rho_bulk: &Array1, + rho_bulk: &mut Array1, res0: f64, logarithm: bool, ) -> FeosResult { @@ -384,8 +382,8 @@ where let m = resm.len() + 1; // calculate residual - let (res, res_bulk, res_norm, _, _) = - self.euler_lagrange_equation(&*rho, &*rho_bulk, anderson.log)?; + let (res, _, res_norm, _, _) = + self.euler_lagrange_equation(&*rho, rho_bulk, anderson.log)?; log.add_residual(solver, k, res_norm); // check for convergence @@ -394,19 +392,19 @@ where } // save residual and x value - resm.push_back((res, res_bulk, res_norm)); + resm.push_back((res, res_norm)); if anderson.log { - rhom.push_back((rho.mapv(f64::ln), rho_bulk.mapv(f64::ln))); + rhom.push_back(rho.mapv(f64::ln)); } else { - rhom.push_back((rho.clone(), rho_bulk.clone())); + rhom.push_back(rho.clone()); } // calculate alpha r = DMatrix::from_fn(m + 1, m + 1, |i, j| match (i == m, j == m) { (false, false) => { - let (resi, resi_bulk, _) = &resm[i]; - let (resj, resj_bulk, _) = &resm[j]; - (resi * resj).sum() + (resi_bulk * resj_bulk).sum() + let (resi, _) = &resm[i]; + let (resj, _) = &resm[j]; + (resi * resj).sum() } (true, true) => 0.0, _ => 1.0, @@ -418,20 +416,15 @@ where // update solution rho.fill(0.0); - rho_bulk.fill(0.0); for i in 0..m { - let (rhoi, rhoi_bulk) = &rhom[i]; - let (resi, resi_bulk, _) = &resm[i]; + let rhoi = &rhom[i]; + let (resi, _) = &resm[i]; *rho += &(alpha[i] * (rhoi + &(anderson.damping_coefficient * resi))); - *rho_bulk += - &(alpha[i] * (rhoi_bulk + &(anderson.damping_coefficient * resi_bulk))); } if anderson.log { rho.mapv_inplace(f64::exp); - rho_bulk.mapv_inplace(f64::exp); } else { rho.mapv_inplace(f64::abs); - rho_bulk.mapv_inplace(f64::abs); } } Ok((false, anderson.max_iter)) @@ -476,7 +469,6 @@ where let lhs = if newton.log { &*rho * res } else { res }; *rho += &Self::gmres(rhs, &lhs, newton.max_iter_gmres, newton.tol * 1e-2, log)?; rho.mapv_inplace(f64::abs); - rho_bulk.mapv_inplace(f64::abs); } Ok((false, newton.max_iter)) diff --git a/crates/feos/benches/dft_pore.rs b/crates/feos/benches/dft_pore.rs index 4c5b3d7ba..dfa2b9f58 100644 --- a/crates/feos/benches/dft_pore.rs +++ b/crates/feos/benches/dft_pore.rs @@ -3,7 +3,9 @@ use criterion::{Criterion, criterion_group, criterion_main}; use feos::core::parameter::IdentifierOption; use feos::core::{PhaseEquilibrium, State}; -use feos::dft::adsorption::{ExternalPotential, Pore1D, PoreSpecification}; +use feos::dft::adsorption::{ + ExternalPotential, Pore, Pore1D, PoreSpecification::ChemicalPotential, +}; use feos::dft::{DFTSolver, Geometry}; use feos::gc_pcsaft::{GcPcSaftFunctional, GcPcSaftParameters}; use feos::hard_sphere::{FMTFunctional, FMTVersion}; @@ -24,7 +26,11 @@ fn fmt(c: &mut Criterion) { ); let bulk = State::new_pure(&func, KELVIN, 0.75 / NAV / ANGSTROM.powi::<3>()).unwrap(); group.bench_function("liquid", |b| { - b.iter(|| pore.initialize(&bulk, None, None).unwrap().solve(None)) + b.iter(|| { + pore.initialize(&bulk, None, None, ChemicalPotential) + .unwrap() + .solve(None) + }) }); } @@ -52,11 +58,19 @@ fn pcsaft(c: &mut Criterion) { let vle = PhaseEquilibrium::pure(&func, 300.0 * KELVIN, None, Default::default()).unwrap(); let bulk = vle.liquid(); group.bench_function("butane_liquid", |b| { - b.iter(|| pore.initialize(bulk, None, None).unwrap().solve(None)) + b.iter(|| { + pore.initialize(bulk, None, None, ChemicalPotential) + .unwrap() + .solve(None) + }) }); let bulk = State::new_pure(&func, 300.0 * KELVIN, vle.vapor().density * 0.2).unwrap(); group.bench_function("butane_vapor", |b| { - b.iter(|| pore.initialize(&bulk, None, None).unwrap().solve(None)) + b.iter(|| { + pore.initialize(&bulk, None, None, ChemicalPotential) + .unwrap() + .solve(None) + }) }); let parameters = PcSaftParameters::from_json( @@ -72,12 +86,20 @@ fn pcsaft(c: &mut Criterion) { .unwrap(); let bulk = vle.liquid(); group.bench_function("butane_pentane_liquid", |b| { - b.iter(|| pore.initialize(bulk, None, None).unwrap().solve(None)) + b.iter(|| { + pore.initialize(bulk, None, None, ChemicalPotential) + .unwrap() + .solve(None) + }) }); let bulk = State::new_density(&func, 300.0 * KELVIN, vle.vapor().partial_density() * 0.2).unwrap(); group.bench_function("butane_pentane_vapor", |b| { - b.iter(|| pore.initialize(&bulk, None, None).unwrap().solve(None)) + b.iter(|| { + pore.initialize(&bulk, None, None, ChemicalPotential) + .unwrap() + .solve(None) + }) }); } @@ -112,7 +134,7 @@ fn gc_pcsaft(c: &mut Criterion) { .anderson_mixing(None, None, None, None, None); group.bench_function("butane_liquid", |b| { b.iter(|| { - pore.initialize(bulk, None, None) + pore.initialize(bulk, None, None, ChemicalPotential) .unwrap() .solve(Some(&solver)) }) diff --git a/crates/feos/tests/gc_pcsaft/dft.rs b/crates/feos/tests/gc_pcsaft/dft.rs index ce5d103cb..9238500d0 100644 --- a/crates/feos/tests/gc_pcsaft/dft.rs +++ b/crates/feos/tests/gc_pcsaft/dft.rs @@ -4,7 +4,7 @@ use approx::assert_relative_eq; use feos::gc_pcsaft::{GcPcSaft, GcPcSaftFunctional, GcPcSaftParameters}; use feos_core::parameter::{ChemicalRecord, Identifier, IdentifierOption, SegmentRecord}; use feos_core::{PhaseEquilibrium, State, Verbosity}; -use feos_dft::adsorption::{ExternalPotential, Pore1D, PoreSpecification}; +use feos_dft::adsorption::{ExternalPotential, Pore, Pore1D, PoreSpecification}; use feos_dft::interface::PlanarInterface; use feos_dft::{DFTSolver, Geometry}; use nalgebra::dvector; @@ -228,7 +228,7 @@ fn test_dft_assoc() -> Result<(), Box> { None, None, ) - .initialize(&bulk, None, None) + .initialize(&bulk, None, None, PoreSpecification::ChemicalPotential) .unwrap() .solve(Some(&solver))?; Ok(()) diff --git a/py-feos/src/dft/adsorption/mod.rs b/py-feos/src/dft/adsorption/mod.rs index 83f454811..a932ccc44 100644 --- a/py-feos/src/dft/adsorption/mod.rs +++ b/py-feos/src/dft/adsorption/mod.rs @@ -5,9 +5,9 @@ use crate::error::PyFeosError; use crate::ideal_gas::IdealGasModel; use crate::residual::ResidualModel; use feos_core::EquationOfState; -use feos_dft::adsorption::{Adsorption, Adsorption1D}; #[cfg(feature = "rayon")] use feos_dft::adsorption::Adsorption3D; +use feos_dft::adsorption::{Adsorption, Adsorption1D}; use nalgebra::DMatrix; use ndarray::*; use numpy::*; @@ -19,7 +19,7 @@ mod external_potential; mod pore; pub use external_potential::PyExternalPotential; -pub use pore::{PyPore1D, PyPore2D, PyPoreProfile1D}; +pub use pore::{PyPore1D, PyPore2D, PyPoreProfile1D, PyPoreSpecification}; #[cfg(feature = "rayon")] pub use pore::{PyPore3D, PyPoreProfile3D}; diff --git a/py-feos/src/dft/adsorption/pore.rs b/py-feos/src/dft/adsorption/pore.rs index 6e51ca02d..7668bebfb 100644 --- a/py-feos/src/dft/adsorption/pore.rs +++ b/py-feos/src/dft/adsorption/pore.rs @@ -56,6 +56,29 @@ macro_rules! impl_pore_profile { }; } +/// Different ways that the thermodynamic state of the fluid in the pore +/// can be specified. +#[pyclass(name = "PoreSpecification", from_py_object)] +#[derive(Clone)] +pub struct PyPoreSpecification(PoreSpecification); + +#[pymethods] +impl PyPoreSpecification { + /// Specify the chemical potential (via the bulk state). + #[classattr] + #[expect(non_snake_case)] + fn ChemicalPotential() -> Self { + Self(PoreSpecification::ChemicalPotential) + } + + /// Specify the amount of moles of every component. + #[staticmethod] + #[expect(non_snake_case)] + fn Moles(moles: Moles>) -> Self { + Self(PoreSpecification::Moles(moles)) + } +} + /// Parameters required to specify a 1D pore. /// /// Parameters @@ -120,17 +143,23 @@ impl PyPore1D { /// The external potential in the pore. Used to /// save computation time in the case of costly /// evaluations of external potentials. + /// specification : PoreSpecification + /// The external constraint that specifies the state + /// in the pore. /// /// Returns /// ------- /// PoreProfile1D - #[pyo3(text_signature = "($self, bulk, density=None, external_potential=None)")] - #[pyo3(signature = (bulk, density=None, external_potential=None))] + #[pyo3( + text_signature = "($self, bulk, density=None, external_potential=None, specification=PoreSpecification.ChemicalPotential)" + )] + #[pyo3(signature = (bulk, density=None, external_potential=None, specification=PyPoreSpecification::ChemicalPotential()))] fn initialize( &self, bulk: &PyState, density: Option>>, external_potential: Option<&Bound<'_, PyArray2>>, + specification: PyPoreSpecification, ) -> PyResult { Ok(PyPoreProfile1D( self.0 @@ -138,6 +167,7 @@ impl PyPore1D { &bulk.0, density.as_ref(), external_potential.map(|e| e.to_owned_array()).as_ref(), + specification.0, ) .map_err(PyFeosError::from)?, )) @@ -210,17 +240,23 @@ impl PyPore2D { /// The external potential in the pore. Used to /// save computation time in the case of costly /// evaluations of external potentials. + /// specification : PoreSpecification + /// The external constraint that specifies the state + /// in the pore. /// /// Returns /// ------- /// PoreProfile2D - #[pyo3(text_signature = "($self, bulk, density=None, external_potential=None)")] - #[pyo3(signature = (bulk, density=None, external_potential=None))] + #[pyo3( + text_signature = "($self, bulk, density=None, external_potential=None, specification=PoreSpecification.ChemicalPotential)" + )] + #[pyo3(signature = (bulk, density=None, external_potential=None, specification=PyPoreSpecification::ChemicalPotential()))] fn initialize( &self, bulk: &PyState, density: Option>>, external_potential: Option<&Bound<'_, PyArray3>>, + specification: PyPoreSpecification, ) -> PyResult { Ok(PyPoreProfile2D( self.0 @@ -228,6 +264,7 @@ impl PyPore2D { &bulk.0, density.as_ref(), external_potential.map(|e| e.to_owned_array()).as_ref(), + specification.0, ) .map_err(PyFeosError::from)?, )) @@ -326,17 +363,23 @@ impl PyPore3D { /// The external potential in the pore. Used to /// save computation time in the case of costly /// evaluations of external potentials. + /// specification : PoreSpecification + /// The external constraint that specifies the state + /// in the pore. /// /// Returns /// ------- /// PoreProfile3D - #[pyo3(text_signature = "($self, bulk, density=None, external_potential=None)")] - #[pyo3(signature = (bulk, density=None, external_potential=None))] + #[pyo3( + text_signature = "($self, bulk, density=None, external_potential=None, specification=PoreSpecification.ChemicalPotential)" + )] + #[pyo3(signature = (bulk, density=None, external_potential=None, specification=PyPoreSpecification::ChemicalPotential()))] fn initialize( &self, bulk: &PyState, density: Option>>, external_potential: Option<&Bound<'_, PyArray4>>, + specification: PyPoreSpecification, ) -> PyResult { Ok(PyPoreProfile3D( self.0 @@ -344,6 +387,7 @@ impl PyPore3D { &bulk.0, density.as_ref(), external_potential.map(|e| e.to_owned_array()).as_ref(), + specification.0, ) .map_err(PyFeosError::from)?, )) diff --git a/py-feos/src/dft/mod.rs b/py-feos/src/dft/mod.rs index 00d027531..36ea92bb9 100644 --- a/py-feos/src/dft/mod.rs +++ b/py-feos/src/dft/mod.rs @@ -15,7 +15,9 @@ mod profile; mod solvation; mod solver; -pub(crate) use adsorption::{PyAdsorption1D, PyExternalPotential, PyPore1D, PyPore2D}; +pub(crate) use adsorption::{ + PyAdsorption1D, PyExternalPotential, PyPore1D, PyPore2D, PyPoreSpecification, +}; // 3D pore / adsorption bindings wrap rayon-gated feos_dft types. #[cfg(feature = "rayon")] pub(crate) use adsorption::{PyAdsorption3D, PyPore3D}; @@ -117,24 +119,3 @@ impl PyHelmholtzEnergyFunctional { )))) } } - -// #[pymodule] -// pub fn dft(m: &Bound<'_, PyModule>) -> PyResult<()> { -// m.add_class::()?; -// m.add_class::()?; - -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; -// m.add_class::()?; - -// Ok(()) -// } diff --git a/py-feos/src/lib.rs b/py-feos/src/lib.rs index 6e8f79644..a164fece0 100644 --- a/py-feos/src/lib.rs +++ b/py-feos/src/lib.rs @@ -203,6 +203,7 @@ fn feos(m: &Bound<'_, PyModule>) -> PyResult<()> { // Adsorption m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; From 50028e4a58ee3cd4fe4e90d5065ea5d61c867c5d Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:08:52 +0200 Subject: [PATCH 3/6] Pass external potentials as quantities and always cap them consistently (#372) --- CHANGELOG.md | 1 + crates/feos-derive/src/dft.rs | 2 +- .../feos-dft/src/adsorption/fea_potential.rs | 4 +- crates/feos-dft/src/adsorption/mod.rs | 2 +- crates/feos-dft/src/adsorption/pore.rs | 24 ++++-------- crates/feos-dft/src/adsorption/pore2d.rs | 4 +- crates/feos-dft/src/adsorption/pore3d.rs | 32 +++------------- crates/feos-dft/src/profile/mod.rs | 38 ++++++++++++++----- .../src/solvation/pair_correlation.rs | 12 ++---- .../src/solvation/solvation_profile.rs | 24 ++---------- crates/feos/src/hard_sphere/dft.rs | 7 ++-- crates/feos/src/pcsaft/dft/mod.rs | 17 +++++---- crates/feos/src/pets/dft/mod.rs | 24 +++++++----- crates/feos/src/saftvrqmie/dft/mod.rs | 12 +++--- py-feos/src/dft/adsorption/pore.rs | 25 ++++++------ py-feos/src/dft/profile.rs | 6 +-- py-feos/src/dft/solvation.rs | 7 +--- 17 files changed, 106 insertions(+), 135 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c507355..fe08b8fe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Changed data type of initial temperatures or pressure for phase equilibrium calculations (`TemperatureOrPressure::Other`) from `D` to `f64`. [#369](https://github.com/feos-org/feos/pull/369) - Reworked DFT solution algorithms slightly for the cases in which additional specifications are given. [#371](https://github.com/feos-org/feos/pull/371) +- External potentials are passed and returned as quantities (energies) instead of reduced units. [#372](https://github.com/feos-org/feos/pull/372) ### Removed - Removed the `DFTSpecification` trait in favor of only using the `DFTSpecification` enum (renamed from `DFTSpecifications`). [#371](https://github.com/feos-org/feos/pull/371) diff --git a/crates/feos-derive/src/dft.rs b/crates/feos-derive/src/dft.rs index d7dcd8e63..e5dd7a343 100644 --- a/crates/feos-derive/src/dft.rs +++ b/crates/feos-derive/src/dft.rs @@ -139,7 +139,7 @@ fn impl_pair_potential( } Ok(quote! { impl feos_dft::solvation::PairPotential for #ident { - fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> ndarray::Array2 { + fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> quantity::Energy> { match self { #(#pair_potential,)* } diff --git a/crates/feos-dft/src/adsorption/fea_potential.rs b/crates/feos-dft/src/adsorption/fea_potential.rs index d05ed6a01..f67e583f1 100644 --- a/crates/feos-dft/src/adsorption/fea_potential.rs +++ b/crates/feos-dft/src/adsorption/fea_potential.rs @@ -1,6 +1,6 @@ use super::pore3d::{calculate_distance2, evaluate_lj_potential}; use crate::Geometry; -use crate::profile::{CUTOFF_RADIUS, MAX_POTENTIAL}; +use crate::profile::CUTOFF_RADIUS; use feos_core::ReferenceSystem; use gauss_quad::GaussLegendre; use ndarray::{Array1, Array2, Zip}; @@ -131,7 +131,7 @@ pub fn calculate_fea_potential( ) / temperature }) .sum(); - potential_2d[[i1, i2]] = (-potential_sum.min(MAX_POTENTIAL)).exp(); + potential_2d[[i1, i2]] = (-potential_sum).exp(); } } *f = (potential_2d * &weights).sum(); diff --git a/crates/feos-dft/src/adsorption/mod.rs b/crates/feos-dft/src/adsorption/mod.rs index 463989f67..3d11c4275 100644 --- a/crates/feos-dft/src/adsorption/mod.rs +++ b/crates/feos-dft/src/adsorption/mod.rs @@ -214,7 +214,7 @@ where .initialize(&bulk, None, None, PoreSpecification::ChemicalPotential)? .solve(solver)? .profile; - let external_potential = Some(&profile.external_potential); + let external_potential = Some(&profile.external_potential()); let mut old_density = Some(&profile.density); for i in 0..pressure.len() { diff --git a/crates/feos-dft/src/adsorption/pore.rs b/crates/feos-dft/src/adsorption/pore.rs index 5db29b1f7..493229289 100644 --- a/crates/feos-dft/src/adsorption/pore.rs +++ b/crates/feos-dft/src/adsorption/pore.rs @@ -3,7 +3,7 @@ use crate::convolver::ConvolverFFT; use crate::functional::{HelmholtzEnergyFunctional, HelmholtzEnergyFunctionalDyn, MoleculeShape}; use crate::functional_contribution::FunctionalContribution; use crate::geometry::{Axis, Geometry, Grid}; -use crate::profile::{DFTProfile, MAX_POTENTIAL}; +use crate::profile::DFTProfile; use crate::solver::DFTSolver; use crate::{DFTSpecification, WeightFunctionInfo}; use feos_core::{Contributions, FeosResult, ReferenceSystem, ResidualDyn, State, StateHD}; @@ -69,7 +69,7 @@ pub trait Pore { &self, bulk: &State, density: Option<&Density>>, - external_potential: Option<&Array>, + external_potential: Option<&Energy>>, specification: PoreSpecification, ) -> FeosResult>; @@ -110,7 +110,7 @@ where pub fn new( grid: Grid, bulk: &State, - external_potential: Option>, + external_potential: Option>>, density: Option<&Density>>, specification: PoreSpecification, ) -> Self { @@ -217,7 +217,7 @@ impl Pore for Pore1D { &self, bulk: &State, density: Option<&Density>>, - external_potential: Option<&Array2>, + external_potential: Option<&Energy>>, specification: PoreSpecification, ) -> FeosResult> { let dft: &F = &bulk.eos; @@ -247,7 +247,6 @@ impl Pore for Pore1D { &self.potential, dft, &axis, - self.potential_cutoff, ) }, |e| e.clone(), @@ -272,9 +271,7 @@ fn external_potential_1d( potential: &ExternalPotential, fluid_parameters: &P, axis: &Axis, - potential_cutoff: Option, -) -> Array2 { - let potential_cutoff = potential_cutoff.unwrap_or(MAX_POTENTIAL); +) -> Energy> { let effective_pore_size = match axis.geometry { Geometry::Spherical => pore_width.to_reduced(), Geometry::Cylindrical => pore_width.to_reduced(), @@ -305,21 +302,16 @@ fn external_potential_1d( fluid_parameters, t, ), - } / t; + }; for (i, &z) in axis.grid.iter().enumerate() { if z > effective_pore_size { external_potential .index_axis_mut(Axis_nd(1), i) - .fill(potential_cutoff); + .fill(f64::INFINITY); } } - external_potential.map_inplace(|x| { - if *x > potential_cutoff { - *x = potential_cutoff - } - }); - external_potential + Energy::from_reduced(external_potential) } const EPSILON_HE: f64 = 10.9; diff --git a/crates/feos-dft/src/adsorption/pore2d.rs b/crates/feos-dft/src/adsorption/pore2d.rs index f7290c6e3..396434187 100644 --- a/crates/feos-dft/src/adsorption/pore2d.rs +++ b/crates/feos-dft/src/adsorption/pore2d.rs @@ -2,7 +2,7 @@ use super::{FluidParameters, Pore, PoreProfile}; use crate::{Axis, Grid, HelmholtzEnergyFunctional, adsorption::pore::PoreSpecification}; use feos_core::{FeosResult, State}; use ndarray::{Array3, Ix2}; -use quantity::{Angle, Density, Length}; +use quantity::{Angle, Density, Energy, Length}; pub struct Pore2D { system_size: [Length; 2], @@ -27,7 +27,7 @@ impl Pore for Pore2D { &self, bulk: &State, density: Option<&Density>>, - external_potential: Option<&Array3>, + external_potential: Option<&Energy>>, specification: PoreSpecification, ) -> FeosResult> { // generate grid diff --git a/crates/feos-dft/src/adsorption/pore3d.rs b/crates/feos-dft/src/adsorption/pore3d.rs index bfbfd38df..fdf02800a 100644 --- a/crates/feos-dft/src/adsorption/pore3d.rs +++ b/crates/feos-dft/src/adsorption/pore3d.rs @@ -1,12 +1,12 @@ -use super::pore::{PoreProfile, Pore}; +use super::pore::{Pore, PoreProfile}; use crate::adsorption::{FluidParameters, PoreSpecification}; use crate::functional::HelmholtzEnergyFunctional; use crate::geometry::{Axis, Grid}; -use crate::profile::{CUTOFF_RADIUS, MAX_POTENTIAL}; +use crate::profile::CUTOFF_RADIUS; use feos_core::{FeosError, FeosResult, ReferenceSystem, State}; use ndarray::Zip; use ndarray::prelude::*; -use quantity::{Angle, DEGREES, Density, Length}; +use quantity::{Angle, DEGREES, Density, Energy, Length}; /// Parameters required to specify a 3D pore. pub struct Pore3D { @@ -16,12 +16,10 @@ pub struct Pore3D { coordinates: Length>, sigma_ss: Array1, epsilon_k_ss: Array1, - potential_cutoff: Option, cutoff_radius: Option, } impl Pore3D { - #[expect(clippy::too_many_arguments)] pub fn new( system_size: [Length; 3], n_grid: [usize; 3], @@ -29,7 +27,6 @@ impl Pore3D { sigma_ss: Array1, epsilon_k_ss: Array1, angles: Option<[Angle; 3]>, - potential_cutoff: Option, cutoff_radius: Option, ) -> Self { Self { @@ -39,7 +36,6 @@ impl Pore3D { coordinates, sigma_ss, epsilon_k_ss, - potential_cutoff, cutoff_radius, } } @@ -53,7 +49,7 @@ impl Pore for Pore3D { &self, bulk: &State, density: Option<&Density>>, - external_potential: Option<&Array4>, + external_potential: Option<&Energy>>, specification: PoreSpecification, ) -> FeosResult> { let dft: &F = &bulk.eos; @@ -65,9 +61,6 @@ impl Pore for Pore3D { let coordinates = self.coordinates.to_reduced(); - // temperature - let t = bulk.temperature.to_reduced(); - // For non-orthorombic unit cells, the external potential has to be // provided at the moment if let (Some(_), None) = (self.angles, external_potential) { @@ -87,8 +80,6 @@ impl Pore for Pore3D { &self.sigma_ss, &self.epsilon_k_ss, self.cutoff_radius, - self.potential_cutoff, - t, ) }, |e| Ok(e.clone()), @@ -105,7 +96,6 @@ impl Pore for Pore3D { } } -#[expect(clippy::too_many_arguments)] pub fn external_potential_3d( functional: &F, axis: [&Axis; 3], @@ -114,9 +104,7 @@ pub fn external_potential_3d( sigma_ss: &Array1, epsilon_ss: &Array1, cutoff_radius: Option, - potential_cutoff: Option, - reduced_temperature: f64, -) -> FeosResult> { +) -> FeosResult>> { // allocate external potential let m = functional.m(); let mut external_potential = Array4::zeros(( @@ -167,17 +155,9 @@ pub fn external_potential_3d( ) }) .sum::() - / reduced_temperature - }); - - let potential_cutoff = potential_cutoff.unwrap_or(MAX_POTENTIAL); - external_potential.map_inplace(|x| { - if *x > potential_cutoff { - *x = potential_cutoff - } }); - Ok(external_potential) + Ok(Energy::from_reduced(external_potential)) } /// Evaluate LJ12-6 potential between solid site "alpha" and fluid segment diff --git a/crates/feos-dft/src/profile/mod.rs b/crates/feos-dft/src/profile/mod.rs index 26b1d0d94..c9423fded 100644 --- a/crates/feos-dft/src/profile/mod.rs +++ b/crates/feos-dft/src/profile/mod.rs @@ -9,13 +9,15 @@ use ndarray::{ RemoveAxis, }; use num_dual::DualNum; -use quantity::{_Volume, DEGREES, Density, Length, Moles, Quantity, Temperature, Volume}; +use quantity::{ + _Volume, DEGREES, Density, Energy, Entropy, Length, Moles, Quantity, Temperature, Volume, +}; use std::ops::{Add, MulAssign}; use std::sync::Arc; mod properties; -pub(crate) const MAX_POTENTIAL: f64 = 50.0; +const MAX_POTENTIAL: f64 = 50.0; #[cfg(feature = "rayon")] pub(crate) const CUTOFF_RADIUS: f64 = 14.0; @@ -161,7 +163,7 @@ where pub fn new( grid: Grid, bulk: &State, - external_potential: Option>, + external_potential: Option>>, density: Option<&Density>>, lanczos: Option, ) -> Self { @@ -171,13 +173,24 @@ where let convolver = ConvolverFFT::plan(&grid, &weight_functions, lanczos); // initialize external potential - let external_potential = external_potential.unwrap_or_else(|| { - let mut n_grid = vec![bulk.eos.component_index().len()]; - grid.axes() - .iter() - .for_each(|&ax| n_grid.push(ax.grid.len())); - Array::zeros(n_grid).into_dimensionality().unwrap() - }); + let external_potential = external_potential.map_or_else( + || { + let mut n_grid = vec![bulk.eos.component_index().len()]; + grid.axes() + .iter() + .for_each(|&ax| n_grid.push(ax.grid.len())); + Array::zeros(n_grid).into_dimensionality().unwrap() + }, + |e| { + let mut external_potential = e.into_reduced() / t; + external_potential.map_inplace(|x| { + if *x > MAX_POTENTIAL { + *x = MAX_POTENTIAL + } + }); + external_potential + }, + ); // initialize density let density = if let Some(density) = density { @@ -223,6 +236,11 @@ where let moles = self.integrate_reduced_comp(&rho).sum(); self.specification = DFTSpecification::TotalMoles(moles); } + + /// Return the external potential in SI units. + pub fn external_potential(&self) -> Energy> { + Entropy::from_reduced(self.external_potential.clone()) * self.temperature + } } impl DFTProfile diff --git a/crates/feos-dft/src/solvation/pair_correlation.rs b/crates/feos-dft/src/solvation/pair_correlation.rs index 25849af4f..85b435d2e 100644 --- a/crates/feos-dft/src/solvation/pair_correlation.rs +++ b/crates/feos-dft/src/solvation/pair_correlation.rs @@ -1,6 +1,5 @@ //! Functionalities for the calculation of pair correlation functions. use crate::functional::HelmholtzEnergyFunctional; -use crate::profile::MAX_POTENTIAL; use crate::solver::DFTSolver; use crate::{Axis, DFTProfile, Grid}; use feos_core::{Contributions, FeosResult, ReferenceSystem, State}; @@ -12,11 +11,11 @@ use std::ops::Deref; /// models. pub trait PairPotential { /// Return the pair potential of particle i with all other particles. - fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> Array2; + fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> Energy>; } impl, T: PairPotential> PairPotential for C { - fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> Array2 { + fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> Energy> { T::pair_potential(self, i, r, temperature) } } @@ -38,12 +37,7 @@ impl PairCorrelation { // calculate external potential let t = bulk.temperature.to_reduced(); - let mut external_potential = dft.pair_potential(test_particle, &axis.grid, t) / t; - external_potential.map_inplace(|x| { - if *x > MAX_POTENTIAL { - *x = MAX_POTENTIAL - } - }); + let external_potential = dft.pair_potential(test_particle, &axis.grid, t) / t; let grid = Grid::Spherical(axis); Self { diff --git a/crates/feos-dft/src/solvation/solvation_profile.rs b/crates/feos-dft/src/solvation/solvation_profile.rs index 8785876d5..e31ca6494 100644 --- a/crates/feos-dft/src/solvation/solvation_profile.rs +++ b/crates/feos-dft/src/solvation/solvation_profile.rs @@ -1,7 +1,7 @@ use crate::adsorption::FluidParameters; use crate::functional::HelmholtzEnergyFunctional; use crate::geometry::{Axis, Grid}; -use crate::profile::{CUTOFF_RADIUS, DFTProfile, MAX_POTENTIAL}; +use crate::profile::{CUTOFF_RADIUS, DFTProfile}; use crate::solver::DFTSolver; use feos_core::{Contributions, FeosResult, ReferenceSystem, State}; use ndarray::Zip; @@ -40,7 +40,6 @@ impl SolvationProfile { } impl SolvationProfile { - #[expect(clippy::too_many_arguments)] pub fn new( bulk: &State, n_grid: [usize; 3], @@ -49,7 +48,6 @@ impl SolvationProfile { epsilon_ss: Array1, system_size: Option<[Length; 3]>, cutoff_radius: Option, - potential_cutoff: Option, ) -> FeosResult { let dft: &F = &bulk.eos; @@ -77,9 +75,6 @@ impl SolvationProfile { coordinates = coordinates + shift; - // temperature - let t = bulk.temperature.to_reduced(); - // calculate external potential let external_potential = external_potential_3d( dft, @@ -88,8 +83,6 @@ impl SolvationProfile { sigma_ss, epsilon_ss, cutoff_radius, - potential_cutoff, - t, )?; let grid = Grid::Cartesian3(x, y, z); @@ -102,7 +95,6 @@ impl SolvationProfile { } } -#[expect(clippy::too_many_arguments)] fn external_potential_3d( functional: &F, axis: [&Axis; 3], @@ -110,9 +102,7 @@ fn external_potential_3d( sigma_ss: Array1, epsilon_ss: Array1, cutoff_radius: Option, - potential_cutoff: Option, - reduced_temperature: f64, -) -> FeosResult> { +) -> FeosResult>> { // allocate external potential let m = functional.m(); let mut external_potential = Array4::zeros(( @@ -150,17 +140,9 @@ fn external_potential_3d( ) }) .sum::() - / reduced_temperature - }); - - let potential_cutoff = potential_cutoff.unwrap_or(MAX_POTENTIAL); - external_potential.map_inplace(|x| { - if *x > potential_cutoff { - *x = potential_cutoff - } }); - Ok(external_potential) + Ok(Energy::from_reduced(external_potential)) } /// Evaluate LJ12-6 potential between solid site "alpha" and fluid segment diff --git a/crates/feos/src/hard_sphere/dft.rs b/crates/feos/src/hard_sphere/dft.rs index ba8edf25a..d48d6327a 100644 --- a/crates/feos/src/hard_sphere/dft.rs +++ b/crates/feos/src/hard_sphere/dft.rs @@ -8,6 +8,7 @@ use feos_dft::{ use nalgebra::DVector; use ndarray::*; use num_dual::DualNum; +use quantity::Energy; use std::f64::consts::PI; use super::{HardSphereProperties, MonomerShape}; @@ -352,15 +353,15 @@ impl HelmholtzEnergyFunctionalDyn for FMTFunctional { } impl PairPotential for FMTFunctional { - fn pair_potential(&self, i: usize, r: &Array1, _: f64) -> Array2 { + fn pair_potential(&self, i: usize, r: &Array1, _: f64) -> Energy> { let s = &self.properties.sigma; - Array::from_shape_fn((s.len(), r.len()), |(j, k)| { + Energy::new(Array::from_shape_fn((s.len(), r.len()), |(j, k)| { if r[k] > 0.5 * (s[i] + s[j]) { 0.0 } else { f64::INFINITY } - }) + })) } } diff --git a/crates/feos/src/pcsaft/dft/mod.rs b/crates/feos/src/pcsaft/dft/mod.rs index b3afa5435..52c16aa4a 100644 --- a/crates/feos/src/pcsaft/dft/mod.rs +++ b/crates/feos/src/pcsaft/dft/mod.rs @@ -3,7 +3,7 @@ use super::parameters::PcSaftPars; use crate::association::{Association, YuWuAssociationFunctional}; use crate::hard_sphere::{FMTContribution, FMTVersion}; use crate::pcsaft::eos::PcSaftOptions; -use feos_core::{FeosResult, Molarweight, ResidualDyn, StateHD, Subset}; +use feos_core::{FeosResult, Molarweight, ReferenceSystem, ResidualDyn, StateHD, Subset}; use feos_derive::FunctionalContribution; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; @@ -14,7 +14,7 @@ use nalgebra::DVector; use ndarray::{Array1, Array2}; use num_dual::DualNum; use num_traits::One; -use quantity::MolarWeight; +use quantity::{Energy, MolarWeight}; use std::f64::consts::FRAC_PI_6; mod dispersion; @@ -164,13 +164,16 @@ impl FluidParameters for PcSaftFunctional { } impl PairPotential for PcSaftFunctional { - fn pair_potential(&self, i: usize, r: &Array1, _: f64) -> Array2 { + fn pair_potential(&self, i: usize, r: &Array1, _: f64) -> Energy> { let sigma_ij = &self.params.sigma_ij; let eps_ij_4 = 4.0 * &self.params.epsilon_k_ij; - Array2::from_shape_fn((self.params.m.len(), r.len()), |(j, k)| { - let att = (sigma_ij[(i, j)] / r[k]).powi(6); - eps_ij_4[(i, j)] * att * (att - 1.0) - }) + Energy::from_reduced(Array2::from_shape_fn( + (self.params.m.len(), r.len()), + |(j, k)| { + let att = (sigma_ij[(i, j)] / r[k]).powi(6); + eps_ij_4[(i, j)] * att * (att - 1.0) + }, + )) } } diff --git a/crates/feos/src/pets/dft/mod.rs b/crates/feos/src/pets/dft/mod.rs index 14215fee9..ef7e5d799 100644 --- a/crates/feos/src/pets/dft/mod.rs +++ b/crates/feos/src/pets/dft/mod.rs @@ -3,7 +3,7 @@ use super::eos::PetsOptions; use super::parameters::PetsParameters; use crate::hard_sphere::{FMTContribution, FMTVersion}; use dispersion::AttractiveFunctional; -use feos_core::FeosResult; +use feos_core::{FeosResult, ReferenceSystem}; use feos_derive::FunctionalContribution; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; @@ -12,6 +12,7 @@ use nalgebra::DVector; use ndarray::{Array1, Array2}; use num_dual::DualNum; use pure_pets_functional::*; +use quantity::Energy; mod dispersion; mod pure_pets_functional; @@ -75,18 +76,21 @@ impl FluidParameters for Pets { } impl PairPotential for Pets { - fn pair_potential(&self, i: usize, r: &Array1, _: f64) -> Array2 { + fn pair_potential(&self, i: usize, r: &Array1, _: f64) -> Energy> { let eps_ij_4 = 4.0 * self.epsilon_k_ij.clone(); let shift_ij = &eps_ij_4 * (2.5.powi(-12) - 2.5.powi(-6)); let rc_ij = 2.5 * &self.sigma_ij; - Array2::from_shape_fn((self.sigma.len(), r.len()), |(j, k)| { - if r[k] > rc_ij[(i, j)] { - 0.0 - } else { - let att = (self.sigma_ij[(i, j)] / r[k]).powi(6); - eps_ij_4[(i, j)] * att * (att - 1.0) - shift_ij[(i, j)] - } - }) + Energy::from_reduced(Array2::from_shape_fn( + (self.sigma.len(), r.len()), + |(j, k)| { + if r[k] > rc_ij[(i, j)] { + 0.0 + } else { + let att = (self.sigma_ij[(i, j)] / r[k]).powi(6); + eps_ij_4[(i, j)] * att * (att - 1.0) - shift_ij[(i, j)] + } + }, + )) } } diff --git a/crates/feos/src/saftvrqmie/dft/mod.rs b/crates/feos/src/saftvrqmie/dft/mod.rs index 8f29059b5..6cde777b4 100644 --- a/crates/feos/src/saftvrqmie/dft/mod.rs +++ b/crates/feos/src/saftvrqmie/dft/mod.rs @@ -3,7 +3,7 @@ use crate::hard_sphere::{FMTContribution, FMTVersion, HardSphereProperties, Mono use crate::saftvrqmie::eos::SaftVRQMieOptions; use crate::saftvrqmie::parameters::{SaftVRQMieParameters, SaftVRQMiePars}; use dispersion::AttractiveFunctional; -use feos_core::FeosResult; +use feos_core::{FeosResult, ReferenceSystem}; use feos_derive::FunctionalContribution; use feos_dft::adsorption::FluidParameters; use feos_dft::solvation::PairPotential; @@ -12,6 +12,7 @@ use nalgebra::DVector; use ndarray::{Array, Array1, Array2}; use non_additive_hs::NonAddHardSphereFunctional; use num_dual::DualNum; +use quantity::Energy; mod dispersion; mod non_additive_hs; @@ -79,10 +80,11 @@ impl FluidParameters for SaftVRQMie { } impl PairPotential for SaftVRQMie { - fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> Array2 { - Array::from_shape_fn((self.params.m.len(), r.len()), |(j, k)| { - self.params.qmie_potential_ij(i, j, r[k], temperature)[0] - }) + fn pair_potential(&self, i: usize, r: &Array1, temperature: f64) -> Energy> { + Energy::from_reduced(Array::from_shape_fn( + (self.params.m.len(), r.len()), + |(j, k)| self.params.qmie_potential_ij(i, j, r[k], temperature)[0], + )) } } diff --git a/py-feos/src/dft/adsorption/pore.rs b/py-feos/src/dft/adsorption/pore.rs index 7668bebfb..6e39d57ab 100644 --- a/py-feos/src/dft/adsorption/pore.rs +++ b/py-feos/src/dft/adsorption/pore.rs @@ -139,7 +139,7 @@ impl PyPore1D { /// The bulk state in equilibrium with the pore. /// density : SIArray2, optional /// Initial values for the density profile. - /// external_potential : numpy.ndarray[float], optional + /// external_potential : SIArray2, optional /// The external potential in the pore. Used to /// save computation time in the case of costly /// evaluations of external potentials. @@ -158,7 +158,7 @@ impl PyPore1D { &self, bulk: &PyState, density: Option>>, - external_potential: Option<&Bound<'_, PyArray2>>, + external_potential: Option>>, specification: PyPoreSpecification, ) -> PyResult { Ok(PyPoreProfile1D( @@ -166,7 +166,7 @@ impl PyPore1D { .initialize( &bulk.0, density.as_ref(), - external_potential.map(|e| e.to_owned_array()).as_ref(), + external_potential.as_ref(), specification.0, ) .map_err(PyFeosError::from)?, @@ -236,7 +236,7 @@ impl PyPore2D { /// The bulk state in equilibrium with the pore. /// density : SIArray3, optional /// Initial values for the density profile. - /// external_potential : numpy.ndarray[float], optional + /// external_potential : SIArray3, optional /// The external potential in the pore. Used to /// save computation time in the case of costly /// evaluations of external potentials. @@ -255,7 +255,7 @@ impl PyPore2D { &self, bulk: &PyState, density: Option>>, - external_potential: Option<&Bound<'_, PyArray3>>, + external_potential: Option>>, specification: PyPoreSpecification, ) -> PyResult { Ok(PyPoreProfile2D( @@ -263,7 +263,7 @@ impl PyPore2D { .initialize( &bulk.0, density.as_ref(), - external_potential.map(|e| e.to_owned_array()).as_ref(), + external_potential.as_ref(), specification.0, ) .map_err(PyFeosError::from)?, @@ -325,10 +325,9 @@ impl_pore_profile!(PyPoreProfile3D); impl PyPore3D { #[new] #[pyo3( - text_signature = "(system_size, n_grid, coordinates, sigma_ss, epsilon_k_ss, angles=None, potential_cutoff=None, cutoff_radius=None)" + text_signature = "(system_size, n_grid, coordinates, sigma_ss, epsilon_k_ss, angles=None, cutoff_radius=None)" )] - #[pyo3(signature = (system_size, n_grid, coordinates, sigma_ss, epsilon_k_ss, angles=None, potential_cutoff=None, cutoff_radius=None))] - #[expect(clippy::too_many_arguments)] + #[pyo3(signature = (system_size, n_grid, coordinates, sigma_ss, epsilon_k_ss, angles=None, cutoff_radius=None))] fn new( system_size: [Length; 3], n_grid: [usize; 3], @@ -336,7 +335,6 @@ impl PyPore3D { sigma_ss: &Bound<'_, PyArray1>, epsilon_k_ss: &Bound<'_, PyArray1>, angles: Option<[Angle; 3]>, - potential_cutoff: Option, cutoff_radius: Option, ) -> Self { Self(Pore3D::new( @@ -346,7 +344,6 @@ impl PyPore3D { sigma_ss.to_owned_array(), epsilon_k_ss.to_owned_array(), angles, - potential_cutoff, cutoff_radius, )) } @@ -359,7 +356,7 @@ impl PyPore3D { /// The bulk state in equilibrium with the pore. /// density : SIArray4, optional /// Initial values for the density profile. - /// external_potential : numpy.ndarray[float], optional + /// external_potential : SIArray4, optional /// The external potential in the pore. Used to /// save computation time in the case of costly /// evaluations of external potentials. @@ -378,7 +375,7 @@ impl PyPore3D { &self, bulk: &PyState, density: Option>>, - external_potential: Option<&Bound<'_, PyArray4>>, + external_potential: Option>>, specification: PyPoreSpecification, ) -> PyResult { Ok(PyPoreProfile3D( @@ -386,7 +383,7 @@ impl PyPore3D { .initialize( &bulk.0, density.as_ref(), - external_potential.map(|e| e.to_owned_array()).as_ref(), + external_potential.as_ref(), specification.0, ) .map_err(PyFeosError::from)?, diff --git a/py-feos/src/dft/profile.rs b/py-feos/src/dft/profile.rs index 68c4aec9c..b60fcafdb 100644 --- a/py-feos/src/dft/profile.rs +++ b/py-feos/src/dft/profile.rs @@ -74,8 +74,8 @@ macro_rules! impl_profile { } #[getter] - fn get_external_potential<'py>(&self, py: Python<'py>) -> Bound<'py, $py_arr2> { - self.0.profile.external_potential.view().to_pyarray(py) + fn get_external_potential(&self) -> Energy<$si_arr2> { + self.0.profile.external_potential().clone() } #[getter] @@ -272,6 +272,6 @@ macro_rules! impl_3d_profile { }; } -pub(crate) use {impl_1d_profile, impl_2d_profile, impl_profile}; #[cfg(feature = "rayon")] pub(crate) use impl_3d_profile; +pub(crate) use {impl_1d_profile, impl_2d_profile, impl_profile}; diff --git a/py-feos/src/dft/solvation.rs b/py-feos/src/dft/solvation.rs index 4dc012972..9509d6b99 100644 --- a/py-feos/src/dft/solvation.rs +++ b/py-feos/src/dft/solvation.rs @@ -55,10 +55,9 @@ impl_3d_profile!(PySolvationProfile, get_x, get_y, get_z); impl PySolvationProfile { #[new] #[pyo3( - text_signature = "(bulk, n_grid, coordinates, sigma, epsilon_k, system_size=None, cutoff_radius=None, potential_cutoff=None)" + text_signature = "(bulk, n_grid, coordinates, sigma, epsilon_k, system_size=None, cutoff_radius=None)" )] - #[pyo3(signature = (bulk, n_grid, coordinates, sigma, epsilon_k, system_size=None, cutoff_radius=None, potential_cutoff=None))] - #[expect(clippy::too_many_arguments)] + #[pyo3(signature = (bulk, n_grid, coordinates, sigma, epsilon_k, system_size=None, cutoff_radius=None))] fn new<'py>( bulk: &PyState, n_grid: [usize; 3], @@ -67,7 +66,6 @@ impl PySolvationProfile { epsilon_k: &Bound<'py, PyArray1>, system_size: Option<[Length; 3]>, cutoff_radius: Option, - potential_cutoff: Option, ) -> PyResult { Ok(Self( SolvationProfile::new( @@ -78,7 +76,6 @@ impl PySolvationProfile { epsilon_k.to_owned_array(), system_size, cutoff_radius, - potential_cutoff, ) .map_err(PyFeosError::from)?, )) From 90539ac48b52a5a26529d3afd252c7f863f68e8b Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:24:23 +0200 Subject: [PATCH 4/6] Restructure Python DFT interface (#376) --- CHANGELOG.md | 7 + Cargo.toml | 1 - crates/feos-dft/Cargo.toml | 3 +- .../src/adsorption/external_potential.rs | 125 ----- .../feos-dft/src/adsorption/fea_potential.rs | 141 ----- crates/feos-dft/src/adsorption/mod.rs | 116 ++-- crates/feos-dft/src/adsorption/pore.rs | 178 +++--- crates/feos-dft/src/adsorption/pore2d.rs | 46 -- crates/feos-dft/src/adsorption/pore3d.rs | 200 ------- crates/feos-dft/src/geometry.rs | 43 +- crates/feos-dft/src/profile/mod.rs | 135 ++--- .../src/solvation/pair_correlation.rs | 4 +- .../src/solvation/solvation_profile.rs | 6 +- crates/feos/benches/dft_pore.rs | 26 +- crates/feos/tests/gc_pcsaft/dft.rs | 6 +- .../src/dft/adsorption/external_potential.rs | 57 -- py-feos/src/dft/adsorption/mod.rs | 511 +++++++++--------- py-feos/src/dft/adsorption/pore.rs | 469 +++++++--------- py-feos/src/dft/interface/mod.rs | 6 +- py-feos/src/dft/mod.rs | 5 +- py-feos/src/dft/profile.rs | 148 ++--- py-feos/src/dft/solvation.rs | 10 +- py-feos/src/lib.rs | 11 +- 23 files changed, 762 insertions(+), 1492 deletions(-) delete mode 100644 crates/feos-dft/src/adsorption/fea_potential.rs delete mode 100644 crates/feos-dft/src/adsorption/pore2d.rs delete mode 100644 crates/feos-dft/src/adsorption/pore3d.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index fe08b8fe4..d626a4051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Breaking] ### Added - Added `PoreSpecification` enum to specify the state of the fluid in the pore (currently chemical potential or moles). [#371](https://github.com/feos-org/feos/pull/371) +- Added the `Grid` and `PoreProfile` classes as an entry points to DFT in various coordinate systems in Python. [#376](https://github.com/feos-org/feos/pull/376) ### Changed - Changed data type of initial temperatures or pressure for phase equilibrium calculations (`TemperatureOrPressure::Other`) from `D` to `f64`. [#369](https://github.com/feos-org/feos/pull/369) - Reworked DFT solution algorithms slightly for the cases in which additional specifications are given. [#371](https://github.com/feos-org/feos/pull/371) - External potentials are passed and returned as quantities (energies) instead of reduced units. [#372](https://github.com/feos-org/feos/pull/372) +- Merged the `Adsorption1D` and `Adsorption3D` classes in Python into `Adsorption` using dynamically dimensioned arrays. [#376](https://github.com/feos-org/feos/pull/376) ### Removed - Removed the `DFTSpecification` trait in favor of only using the `DFTSpecification` enum (renamed from `DFTSpecifications`). [#371](https://github.com/feos-org/feos/pull/371) +- Removed the `Pore2D` and `Pore3D` interfaces including the (limited) calculation of external potentials for complex pore grometries. [#376](https://github.com/feos-org/feos/pull/376) +- Removed the free energy-averaged external (FEA) potential. [#376](https://github.com/feos-org/feos/pull/376) + +### Packaging +- Removed the `gauss-quad` dependency which was only used in the FEA potential calculation. [#376](https://github.com/feos-org/feos/pull/376) ## [Unreleased] diff --git a/Cargo.toml b/Cargo.toml index 32771d66a..d18be3929 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ petgraph = "0.8" rustdct = "0.7" rustfft = "6.0" libm = "0.2" -gauss-quad = "0.3" approx = "0.5" criterion = "0.8" paste = "1.0" diff --git a/crates/feos-dft/Cargo.toml b/crates/feos-dft/Cargo.toml index 2e9e52a7f..400504e9e 100644 --- a/crates/feos-dft/Cargo.toml +++ b/crates/feos-dft/Cargo.toml @@ -23,11 +23,10 @@ rustdct = { workspace = true } rustfft = { workspace = true } num-traits = { workspace = true } libm = { workspace = true } -gauss-quad = { workspace = true, optional = true } petgraph = { workspace = true } feos-core = { workspace = true } [features] default = [] -rayon = ["gauss-quad", "ndarray/rayon"] +rayon = ["ndarray/rayon"] diff --git a/crates/feos-dft/src/adsorption/external_potential.rs b/crates/feos-dft/src/adsorption/external_potential.rs index 583263866..5d911d4de 100644 --- a/crates/feos-dft/src/adsorption/external_potential.rs +++ b/crates/feos-dft/src/adsorption/external_potential.rs @@ -1,13 +1,7 @@ -#[cfg(feature = "rayon")] -use crate::adsorption::fea_potential::calculate_fea_potential; use crate::functional::HelmholtzEnergyFunctional; -#[cfg(feature = "rayon")] -use crate::geometry::Geometry; use libm::tgamma; use nalgebra::DVector; use ndarray::{Array1, Array2, Axis as Axis_nd}; -#[cfg(feature = "rayon")] -use quantity::Length; use std::f64::consts::PI; use std::ops::Deref; @@ -52,20 +46,6 @@ pub enum ExternalPotential { epsilon2_k_ss: f64, rho_s: f64, }, - /// Free-energy averaged potential: - #[cfg(feature = "rayon")] - FreeEnergyAveraged { - coordinates: Length>, - sigma_ss: Array1, - epsilon_k_ss: Array1, - pore_center: [f64; 3], - system_size: [Length; 3], - n_grid: [usize; 2], - cutoff_radius: Option, - }, - - /// Custom potential - Custom(Array2), } /// Parameters of the fluid required to evaluate the external potential. @@ -89,12 +69,7 @@ impl ExternalPotential { &self, z_grid: &Array1, fluid_parameters: &P, - #[cfg_attr(not(feature = "rayon"), expect(unused_variables))] temperature: f64, ) -> Array2 { - if let ExternalPotential::Custom(potential) = self { - return potential.clone(); - } - // Allocate external potential let m = fluid_parameters.m(); let mut ext_pot = Array2::zeros((m.len(), z_grid.len())); @@ -197,36 +172,6 @@ impl ExternalPotential { * (2.0 * (sigma_sf[i] / z_grid).mapv(|x| x.powi(9)) - 15.0 * (sigma_sf[i] / z_grid).mapv(|x| x.powi(3)))) } - #[cfg(feature = "rayon")] - Self::FreeEnergyAveraged { - coordinates, - sigma_ss, - epsilon_k_ss, - pore_center, - system_size, - n_grid, - cutoff_radius, - } => { - // combining rules - let epsilon_k_sf = - (fluid_parameters.epsilon_k_ff()[i] * epsilon_k_ss).map(|e| e.sqrt()); - let sigma_sf = (fluid_parameters.sigma_ff()[i] + sigma_ss) * 0.5; - - calculate_fea_potential( - z_grid, - mi, - coordinates, - sigma_sf, - epsilon_k_sf, - pore_center, - system_size, - n_grid, - temperature, - Geometry::Cartesian, - *cutoff_radius, - ) - } - _ => unreachable!(), }); } ext_pot @@ -238,12 +183,7 @@ impl ExternalPotential { r_grid: &Array1, pore_size: f64, fluid_parameters: &P, - #[cfg_attr(not(feature = "rayon"), expect(unused_variables))] temperature: f64, ) -> Array2 { - if let ExternalPotential::Custom(potential) = self { - return potential.clone(); - } - // Allocate external potential let m = fluid_parameters.m(); let mut ext_pot = Array2::zeros((m.len(), r_grid.len())); @@ -363,36 +303,6 @@ impl ExternalPotential { * sigma_sf[i].powi(3) * *rho_s) } - #[cfg(feature = "rayon")] - Self::FreeEnergyAveraged { - coordinates, - sigma_ss, - epsilon_k_ss, - pore_center, - system_size, - n_grid, - cutoff_radius, - } => { - // combining rules - let epsilon_k_sf = - (fluid_parameters.epsilon_k_ff()[i] * epsilon_k_ss).map(|e| e.sqrt()); - let sigma_sf = (fluid_parameters.sigma_ff()[i] + sigma_ss) * 0.5; - - calculate_fea_potential( - r_grid, - mi, - coordinates, - sigma_sf, - epsilon_k_sf, - pore_center, - system_size, - n_grid, - temperature, - Geometry::Cylindrical, - *cutoff_radius, - ) - } - _ => unreachable!(), }); } ext_pot @@ -404,12 +314,7 @@ impl ExternalPotential { r_grid: &Array1, pore_size: f64, fluid_parameters: &P, - #[cfg_attr(not(feature = "rayon"), expect(unused_variables))] temperature: f64, ) -> Array2 { - if let ExternalPotential::Custom(potential) = self { - return potential.clone(); - } - // Allocate external potential let m = fluid_parameters.m(); let mut ext_pot = Array2::zeros((m.len(), r_grid.len())); @@ -553,36 +458,6 @@ impl ExternalPotential { * (2.0 / 5.0 * sum_n(10, r_grid, sigma_sf[i], pore_size) - sum_n(4, r_grid, sigma_sf[i], pore_size))) } - #[cfg(feature = "rayon")] - Self::FreeEnergyAveraged { - coordinates, - sigma_ss, - epsilon_k_ss, - pore_center, - system_size, - n_grid, - cutoff_radius, - } => { - // combining rules - let epsilon_k_sf = - (fluid_parameters.epsilon_k_ff()[i] * epsilon_k_ss).map(|e| e.sqrt()); - let sigma_sf = (fluid_parameters.sigma_ff()[i] + sigma_ss) * 0.5; - - calculate_fea_potential( - r_grid, - mi, - coordinates, - sigma_sf, - epsilon_k_sf, - pore_center, - system_size, - n_grid, - temperature, - Geometry::Spherical, - *cutoff_radius, - ) - } - _ => unreachable!(), }); } ext_pot diff --git a/crates/feos-dft/src/adsorption/fea_potential.rs b/crates/feos-dft/src/adsorption/fea_potential.rs deleted file mode 100644 index f67e583f1..000000000 --- a/crates/feos-dft/src/adsorption/fea_potential.rs +++ /dev/null @@ -1,141 +0,0 @@ -use super::pore3d::{calculate_distance2, evaluate_lj_potential}; -use crate::Geometry; -use crate::profile::CUTOFF_RADIUS; -use feos_core::ReferenceSystem; -use gauss_quad::GaussLegendre; -use ndarray::{Array1, Array2, Zip}; -use quantity::Length; -use std::f64::consts::PI; -use std::num::NonZero; - -// Calculate free-energy average potential for given solid structure. -#[expect(clippy::too_many_arguments)] -pub fn calculate_fea_potential( - grid: &Array1, - mi: f64, - coordinates: &Length>, - sigma_sf: Array1, - epsilon_k_sf: Array1, - pore_center: &[f64; 3], - system_size: &[Length; 3], - n_grid: &[usize; 2], - temperature: f64, - geometry: Geometry, - cutoff_radius: Option, -) -> Array1 { - // allocate external potential - let mut potential: Array1 = Array1::zeros(grid.len()); - - // calculate squared cutoff radius - let cutoff_radius2 = cutoff_radius.unwrap_or(CUTOFF_RADIUS).powi(2); - - // dimensionless solid coordinates - let coordinates = Array2::from_shape_fn(coordinates.raw_dim(), |(i, j)| { - (coordinates.get((i, j))).to_reduced() - }); - - let system_size = [ - system_size[0].to_reduced(), - system_size[1].to_reduced(), - system_size[2].to_reduced(), - ]; - - // Create secondary axis: - // Cartesian coordinates => y - // Cylindrical coordinates => phi - // Spherical coordinates => phi - let (nodes1, weights1) = match geometry { - Geometry::Cartesian => { - let nodes = Array1::linspace( - 0.5 * system_size[1] / n_grid[0] as f64, - system_size[1] - 0.5 * system_size[1] / n_grid[0] as f64, - n_grid[0], - ); - let weights = Array1::from_elem(n_grid[0], system_size[1] / n_grid[0] as f64); - (nodes, weights) - } - Geometry::Spherical | Geometry::Cylindrical => { - let (unscaled_nodes, unscaled_weights) = - GaussLegendre::new(NonZero::new(n_grid[0]).unwrap()) - .into_iter() - .unzip(); - - let nodes = PI + Array1::from_vec(unscaled_nodes) * PI; - let weights = Array1::from_vec(unscaled_weights) * PI; - - (nodes, weights) - } - }; - - // Create tertiary axis - // Cartesian coordinates => z - // Cylindrical coordinates => z - // Spherical coordinates => theta - let (nodes2, weights2) = match geometry { - Geometry::Cylindrical | Geometry::Cartesian => { - let nodes = Array1::linspace( - 0.5 * system_size[2] / n_grid[1] as f64, - system_size[2] - 0.5 * system_size[2] / n_grid[1] as f64, - n_grid[1], - ); - let weights = Array1::from_elem(n_grid[1], system_size[2] / n_grid[1] as f64); - (nodes, weights) - } - Geometry::Spherical => { - let (unscaled_nodes, unscaled_weights) = - GaussLegendre::new(NonZero::new(n_grid[1]).unwrap()) - .into_iter() - .unzip(); - - let nodes = PI / 2.0 + Array1::from_vec(unscaled_nodes) * PI / 2.0; - let weights = Array1::from_vec(unscaled_weights) * PI / 2.0 - * Array1::from_shape_fn(n_grid[1], |i| nodes[i].sin()); - - (nodes, weights) - } - }; - - // calculate weights - let weights = Array2::from_shape_fn((n_grid[0], n_grid[1]), |(i, j)| weights1[i] * weights2[j]); - - // calculate sum of weights - let weights_sum = weights.sum(); - - // calculate FEA potential - Zip::indexed(&mut potential).par_for_each(|i0, f| { - let mut potential_2d: Array2 = Array2::zeros((n_grid[0], n_grid[1])); - for (i1, &n1) in nodes1.iter().enumerate() { - for (i2, &n2) in nodes2.iter().enumerate() { - let point = match geometry { - Geometry::Cartesian => [grid[i0], n1, n2], - Geometry::Cylindrical => [ - pore_center[0] + grid[i0] * n1.cos(), - pore_center[1] + grid[i0] * n1.sin(), - n2, - ], - Geometry::Spherical => [ - pore_center[0] + grid[i0] * n2.sin() * n1.cos(), - pore_center[1] + grid[i0] * n2.sin() * n1.sin(), - pore_center[2] + grid[i0] * n2.cos(), - ], - }; - - let distance2 = calculate_distance2(point, &coordinates, system_size); - let potential_sum: f64 = (0..sigma_sf.len()) - .map(|alpha| { - mi * evaluate_lj_potential( - distance2[alpha], - sigma_sf[alpha], - epsilon_k_sf[alpha], - cutoff_radius2, - ) / temperature - }) - .sum(); - potential_2d[[i1, i2]] = (-potential_sum).exp(); - } - } - *f = (potential_2d * &weights).sum(); - }); - - -temperature * potential.map(|p| (p / weights_sum).ln()) -} diff --git a/crates/feos-dft/src/adsorption/mod.rs b/crates/feos-dft/src/adsorption/mod.rs index 3d11c4275..fe3391223 100644 --- a/crates/feos-dft/src/adsorption/mod.rs +++ b/crates/feos-dft/src/adsorption/mod.rs @@ -1,5 +1,6 @@ //! Adsorption profiles and isotherms. use super::functional::HelmholtzEnergyFunctional; +use super::geometry::Grid; use super::solver::DFTSolver; use feos_core::DensityInitialization::{Liquid, Vapor}; use feos_core::{ @@ -7,23 +8,14 @@ use feos_core::{ SolverOptions, State, }; use nalgebra::{DMatrix, DVector, Dyn}; -use ndarray::{Array1, Array2, Dimension, Ix1, Ix3, RemoveAxis}; +use ndarray::{Array, Array1, Array2, Dimension, Ix1, Ix3, RemoveAxis}; use quantity::{Energy, MolarEnergy, Moles, Pressure, Temperature}; use std::iter; mod external_potential; -#[cfg(feature = "rayon")] -mod fea_potential; mod pore; -mod pore2d; pub use external_potential::{ExternalPotential, FluidParameters}; -pub use pore::{HenryCoefficient, Pore, Pore1D, PoreProfile, PoreProfile1D, PoreSpecification}; -pub use pore2d::{Pore2D, PoreProfile2D}; - -#[cfg(feature = "rayon")] -mod pore3d; -#[cfg(feature = "rayon")] -pub use pore3d::{Pore3D, PoreProfile3D}; +pub use pore::{HenryCoefficient, Pore1D, PoreProfile, PoreSpecification}; const MAX_ITER_ADSORPTION_EQUILIBRIUM: usize = 50; const TOL_ADSORPTION_EQUILIBRIUM: f64 = 1e-8; @@ -54,11 +46,12 @@ where } /// Calculate an adsorption isotherm (starting at low pressure) - pub fn adsorption_isotherm, X: Composition + Clone>( + pub fn adsorption_isotherm + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, - pore: &S, + grid: &Grid, + external_potential: &Energy>, composition: X, solver: Option<&DFTSolver>, ) -> FeosResult> { @@ -66,7 +59,8 @@ where functional, temperature, pressure, - pore, + grid, + external_potential, composition, DensityInitialization::Vapor, solver, @@ -74,11 +68,12 @@ where } /// Calculate an desorption isotherm (starting at high pressure) - pub fn desorption_isotherm, X: Composition + Clone>( + pub fn desorption_isotherm + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, - pore: &S, + grid: &Grid, + external_potential: &Energy>, composition: X, solver: Option<&DFTSolver>, ) -> FeosResult> { @@ -87,7 +82,8 @@ where functional, temperature, &pressure, - pore, + grid, + external_potential, composition, DensityInitialization::Liquid, solver, @@ -99,11 +95,12 @@ where } /// Calculate an equilibrium isotherm - pub fn equilibrium_isotherm, X: Composition + Clone>( + pub fn equilibrium_isotherm + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, - pore: &S, + grid: &Grid, + external_potential: &Energy>, composition: X, solver: Option<&DFTSolver>, ) -> FeosResult> { @@ -113,7 +110,8 @@ where temperature, p_min, p_max, - pore, + grid, + external_potential, composition.clone(), solver, SolverOptions::default(), @@ -132,7 +130,8 @@ where functional, temperature, &p_ads, - pore, + grid, + external_potential, composition.clone(), solver, )? @@ -141,7 +140,8 @@ where functional, temperature, &p_des, - pore, + grid, + external_potential, composition, solver, )? @@ -155,7 +155,8 @@ where functional, temperature, pressure, - pore, + grid, + external_potential, composition.clone(), solver, )?; @@ -163,7 +164,8 @@ where functional, temperature, pressure, - pore, + grid, + external_potential, composition, solver, )?; @@ -182,11 +184,13 @@ where } } - fn isotherm, X: Composition + Clone>( + #[expect(clippy::too_many_arguments)] + fn isotherm + Clone>( functional: &F, temperature: Temperature, pressure: &Pressure>, - pore: &S, + grid: &Grid, + external_potential: &Energy>, composition: X, density_initialization: DensityInitialization, solver: Option<&DFTSolver>, @@ -210,11 +214,15 @@ where _ => unreachable!(), }; } - let profile = pore - .initialize(&bulk, None, None, PoreSpecification::ChemicalPotential)? - .solve(solver)? - .profile; - let external_potential = Some(&profile.external_potential()); + let profile = PoreProfile::::new( + grid.clone(), + &bulk, + external_potential, + None, + PoreSpecification::ChemicalPotential, + ) + .solve(solver)? + .profile; let mut old_density = Some(&profile.density); for i in 0..pressure.len() { @@ -232,18 +240,20 @@ where .clone(); } - let p = pore.initialize( + let p = PoreProfile::new( + grid.clone(), &bulk, - old_density, external_potential, + old_density, PoreSpecification::ChemicalPotential, - )?; - let p2 = pore.initialize( + ); + let p2 = PoreProfile::new( + grid.clone(), &bulk, - None, external_potential, + None, PoreSpecification::ChemicalPotential, - )?; + ); profiles.push(p.solve(solver).or_else(|_| p2.solve(solver))); old_density = if let Some(Ok(l)) = profiles.last() { @@ -258,12 +268,13 @@ where /// Calculate the phase transition from an empty to a filled pore. #[expect(clippy::too_many_arguments)] - pub fn phase_equilibrium, X: Composition + Clone>( + pub fn phase_equilibrium + Clone>( functional: &F, temperature: Temperature, p_min: Pressure, p_max: Pressure, - pore: &S, + grid: &Grid, + external_potential: &Energy>, composition: X, solver: Option<&DFTSolver>, options: SolverOptions, @@ -274,17 +285,22 @@ where let bulk_init = State::new_npt(functional, temperature, p_max, x.clone(), Some(Liquid))?; let liquid_bulk = State::new_npt(functional, temperature, p_max, x.clone(), Some(Vapor))?; - let mut vapor = pore - .initialize( - &vapor_bulk, - None, - None, - PoreSpecification::ChemicalPotential, - )? - .solve(solver)?; - let mut liquid = pore - .initialize(&bulk_init, None, None, PoreSpecification::ChemicalPotential)? - .solve(solver)?; + let mut vapor = PoreProfile::new( + grid.clone(), + &vapor_bulk, + external_potential, + None, + PoreSpecification::ChemicalPotential, + ) + .solve(solver)?; + let mut liquid = PoreProfile::new( + grid.clone(), + &bulk_init, + external_potential, + None, + PoreSpecification::ChemicalPotential, + ) + .solve(solver)?; // calculate initial value for bulk density let n_dp_drho_v = (vapor.profile.moles() * vapor_bulk.dp_drho(Contributions::Total)).sum(); diff --git a/crates/feos-dft/src/adsorption/pore.rs b/crates/feos-dft/src/adsorption/pore.rs index 493229289..b59474085 100644 --- a/crates/feos-dft/src/adsorption/pore.rs +++ b/crates/feos-dft/src/adsorption/pore.rs @@ -14,12 +14,13 @@ use num_dual::linalg::LU; use num_dual::{Dual64, DualNum}; use quantity::{ _Moles, _Pressure, Density, Dimensionless, Energy, KELVIN, Length, MolarEnergy, Moles, - Quantity, RGAS, Temperature, Volume, + Quantity, RGAS, Volume, }; use rustdct::DctNum; use std::ops::Sub; -const POTENTIAL_OFFSET: f64 = 2.0; +// A 5 Angstrom buffer is added to Cartesian axes to avoid the molecules seeing each other through the pore walls. +const POTENTIAL_OFFSET: f64 = 5.0; const DEFAULT_GRID_POINTS: usize = 2048; pub type _HenryCoefficient = <_Moles as Sub<_Pressure>>::Output; @@ -27,59 +28,58 @@ pub type HenryCoefficient = Quantity; /// Parameters required to specify a 1D pore. pub struct Pore1D { - pub geometry: Geometry, + pub external_potential: Energy>, pub pore_size: Length, - pub potential: ExternalPotential, - pub n_grid: Option, - pub potential_cutoff: Option, + pub grid: Grid, } impl Pore1D { - pub fn new( + pub fn new( + functional: &F, geometry: Geometry, pore_size: Length, - potential: ExternalPotential, + external_potential: ExternalPotential, n_grid: Option, - potential_cutoff: Option, ) -> Self { + let n_grid = n_grid.unwrap_or(DEFAULT_GRID_POINTS); + let axis = match geometry { + Geometry::Cartesian => { + Axis::new_cartesian(n_grid, 0.5 * pore_size, Some(POTENTIAL_OFFSET)) + } + Geometry::Cylindrical => Axis::new_polar(n_grid, pore_size), + Geometry::Spherical => Axis::new_spherical(n_grid, pore_size), + }; + + let external_potential = + external_potential_1d(pore_size, &external_potential, functional, &axis); + + let grid = Grid::new_1d(axis); + Self { - geometry, + external_potential, pore_size, - potential, - n_grid, - potential_cutoff, + grid, } } -} - -/// Different ways that the thermodynamic state of the fluid in the pore -/// can be specified. -#[derive(Clone)] -pub enum PoreSpecification { - /// Specify the chemical potential (via the bulk state). - ChemicalPotential, - /// Specify the amount of moles of every component. - Moles(Moles>), -} - -/// Trait for the generic implementation of adsorption applications. -pub trait Pore { - /// Initialize a new single pore. - fn initialize( + pub fn initialize( &self, bulk: &State, - density: Option<&Density>>, - external_potential: Option<&Energy>>, + density: Option<&Density>>, specification: PoreSpecification, - ) -> FeosResult>; + ) -> FeosResult> { + Ok(PoreProfile::new( + self.grid.clone(), + bulk, + &self.external_potential, + density, + specification, + )) + } /// Return the pore volume using Helium at 298 K as reference. - fn pore_volume(&self) -> FeosResult - where - D::Larger: Dimension, - { + pub fn pore_volume(&self) -> FeosResult { let bulk = State::new_pure(&&Helium, 298.0 * KELVIN, Density::from_reduced(1.0))?; - let pore = self.initialize(&bulk, None, None, PoreSpecification::ChemicalPotential)?; + let pore = self.initialize(&bulk, None, PoreSpecification::ChemicalPotential)?; let pot = Dimensionless::from_reduced( pore.profile .external_potential @@ -90,6 +90,18 @@ pub trait Pore { } } +/// Different ways that the thermodynamic state of the fluid in the pore +/// can be specified. +#[derive(Clone)] +pub enum PoreSpecification { + /// Specify the chemical potential (via the bulk state). + ChemicalPotential, + /// Specify the amount of moles of every component. + Moles(Moles>), + /// Fix the amount of moles of every component based on the initial density profile. + FixedMoles, +} + /// Density profile and properties of a confined system in arbitrary dimensions. #[derive(Clone)] pub struct PoreProfile { @@ -98,9 +110,6 @@ pub struct PoreProfile { pub interfacial_tension: Option, } -/// Density profile and properties of a 1D confined system. -pub type PoreProfile1D = PoreProfile; - impl PoreProfile where D::Larger: Dimension, @@ -110,15 +119,19 @@ where pub fn new( grid: Grid, bulk: &State, - external_potential: Option>>, + external_potential: &Energy>, density: Option<&Density>>, specification: PoreSpecification, ) -> Self { - let mut profile = DFTProfile::new(grid, bulk, external_potential, density, Some(1)); + let mut profile = DFTProfile::new(grid, bulk, Some(external_potential), density, Some(1)); // fix the number of particles - if let PoreSpecification::Moles(moles) = specification { - profile.specification = DFTSpecification::Moles(moles.to_reduced()) + match specification { + PoreSpecification::ChemicalPotential => (), + PoreSpecification::Moles(moles) => { + profile.specification = DFTSpecification::Moles(moles.to_reduced()) + } + PoreSpecification::FixedMoles => profile.fix_moles(), } Self { @@ -210,64 +223,34 @@ where RGAS * self.profile.temperature * Dimensionless::from_reduced((&h - t * dh).component_div(&h)) } -} - -impl Pore for Pore1D { - fn initialize( - &self, - bulk: &State, - density: Option<&Density>>, - external_potential: Option<&Energy>>, - specification: PoreSpecification, - ) -> FeosResult> { - let dft: &F = &bulk.eos; - let n_grid = self.n_grid.unwrap_or(DEFAULT_GRID_POINTS); - let axis = match self.geometry { - Geometry::Cartesian => { - let potential_offset = POTENTIAL_OFFSET - * bulk - .eos - .sigma_ff() - .iter() - .max_by(|a, b| a.total_cmp(b)) - .unwrap(); - Axis::new_cartesian(n_grid, 0.5 * self.pore_size, Some(potential_offset)) - } - Geometry::Cylindrical => Axis::new_polar(n_grid, self.pore_size), - Geometry::Spherical => Axis::new_spherical(n_grid, self.pore_size), - }; - - // calculate external potential - let external_potential = external_potential.map_or_else( - || { - external_potential_1d( - self.pore_size, - bulk.temperature, - &self.potential, - dft, - &axis, - ) + pub fn into_dyn(self) -> PoreProfile { + // initialize convolver + let t = self.profile.bulk.temperature.to_reduced(); + let weight_functions = self.profile.bulk.eos.weight_functions(t); + let convolver = + ConvolverFFT::plan(&self.profile.grid, &weight_functions, self.profile.lanczos); + + PoreProfile { + profile: DFTProfile { + grid: self.profile.grid, + convolver, + temperature: self.profile.temperature, + density: self.profile.density.into_dyn(), + specification: self.profile.specification, + external_potential: self.profile.external_potential.into_dyn(), + bulk: self.profile.bulk, + solver_log: self.profile.solver_log, + lanczos: self.profile.lanczos, }, - |e| e.clone(), - ); - - // initialize grid - let grid = Grid::new_1d(axis); - - Ok(PoreProfile::new( - grid, - bulk, - Some(external_potential), - density, - specification, - )) + grand_potential: self.grand_potential, + interfacial_tension: self.interfacial_tension, + } } } fn external_potential_1d( pore_width: Length, - temperature: Temperature, potential: &ExternalPotential, fluid_parameters: &P, axis: &Axis, @@ -277,30 +260,25 @@ fn external_potential_1d( Geometry::Cylindrical => pore_width.to_reduced(), Geometry::Cartesian => 0.5 * pore_width.to_reduced(), }; - let t = temperature.to_reduced(); let mut external_potential = match &axis.geometry { Geometry::Cartesian => { potential.calculate_cartesian_potential( &(effective_pore_size + &axis.grid), fluid_parameters, - t, ) + &potential.calculate_cartesian_potential( &(effective_pore_size - &axis.grid), fluid_parameters, - t, ) } Geometry::Spherical => potential.calculate_spherical_potential( &axis.grid, effective_pore_size, fluid_parameters, - t, ), Geometry::Cylindrical => potential.calculate_cylindrical_potential( &axis.grid, effective_pore_size, fluid_parameters, - t, ), }; diff --git a/crates/feos-dft/src/adsorption/pore2d.rs b/crates/feos-dft/src/adsorption/pore2d.rs deleted file mode 100644 index 396434187..000000000 --- a/crates/feos-dft/src/adsorption/pore2d.rs +++ /dev/null @@ -1,46 +0,0 @@ -use super::{FluidParameters, Pore, PoreProfile}; -use crate::{Axis, Grid, HelmholtzEnergyFunctional, adsorption::pore::PoreSpecification}; -use feos_core::{FeosResult, State}; -use ndarray::{Array3, Ix2}; -use quantity::{Angle, Density, Energy, Length}; - -pub struct Pore2D { - system_size: [Length; 2], - angle: Angle, - n_grid: [usize; 2], -} - -pub type PoreProfile2D = PoreProfile; - -impl Pore2D { - pub fn new(system_size: [Length; 2], angle: Angle, n_grid: [usize; 2]) -> Self { - Self { - system_size, - angle, - n_grid, - } - } -} - -impl Pore for Pore2D { - fn initialize( - &self, - bulk: &State, - density: Option<&Density>>, - external_potential: Option<&Energy>>, - specification: PoreSpecification, - ) -> FeosResult> { - // generate grid - let x = Axis::new_cartesian(self.n_grid[0], self.system_size[0], None); - let y = Axis::new_cartesian(self.n_grid[1], self.system_size[1], None); - let grid = Grid::Periodical2(x, y, self.angle); - - Ok(PoreProfile::new( - grid, - bulk, - external_potential.cloned(), - density, - specification, - )) - } -} diff --git a/crates/feos-dft/src/adsorption/pore3d.rs b/crates/feos-dft/src/adsorption/pore3d.rs deleted file mode 100644 index fdf02800a..000000000 --- a/crates/feos-dft/src/adsorption/pore3d.rs +++ /dev/null @@ -1,200 +0,0 @@ -use super::pore::{Pore, PoreProfile}; -use crate::adsorption::{FluidParameters, PoreSpecification}; -use crate::functional::HelmholtzEnergyFunctional; -use crate::geometry::{Axis, Grid}; -use crate::profile::CUTOFF_RADIUS; -use feos_core::{FeosError, FeosResult, ReferenceSystem, State}; -use ndarray::Zip; -use ndarray::prelude::*; -use quantity::{Angle, DEGREES, Density, Energy, Length}; - -/// Parameters required to specify a 3D pore. -pub struct Pore3D { - system_size: [Length; 3], - angles: Option<[Angle; 3]>, - n_grid: [usize; 3], - coordinates: Length>, - sigma_ss: Array1, - epsilon_k_ss: Array1, - cutoff_radius: Option, -} - -impl Pore3D { - pub fn new( - system_size: [Length; 3], - n_grid: [usize; 3], - coordinates: Length>, - sigma_ss: Array1, - epsilon_k_ss: Array1, - angles: Option<[Angle; 3]>, - cutoff_radius: Option, - ) -> Self { - Self { - system_size, - angles, - n_grid, - coordinates, - sigma_ss, - epsilon_k_ss, - cutoff_radius, - } - } -} - -/// Density profile and properties of a 3D confined system. -pub type PoreProfile3D = PoreProfile; - -impl Pore for Pore3D { - fn initialize( - &self, - bulk: &State, - density: Option<&Density>>, - external_potential: Option<&Energy>>, - specification: PoreSpecification, - ) -> FeosResult> { - let dft: &F = &bulk.eos; - - // generate grid - let x = Axis::new_cartesian(self.n_grid[0], self.system_size[0], None); - let y = Axis::new_cartesian(self.n_grid[1], self.system_size[1], None); - let z = Axis::new_cartesian(self.n_grid[2], self.system_size[2], None); - - let coordinates = self.coordinates.to_reduced(); - - // For non-orthorombic unit cells, the external potential has to be - // provided at the moment - if let (Some(_), None) = (self.angles, external_potential) { - return Err(FeosError::UndeterminedState( - "For non-orthorombic unit cells, the external potential has to provided!".into(), - )); - } - - // calculate external potential - let external_potential = external_potential.map_or_else( - || { - external_potential_3d( - dft, - [&x, &y, &z], - self.system_size, - coordinates, - &self.sigma_ss, - &self.epsilon_k_ss, - self.cutoff_radius, - ) - }, - |e| Ok(e.clone()), - )?; - let grid = Grid::Periodical3(x, y, z, self.angles.unwrap_or([90.0 * DEGREES; 3])); - - Ok(PoreProfile::new( - grid, - bulk, - Some(external_potential), - density, - specification, - )) - } -} - -pub fn external_potential_3d( - functional: &F, - axis: [&Axis; 3], - system_size: [Length; 3], - coordinates: Array2, - sigma_ss: &Array1, - epsilon_ss: &Array1, - cutoff_radius: Option, -) -> FeosResult>> { - // allocate external potential - let m = functional.m(); - let mut external_potential = Array4::zeros(( - m.len(), - axis[0].grid.len(), - axis[1].grid.len(), - axis[2].grid.len(), - )); - - let system_size = [ - system_size[0].to_reduced(), - system_size[1].to_reduced(), - system_size[2].to_reduced(), - ]; - - let cutoff_radius = cutoff_radius - .unwrap_or(Length::from_reduced(CUTOFF_RADIUS)) - .to_reduced(); - - if system_size.iter().any(|&s| s < 2.0 * cutoff_radius) { - return Err(FeosError::UndeterminedState( - "The unit cell is smaller than 2*cutoff".into(), - )); - } - - // square cut-off radius - let cutoff_radius2 = cutoff_radius.powi(2); - - // calculate external potential - let sigma_ff = functional.sigma_ff(); - let epsilon_k_ff = functional.epsilon_k_ff(); - - Zip::indexed(&mut external_potential).par_for_each(|(i, ix, iy, iz), u| { - let distance2 = calculate_distance2( - [axis[0].grid[ix], axis[1].grid[iy], axis[2].grid[iz]], - &coordinates, - system_size, - ); - let sigma_sf = sigma_ss.mapv(|s| (s + sigma_ff[i]) / 2.0); - let epsilon_sf = epsilon_ss.mapv(|e| (e * epsilon_k_ff[i]).sqrt()); - *u = (0..sigma_ss.len()) - .map(|alpha| { - m[i] * evaluate_lj_potential( - distance2[alpha], - sigma_sf[alpha], - epsilon_sf[alpha], - cutoff_radius2, - ) - }) - .sum::() - }); - - Ok(Energy::from_reduced(external_potential)) -} - -/// Evaluate LJ12-6 potential between solid site "alpha" and fluid segment -pub(super) fn evaluate_lj_potential( - distance2: f64, - sigma: f64, - epsilon: f64, - cutoff_radius2: f64, -) -> f64 { - let sigma_r = sigma.powi(2) / distance2; - - let potential: f64 = if distance2 > cutoff_radius2 { - 0.0 - } else if distance2 == 0.0 { - f64::INFINITY - } else { - 4.0 * epsilon * (sigma_r.powi(6) - sigma_r.powi(3)) - }; - - potential -} - -/// Evaluate the squared euclidian distance between a point and the coordinates of all solid atoms. -pub(super) fn calculate_distance2( - point: [f64; 3], - coordinates: &Array2, - system_size: [f64; 3], -) -> Array1 { - Array1::from_shape_fn(coordinates.ncols(), |i| { - let mut rx = coordinates[[0, i]] - point[0]; - let mut ry = coordinates[[1, i]] - point[1]; - let mut rz = coordinates[[2, i]] - point[2]; - - rx -= system_size[0] * (rx / system_size[0]).round(); - ry -= system_size[1] * (ry / system_size[1]).round(); - rz -= system_size[2] * (rz / system_size[2]).round(); - - rx.powi(2) + ry.powi(2) + rz.powi(2) - }) -} diff --git a/crates/feos-dft/src/geometry.rs b/crates/feos-dft/src/geometry.rs index ca5ac01c6..e69409e9b 100644 --- a/crates/feos-dft/src/geometry.rs +++ b/crates/feos-dft/src/geometry.rs @@ -1,6 +1,6 @@ use feos_core::ReferenceSystem; -use ndarray::{Array1, Array2}; -use quantity::{Angle, Length, Quantity}; +use ndarray::{Array1, Array2, Array3, ArrayD}; +use quantity::{Angle, DEGREES, Length, Quantity}; use std::f64::consts::{FRAC_PI_3, PI}; /// Grids with up to three dimensions. @@ -69,6 +69,45 @@ impl Grid { _ => 1.0, } } + + pub fn mesh(&self) -> Vec>> { + match self { + Grid::Cartesian1(ax) | Grid::Spherical(ax) | Grid::Polar(ax) => { + vec![Length::from_reduced(ax.grid.clone()).into_dyn()] + } + Grid::Cartesian2(u, v) => mesh_2d(u, v, 90.0 * DEGREES), + Grid::Periodical2(u, v, alpha) => mesh_2d(u, v, *alpha), + Grid::Cylindrical { r, z } => mesh_2d(r, z, 90.0 * DEGREES), + Grid::Cartesian3(u, v, w) => mesh_3d(u, v, w, [90.0 * DEGREES; 3]), + Grid::Periodical3(u, v, w, angles) => mesh_3d(u, v, w, *angles), + } + } +} + +fn mesh_2d(u: &Axis, v: &Axis, alpha: Angle) -> Vec>> { + let u_grid = Array2::from_shape_fn([u.grid.len(), v.grid.len()], |(i, _)| u.grid[i]); + let v_grid = Array2::from_shape_fn([u.grid.len(), v.grid.len()], |(_, j)| v.grid[j]); + let x = Length::from_reduced(u_grid + &v_grid * alpha.cos()); + let y = Length::from_reduced(v_grid * alpha.sin()); + vec![x.into_dyn(), y.into_dyn()] +} + +fn mesh_3d( + u: &Axis, + v: &Axis, + w: &Axis, + [alpha, beta, gamma]: [Angle; 3], +) -> Vec>> { + let shape = [u.grid.len(), v.grid.len(), w.grid.len()]; + let u_grid = Array3::from_shape_fn(shape, |(i, _, _)| u.grid[i]); + let v_grid = Array3::from_shape_fn(shape, |(_, j, _)| v.grid[j]); + let w_grid = Array3::from_shape_fn(shape, |(_, _, k)| w.grid[k]); + let xi = (alpha.cos() - gamma.cos() * beta.cos()) / gamma.sin(); + let zeta = (1.0_f64 - beta.cos().powi(2) - xi * xi).sqrt(); + let x = Length::from_reduced(u_grid + &v_grid * gamma.cos() + &w_grid * beta.cos()); + let y = Length::from_reduced(v_grid * gamma.sin() + &w_grid * xi); + let z = Length::from_reduced(w_grid * zeta); + vec![x.into_dyn(), y.into_dyn(), z.into_dyn()] } /// Geometries of individual axes. diff --git a/crates/feos-dft/src/profile/mod.rs b/crates/feos-dft/src/profile/mod.rs index c9423fded..6497faa23 100644 --- a/crates/feos-dft/src/profile/mod.rs +++ b/crates/feos-dft/src/profile/mod.rs @@ -4,22 +4,15 @@ use crate::geometry::Grid; use crate::solver::{DFTSolver, DFTSolverLog}; use feos_core::{FeosError, FeosResult, ReferenceSystem, State}; use nalgebra::{DVector, Dyn, U1}; -use ndarray::{ - Array, Array1, Array2, Array3, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix1, Ix2, Ix3, - RemoveAxis, -}; +use ndarray::{Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, RemoveAxis}; use num_dual::DualNum; -use quantity::{ - _Volume, DEGREES, Density, Energy, Entropy, Length, Moles, Quantity, Temperature, Volume, -}; +use quantity::{_Volume, Density, Energy, Entropy, Length, Moles, Quantity, Temperature, Volume}; use std::ops::{Add, MulAssign}; use std::sync::Arc; mod properties; const MAX_POTENTIAL: f64 = 50.0; -#[cfg(feature = "rayon")] -pub(crate) const CUTOFF_RADIUS: f64 = 14.0; /// General specifications for the chemical potential in a DFT calculation. /// @@ -68,83 +61,22 @@ pub struct DFTProfile { pub lanczos: Option, } -impl DFTProfile { - pub fn r(&self) -> Length> { - Length::from_reduced(self.grid.grids()[0].to_owned()) - } - - pub fn z(&self) -> Length> { - Length::from_reduced(self.grid.grids()[0].to_owned()) - } -} - -impl DFTProfile { - pub fn edges(&self) -> [Length>; 2] { - [ - Length::from_reduced(self.grid.axes()[0].edges.to_owned()), - Length::from_reduced(self.grid.axes()[1].edges.to_owned()), - ] - } - - pub fn meshgrid(&self) -> [Length>; 2] { - let (u, v, alpha) = match &self.grid { - Grid::Cartesian2(u, v) => (u, v, 90.0 * DEGREES), - Grid::Periodical2(u, v, alpha) => (u, v, *alpha), - _ => unreachable!(), - }; - let u_grid = Array::from_shape_fn([u.grid.len(), v.grid.len()], |(i, _)| u.grid[i]); - let v_grid = Array::from_shape_fn([u.grid.len(), v.grid.len()], |(_, j)| v.grid[j]); - let x = Length::from_reduced(u_grid + &v_grid * alpha.cos()); - let y = Length::from_reduced(v_grid * alpha.sin()); - [x, y] - } - - pub fn r(&self) -> Length> { - Length::from_reduced(self.grid.grids()[0].to_owned()) - } - - pub fn z(&self) -> Length> { - Length::from_reduced(self.grid.grids()[1].to_owned()) - } -} - -impl DFTProfile { - pub fn edges(&self) -> [Length>; 3] { - [ - Length::from_reduced(self.grid.axes()[0].edges.to_owned()), - Length::from_reduced(self.grid.axes()[1].edges.to_owned()), - Length::from_reduced(self.grid.axes()[2].edges.to_owned()), - ] - } - - pub fn meshgrid(&self) -> [Length>; 3] { - let (u, v, w, [alpha, beta, gamma]) = match &self.grid { - Grid::Cartesian3(u, v, w) => (u, v, w, [90.0 * DEGREES; 3]), - Grid::Periodical3(u, v, w, angles) => (u, v, w, *angles), - _ => unreachable!(), - }; - let shape = [u.grid.len(), v.grid.len(), w.grid.len()]; - let u_grid = Array::from_shape_fn(shape, |(i, _, _)| u.grid[i]); - let v_grid = Array::from_shape_fn(shape, |(_, j, _)| v.grid[j]); - let w_grid = Array::from_shape_fn(shape, |(_, _, k)| w.grid[k]); - let xi = (alpha.cos() - gamma.cos() * beta.cos()) / gamma.sin(); - let zeta = (1.0_f64 - beta.cos().powi(2) - xi * xi).sqrt(); - let x = Length::from_reduced(u_grid + &v_grid * gamma.cos() + &w_grid * beta.cos()); - let y = Length::from_reduced(v_grid * gamma.sin() + &w_grid * xi); - let z = Length::from_reduced(w_grid * zeta); - [x, y, z] - } - - pub fn x(&self) -> Length> { - Length::from_reduced(self.grid.grids()[0].to_owned()) - } - - pub fn y(&self) -> Length> { - Length::from_reduced(self.grid.grids()[1].to_owned()) +impl DFTProfile { + pub fn axes(&self) -> Vec>> { + self.grid + .grids() + .into_iter() + .cloned() + .map(Length::from_reduced) + .collect() } - pub fn z(&self) -> Length> { - Length::from_reduced(self.grid.grids()[2].to_owned()) + pub fn edges(&self) -> Vec>> { + self.grid + .axes() + .into_iter() + .map(|a| Length::from_reduced(a.edges.clone())) + .collect() } } @@ -163,7 +95,7 @@ where pub fn new( grid: Grid, bulk: &State, - external_potential: Option>>, + external_potential: Option<&Energy>>, density: Option<&Density>>, lanczos: Option, ) -> Self { @@ -182,7 +114,7 @@ where Array::zeros(n_grid).into_dimensionality().unwrap() }, |e| { - let mut external_potential = e.into_reduced() / t; + let mut external_potential = e.to_reduced() / t; external_potential.map_inplace(|x| { if *x > MAX_POTENTIAL { *x = MAX_POTENTIAL @@ -193,21 +125,22 @@ where ); // initialize density - let density = if let Some(density) = density { - density.to_owned() - } else { - let exp_dfdrho = (-&external_potential).mapv(f64::exp); - let mut bonds = bulk.eos.bond_integrals(t, &exp_dfdrho, convolver.as_ref()); - bonds *= &exp_dfdrho; - let mut density = Array::zeros(external_potential.raw_dim()); - let bulk_density = bulk.partial_density().into_reduced(); - for (s, &c) in bulk.eos.component_index().iter().enumerate() { - density.index_axis_mut(Axis_nd(0), s).assign( - &(bonds.index_axis(Axis_nd(0), s).map(|is| is.min(1.0)) * bulk_density[c]), - ); - } - Density::from_reduced(density) - }; + let density = density.map_or_else( + || { + let exp_dfdrho = (-&external_potential).mapv(f64::exp); + let mut bonds = bulk.eos.bond_integrals(t, &exp_dfdrho, convolver.as_ref()); + bonds *= &exp_dfdrho; + let mut density = Array::zeros(external_potential.raw_dim()); + let bulk_density = bulk.partial_density().into_reduced(); + for (s, &c) in bulk.eos.component_index().iter().enumerate() { + density.index_axis_mut(Axis_nd(0), s).assign( + &(bonds.index_axis(Axis_nd(0), s).map(|is| is.min(1.0)) * bulk_density[c]), + ); + } + Density::from_reduced(density) + }, + Clone::clone, + ); Self { grid, diff --git a/crates/feos-dft/src/solvation/pair_correlation.rs b/crates/feos-dft/src/solvation/pair_correlation.rs index 85b435d2e..ba71e35e7 100644 --- a/crates/feos-dft/src/solvation/pair_correlation.rs +++ b/crates/feos-dft/src/solvation/pair_correlation.rs @@ -37,11 +37,11 @@ impl PairCorrelation { // calculate external potential let t = bulk.temperature.to_reduced(); - let external_potential = dft.pair_potential(test_particle, &axis.grid, t) / t; + let external_potential = dft.pair_potential(test_particle, &axis.grid, t); let grid = Grid::Spherical(axis); Self { - profile: DFTProfile::new(grid, bulk, Some(external_potential), None, Some(1)), + profile: DFTProfile::new(grid, bulk, Some(&external_potential), None, Some(1)), pair_correlation_function: None, self_solvation_free_energy: None, structure_factor: None, diff --git a/crates/feos-dft/src/solvation/solvation_profile.rs b/crates/feos-dft/src/solvation/solvation_profile.rs index e31ca6494..5bbd6ec48 100644 --- a/crates/feos-dft/src/solvation/solvation_profile.rs +++ b/crates/feos-dft/src/solvation/solvation_profile.rs @@ -1,13 +1,15 @@ use crate::adsorption::FluidParameters; use crate::functional::HelmholtzEnergyFunctional; use crate::geometry::{Axis, Grid}; -use crate::profile::{CUTOFF_RADIUS, DFTProfile}; +use crate::profile::DFTProfile; use crate::solver::DFTSolver; use feos_core::{Contributions, FeosResult, ReferenceSystem, State}; use ndarray::Zip; use ndarray::prelude::*; use quantity::{Energy, Length, MolarEnergy, Moles}; +const CUTOFF_RADIUS: f64 = 14.0; + /// Density profile and properties of a solute in a inhomogeneous bulk fluid. pub struct SolvationProfile { pub profile: DFTProfile, @@ -88,7 +90,7 @@ impl SolvationProfile { let grid = Grid::Cartesian3(x, y, z); Ok(Self { - profile: DFTProfile::new(grid, bulk, Some(external_potential), None, Some(1)), + profile: DFTProfile::new(grid, bulk, Some(&external_potential), None, Some(1)), grand_potential: None, solvation_free_energy: None, }) diff --git a/crates/feos/benches/dft_pore.rs b/crates/feos/benches/dft_pore.rs index dfa2b9f58..15dc918ff 100644 --- a/crates/feos/benches/dft_pore.rs +++ b/crates/feos/benches/dft_pore.rs @@ -3,9 +3,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use feos::core::parameter::IdentifierOption; use feos::core::{PhaseEquilibrium, State}; -use feos::dft::adsorption::{ - ExternalPotential, Pore, Pore1D, PoreSpecification::ChemicalPotential, -}; +use feos::dft::adsorption::{ExternalPotential, Pore1D, PoreSpecification::ChemicalPotential}; use feos::dft::{DFTSolver, Geometry}; use feos::gc_pcsaft::{GcPcSaftFunctional, GcPcSaftParameters}; use feos::hard_sphere::{FMTFunctional, FMTVersion}; @@ -18,16 +16,16 @@ fn fmt(c: &mut Criterion) { let func = &FMTFunctional::new(dvector![1.0], FMTVersion::WhiteBear); let pore = Pore1D::new( + &func, Geometry::Cartesian, 10.0 * ANGSTROM, ExternalPotential::HardWall { sigma_ss: 1.0 }, None, - None, ); let bulk = State::new_pure(&func, KELVIN, 0.75 / NAV / ANGSTROM.powi::<3>()).unwrap(); group.bench_function("liquid", |b| { b.iter(|| { - pore.initialize(&bulk, None, None, ChemicalPotential) + pore.initialize(&bulk, None, ChemicalPotential) .unwrap() .solve(None) }) @@ -45,6 +43,7 @@ fn pcsaft(c: &mut Criterion) { .unwrap(); let func = &PcSaftFunctional::new(parameters); let pore = Pore1D::new( + &func, Geometry::Cartesian, 20.0 * ANGSTROM, ExternalPotential::LJ93 { @@ -53,13 +52,12 @@ fn pcsaft(c: &mut Criterion) { rho_s: 0.08, }, None, - None, ); let vle = PhaseEquilibrium::pure(&func, 300.0 * KELVIN, None, Default::default()).unwrap(); let bulk = vle.liquid(); group.bench_function("butane_liquid", |b| { b.iter(|| { - pore.initialize(bulk, None, None, ChemicalPotential) + pore.initialize(bulk, None, ChemicalPotential) .unwrap() .solve(None) }) @@ -67,7 +65,7 @@ fn pcsaft(c: &mut Criterion) { let bulk = State::new_pure(&func, 300.0 * KELVIN, vle.vapor().density * 0.2).unwrap(); group.bench_function("butane_vapor", |b| { b.iter(|| { - pore.initialize(&bulk, None, None, ChemicalPotential) + pore.initialize(&bulk, None, ChemicalPotential) .unwrap() .solve(None) }) @@ -87,7 +85,7 @@ fn pcsaft(c: &mut Criterion) { let bulk = vle.liquid(); group.bench_function("butane_pentane_liquid", |b| { b.iter(|| { - pore.initialize(bulk, None, None, ChemicalPotential) + pore.initialize(bulk, None, ChemicalPotential) .unwrap() .solve(None) }) @@ -96,7 +94,7 @@ fn pcsaft(c: &mut Criterion) { State::new_density(&func, 300.0 * KELVIN, vle.vapor().partial_density() * 0.2).unwrap(); group.bench_function("butane_pentane_vapor", |b| { b.iter(|| { - pore.initialize(&bulk, None, None, ChemicalPotential) + pore.initialize(&bulk, None, ChemicalPotential) .unwrap() .solve(None) }) @@ -115,8 +113,9 @@ fn gc_pcsaft(c: &mut Criterion) { IdentifierOption::Name, ) .unwrap(); - let func = GcPcSaftFunctional::new(parameters); + let func = &GcPcSaftFunctional::new(parameters); let pore = Pore1D::new( + &func, Geometry::Cartesian, 20.0 * ANGSTROM, ExternalPotential::LJ93 { @@ -125,16 +124,15 @@ fn gc_pcsaft(c: &mut Criterion) { rho_s: 0.08, }, None, - None, ); - let vle = PhaseEquilibrium::pure(&&func, 300.0 * KELVIN, None, Default::default()).unwrap(); + let vle = PhaseEquilibrium::pure(&func, 300.0 * KELVIN, None, Default::default()).unwrap(); let bulk = vle.liquid(); let solver = DFTSolver::new(None) .picard_iteration(None, None, Some(1e-5), None) .anderson_mixing(None, None, None, None, None); group.bench_function("butane_liquid", |b| { b.iter(|| { - pore.initialize(bulk, None, None, ChemicalPotential) + pore.initialize(bulk, None, ChemicalPotential) .unwrap() .solve(Some(&solver)) }) diff --git a/crates/feos/tests/gc_pcsaft/dft.rs b/crates/feos/tests/gc_pcsaft/dft.rs index 9238500d0..6046ab4c3 100644 --- a/crates/feos/tests/gc_pcsaft/dft.rs +++ b/crates/feos/tests/gc_pcsaft/dft.rs @@ -4,7 +4,7 @@ use approx::assert_relative_eq; use feos::gc_pcsaft::{GcPcSaft, GcPcSaftFunctional, GcPcSaftParameters}; use feos_core::parameter::{ChemicalRecord, Identifier, IdentifierOption, SegmentRecord}; use feos_core::{PhaseEquilibrium, State, Verbosity}; -use feos_dft::adsorption::{ExternalPotential, Pore, Pore1D, PoreSpecification}; +use feos_dft::adsorption::{ExternalPotential, Pore1D, PoreSpecification}; use feos_dft::interface::PlanarInterface; use feos_dft::{DFTSolver, Geometry}; use nalgebra::dvector; @@ -218,6 +218,7 @@ fn test_dft_assoc() -> Result<(), Box> { .anderson_mixing(None, None, None, None, None); let bulk = State::new_npt(&func, t, 5.0 * BAR, (), None)?; Pore1D::new( + &func, Geometry::Cartesian, 20.0 * ANGSTROM, ExternalPotential::LJ93 { @@ -226,9 +227,8 @@ fn test_dft_assoc() -> Result<(), Box> { rho_s: 0.08, }, None, - None, ) - .initialize(&bulk, None, None, PoreSpecification::ChemicalPotential) + .initialize(&bulk, None, PoreSpecification::ChemicalPotential) .unwrap() .solve(Some(&solver))?; Ok(()) diff --git a/py-feos/src/dft/adsorption/external_potential.rs b/py-feos/src/dft/adsorption/external_potential.rs index 03adbb762..909db3291 100644 --- a/py-feos/src/dft/adsorption/external_potential.rs +++ b/py-feos/src/dft/adsorption/external_potential.rs @@ -1,12 +1,7 @@ use feos_dft::adsorption::ExternalPotential; -// Only the rayon-gated `FreeEnergyAveraged` constructor uses these. -#[cfg(feature = "rayon")] -use ndarray::Array2; use numpy::PyArray1; use numpy::prelude::*; use pyo3::prelude::*; -#[cfg(feature = "rayon")] -use quantity::Length; /// A collection of external potentials. #[pyclass(name = "ExternalPotential", from_py_object)] @@ -205,56 +200,4 @@ impl PyExternalPotential { rho_s, }) } - - /// Free-energy averaged potential - /// - /// for details see: `J. Eller, J. Gross (2021) `_ - /// - /// Parameters - /// ---------- - /// coordinates: SIArray2 - /// The positions of all interaction sites in the solid. - /// sigma_ss : numpy.ndarray[float] - /// The size parameters of all interaction sites. - /// epsilon_k_ss : numpy.ndarray[float] - /// The energy parameter of all interaction sites. - /// pore_center : [SINumber; 3] - /// The cartesian coordinates of the center of the pore - /// system_size : [SINumber; 3] - /// The size of the unit cell. - /// n_grid : [int; 2] - /// The number of grid points in each direction. - /// cutoff_radius : float, optional - /// The cutoff used in the calculation of fluid/wall interactions. - /// Returns - /// ------- - /// ExternalPotential - /// - // The free-energy-averaged potential is rayon-gated in feos-dft (3D FFT), - // so it is absent from the threadless wasm/emscripten build. - #[cfg(feature = "rayon")] - #[staticmethod] - #[pyo3( - text_signature = "(coordinates, sigma_ss, epsilon_k_ss, pore_center, system_size, n_grid, cutoff_radius=None)", - signature = (coordinates, sigma_ss, epsilon_k_ss, pore_center, system_size, n_grid, cutoff_radius=None) - )] - pub fn FreeEnergyAveraged( - coordinates: Length>, - sigma_ss: &Bound<'_, PyArray1>, - epsilon_k_ss: &Bound<'_, PyArray1>, - pore_center: [f64; 3], - system_size: [Length; 3], - n_grid: [usize; 2], - cutoff_radius: Option, - ) -> Self { - Self(ExternalPotential::FreeEnergyAveraged { - coordinates, - sigma_ss: sigma_ss.to_owned_array(), - epsilon_k_ss: epsilon_k_ss.to_owned_array(), - pore_center, - system_size, - n_grid, - cutoff_radius, - }) - } } diff --git a/py-feos/src/dft/adsorption/mod.rs b/py-feos/src/dft/adsorption/mod.rs index a932ccc44..911fdc7b5 100644 --- a/py-feos/src/dft/adsorption/mod.rs +++ b/py-feos/src/dft/adsorption/mod.rs @@ -5,9 +5,7 @@ use crate::error::PyFeosError; use crate::ideal_gas::IdealGasModel; use crate::residual::ResidualModel; use feos_core::EquationOfState; -#[cfg(feature = "rayon")] -use feos_dft::adsorption::Adsorption3D; -use feos_dft::adsorption::{Adsorption, Adsorption1D}; +use feos_dft::adsorption::Adsorption; use nalgebra::DMatrix; use ndarray::*; use numpy::*; @@ -19,261 +17,278 @@ mod external_potential; mod pore; pub use external_potential::PyExternalPotential; -pub use pore::{PyPore1D, PyPore2D, PyPoreProfile1D, PyPoreSpecification}; -#[cfg(feature = "rayon")] -pub use pore::{PyPore3D, PyPoreProfile3D}; +pub use pore::{PyGrid, PyPore1D, PyPoreProfile, PyPoreSpecification}; -/// Container structure for adsorption isotherms in 1D pores. -#[pyclass(name = "Adsorption1D")] -pub struct PyAdsorption1D(Adsorption1D, ResidualModel>>>); +/// Container structure for adsorption isotherms. +#[pyclass(name = "Adsorption")] +pub struct PyAdsorption(Adsorption, ResidualModel>>>); -/// Container structure for adsorption isotherms in 3D pores. -#[cfg(feature = "rayon")] -#[pyclass(name = "Adsorption3D")] -pub struct PyAdsorption3D(Adsorption3D, ResidualModel>>>); +#[pymethods] +impl PyAdsorption { + /// Calculate an adsorption isotherm for the given pressure range. + /// The profiles are evaluated starting from the lowest pressure. + /// The resulting density profiles can be metastable. + /// + /// Parameters + /// ---------- + /// functional : HelmholtzEnergyFunctional + /// The Helmholtz energy functional. + /// temperature : SINumber + /// The temperature. + /// pressure : SIArray1 + /// The pressures for which the profiles are calculated. + /// grid : Grid + /// The grid on which the density is calculated. + /// external_potential : SIArray + /// The external potential used to model wall-fluid interactions. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// The composition of the mixture. + /// solver: DFTSolver, optional + /// Custom solver options. + /// + /// Returns + /// ------- + /// Adsorption + /// + #[staticmethod] + #[pyo3( + text_signature = "(functional, temperature, pressure, grid, external_potential, composition=None, solver=None)" + )] + #[pyo3(signature = (functional, temperature, pressure, grid, external_potential, composition=None, solver=None))] + fn adsorption_isotherm( + functional: &PyEquationOfState, + temperature: Temperature, + pressure: Pressure>, + grid: PyGrid, + external_potential: Energy>, + composition: Option<&Bound<'_, PyAny>>, + solver: Option, + ) -> PyResult { + Ok(Self( + Adsorption::adsorption_isotherm( + &functional.0, + temperature, + &pressure, + &grid.0, + &external_potential, + Compositions::try_from(composition)?, + solver.map(|s| s.0).as_ref(), + ) + .map_err(PyFeosError::from)?, + )) + } -macro_rules! impl_adsorption_isotherm { - ($py_adsorption:ty, $py_pore:ty, $py_pore_profile:ident) => { - #[pymethods] - impl $py_adsorption { - /// Calculate an adsorption isotherm for the given pressure range. - /// The profiles are evaluated starting from the lowest pressure. - /// The resulting density profiles can be metastable. - /// - /// Parameters - /// ---------- - /// functional : HelmholtzEnergyFunctional - /// The Helmholtz energy functional. - /// temperature : SINumber - /// The temperature. - /// pressure : SIArray1 - /// The pressures for which the profiles are calculated. - /// pore : Pore - /// The pore parameters. - /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional - /// The composition of the mixture. - /// solver: DFTSolver, optional - /// Custom solver options. - /// - /// Returns - /// ------- - /// Adsorption - /// - #[staticmethod] - #[pyo3(text_signature = "(functional, temperature, pressure, pore, composition=None, solver=None)")] - #[pyo3(signature = (functional, temperature, pressure, pore, composition=None, solver=None))] - fn adsorption_isotherm( - functional: &PyEquationOfState, - temperature: Temperature, - pressure: Pressure>, - pore: &$py_pore, - composition: Option<&Bound<'_, PyAny>>, - solver: Option, - ) -> PyResult { - Ok(Self(Adsorption::adsorption_isotherm( - &functional.0, - temperature, - &pressure, - &pore.0, - Compositions::try_from(composition)?, - solver.map(|s| s.0).as_ref(), - ).map_err(PyFeosError::from)?)) - } + /// Calculate a desorption isotherm for the given pressure range. + /// The profiles are evaluated starting from the highest pressure. + /// The resulting density profiles can be metastable. + /// + /// Parameters + /// ---------- + /// functional : HelmholtzEnergyFunctional + /// The Helmholtz energy functional. + /// temperature : SINumber + /// The temperature. + /// pressure : SIArray1 + /// The pressures for which the profiles are calculated. + /// grid : Grid + /// The grid on which the density is calculated. + /// external_potential : SIArray + /// The external potential used to model wall-fluid interactions. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// The composition of the mixture. + /// solver: DFTSolver, optional + /// Custom solver options. + /// + /// Returns + /// ------- + /// Adsorption + /// + #[staticmethod] + #[pyo3( + text_signature = "(functional, temperature, pressure, grid, external_potential, composition=None, solver=None)" + )] + #[pyo3(signature = (functional, temperature, pressure, grid, external_potential, composition=None, solver=None))] + fn desorption_isotherm( + functional: &PyEquationOfState, + temperature: Temperature, + pressure: Pressure>, + grid: PyGrid, + external_potential: Energy>, + composition: Option<&Bound<'_, PyAny>>, + solver: Option, + ) -> PyResult { + Ok(Self( + Adsorption::desorption_isotherm( + &functional.0, + temperature, + &pressure, + &grid.0, + &external_potential, + Compositions::try_from(composition)?, + solver.map(|s| s.0).as_ref(), + ) + .map_err(PyFeosError::from)?, + )) + } - /// Calculate a desorption isotherm for the given pressure range. - /// The profiles are evaluated starting from the highest pressure. - /// The resulting density profiles can be metastable. - /// - /// Parameters - /// ---------- - /// functional : HelmholtzEnergyFunctional - /// The Helmholtz energy functional. - /// temperature : SINumber - /// The temperature. - /// pressure : SIArray1 - /// The pressures for which the profiles are calculated. - /// pore : Pore - /// The pore parameters. - /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional - /// The composition of the mixture. - /// solver: DFTSolver, optional - /// Custom solver options. - /// - /// Returns - /// ------- - /// Adsorption - /// - #[staticmethod] - #[pyo3(text_signature = "(functional, temperature, pressure, pore, composition=None, solver=None)")] - #[pyo3(signature = (functional, temperature, pressure, pore, composition=None, solver=None))] - fn desorption_isotherm( - functional: &PyEquationOfState, - temperature: Temperature, - pressure: Pressure>, - pore: &$py_pore, - composition: Option<&Bound<'_, PyAny>>, - solver: Option, - ) -> PyResult { - Ok(Self(Adsorption::desorption_isotherm( - &functional.0, - temperature, - &pressure, - &pore.0, - Compositions::try_from(composition)?, - solver.map(|s| s.0).as_ref(), - ).map_err(PyFeosError::from)?)) - } + /// Calculate an equilibrium isotherm for the given pressure range. + /// A phase equilibrium in the pore is calculated to determine the + /// stable phases for every pressure. If no phase equilibrium can be + /// calculated, the isotherm is calculated twice, one in the adsorption + /// direction and once in the desorption direction to determine the + /// stability of the profiles. + /// + /// Parameters + /// ---------- + /// functional : HelmholtzEnergyFunctional + /// The Helmholtz energy functional. + /// temperature : SINumber + /// The temperature. + /// pressure : SIArray1 + /// The pressures for which the profiles are calculated. + /// grid : Grid + /// The grid on which the density is calculated. + /// external_potential : SIArray + /// The external potential used to model wall-fluid interactions. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// The composition of the mixture. + /// solver: DFTSolver, optional + /// Custom solver options. + /// + /// Returns + /// ------- + /// Adsorption + /// + #[staticmethod] + #[pyo3( + text_signature = "(functional, temperature, pressure, grid, external_potential, composition=None, solver=None)" + )] + #[pyo3(signature = (functional, temperature, pressure, grid, external_potential, composition=None, solver=None))] + fn equilibrium_isotherm( + functional: &PyEquationOfState, + temperature: Temperature, + pressure: Pressure>, + grid: PyGrid, + external_potential: Energy>, + composition: Option<&Bound<'_, PyAny>>, + solver: Option, + ) -> PyResult { + Ok(Self( + Adsorption::equilibrium_isotherm( + &functional.0, + temperature, + &pressure, + &grid.0, + &external_potential, + Compositions::try_from(composition)?, + solver.map(|s| s.0).as_ref(), + ) + .map_err(PyFeosError::from)?, + )) + } - /// Calculate an equilibrium isotherm for the given pressure range. - /// A phase equilibrium in the pore is calculated to determine the - /// stable phases for every pressure. If no phase equilibrium can be - /// calculated, the isotherm is calculated twice, one in the adsorption - /// direction and once in the desorption direction to determine the - /// stability of the profiles. - /// - /// Parameters - /// ---------- - /// functional : HelmholtzEnergyFunctional - /// The Helmholtz energy functional. - /// temperature : SINumber - /// The temperature. - /// pressure : SIArray1 - /// The pressures for which the profiles are calculated. - /// pore : Pore - /// The pore parameters. - /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional - /// The composition of the mixture. - /// solver: DFTSolver, optional - /// Custom solver options. - /// - /// Returns - /// ------- - /// Adsorption - /// - #[staticmethod] - #[pyo3(text_signature = "(functional, temperature, pressure, pore, composition=None, solver=None)")] - #[pyo3(signature = (functional, temperature, pressure, pore, composition=None, solver=None))] - fn equilibrium_isotherm( - functional: &PyEquationOfState, - temperature: Temperature, - pressure: Pressure>, - pore: &$py_pore, - composition: Option<&Bound<'_, PyAny>>, - solver: Option, - ) -> PyResult { - Ok(Self(Adsorption::equilibrium_isotherm( - &functional.0, - temperature, - &pressure, - &pore.0, - Compositions::try_from(composition)?, - solver.map(|s| s.0).as_ref(), - ).map_err(PyFeosError::from)?)) - } + /// Calculate a phase equilibrium in a pore. + /// + /// Parameters + /// ---------- + /// functional : HelmholtzEnergyFunctional + /// The Helmholtz energy functional. + /// temperature : SINumber + /// The temperature. + /// p_min : SINumber + /// A suitable lower limit for the pressure. + /// p_max : SINumber + /// A suitable upper limit for the pressure. + /// grid : Grid + /// The grid on which the density is calculated. + /// external_potential : SIArray + /// The external potential used to model wall-fluid interactions. + /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional + /// The composition of the mixture. + /// solver: DFTSolver, optional + /// Custom solver options. + /// max_iter : int, optional + /// The maximum number of iterations of the phase equilibrium calculation. + /// tol: float, optional + /// The tolerance of the phase equilibrium calculation. + /// verbosity: Verbosity, optional + /// The verbosity of the phase equilibrium calculation. + /// + /// Returns + /// ------- + /// Adsorption + /// + #[staticmethod] + #[pyo3( + text_signature = "(functional, temperature, p_min, p_max, grid, external_potential, composition=None, solver=None, max_iter=None, tol=None, verbosity=None)" + )] + #[pyo3(signature = (functional, temperature, p_min, p_max, grid, external_potential, composition=None, solver=None, max_iter=None, tol=None, verbosity=None))] + #[expect(clippy::too_many_arguments)] + fn phase_equilibrium( + functional: &PyEquationOfState, + temperature: Temperature, + p_min: Pressure, + p_max: Pressure, + grid: PyGrid, + external_potential: Energy>, + composition: Option<&Bound<'_, PyAny>>, + solver: Option, + max_iter: Option, + tol: Option, + verbosity: Option, + ) -> PyResult { + Ok(Self( + Adsorption::phase_equilibrium( + &functional.0, + temperature, + p_min, + p_max, + &grid.0, + &external_potential, + Compositions::try_from(composition)?, + solver.map(|s| s.0).as_ref(), + (max_iter, tol, verbosity.map(|v| v.into())).into(), + ) + .map_err(PyFeosError::from)?, + )) + } - /// Calculate a phase equilibrium in a pore. - /// - /// Parameters - /// ---------- - /// functional : HelmholtzEnergyFunctional - /// The Helmholtz energy functional. - /// temperature : SINumber - /// The temperature. - /// p_min : SINumber - /// A suitable lower limit for the pressure. - /// p_max : SINumber - /// A suitable upper limit for the pressure. - /// pore : Pore - /// The pore parameters. - /// composition : float | SINumber | numpy.ndarray[float] | SIArray1 | list[float], optional - /// The composition of the mixture. - /// solver: DFTSolver, optional - /// Custom solver options. - /// max_iter : int, optional - /// The maximum number of iterations of the phase equilibrium calculation. - /// tol: float, optional - /// The tolerance of the phase equilibrium calculation. - /// verbosity: Verbosity, optional - /// The verbosity of the phase equilibrium calculation. - /// - /// Returns - /// ------- - /// Adsorption - /// - #[staticmethod] - #[pyo3(text_signature = "(functional, temperature, p_min, p_max, pore, composition=None, solver=None, max_iter=None, tol=None, verbosity=None)")] - #[pyo3(signature = (functional, temperature, p_min, p_max, pore, composition=None, solver=None, max_iter=None, tol=None, verbosity=None))] - #[expect(clippy::too_many_arguments)] - fn phase_equilibrium( - functional: &PyEquationOfState, - temperature: Temperature, - p_min: Pressure, - p_max: Pressure, - pore: &$py_pore, - composition: Option<&Bound<'_, PyAny>>, - solver: Option, - max_iter: Option, - tol: Option, - verbosity: Option, - ) -> PyResult { - Ok(Self(Adsorption::phase_equilibrium( - &functional.0, - temperature, - p_min, - p_max, - &pore.0, - Compositions::try_from(composition)?, - solver.map(|s| s.0).as_ref(), - (max_iter, tol, verbosity.map(|v| v.into())).into(), - ).map_err(PyFeosError::from)?)) - } + #[getter] + fn get_profiles(&self) -> Vec { + self.0 + .profiles + .iter() + .filter_map(|p| p.as_ref().ok().map(|p| PyPoreProfile(p.clone()))) + .collect() + } - #[getter] - fn get_profiles(&self) -> Vec<$py_pore_profile> { - self.0 - .profiles - .iter() - .filter_map(|p| { - p.as_ref() - .ok() - .map(|p| $py_pore_profile(p.clone())) - }) - .collect() - } + #[getter] + fn get_pressure(&self) -> Pressure> { + self.0.pressure() + } - #[getter] - fn get_pressure(&self) -> Pressure> { - self.0.pressure() - } + #[getter] + fn get_adsorption(&self) -> Moles> { + self.0.adsorption() + } - #[getter] - fn get_adsorption(&self) -> Moles> { - self.0.adsorption() - } + #[getter] + fn get_total_adsorption(&self) -> Moles> { + self.0.total_adsorption() + } - #[getter] - fn get_total_adsorption(&self) -> Moles> { - self.0.total_adsorption() - } + #[getter] + fn get_grand_potential(&mut self) -> Energy> { + self.0.grand_potential() + } - #[getter] - fn get_grand_potential(&mut self) -> Energy> { - self.0.grand_potential() - } + #[getter] + fn get_partial_molar_enthalpy_of_adsorption(&self) -> MolarEnergy> { + self.0.partial_molar_enthalpy_of_adsorption() + } - #[getter] - fn get_partial_molar_enthalpy_of_adsorption(&self) -> MolarEnergy> { - self.0.partial_molar_enthalpy_of_adsorption() - } - - #[getter] - fn get_enthalpy_of_adsorption(&self) -> MolarEnergy> { - self.0.enthalpy_of_adsorption() - } - } - }; + #[getter] + fn get_enthalpy_of_adsorption(&self) -> MolarEnergy> { + self.0.enthalpy_of_adsorption() + } } - -impl_adsorption_isotherm!(PyAdsorption1D, PyPore1D, PyPoreProfile1D); -#[cfg(feature = "rayon")] -impl_adsorption_isotherm!(PyAdsorption3D, PyPore3D, PyPoreProfile3D); diff --git a/py-feos/src/dft/adsorption/pore.rs b/py-feos/src/dft/adsorption/pore.rs index 6e39d57ab..ab66684e2 100644 --- a/py-feos/src/dft/adsorption/pore.rs +++ b/py-feos/src/dft/adsorption/pore.rs @@ -1,12 +1,13 @@ use super::PyExternalPotential; use crate::dft::profile::*; use crate::dft::{PyDFTSolver, PyDFTSolverLog, PyGeometry}; +use crate::eos::PyEquationOfState; use crate::error::PyFeosError; use crate::ideal_gas::IdealGasModel; use crate::residual::ResidualModel; use crate::state::{PyContributions, PyState}; use feos_core::{EquationOfState, ReferenceSystem}; -use feos_dft::adsorption::*; +use feos_dft::{Axis as AxisDFT, Grid, adsorption::*}; use nalgebra::{DMatrix, DVector}; use ndarray::*; use numpy::*; @@ -14,48 +15,184 @@ use pyo3::prelude::*; use quantity::*; use std::sync::Arc; -macro_rules! impl_pore_profile { - ($py_profile:ty) => { - #[pymethods] - impl $py_profile { - #[getter] - fn get_grand_potential(&self) -> Option { - self.0.grand_potential - } +#[pyclass(name = "Grid", from_py_object)] +#[derive(Clone)] +pub struct PyGrid(pub Grid); + +#[pymethods] +impl PyGrid { + /// Generate a 1D Cartesian grid with mirror boundary conditions on both sides. + #[staticmethod] + pub fn cartesian_1d(n_points: usize, length: Length) -> Self { + let x = AxisDFT::new_cartesian(n_points, length, None); + Self(Grid::Cartesian1(x)) + } + + /// Generate a polar grid with radial axis. + #[staticmethod] + pub fn polar(n_points: usize, length: Length) -> Self { + let x = AxisDFT::new_polar(n_points, length); + Self(Grid::Polar(x)) + } + + /// Generate a spherical grid with radial axis. + #[staticmethod] + pub fn spherical(n_points: usize, length: Length) -> Self { + let x = AxisDFT::new_spherical(n_points, length); + Self(Grid::Spherical(x)) + } + + /// Generate a 2D Cartesian grid with mirror boundary conditions on all sides. + #[staticmethod] + pub fn cartesian_2d(n_points: [usize; 2], length: [Length; 2]) -> Self { + let [n_x, n_y] = n_points; + let [l_x, l_y] = length; + let x = AxisDFT::new_cartesian(n_x, l_x, None); + let y = AxisDFT::new_cartesian(n_y, l_y, None); + Self(Grid::Cartesian2(x, y)) + } + + /// Generate a 2D Cartesian (possibly oblique) grid with periodic boundary conditions on all sides. + #[staticmethod] + pub fn periodical_2d(n_points: [usize; 2], length: [Length; 2], alpha: Angle) -> Self { + let [n_x, n_y] = n_points; + let [l_x, l_y] = length; + let x = AxisDFT::new_cartesian(n_x, l_x, None); + let y = AxisDFT::new_cartesian(n_y, l_y, None); + Self(Grid::Periodical2(x, y, alpha)) + } + + /// Generate a cylindrical grid with axes (in this order) r and z. + #[staticmethod] + pub fn cylindrical(n_points: [usize; 2], length: [Length; 2]) -> Self { + let [n_r, n_z] = n_points; + let [l_r, l_z] = length; + let r = AxisDFT::new_polar(n_r, l_r); + let z = AxisDFT::new_cartesian(n_z, l_z, None); + Self(Grid::Cylindrical { r, z }) + } + + /// Generate a 3D Cartesian grid with mirror boundary conditions on all sides. + #[staticmethod] + pub fn cartesian_3d(n_points: [usize; 3], length: [Length; 3]) -> Self { + let [n_x, n_y, n_z] = n_points; + let [l_x, l_y, l_z] = length; + let x = AxisDFT::new_cartesian(n_x, l_x, None); + let y = AxisDFT::new_cartesian(n_y, l_y, None); + let z = AxisDFT::new_cartesian(n_z, l_z, None); + Self(Grid::Cartesian3(x, y, z)) + } + + /// Generate a 3D Cartesian (possibly oblique) grid with periodic boundary conditions on all sides. + #[staticmethod] + pub fn periodical_3d(n_points: [usize; 3], length: [Length; 3], angles: [Angle; 3]) -> Self { + let [n_x, n_y, n_z] = n_points; + let [l_x, l_y, l_z] = length; + let x = AxisDFT::new_cartesian(n_x, l_x, None); + let y = AxisDFT::new_cartesian(n_y, l_y, None); + let z = AxisDFT::new_cartesian(n_z, l_z, None); + Self(Grid::Periodical3(x, y, z, angles)) + } + + #[getter] + pub fn get_axes(&self) -> Vec>> { + self.0 + .grids() + .into_iter() + .map(|ax| Length::from_reduced(ax.clone())) + .collect() + } + + #[getter] + pub fn get_grid(&self) -> Vec>> { + self.0.mesh() + } +} + +/// The base class for studying adsorption phenomena. +/// +/// Parameters +/// ---------- +/// grid : Grid +/// The grid on which the density is calculated. +/// bulk : State +/// The (initial) bulk state in equilibrium with the pore. +/// external_potential : SIArray +/// The external potential used to model wall-fluid interactions. +/// density : SIArray, optional +/// The initial density distribution. +/// specification : PoreSpecification +/// The external constraint that specifies the state +/// in the pore. +/// +/// Returns +/// ------- +/// PoreProfile +/// +#[pyclass(name = "PoreProfile")] +pub struct PyPoreProfile( + pub PoreProfile, ResidualModel>>>, +); + +#[pymethods] +impl PyPoreProfile { + #[new] + #[pyo3( + text_signature = "(grid, bulk, external_potential, density=None, specification=PyPoreSpecification.ChemicalPotential)" + )] + #[pyo3(signature = (grid, bulk, external_potential, density=None, specification=PyPoreSpecification::ChemicalPotential()))] + fn new( + grid: PyGrid, + bulk: &PyState, + external_potential: Energy>, + density: Option>>, + specification: PyPoreSpecification, + ) -> Self { + Self(PoreProfile::new( + grid.0, + &bulk.0, + &external_potential, + density.as_ref(), + specification.0, + )) + } + + #[getter] + fn get_grand_potential(&self) -> Option { + self.0.grand_potential + } - #[getter] - fn get_interfacial_tension(&self) -> Option { - self.0.interfacial_tension - } + #[getter] + fn get_interfacial_tension(&self) -> Option { + self.0.interfacial_tension + } - #[getter] - fn get_partial_molar_enthalpy_of_adsorption( - &self, - ) -> PyResult>> { - Ok(self - .0 - .partial_molar_enthalpy_of_adsorption() - .map_err(PyFeosError::from)?) - } + #[getter] + fn get_partial_molar_enthalpy_of_adsorption(&self) -> PyResult>> { + Ok(self + .0 + .partial_molar_enthalpy_of_adsorption() + .map_err(PyFeosError::from)?) + } - #[getter] - fn get_enthalpy_of_adsorption(&self) -> PyResult { - Ok(self.0.enthalpy_of_adsorption().map_err(PyFeosError::from)?) - } + #[getter] + fn get_enthalpy_of_adsorption(&self) -> PyResult { + Ok(self.0.enthalpy_of_adsorption().map_err(PyFeosError::from)?) + } - #[getter] - fn get_henry_coefficients(&self) -> HenryCoefficient> { - self.0.henry_coefficients() - } + #[getter] + fn get_henry_coefficients(&self) -> HenryCoefficient> { + self.0.henry_coefficients() + } - #[getter] - fn get_ideal_gas_enthalpy_of_adsorption(&self) -> MolarEnergy> { - self.0.ideal_gas_enthalpy_of_adsorption() - } - } - }; + #[getter] + fn get_ideal_gas_enthalpy_of_adsorption(&self) -> MolarEnergy> { + self.0.ideal_gas_enthalpy_of_adsorption() + } } +impl_profile!(PyPoreProfile); + /// Different ways that the thermodynamic state of the fluid in the pore /// can be specified. #[pyclass(name = "PoreSpecification", from_py_object)] @@ -77,6 +214,13 @@ impl PyPoreSpecification { fn Moles(moles: Moles>) -> Self { Self(PoreSpecification::Moles(moles)) } + + /// Fix the amount of moles of every component based on the initial density profile. + #[classattr] + #[expect(non_snake_case)] + fn FixedMoles() -> Self { + Self(PoreSpecification::FixedMoles) + } } /// Parameters required to specify a 1D pore. @@ -102,32 +246,24 @@ impl PyPoreSpecification { #[pyclass(name = "Pore1D")] pub struct PyPore1D(pub Pore1D); -#[pyclass(name = "PoreProfile1D")] -pub struct PyPoreProfile1D( - pub PoreProfile1D, ResidualModel>>>, -); - -impl_1d_profile!(PyPoreProfile1D, [get_r, get_z]); -impl_pore_profile!(PyPoreProfile1D); - #[pymethods] impl PyPore1D { #[new] - #[pyo3(text_signature = "(geometry, pore_size, potential, n_grid=None, potential_cutoff=None)")] - #[pyo3(signature = (geometry, pore_size, potential, n_grid=None, potential_cutoff=None))] + #[pyo3(text_signature = "(functional, geometry, pore_size, external_potential, n_grid=None)")] + #[pyo3(signature = (functional, geometry, pore_size, external_potential, n_grid=None))] fn new( + functional: &PyEquationOfState, geometry: PyGeometry, pore_size: Length, - potential: PyExternalPotential, + external_potential: PyExternalPotential, n_grid: Option, - potential_cutoff: Option, ) -> PyResult { Ok(Self(Pore1D::new( + &functional.0, geometry.into(), pore_size, - potential.0, + external_potential.0, n_grid, - potential_cutoff, ))) } @@ -139,10 +275,6 @@ impl PyPore1D { /// The bulk state in equilibrium with the pore. /// density : SIArray2, optional /// Initial values for the density profile. - /// external_potential : SIArray2, optional - /// The external potential in the pore. Used to - /// save computation time in the case of costly - /// evaluations of external potentials. /// specification : PoreSpecification /// The external constraint that specifies the state /// in the pore. @@ -151,243 +283,36 @@ impl PyPore1D { /// ------- /// PoreProfile1D #[pyo3( - text_signature = "($self, bulk, density=None, external_potential=None, specification=PoreSpecification.ChemicalPotential)" + text_signature = "($self, bulk, density=None, specification=PoreSpecification.ChemicalPotential)" )] - #[pyo3(signature = (bulk, density=None, external_potential=None, specification=PyPoreSpecification::ChemicalPotential()))] + #[pyo3(signature = (bulk, density=None, specification=PyPoreSpecification::ChemicalPotential()))] fn initialize( &self, bulk: &PyState, density: Option>>, - external_potential: Option>>, specification: PyPoreSpecification, - ) -> PyResult { - Ok(PyPoreProfile1D( + ) -> PyResult { + Ok(PyPoreProfile( self.0 - .initialize( - &bulk.0, - density.as_ref(), - external_potential.as_ref(), - specification.0, - ) - .map_err(PyFeosError::from)?, + .initialize(&bulk.0, density.as_ref(), specification.0) + .map_err(PyFeosError::from)? + .into_dyn(), )) } - #[getter] - fn get_geometry(&self) -> PyGeometry { - self.0.geometry.into() - } - #[getter] fn get_pore_size(&self) -> Length { self.0.pore_size } #[getter] - fn get_potential(&self) -> PyExternalPotential { - PyExternalPotential(self.0.potential.clone()) - } - - #[getter] - fn get_n_grid(&self) -> Option { - self.0.n_grid - } - - #[getter] - fn get_potential_cutoff(&self) -> Option { - self.0.potential_cutoff - } - - /// The pore volume using Helium at 298 K as reference. - #[getter] - fn get_pore_volume(&self) -> PyResult { - Ok(self.0.pore_volume().map_err(PyFeosError::from)?) - } -} - -#[pyclass(name = "Pore2D")] -pub struct PyPore2D(pub Pore2D); - -#[pyclass(name = "PoreProfile2D")] -pub struct PyPoreProfile2D( - pub PoreProfile2D, ResidualModel>>>, -); - -impl_2d_profile!(PyPoreProfile2D, get_x, get_y); -impl_pore_profile!(PyPoreProfile2D); - -#[pymethods] -impl PyPore2D { - #[new] - #[pyo3(text_signature = "(system_size, angle, n_grid)")] - fn new(system_size: [Length; 2], angle: Angle, n_grid: [usize; 2]) -> PyResult { - Ok(Self(Pore2D::new( - [system_size[0], system_size[1]], - angle, - n_grid, - ))) + fn get_external_potential(&self) -> Energy> { + self.0.external_potential.clone() } - /// Initialize the pore for the given bulk state. - /// - /// Parameters - /// ---------- - /// bulk : State - /// The bulk state in equilibrium with the pore. - /// density : SIArray3, optional - /// Initial values for the density profile. - /// external_potential : SIArray3, optional - /// The external potential in the pore. Used to - /// save computation time in the case of costly - /// evaluations of external potentials. - /// specification : PoreSpecification - /// The external constraint that specifies the state - /// in the pore. - /// - /// Returns - /// ------- - /// PoreProfile2D - #[pyo3( - text_signature = "($self, bulk, density=None, external_potential=None, specification=PoreSpecification.ChemicalPotential)" - )] - #[pyo3(signature = (bulk, density=None, external_potential=None, specification=PyPoreSpecification::ChemicalPotential()))] - fn initialize( - &self, - bulk: &PyState, - density: Option>>, - external_potential: Option>>, - specification: PyPoreSpecification, - ) -> PyResult { - Ok(PyPoreProfile2D( - self.0 - .initialize( - &bulk.0, - density.as_ref(), - external_potential.as_ref(), - specification.0, - ) - .map_err(PyFeosError::from)?, - )) - } - - /// The pore volume using Helium at 298 K as reference. #[getter] - fn get_pore_volume(&self) -> PyResult { - Ok(self.0.pore_volume().map_err(PyFeosError::from)?) - } -} - -/// Parameters required to specify a 3D pore. -/// -/// Parameters -/// ---------- -/// system_size : [SINumber; 3] -/// The size of the unit cell. -/// n_grid : [int; 3] -/// The number of grid points in each direction. -/// coordinates : numpy.ndarray[float] -/// The positions of all interaction sites in the solid. -/// sigma_ss : numpy.ndarray[float] -/// The size parameters of all interaction sites. -/// epsilon_k_ss : numpy.ndarray[float] -/// The energy parameter of all interaction sites. -/// angles : [Angle; 3], optional -/// The angles of the unit cell or `None` if the unit cell -/// is orthorombic -/// potential_cutoff: float, optional -/// Maximum value for the external potential. -/// cutoff_radius: SINumber, optional -/// The cutoff radius for the calculation of solid-fluid interactions. -/// -/// Returns -/// ------- -/// Pore3D -/// -// Pore3D/PoreProfile3D wrap feos_dft types that only exist with `rayon` -// (3D FFT). Gated out of the threadless wasm32-unknown-emscripten build. -#[cfg(feature = "rayon")] -#[pyclass(name = "Pore3D")] -pub struct PyPore3D(pub Pore3D); - -#[cfg(feature = "rayon")] -#[pyclass(name = "PoreProfile3D")] -pub struct PyPoreProfile3D( - pub PoreProfile3D, ResidualModel>>>, -); - -#[cfg(feature = "rayon")] -impl_3d_profile!(PyPoreProfile3D, get_x, get_y, get_z); -#[cfg(feature = "rayon")] -impl_pore_profile!(PyPoreProfile3D); - -#[cfg(feature = "rayon")] -#[pymethods] -impl PyPore3D { - #[new] - #[pyo3( - text_signature = "(system_size, n_grid, coordinates, sigma_ss, epsilon_k_ss, angles=None, cutoff_radius=None)" - )] - #[pyo3(signature = (system_size, n_grid, coordinates, sigma_ss, epsilon_k_ss, angles=None, cutoff_radius=None))] - fn new( - system_size: [Length; 3], - n_grid: [usize; 3], - coordinates: Length>, - sigma_ss: &Bound<'_, PyArray1>, - epsilon_k_ss: &Bound<'_, PyArray1>, - angles: Option<[Angle; 3]>, - cutoff_radius: Option, - ) -> Self { - Self(Pore3D::new( - system_size, - n_grid, - coordinates, - sigma_ss.to_owned_array(), - epsilon_k_ss.to_owned_array(), - angles, - cutoff_radius, - )) - } - - /// Initialize the pore for the given bulk state. - /// - /// Parameters - /// ---------- - /// bulk : State - /// The bulk state in equilibrium with the pore. - /// density : SIArray4, optional - /// Initial values for the density profile. - /// external_potential : SIArray4, optional - /// The external potential in the pore. Used to - /// save computation time in the case of costly - /// evaluations of external potentials. - /// specification : PoreSpecification - /// The external constraint that specifies the state - /// in the pore. - /// - /// Returns - /// ------- - /// PoreProfile3D - #[pyo3( - text_signature = "($self, bulk, density=None, external_potential=None, specification=PoreSpecification.ChemicalPotential)" - )] - #[pyo3(signature = (bulk, density=None, external_potential=None, specification=PyPoreSpecification::ChemicalPotential()))] - fn initialize( - &self, - bulk: &PyState, - density: Option>>, - external_potential: Option>>, - specification: PyPoreSpecification, - ) -> PyResult { - Ok(PyPoreProfile3D( - self.0 - .initialize( - &bulk.0, - density.as_ref(), - external_potential.as_ref(), - specification.0, - ) - .map_err(PyFeosError::from)?, - )) + fn get_grid(&self) -> PyGrid { + PyGrid(self.0.grid.clone()) } /// The pore volume using Helium at 298 K as reference. diff --git a/py-feos/src/dft/interface/mod.rs b/py-feos/src/dft/interface/mod.rs index aae76c75c..dd628605f 100644 --- a/py-feos/src/dft/interface/mod.rs +++ b/py-feos/src/dft/interface/mod.rs @@ -1,11 +1,11 @@ -use super::profile::{impl_1d_profile, impl_profile}; +use super::profile::impl_profile; use super::{PyDFTSolver, PyDFTSolverLog}; use crate::error::PyFeosError; use crate::ideal_gas::IdealGasModel; use crate::phase_equilibria::PyPhaseEquilibrium; use crate::residual::ResidualModel; use crate::state::{PyContributions, PyState}; -use feos_core::{EquationOfState, ReferenceSystem}; +use feos_core::EquationOfState; use feos_dft::interface::PlanarInterface; use nalgebra::{DMatrix, DVector}; use ndarray::*; @@ -23,7 +23,7 @@ pub struct PyPlanarInterface( PlanarInterface, ResidualModel>>>, ); -impl_1d_profile!(PyPlanarInterface, [get_z]); +impl_profile!(PyPlanarInterface); #[pymethods] impl PyPlanarInterface { diff --git a/py-feos/src/dft/mod.rs b/py-feos/src/dft/mod.rs index 36ea92bb9..07d1a6839 100644 --- a/py-feos/src/dft/mod.rs +++ b/py-feos/src/dft/mod.rs @@ -16,11 +16,8 @@ mod solvation; mod solver; pub(crate) use adsorption::{ - PyAdsorption1D, PyExternalPotential, PyPore1D, PyPore2D, PyPoreSpecification, + PyAdsorption, PyExternalPotential, PyGrid, PyPore1D, PyPoreProfile, PyPoreSpecification, }; -// 3D pore / adsorption bindings wrap rayon-gated feos_dft types. -#[cfg(feature = "rayon")] -pub(crate) use adsorption::{PyAdsorption3D, PyPore3D}; pub(crate) use interface::{PyPlanarInterface, PySurfaceTensionDiagram}; pub(crate) use solvation::PyPairCorrelation; #[cfg(feature = "rayon")] diff --git a/py-feos/src/dft/profile.rs b/py-feos/src/dft/profile.rs index b60fcafdb..40104de89 100644 --- a/py-feos/src/dft/profile.rs +++ b/py-feos/src/dft/profile.rs @@ -1,7 +1,5 @@ macro_rules! impl_profile { - ($struct:ident, $arr:ident, $arr2:ident, $si_arr:ident, $si_arr2:ident, $py_arr2:ident, [$([$ind:expr, $ax:ident]),+]$(, $si_arr3:ident)?) => { - - + ($struct:ident) => { #[pymethods] impl $struct { /// Calculate the residual for the given profile. @@ -20,9 +18,9 @@ macro_rules! impl_profile { &self, log: bool, py: Python<'py>, - ) -> PyResult<(Bound<'py, $arr2>, Bound<'py, PyArray1>, f64)> { + ) -> PyResult<(Bound<'py, PyArrayDyn>, Bound<'py, PyArray1>, f64)> { let (res_rho, res_mu, res_norm) = self.0.profile.residual(log).map_err(PyFeosError::from)?; - Ok((res_rho.view().to_pyarray(py), res_mu.view().to_pyarray(py), res_norm)) + Ok((res_rho.view().into_dyn().to_pyarray(py), res_mu.view().to_pyarray(py), res_norm)) } /// Solve the profile in-place. A non-default solver can be provided @@ -47,11 +45,25 @@ macro_rules! impl_profile { Ok(slf) } - $( #[getter] - fn $ax(&self) -> Length> { - Length::from_reduced(self.0.profile.grid.grids()[$ind].clone()) - })+ + fn get_axes(&self) -> Vec>>{ + self.0.profile.axes() + } + + #[getter] + fn get_edges(&self) -> Vec>> { + self.0.profile.edges() + } + + #[getter] + fn get_grid<'py>(&self, py: Python<'py>) -> PyResult> { + let mut grid = self.0.profile.grid.mesh(); + if grid.len() == 1 { + grid.pop().unwrap().into_pyobject(py) + } else { + grid.into_pyobject(py) + } + } #[getter] fn get_temperature(&self) -> Temperature { @@ -59,8 +71,8 @@ macro_rules! impl_profile { } #[getter] - fn get_density(&self) -> Density<$si_arr2> { - self.0.profile.density.clone() + fn get_density(&self) -> Density> { + self.0.profile.density.clone().into_dyn() } #[getter] @@ -74,8 +86,8 @@ macro_rules! impl_profile { } #[getter] - fn get_external_potential(&self) -> Energy<$si_arr2> { - self.0.profile.external_potential().clone() + fn get_external_potential(&self) -> Energy> { + self.0.profile.external_potential().clone().into_dyn() } #[getter] @@ -92,17 +104,17 @@ macro_rules! impl_profile { fn get_weighted_densities<'py>( &self, py: Python<'py>, - ) -> PyResult>>> { + ) -> PyResult>>> { let n = self.0.profile.weighted_densities().map_err(PyFeosError::from)?; - Ok(n.into_iter().map(|n| n.view().to_pyarray(py)).collect()) + Ok(n.into_iter().map(|n| n.view().into_dyn().to_pyarray(py)).collect()) } #[getter] fn get_functional_derivative<'py>( &self, py: Python<'py>, - ) -> PyResult>> { - Ok(self.0.profile.functional_derivative().map_err(PyFeosError::from)?.view().to_pyarray(py)) + ) -> PyResult>> { + Ok(self.0.profile.functional_derivative().map_err(PyFeosError::from)?.view().into_dyn().to_pyarray(py)) } /// Calculate the entropy density of the inhomogeneous system. @@ -120,8 +132,8 @@ macro_rules! impl_profile { fn entropy_density( &mut self, contributions: PyContributions, - ) -> PyResult<> as std::ops::Div>::Output> { - Ok(self.0.profile.entropy_density(contributions.into()).map_err(PyFeosError::from)?) + ) -> PyResult<> as std::ops::Div>::Output> { + Ok(self.0.profile.entropy_density(contributions.into()).map_err(PyFeosError::from)?.into_dyn()) } /// Calculate the entropy of the inhomogeneous system. @@ -163,15 +175,14 @@ macro_rules! impl_profile { } #[getter] - fn get_grand_potential_density(&self) -> PyResult>> { - Ok(self.0.profile.grand_potential_density().map_err(PyFeosError::from)?) + fn get_grand_potential_density(&self) -> PyResult>> { + Ok(self.0.profile.grand_potential_density().map_err(PyFeosError::from)?.into_dyn()) + } + + #[getter] + fn get_drho_dmu(&self) -> PyResult<> as std::ops::Div>::Output> { + Ok(self.0.profile.drho_dmu().map_err(PyFeosError::from)?.into_dyn()) } - $( - #[getter] - fn get_drho_dmu(&self) -> PyResult<> as std::ops::Div>::Output> { - Ok(self.0.profile.drho_dmu().map_err(PyFeosError::from)?) - } - )? #[getter] fn get_dn_dmu(&self) -> PyResult<> as std::ops::Div>::Output> { @@ -179,8 +190,8 @@ macro_rules! impl_profile { } #[getter] - fn get_drho_dp(&self) -> PyResult<> as std::ops::Div>::Output> { - Ok(self.0.profile.drho_dp().map_err(PyFeosError::from)?) + fn get_drho_dp(&self) -> PyResult<> as std::ops::Div>::Output> { + Ok(self.0.profile.drho_dp().map_err(PyFeosError::from)?.into_dyn()) } #[getter] @@ -189,8 +200,8 @@ macro_rules! impl_profile { } #[getter] - fn get_drho_dt(&self) -> PyResult<> as std::ops::Div>::Output> { - Ok(self.0.profile.drho_dt().map_err(PyFeosError::from)?) + fn get_drho_dt(&self) -> PyResult<> as std::ops::Div>::Output> { + Ok(self.0.profile.drho_dt().map_err(PyFeosError::from)?.into_dyn()) } #[getter] @@ -201,77 +212,4 @@ macro_rules! impl_profile { }; } -macro_rules! impl_1d_profile { - ($struct:ident, [$($ax:ident),+]) => { - impl_profile!( - $struct, - PyArray1, - PyArray2, - Array1, - Array2, - PyArray2, - [$([0, $ax]),+], - Array3 - ); - }; -} - -macro_rules! impl_2d_profile { - ($struct:ident, $ax1:ident, $ax2:ident) => { - impl_profile!( - $struct, - PyArray2, - PyArray3, - Array2, - Array3, - PyArray3, - [[0, $ax1], [1, $ax2]] - ); - - #[pymethods] - impl $struct { - #[getter] - fn get_edges(&self) -> [Length>; 2] { - self.0.profile.edges() - } - - #[getter] - fn get_meshgrid(&self) -> [Length>; 2] { - self.0.profile.meshgrid() - } - } - }; -} - -// Only used by rayon-gated 3D profiles (PoreProfile3D, SolvationProfile). -#[cfg(feature = "rayon")] -macro_rules! impl_3d_profile { - ($struct:ident, $ax1:ident, $ax2:ident, $ax3:ident) => { - impl_profile!( - $struct, - PyArray3, - PyArray4, - Array3, - Array4, - PyArray4, - [[0, $ax1], [1, $ax2], [2, $ax3]] - ); - - #[pymethods] - impl $struct { - #[getter] - fn get_edges(&self) -> [Length>; 3] { - self.0.profile.edges() - } - - #[getter] - fn get_meshgrid(&self) -> [Length>; 3] { - self.0.profile.meshgrid() - } - } - }; -} - -#[cfg(feature = "rayon")] -pub(crate) use impl_3d_profile; -pub(crate) use {impl_1d_profile, impl_2d_profile, impl_profile}; +pub(crate) use impl_profile; diff --git a/py-feos/src/dft/solvation.rs b/py-feos/src/dft/solvation.rs index 9509d6b99..5da6ac9f8 100644 --- a/py-feos/src/dft/solvation.rs +++ b/py-feos/src/dft/solvation.rs @@ -1,11 +1,9 @@ -#[cfg(feature = "rayon")] -use super::profile::impl_3d_profile; -use super::profile::{impl_1d_profile, impl_profile}; +use super::profile::impl_profile; use super::{PyDFTSolver, PyDFTSolverLog}; use crate::residual::ResidualModel; use crate::state::{PyContributions, PyState}; use crate::{error::PyFeosError, ideal_gas::IdealGasModel}; -use feos_core::{EquationOfState, ReferenceSystem}; +use feos_core::EquationOfState; use feos_dft::solvation::PairCorrelation; #[cfg(feature = "rayon")] use feos_dft::solvation::SolvationProfile; @@ -48,7 +46,7 @@ pub struct PySolvationProfile( ); #[cfg(feature = "rayon")] -impl_3d_profile!(PySolvationProfile, get_x, get_y, get_z); +impl_profile!(PySolvationProfile); #[cfg(feature = "rayon")] #[pymethods] @@ -114,7 +112,7 @@ pub struct PyPairCorrelation( PairCorrelation, ResidualModel>>>, ); -impl_1d_profile!(PyPairCorrelation, [get_r]); +impl_profile!(PyPairCorrelation); #[pymethods] impl PyPairCorrelation { diff --git a/py-feos/src/lib.rs b/py-feos/src/lib.rs index a164fece0..ee84f1fbb 100644 --- a/py-feos/src/lib.rs +++ b/py-feos/src/lib.rs @@ -201,17 +201,12 @@ fn feos(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; // Adsorption - m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; - - #[cfg(feature = "rayon")] - { - m.add_class::()?; - m.add_class::()?; - } + m.add_class::()?; + m.add_class::()?; // Interface m.add_class::()?; From d03cd7cee51cebbda70a8020aef4b508434b7f99 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 11 Aug 2026 16:34:58 +0200 Subject: [PATCH 5/6] Update to num-dual v0.15 --- Cargo.toml | 4 +- crates/feos-core/src/ad/mod.rs | 7 +- .../src/ad/properties/boiling_temperature.rs | 2 +- .../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 | 2 +- .../residual_isobaric_heat_capacity.rs | 2 +- .../src/ad/properties/vapor_pressure.rs | 2 +- crates/feos-core/src/cubic.rs | 4 +- crates/feos-core/src/density_iteration.rs | 2 +- crates/feos-core/src/equation_of_state/mod.rs | 35 +++++----- .../src/equation_of_state/residual.rs | 34 +++++----- crates/feos-core/src/lib.rs | 2 +- .../src/phase_equilibria/bubble_dew.rs | 13 ++-- crates/feos-core/src/phase_equilibria/mod.rs | 13 ++-- .../src/phase_equilibria/px_flashes.rs | 24 +++---- .../src/phase_equilibria/tp_flash.rs | 4 +- .../src/phase_equilibria/vle_pure.rs | 8 ++- crates/feos-core/src/state/composition.rs | 25 +++---- crates/feos-core/src/state/critical_point.rs | 18 ++--- crates/feos-core/src/state/mod.rs | 18 ++--- crates/feos-core/src/state/properties.rs | 4 +- .../src/state/residual_properties.rs | 8 +-- crates/feos-derive/src/dft.rs | 2 +- .../src/functional_contribution.rs | 6 +- crates/feos-derive/src/ideal_gas.rs | 2 +- crates/feos-derive/src/residual.rs | 4 +- crates/feos-dft/src/adsorption/pore.rs | 13 ++-- crates/feos-dft/src/convolver/mod.rs | 16 ++--- .../src/convolver/periodic_convolver.rs | 4 +- crates/feos-dft/src/convolver/transform.rs | 19 +++--- crates/feos-dft/src/functional.rs | 17 +++-- .../feos-dft/src/functional_contribution.rs | 16 +++-- .../feos-dft/src/ideal_chain_contribution.rs | 4 +- crates/feos-dft/src/profile/mod.rs | 9 ++- crates/feos-dft/src/profile/properties.rs | 2 +- crates/feos-dft/src/weight_functions.rs | 8 +-- crates/feos/benches/dual_numbers.rs | 2 +- crates/feos/benches/dual_numbers_saftvrmie.rs | 4 +- crates/feos/src/association/dft.rs | 12 ++-- crates/feos/src/association/mod.rs | 14 ++-- crates/feos/src/epcsaft/eos/born.rs | 2 +- crates/feos/src/epcsaft/eos/dispersion.rs | 6 +- crates/feos/src/epcsaft/eos/hard_chain.rs | 2 +- crates/feos/src/epcsaft/eos/ionic.rs | 4 +- crates/feos/src/epcsaft/eos/mod.rs | 4 +- crates/feos/src/epcsaft/eos/permittivity.rs | 4 +- crates/feos/src/epcsaft/parameters.rs | 10 +-- crates/feos/src/gc_pcsaft/dft/dispersion.rs | 4 +- crates/feos/src/gc_pcsaft/dft/hard_chain.rs | 4 +- crates/feos/src/gc_pcsaft/dft/mod.rs | 12 ++-- crates/feos/src/gc_pcsaft/eos/ad.rs | 18 ++--- crates/feos/src/gc_pcsaft/eos/dispersion.rs | 2 +- crates/feos/src/gc_pcsaft/eos/hard_chain.rs | 2 +- crates/feos/src/gc_pcsaft/eos/mod.rs | 4 +- crates/feos/src/gc_pcsaft/eos/parameter.rs | 6 +- crates/feos/src/gc_pcsaft/eos/polar.rs | 6 +- crates/feos/src/hard_sphere/dft.rs | 10 +-- crates/feos/src/hard_sphere/mod.rs | 12 ++-- crates/feos/src/ideal_gas/dippr.rs | 12 ++-- crates/feos/src/ideal_gas/joback.rs | 23 ++++--- .../src/multiparameter/ideal_gas_function.rs | 2 +- crates/feos/src/multiparameter/mod.rs | 14 ++-- .../src/multiparameter/residual_function.rs | 2 +- crates/feos/src/pcsaft/dft/dispersion.rs | 8 +-- crates/feos/src/pcsaft/dft/hard_chain.rs | 4 +- crates/feos/src/pcsaft/dft/mod.rs | 4 +- crates/feos/src/pcsaft/dft/polar.rs | 14 ++-- .../src/pcsaft/dft/pure_saft_functional.rs | 14 ++-- crates/feos/src/pcsaft/eos/dispersion.rs | 2 +- crates/feos/src/pcsaft/eos/hard_chain.rs | 2 +- crates/feos/src/pcsaft/eos/mod.rs | 10 +-- crates/feos/src/pcsaft/eos/pcsaft_binary.rs | 28 ++++---- crates/feos/src/pcsaft/eos/pcsaft_pure.rs | 22 +++--- crates/feos/src/pcsaft/eos/polar.rs | 12 ++-- crates/feos/src/pets/dft/dispersion.rs | 8 +-- .../feos/src/pets/dft/pure_pets_functional.rs | 8 +-- crates/feos/src/pets/eos/dispersion.rs | 2 +- crates/feos/src/pets/eos/mod.rs | 8 +-- crates/feos/src/saftvrmie/eos/dispersion.rs | 14 ++-- crates/feos/src/saftvrmie/eos/mod.rs | 6 +- crates/feos/src/saftvrmie/parameters.rs | 12 ++-- crates/feos/src/saftvrqmie/dft/dispersion.rs | 8 +-- crates/feos/src/saftvrqmie/dft/mod.rs | 4 +- .../src/saftvrqmie/dft/non_additive_hs.rs | 6 +- crates/feos/src/saftvrqmie/eos/dispersion.rs | 34 +++++----- crates/feos/src/saftvrqmie/eos/hard_sphere.rs | 39 ++++++----- crates/feos/src/saftvrqmie/eos/mod.rs | 6 +- .../src/saftvrqmie/eos/non_additive_hs.rs | 4 +- .../eos/bh/attractive_perturbation.rs | 20 +++--- .../feos/src/uvtheory/eos/bh/hard_sphere.rs | 8 +-- crates/feos/src/uvtheory/eos/bh/mod.rs | 2 +- .../uvtheory/eos/bh/reference_perturbation.rs | 2 +- crates/feos/src/uvtheory/eos/mod.rs | 4 +- .../eos/wca/attractive_perturbation.rs | 18 ++--- .../eos/wca/attractive_perturbation_uvb3.rs | 20 +++--- .../feos/src/uvtheory/eos/wca/hard_sphere.rs | 16 ++--- crates/feos/src/uvtheory/eos/wca/mod.rs | 4 +- .../eos/wca/reference_perturbation.rs | 2 +- .../eos/wca/reference_perturbation_uvb3.rs | 2 +- crates/feos/src/uvtheory/parameters.rs | 8 +-- docs/rustguide/core/equation_of_state.rst | 6 +- docs/rustguide/core/state.rst | 4 +- py-feos/src/ad/mod.rs | 4 +- py-feos/src/user_defined.rs | 68 ++++++++----------- 108 files changed, 524 insertions(+), 503 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d18be3929..76f48e086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,8 @@ keywords = [ categories = ["science"] [workspace.dependencies] -quantity = "0.14" -num-dual = "0.14" +quantity = "0.15" +num-dual = "0.15" ndarray = "0.17" nalgebra = "0.35" thiserror = "2.0" diff --git a/crates/feos-core/src/ad/mod.rs b/crates/feos-core/src/ad/mod.rs index 1965207da..87d6a9b26 100644 --- a/crates/feos-core/src/ad/mod.rs +++ b/crates/feos-core/src/ad/mod.rs @@ -10,7 +10,7 @@ mod properties; pub use dataset::*; pub use properties::*; -pub(crate) type Gradient = DualSVec; +pub(crate) type Gradient = DualSVec; /// A model that can be evaluated with derivatives of its parameters. pub trait ParametersAD: Residual @@ -23,7 +23,7 @@ where /// defines the canonical parameter order. /// /// Set `differentiable` to `false` for fixed parameters. - fn build + Copy>( + fn build + Copy>( f: impl FnMut(&'static str, bool) -> D, ) -> Self::Lifted; @@ -66,8 +66,7 @@ where 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::<_, _, Const

, _>::derivative_generic(Const::

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

, _>::derivative_generic(Const::

, U1, seed_idx); } d }) diff --git a/crates/feos-core/src/ad/properties/boiling_temperature.rs b/crates/feos-core/src/ad/properties/boiling_temperature.rs index c597fb30e..8a9b58821 100644 --- a/crates/feos-core/src/ad/properties/boiling_temperature.rs +++ b/crates/feos-core/src/ad/properties/boiling_temperature.rs @@ -22,7 +22,7 @@ where type Unit = _Temperature; const REFERENCE: Temperature = KELVIN; - fn evaluate, D: DualNum + Copy>( + fn evaluate, D: DualNum + Copy>( &self, eos: &E, ) -> FeosResult> { 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 9e838b9f7..75b7260c6 100644 --- a/crates/feos-core/src/ad/properties/bubble_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/bubble_point_pressure.rs @@ -28,7 +28,7 @@ where type Unit = _Pressure; const REFERENCE: Pressure = PASCAL; - fn evaluate, D: DualNum + Copy>( + fn evaluate, D: DualNum + Copy>( &self, eos: &E, ) -> FeosResult> 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 4ce733484..9b4c813bb 100644 --- a/crates/feos-core/src/ad/properties/dew_point_pressure.rs +++ b/crates/feos-core/src/ad/properties/dew_point_pressure.rs @@ -27,7 +27,7 @@ where type Unit = _Pressure; const REFERENCE: Pressure = PASCAL; - fn evaluate, D: DualNum + Copy>( + fn evaluate, D: DualNum + Copy>( &self, eos: &E, ) -> FeosResult> 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 ee0d3d3aa..77b8bb1e9 100644 --- a/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs +++ b/crates/feos-core/src/ad/properties/enthalpy_of_vaporization.rs @@ -21,7 +21,7 @@ where type Unit = _MolarEnergy; const REFERENCE: MolarEnergy = MolarEnergy::new(1.0); - fn evaluate, D: DualNum + Copy>( + fn evaluate, D: DualNum + Copy>( &self, eos: &E, ) -> FeosResult> { 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 85ded0885..fab58a9a8 100644 --- a/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs +++ b/crates/feos-core/src/ad/properties/equilibrium_liquid_density.rs @@ -21,7 +21,7 @@ where type Unit = _Density; const REFERENCE: Density = Density::new(1000.0); - fn evaluate, D: DualNum + Copy>( + fn evaluate, D: DualNum + Copy>( &self, eos: &E, ) -> FeosResult> { diff --git a/crates/feos-core/src/ad/properties/liquid_density.rs b/crates/feos-core/src/ad/properties/liquid_density.rs index f8992c3fe..1483ab0c4 100644 --- a/crates/feos-core/src/ad/properties/liquid_density.rs +++ b/crates/feos-core/src/ad/properties/liquid_density.rs @@ -23,7 +23,7 @@ where type Unit = _Density; const REFERENCE: Density = Density::new(1000.0); - fn evaluate, D: DualNum + Copy>( + fn evaluate, D: DualNum + Copy>( &self, eos: &E, ) -> FeosResult> { diff --git a/crates/feos-core/src/ad/properties/mod.rs b/crates/feos-core/src/ad/properties/mod.rs index 660716e1d..4f2af32da 100644 --- a/crates/feos-core/src/ad/properties/mod.rs +++ b/crates/feos-core/src/ad/properties/mod.rs @@ -34,7 +34,7 @@ where const REFERENCE: Quantity; /// Evaluate the property for an arbitrary derivative. - fn evaluate, D: DualNum + Copy>( + fn evaluate, D: DualNum + Copy>( &self, eos: &E, ) -> FeosResult>; 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 b8f962757..2dffe6093 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 @@ -23,7 +23,7 @@ where type Unit = _MolarEntropy; const REFERENCE: MolarEntropy = MolarEntropy::new(1.0); - fn evaluate, D: DualNum + Copy>( + fn evaluate, D: DualNum + Copy>( &self, eos: &E, ) -> FeosResult> { diff --git a/crates/feos-core/src/ad/properties/vapor_pressure.rs b/crates/feos-core/src/ad/properties/vapor_pressure.rs index 8192d3ff0..27f6b1740 100644 --- a/crates/feos-core/src/ad/properties/vapor_pressure.rs +++ b/crates/feos-core/src/ad/properties/vapor_pressure.rs @@ -22,7 +22,7 @@ where type Unit = _Pressure; const REFERENCE: Pressure = PASCAL; - fn evaluate, D: DualNum + Copy>( + fn evaluate, D: DualNum + Copy>( &self, eos: &E, ) -> FeosResult> { diff --git a/crates/feos-core/src/cubic.rs b/crates/feos-core/src/cubic.rs index a12f615e5..c6b392e93 100644 --- a/crates/feos-core/src/cubic.rs +++ b/crates/feos-core/src/cubic.rs @@ -121,11 +121,11 @@ impl ResidualDyn for PengRobinson { self.tc.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { D::from(0.9) / molefracs.dot(&self.b.map(D::from)) } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { diff --git a/crates/feos-core/src/density_iteration.rs b/crates/feos-core/src/density_iteration.rs index e27ca0fba..eb9bd5cdb 100644 --- a/crates/feos-core/src/density_iteration.rs +++ b/crates/feos-core/src/density_iteration.rs @@ -6,7 +6,7 @@ use nalgebra::{DefaultAllocator, Dim, OVector}; use num_dual::{Dual, DualNum, first_derivative}; use quantity::{Density, Pressure, Temperature}; -pub fn density_iteration, N: Dim, D: DualNum + Copy>( +pub fn density_iteration, N: Dim, D: DualNum + Copy>( eos: &E, temperature: Temperature, pressure: Pressure, diff --git a/crates/feos-core/src/equation_of_state/mod.rs b/crates/feos-core/src/equation_of_state/mod.rs index 14e45c07a..4fbb1829f 100644 --- a/crates/feos-core/src/equation_of_state/mod.rs +++ b/crates/feos-core/src/equation_of_state/mod.rs @@ -53,11 +53,11 @@ impl ResidualDyn for EquationOfState, R> { self.residual.components() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { self.residual.compute_max_density(molefracs) } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { @@ -79,7 +79,7 @@ impl Subset for EquationOfState, R> { } } -impl, D>, D: DualNum + Copy, const N: usize> +impl, D>, D: DualNum + Copy, const N: usize> Residual, D> for EquationOfState<[I; N], R> { fn components(&self) -> usize { @@ -87,11 +87,12 @@ impl, D>, D: DualNum + Copy, const N: usize> } type Real = EquationOfState<[I; N], R::Real>; - type Lifted + Copy> = EquationOfState<[I; N], R::Lifted>; + type Lifted + Copy> = + EquationOfState<[I; N], R::Lifted>; fn re(&self) -> Self::Real { EquationOfState::new(self.ideal_gas.clone(), self.residual.re()) } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { EquationOfState::new(self.ideal_gas.clone(), self.residual.lift()) } @@ -118,7 +119,7 @@ pub trait IdealGas { /// Implementation of an ideal gas model in terms of the /// logarithm of the cubic thermal de Broglie wavelength /// in units ln(A³) for each component in the system. - fn ln_lambda3 + Copy>(&self, temperature: D) -> D; + fn ln_lambda3 + Copy>(&self, temperature: D) -> D; /// The name of the ideal gas model. fn ideal_gas_model(&self) -> &'static str; @@ -128,9 +129,9 @@ pub trait IdealGas { /// respect to parameters. pub trait IdealGasAD: Clone { type Real: IdealGasAD; - type Lifted + Copy>: IdealGasAD; + type Lifted + Copy>: IdealGasAD; fn re(&self) -> Self::Real; - fn lift + Copy>(&self) -> Self::Lifted; + fn lift + Copy>(&self) -> Self::Lifted; /// Implementation of an ideal gas model in terms of the /// logarithm of the cubic thermal de Broglie wavelength @@ -142,14 +143,14 @@ pub trait IdealGasAD: Clone { } /// A total Helmholtz energy model consisting of a [Residual] model and an [IdealGas] part. -pub trait Total + Copy = f64>: Residual +pub trait Total + Copy = f64>: Residual where DefaultAllocator: Allocator, { type RealTotal: Total; - type LiftedTotal + Copy>: Total; + type LiftedTotal + Copy>: Total; fn re_total(&self) -> Self::RealTotal; - fn lift_total + Copy>(&self) -> Self::LiftedTotal; + fn lift_total + Copy>(&self) -> Self::LiftedTotal; fn ideal_gas_model(&self) -> &'static str; @@ -199,15 +200,15 @@ impl< I: IdealGas + 'static, C: Deref, R>> + Clone, R: ResidualDyn + 'static, - D: DualNum + Copy, + D: DualNum + Copy, > Total for C { type RealTotal = Self; - type LiftedTotal + Copy> = Self; + type LiftedTotal + Copy> = Self; fn re_total(&self) -> Self::RealTotal { self.clone() } - fn lift_total + Copy>(&self) -> Self::LiftedTotal { + fn lift_total + Copy>(&self) -> Self::LiftedTotal { self.clone() } @@ -225,11 +226,11 @@ impl< } } -impl, R: Residual, D>, D: DualNum + Copy, const N: usize> +impl, R: Residual, D>, D: DualNum + Copy, const N: usize> Total, D> for EquationOfState<[I; N], R> { type RealTotal = EquationOfState<[I::Real; N], R::Real>; - type LiftedTotal + Copy> = + type LiftedTotal + Copy> = EquationOfState<[I::Lifted; N], R::Lifted>; fn re_total(&self) -> Self::RealTotal { EquationOfState::new( @@ -237,7 +238,7 @@ impl, R: Residual, D>, D: DualNum + Copy, const N self.residual.re(), ) } - fn lift_total + Copy>(&self) -> Self::LiftedTotal { + fn lift_total + Copy>(&self) -> Self::LiftedTotal { EquationOfState::new( self.ideal_gas.each_ref().map(|i| i.lift()), self.residual.lift(), diff --git a/crates/feos-core/src/equation_of_state/residual.rs b/crates/feos-core/src/equation_of_state/residual.rs index af5fc88f6..5ade6b1c6 100644 --- a/crates/feos-core/src/equation_of_state/residual.rs +++ b/crates/feos-core/src/equation_of_state/residual.rs @@ -15,15 +15,15 @@ type Quot = >::Output; /// Molar weight of all components. /// /// Enables calculation of (mass) specific properties. -pub trait Molarweight + Copy = f64> +pub trait Molarweight + Copy = f64> where DefaultAllocator: Allocator, { fn molar_weight(&self) -> MolarWeight>; } -impl, T: Molarweight, N: Dim, D: DualNum + Copy> Molarweight - for C +impl, T: Molarweight, N: Dim, D: DualNum + Copy> + Molarweight for C where DefaultAllocator: Allocator, { @@ -62,23 +62,25 @@ pub trait ResidualDyn { /// equilibria and other iterations. It is not explicitly meant to /// be a mathematical limit for the density (if those exist in the /// equation of state anyways). - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D; + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D; /// Evaluate the reduced Helmholtz energy density of each individual contribution /// and return them together with a string representation of the contribution. - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)>; } -impl + Clone, T: ResidualDyn, D: DualNum + Copy> Residual for C { +impl + Clone, T: ResidualDyn, D: DualNum + Copy> + Residual for C +{ type Real = Self; - type Lifted + Copy> = Self; + type Lifted + Copy> = Self; fn re(&self) -> Self::Real { self.clone() } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { self.clone() } fn components(&self) -> usize { @@ -96,7 +98,7 @@ impl + Clone, T: ResidualDyn, D: DualNum + Copy> Resid } /// A residual Helmholtz energy model. -pub trait Residual + Copy = f64>: Clone +pub trait Residual + Copy = f64>: Clone where DefaultAllocator: Allocator, { @@ -114,13 +116,13 @@ where type Real: Residual; /// The residual model with the model parameters lifted to a higher dual number. - type Lifted + Copy>: Residual; + type Lifted + Copy>: Residual; /// Return the real part of the residual model. fn re(&self) -> Self::Real; /// Return the lifted residual model. - fn lift + Copy>(&self) -> Self::Lifted; + fn lift + Copy>(&self) -> Self::Lifted; /// Return the maximum density in Angstrom^-3. /// @@ -303,7 +305,7 @@ where molefracs: &OVector, ) -> (D, D, D, D, D) { let molar_volume = density.recip(); - let (a, da, d2a) = hessian::<_, _, _, U2, _>( + let (a, da, d2a) = hessian::<_, _, U2, _>( partial( |vt: SVector<_, 2>, x: &OVector<_, N>| { let [[v, t]] = vt.data.0; @@ -418,7 +420,7 @@ where } /// Reference values and residual entropy correlations for entropy scaling. -pub trait EntropyScaling + Copy = f64> +pub trait EntropyScaling + Copy = f64> where DefaultAllocator: Allocator, { @@ -445,7 +447,7 @@ where fn thermal_conductivity_correlation(&self, s_res: D, x: &OVector) -> D; } -impl, T: EntropyScaling, N: Dim, D: DualNum + Copy> +impl, T: EntropyScaling, N: Dim, D: DualNum + Copy> EntropyScaling for C where DefaultAllocator: Allocator, @@ -502,11 +504,11 @@ impl ResidualDyn for NoResidual { self.0 } - fn compute_max_density + Copy>(&self, _: &DVector) -> D { + fn compute_max_density + Copy>(&self, _: &DVector) -> D { D::one() } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, _: &StateHD, ) -> Vec<(&'static str, D)> { diff --git a/crates/feos-core/src/lib.rs b/crates/feos-core/src/lib.rs index 156894648..5a117790f 100644 --- a/crates/feos-core/src/lib.rs +++ b/crates/feos-core/src/lib.rs @@ -221,7 +221,7 @@ mod tests { "NoIdealGas" } - fn ln_lambda3 + Copy>(&self, _: D) -> D { + fn ln_lambda3 + Copy>(&self, _: D) -> D { unreachable!() } } diff --git a/crates/feos-core/src/phase_equilibria/bubble_dew.rs b/crates/feos-core/src/phase_equilibria/bubble_dew.rs index 6d2c9333d..9033df5a2 100644 --- a/crates/feos-core/src/phase_equilibria/bubble_dew.rs +++ b/crates/feos-core/src/phase_equilibria/bubble_dew.rs @@ -23,7 +23,7 @@ const MAX_LNPSTEP: f64 = 0.1; const NEWTON_TOL: f64 = 1e-3; /// Trait that enables functions to be generic over their input unit. -pub trait TemperatureOrPressure + Copy = f64>: Copy { +pub trait TemperatureOrPressure + Copy = f64>: Copy { type Other: Copy; const IDENTIFIER: &'static str; @@ -46,7 +46,7 @@ pub trait TemperatureOrPressure + Copy = f64>: Copy { ) -> (Temperature>, Pressure>); } -impl + Copy> TemperatureOrPressure for Temperature { +impl + Copy> TemperatureOrPressure for Temperature { type Other = Pressure; const IDENTIFIER: &'static str = "temperature"; @@ -86,7 +86,7 @@ impl + Copy> TemperatureOrPressure for Temperature { // For some inexplicable reason this does not compile if the `Pressure` type is // used instead of the explicit unit. Maybe the type is too complicated for the // compiler? -impl + Copy> TemperatureOrPressure +impl + Copy> TemperatureOrPressure for Quantity> { type Other = Temperature; @@ -138,7 +138,8 @@ impl TemperatureOrPressureSpecification { } /// # Bubble and dew point calculations -impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium +impl, N: Gradients, D: DualNum + Copy> + PhaseEquilibrium where DefaultAllocator: Allocator + Allocator + Allocator, { @@ -399,7 +400,7 @@ where }); // calculate Newton step - let dx = LU::<_, _, Dyn>::new(jac)?.solve(&f); + let dx = LU::<_, Dyn>::new(jac)?.solve(&f); // apply Newton step for i in 0..n { @@ -475,7 +476,7 @@ where }); // calculate Newton step - let dx = LU::<_, _, Dyn>::new(jac)?.solve(&f); + let dx = LU::<_, Dyn>::new(jac)?.solve(&f); // apply Newton step for i in 0..n { diff --git a/crates/feos-core/src/phase_equilibria/mod.rs b/crates/feos-core/src/phase_equilibria/mod.rs index 5ed15a767..ca6fd42b2 100644 --- a/crates/feos-core/src/phase_equilibria/mod.rs +++ b/crates/feos-core/src/phase_equilibria/mod.rs @@ -46,7 +46,7 @@ pub use phase_diagram_pure::PhaseDiagram; /// + [Heteroazeotropes](#heteroazeotropes) /// + [Utility functions](#utility-functions) #[derive(Debug, Clone)] -pub struct PhaseEquilibrium + Copy = f64> +pub struct PhaseEquilibrium + Copy = f64> where DefaultAllocator: Allocator, { @@ -100,7 +100,7 @@ impl PhaseEquilibrium { } } -impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium +impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator, { @@ -131,7 +131,7 @@ impl PhaseEquilibrium { } } -impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium +impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator, { @@ -164,7 +164,7 @@ where } } -impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium +impl, N: Dim, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator, { @@ -177,7 +177,7 @@ where } } -impl, N: Gradients, const P: usize, D: DualNum + Copy> +impl, N: Gradients, const P: usize, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator, @@ -187,8 +187,7 @@ where } } -impl, N: Gradients, const P: usize, D: DualNum + Copy> - PhaseEquilibrium +impl, N: Gradients, const P: usize, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator, { diff --git a/crates/feos-core/src/phase_equilibria/px_flashes.rs b/crates/feos-core/src/phase_equilibria/px_flashes.rs index aadeffb76..5ee40351f 100644 --- a/crates/feos-core/src/phase_equilibria/px_flashes.rs +++ b/crates/feos-core/src/phase_equilibria/px_flashes.rs @@ -17,7 +17,7 @@ const TOL_PX: f64 = 1e-11; type PXVars = >::Output; type TPVars = >::Output; -impl, N: Gradients + DimAdd + DimAdd, D: DualNum + Copy> +impl, N: Gradients + DimAdd + DimAdd, D: DualNum + Copy> PhaseEquilibrium where DefaultAllocator: Allocator @@ -213,7 +213,7 @@ where } } -fn unpack_variables + Copy, N: Dim + DimAdd>( +fn unpack_variables + Copy, N: Dim + DimAdd>( molefracs: &OVector, variables: &OVector>, ) -> (D, D, D, D, OVector, OVector) @@ -229,7 +229,7 @@ where (t, beta, rho_l, rho_v, x, y) } -fn unpack_tp_variables + Copy, N: Dim + DimAdd>( +fn unpack_tp_variables + Copy, N: Dim + DimAdd>( molefracs: &OVector, variables: &OVector>, ) -> (D, D, D, OVector, OVector) @@ -247,7 +247,7 @@ where trait PXFlash: Sized + Copy { // potential function for which the flash solution is a saddle point. - fn state_function, N: Dim + DimAdd, D: DualNum + Copy>( + fn state_function, N: Dim + DimAdd, D: DualNum + Copy>( eos: &E, variables: OVector>, args: &(D, D, OVector), @@ -255,14 +255,14 @@ trait PXFlash: Sized + Copy { where DefaultAllocator: Allocator + Allocator>; - fn evaluate_property, N: Gradients, D: DualNum + Copy>( + fn evaluate_property, N: Gradients, D: DualNum + Copy>( vle: &PhaseEquilibrium, ) -> Quantity where DefaultAllocator: Allocator; // the potential function for a tp-flash specification (Q = A + V*p_spec) - fn tp_state_function, N: Dim + DimAdd, D: DualNum + Copy>( + fn tp_state_function, N: Dim + DimAdd, D: DualNum + Copy>( eos: &E, variables: OVector>, &(t, p, ref z): &(D, D, OVector), @@ -284,7 +284,7 @@ trait PXFlash: Sized + Copy { // Because the ps and ph flashes are saddle points rather then extrema, // the value of the potential can not be used as convergence criterion. #[expect(clippy::type_complexity)] - fn newton_step, N: Dim + DimAdd, D: DualNum + Copy>( + fn newton_step, N: Dim + DimAdd, D: DualNum + Copy>( eos: &E, variables: &OVector>, specifications: &(D, D, OVector), @@ -327,7 +327,7 @@ trait PXFlash: Sized + Copy { let rho_i_l = rho_l * x; let (hs, dhs) = first_derivative( partial( - |t: Dual<_, _>, args: &(_, OVector<_, _>)| { + |t: Dual<_>, args: &(_, OVector<_, _>)| { let &(p, ref z) = args; let args = (t, p, z.clone_owned()); @@ -403,7 +403,7 @@ trait PXFlash: Sized + Copy { impl PXFlash for SIUnit<-2, 2, 1, 0, 0, -1, 0> { // the potential function for a ph-flash specification (Q = (A + V*p_spec - H_spec) / T) - fn state_function, N: Dim + DimAdd, D: DualNum + Copy>( + fn state_function, N: Dim + DimAdd, D: DualNum + Copy>( eos: &E, variables: OVector>, &(p, h, ref z): &(D, D, OVector), @@ -421,7 +421,7 @@ impl PXFlash for SIUnit<-2, 2, 1, 0, 0, -1, 0> { potential(y, rho_v, t) * beta + potential(x, rho_l, t) * (-beta + 1.0) } - fn evaluate_property, N: Gradients, D: DualNum + Copy>( + fn evaluate_property, N: Gradients, D: DualNum + Copy>( vle: &PhaseEquilibrium, ) -> Quantity where @@ -433,7 +433,7 @@ impl PXFlash for SIUnit<-2, 2, 1, 0, 0, -1, 0> { impl PXFlash for SIUnit<-2, 2, 1, 0, -1, -1, 0> { // the potential function for a ps-flash specification (Q = A + T*S_spec + V*p_spec) - fn state_function, N: Dim + DimAdd, D: DualNum + Copy>( + fn state_function, N: Dim + DimAdd, D: DualNum + Copy>( eos: &E, variables: OVector>, &(p, s, ref z): &(D, D, OVector), @@ -454,7 +454,7 @@ impl PXFlash for SIUnit<-2, 2, 1, 0, -1, -1, 0> { potential(y, rho_v, t) * beta + potential(x, rho_l, t) * (-beta + 1.0) } - fn evaluate_property, N: Gradients, D: DualNum + Copy>( + fn evaluate_property, N: Gradients, D: DualNum + Copy>( vle: &PhaseEquilibrium, ) -> Quantity where diff --git a/crates/feos-core/src/phase_equilibria/tp_flash.rs b/crates/feos-core/src/phase_equilibria/tp_flash.rs index 88b30679e..761ae18e6 100644 --- a/crates/feos-core/src/phase_equilibria/tp_flash.rs +++ b/crates/feos-core/src/phase_equilibria/tp_flash.rs @@ -40,7 +40,7 @@ where } } -impl, D: DualNum + Copy> PhaseEquilibrium { +impl, D: DualNum + Copy> PhaseEquilibrium { /// Perform a Tp-flash calculation for a binary mixture. /// Compared to the version of the algorithm for a generic /// number of components ([tp_flash](PhaseEquilibrium::tp_flash)), @@ -79,7 +79,7 @@ impl, D: DualNum + Copy> PhaseEquilibrium { let [[v_l, v_v, x, y]] = variables.data.0; let beta = (z - x) / (y - x); let eos = eos.lift(); - let molar_gibbs_energy = |x: Dual2Vec<_, _, _>, v| { + let molar_gibbs_energy = |x: Dual2Vec<_, _>, v| { let molefracs = vector![x, -x + 1.0]; let a_res = eos.residual_helmholtz_energy(t, v, &molefracs); let a_ig = (x * (x / v).ln() - (x - 1.0) * ((-x + 1.0) / v).ln() - 1.0) * t; diff --git a/crates/feos-core/src/phase_equilibria/vle_pure.rs b/crates/feos-core/src/phase_equilibria/vle_pure.rs index f58fc6e21..97119283e 100644 --- a/crates/feos-core/src/phase_equilibria/vle_pure.rs +++ b/crates/feos-core/src/phase_equilibria/vle_pure.rs @@ -14,7 +14,8 @@ const MAX_ITER_PURE: usize = 50; const TOL_PURE: f64 = 1e-12; /// # Pure component phase equilibria -impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium +impl, N: Gradients, D: DualNum + Copy> + PhaseEquilibrium where DefaultAllocator: Allocator + Allocator + Allocator, { @@ -238,7 +239,8 @@ where Ok((p, [rho_v, rho_l])) } -impl, N: Gradients, D: DualNum + Copy> PhaseEquilibrium +impl, N: Gradients, D: DualNum + Copy> + PhaseEquilibrium where DefaultAllocator: Allocator + Allocator + Allocator, { @@ -447,7 +449,7 @@ where // both phases have the same temperature for _ in 0..20 { let h_s = |t, v| { - let (a_res, da_res) = gradient::<_, _, _, U2, _>( + let (a_res, da_res) = gradient::<_, _, U2, _>( partial( |t_v: SVector<_, _>, x| { let [[t, v]] = t_v.data.0; diff --git a/crates/feos-core/src/state/composition.rs b/crates/feos-core/src/state/composition.rs index 973af8ffb..7040a1314 100644 --- a/crates/feos-core/src/state/composition.rs +++ b/crates/feos-core/src/state/composition.rs @@ -21,7 +21,7 @@ use quantity::Moles; /// |N|`&OVector`|-|`Dyn` only| /// |N|`Moles>`|✅| /// |N|`&Moles>`|✅| -pub trait Composition + Copy, N: Dim> +pub trait Composition + Copy, N: Dim> where DefaultAllocator: Allocator, { @@ -34,7 +34,7 @@ where } // trivial implementations -impl + Copy, N: Dim> Composition for (OVector, Moles) +impl + Copy, N: Dim> Composition for (OVector, Moles) where DefaultAllocator: Allocator, { @@ -46,7 +46,8 @@ where } } -impl + Copy, N: Dim> Composition for (OVector, Option>) +impl + Copy, N: Dim> Composition + for (OVector, Option>) where DefaultAllocator: Allocator, { @@ -59,7 +60,7 @@ where } // a pure component needs no specification -impl + Copy> Composition for () { +impl + Copy> Composition for () { fn into_molefracs>( self, _: &E, @@ -67,7 +68,7 @@ impl + Copy> Composition for () { Ok(((vector![D::one()]), None)) } } -impl + Copy> Composition for () { +impl + Copy> Composition for () { fn into_molefracs>( self, eos: &E, @@ -84,7 +85,7 @@ impl + Copy> Composition for () { } // a binary mixture can be specified by a scalar (x1) -impl + Copy> Composition for D { +impl + Copy> Composition for D { fn into_molefracs>( self, _: &E, @@ -111,7 +112,7 @@ impl Composition for f64 { } // a pure component can be specified by the total mole number -impl + Copy> Composition for Moles { +impl + Copy> Composition for Moles { fn into_molefracs>( self, _: &E, @@ -120,7 +121,7 @@ impl + Copy> Composition for Moles { } } -impl + Copy> Composition for Moles { +impl + Copy> Composition for Moles { fn into_molefracs>( self, eos: &E, @@ -140,7 +141,7 @@ impl + Copy> Composition for Moles { // // for a dynamic number of components, it is also possible to specify only the // N-1 first components -impl + Copy, N: Dim> Composition for OVector +impl + Copy, N: Dim> Composition for OVector where DefaultAllocator: Allocator, { @@ -152,7 +153,7 @@ where } } -impl + Copy, N: Dim> Composition for &OVector +impl + Copy, N: Dim> Composition for &OVector where DefaultAllocator: Allocator, { @@ -181,7 +182,7 @@ where } // the mixture can be specified by its moles -impl + Copy, N: Dim> Composition for Moles> +impl + Copy, N: Dim> Composition for Moles> where DefaultAllocator: Allocator, { @@ -193,7 +194,7 @@ where } } -impl + Copy, N: Dim> Composition for &Moles> +impl + Copy, N: Dim> Composition for &Moles> where DefaultAllocator: Allocator, { diff --git a/crates/feos-core/src/state/critical_point.rs b/crates/feos-core/src/state/critical_point.rs index 4de9f9562..5dfe14a26 100644 --- a/crates/feos-core/src/state/critical_point.rs +++ b/crates/feos-core/src/state/critical_point.rs @@ -44,7 +44,7 @@ impl State { } } -impl, N: Gradients, D: DualNum + Copy> State +impl, N: Gradients, D: DualNum + Copy> State where DefaultAllocator: Allocator + Allocator + Allocator, { @@ -91,7 +91,7 @@ where initial_density, options, )?; - let trho = implicit_derivative_vec::<_, _, _, _, U3>( + let trho = implicit_derivative_vec::<_, _, _, U3>( |x, &p| { let t = x[0]; let rho = [x[1], x[2]]; @@ -184,7 +184,7 @@ where for i in 1..=max_iter { // calculate residuals and derivatives w.r.t. temperature and density - let (res, jac) = jacobian::<_, _, _, U2, U2, _>( + let (res, jac) = jacobian::<_, _, U2, U2, _>( |x: SVector, 2>| { SVector::from(criticality_conditions( &eos.lift(), @@ -269,7 +269,7 @@ where for i in 1..=max_iter { // calculate residuals and derivatives w.r.t. partial densities - let (res, jac) = jacobian::<_, _, _, U2, U2, _>( + let (res, jac) = jacobian::<_, _, U2, U2, _>( |rho: SVector, 2>| { let density = rho.sum(); let x = rho / density; @@ -360,7 +360,7 @@ where let p = DualSVec::from_re(p); criticality_conditions_p(&eos.lift(), p, t, partial_density) }; - let (res, jac) = jacobian::<_, _, _, U3, U3, _>(res, &SVector::from([t, rho[0], rho[1]])); + let (res, jac) = jacobian::<_, _, U3, U3, _>(res, &SVector::from([t, rho[0], rho[1]])); // calculate Newton step let delta = jac.lu().solve(&res); @@ -507,7 +507,7 @@ where } } -fn criticality_conditions, N: Gradients, D: DualNum + Copy>( +fn criticality_conditions, N: Gradients, D: DualNum + Copy>( eos: &E, temperature: D, density: D, @@ -548,7 +548,7 @@ where [l, c2] } -fn criticality_conditions_p, N: Gradients, D: DualNum + Copy>( +fn criticality_conditions_p, N: Gradients, D: DualNum + Copy>( eos: &E, pressure: D, temperature: D, @@ -575,7 +575,7 @@ where SVector::from([c1, c2, -p_calc + pressure]) } -fn stability_condition, N: Gradients, D: DualNum + Copy>( +fn stability_condition, N: Gradients, D: DualNum + Copy>( eos: &E, temperature: D, density: D, @@ -598,7 +598,7 @@ where l } -fn dmu_dn, N: Gradients, D: DualNum + Copy>( +fn dmu_dn, N: Gradients, D: DualNum + Copy>( eos: &E, temperature: D, molar_volume: D, diff --git a/crates/feos-core/src/state/mod.rs b/crates/feos-core/src/state/mod.rs index daa7a7ff4..94ea3c6e9 100644 --- a/crates/feos-core/src/state/mod.rs +++ b/crates/feos-core/src/state/mod.rs @@ -64,7 +64,7 @@ impl DensityInitialization { /// Properties are stored as generalized (hyper) dual numbers which allows /// for automatic differentiation. #[derive(Clone, Debug)] -pub struct StateHD + Copy, N: Dim = Dyn> +pub struct StateHD + Copy, N: Dim = Dyn> where DefaultAllocator: Allocator, { @@ -76,7 +76,7 @@ where pub partial_density: OVector, } -impl + Copy> StateHD +impl + Copy> StateHD where DefaultAllocator: Allocator, { @@ -137,7 +137,7 @@ where /// + [Stability analysis](#stability-analysis) /// + [Flash calculations](#flash-calculations) #[derive(Debug, Clone)] -pub struct State + Copy = f64> +pub struct State + Copy = f64> where DefaultAllocator: Allocator, { @@ -157,7 +157,7 @@ where cache: Cache, } -impl + Copy> State +impl + Copy> State where DefaultAllocator: Allocator, { @@ -192,7 +192,7 @@ where } } -impl, N: Dim, D: DualNum + Copy> fmt::Display for State +impl, N: Dim, D: DualNum + Copy> fmt::Display for State where DefaultAllocator: Allocator, { @@ -216,7 +216,7 @@ where } } -impl, N: Dim, D: DualNum + Copy> State +impl, N: Dim, D: DualNum + Copy> State where DefaultAllocator: Allocator, { @@ -403,7 +403,7 @@ where } } -impl, N: Gradients, D: DualNum + Copy> State +impl, N: Gradients, D: DualNum + Copy> State where DefaultAllocator: Allocator, { @@ -600,7 +600,7 @@ fn is_close( (x - y).abs() <= atol + rtol * y.abs() } -fn newton, N: Dim, D: DualNum + Copy, F, X: Copy, Y>( +fn newton, N: Dim, D: DualNum + Copy, F, X: Copy, Y>( mut x0: Quantity, mut f: F, atol: Quantity, @@ -644,7 +644,7 @@ where /// /// There is no validation of the physical state, e.g. /// if resulting densities are below maximum packing fraction. -fn validate>( +fn validate>( temperature: Temperature, density: Density, molefracs: &OVector, diff --git a/crates/feos-core/src/state/properties.rs b/crates/feos-core/src/state/properties.rs index 132bad936..948d77dc0 100644 --- a/crates/feos-core/src/state/properties.rs +++ b/crates/feos-core/src/state/properties.rs @@ -10,7 +10,7 @@ use std::ops::{Div, Neg}; type InvP = Quantity::Output>; type InvT = Quantity::Output>; -impl, N: Gradients, D: DualNum + Copy> State +impl, N: Gradients, D: DualNum + Copy> State where DefaultAllocator: Allocator, { @@ -269,7 +269,7 @@ where } } -impl + Molarweight, N: Gradients, D: DualNum + Copy> State +impl + Molarweight, N: Gradients, D: DualNum + Copy> State where DefaultAllocator: Allocator, { diff --git a/crates/feos-core/src/state/residual_properties.rs b/crates/feos-core/src/state/residual_properties.rs index 184f1aa53..317961a3a 100644 --- a/crates/feos-core/src/state/residual_properties.rs +++ b/crates/feos-core/src/state/residual_properties.rs @@ -12,7 +12,7 @@ type InvP = Quantity::Output>; type POverT = Quantity>::Output>; /// # State properties -impl, N: Gradients, D: DualNum + Copy> State +impl, N: Gradients, D: DualNum + Copy> State where DefaultAllocator: Allocator, { @@ -480,7 +480,7 @@ impl State { } } -impl, N: Gradients, D: DualNum + Copy> State +impl, N: Gradients, D: DualNum + Copy> State where DefaultAllocator: Allocator, { @@ -585,7 +585,7 @@ where } } -impl + Molarweight, N: Gradients, D: DualNum + Copy> State +impl + Molarweight, N: Gradients, D: DualNum + Copy> State where DefaultAllocator: Allocator, { @@ -628,7 +628,7 @@ where /// /// These properties are available for equations of state /// that implement the [EntropyScaling] trait. -impl + EntropyScaling, N: Gradients, D: DualNum + Copy> State +impl + EntropyScaling, N: Gradients, D: DualNum + Copy> State where DefaultAllocator: Allocator, { diff --git a/crates/feos-derive/src/dft.rs b/crates/feos-derive/src/dft.rs index e5dd7a343..185d2f7a1 100644 --- a/crates/feos-derive/src/dft.rs +++ b/crates/feos-derive/src/dft.rs @@ -67,7 +67,7 @@ pub(crate) fn impl_helmholtz_energy_functional( #(#contributions,)* } } - fn bond_lengths + Copy>(&self, temperature: N) -> petgraph::graph::UnGraph<(), N> { + fn bond_lengths + Copy>(&self, temperature: N) -> petgraph::graph::UnGraph<(), N> { match self { #(#bond_lengths,)* _ => petgraph::Graph::with_capacity(0, 0), diff --git a/crates/feos-derive/src/functional_contribution.rs b/crates/feos-derive/src/functional_contribution.rs index 44b8c957a..19519c0ce 100644 --- a/crates/feos-derive/src/functional_contribution.rs +++ b/crates/feos-derive/src/functional_contribution.rs @@ -56,17 +56,17 @@ fn impl_functional_contribution( #(#name,)* } } - fn weight_functions + Copy>(&self, temperature: N) -> feos_dft::WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> feos_dft::WeightFunctionInfo { match self { #(#weight_functions,)* } } - fn weight_functions_pdgt + Copy>(&self, temperature: N) -> feos_dft::WeightFunctionInfo { + fn weight_functions_pdgt + Copy>(&self, temperature: N) -> feos_dft::WeightFunctionInfo { match self { #(#weight_functions_pdgt,)* } } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ndarray::ArrayView2, diff --git a/crates/feos-derive/src/ideal_gas.rs b/crates/feos-derive/src/ideal_gas.rs index f9b5eb671..a1058b907 100644 --- a/crates/feos-derive/src/ideal_gas.rs +++ b/crates/feos-derive/src/ideal_gas.rs @@ -42,7 +42,7 @@ fn impl_ideal_gas( }); quote! { impl IdealGas for IdealGasModel { - fn ln_lambda3 + Copy>(&self, temperature: D) -> D { + fn ln_lambda3 + Copy>(&self, temperature: D) -> D { match self { #(#ln_lambda3,)* } diff --git a/crates/feos-derive/src/residual.rs b/crates/feos-derive/src/residual.rs index df3f5367f..1fab1e4bb 100644 --- a/crates/feos-derive/src/residual.rs +++ b/crates/feos-derive/src/residual.rs @@ -56,12 +56,12 @@ fn impl_residual( #(#components,)* } } - fn compute_max_density + Copy>(&self, moles: &DVector) -> D { + fn compute_max_density + Copy>(&self, moles: &DVector) -> D { match self { #(#compute_max_density,)* } } - fn reduced_helmholtz_energy_density_contributions + Copy>(&self, state: &StateHD) -> Vec<(&'static str, D)> { + fn reduced_helmholtz_energy_density_contributions + Copy>(&self, state: &StateHD) -> Vec<(&'static str, D)> { match self { #(#reduced_helmholtz_energy_density_contributions,)* } diff --git a/crates/feos-dft/src/adsorption/pore.rs b/crates/feos-dft/src/adsorption/pore.rs index b59474085..fc3cf93be 100644 --- a/crates/feos-dft/src/adsorption/pore.rs +++ b/crates/feos-dft/src/adsorption/pore.rs @@ -184,7 +184,10 @@ where .dot(&Dimensionless::new(self.profile.bulk.molefracs.clone()))) } - fn _henry_coefficients + Copy + DctNum>(&self, temperature: N) -> DVector { + fn _henry_coefficients + Copy + DctNum>( + &self, + temperature: N, + ) -> DVector { if self.profile.bulk.eos.m().iter().any(|&m| m != 1.0) { panic!( "Henry coefficients can only be calculated for spherical and heterosegmented molecules!" @@ -302,11 +305,11 @@ impl ResidualDyn for Helium { fn components(&self) -> usize { 1 } - fn compute_max_density + Copy>(&self, _: &DVector) -> D { + fn compute_max_density + Copy>(&self, _: &DVector) -> D { D::from(1.0) } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { @@ -342,11 +345,11 @@ impl FluidParameters for &Helium { struct HeliumContribution; impl FunctionalContribution for HeliumContribution { - fn weight_functions + Copy>(&self, _: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, _: N) -> WeightFunctionInfo { unreachable!() } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, _: N, _: ArrayView2, diff --git a/crates/feos-dft/src/convolver/mod.rs b/crates/feos-dft/src/convolver/mod.rs index edca1f7e4..c047bcd0e 100644 --- a/crates/feos-dft/src/convolver/mod.rs +++ b/crates/feos-dft/src/convolver/mod.rs @@ -40,7 +40,7 @@ pub(crate) struct BulkConvolver { weight_constants: Vec>, } -impl + Copy + Send + Sync> BulkConvolver { +impl + Copy + Send + Sync> BulkConvolver { #[expect(clippy::new_ret_no_self)] pub(crate) fn new(weight_functions: Vec>) -> Arc> { let weight_constants = weight_functions @@ -51,7 +51,7 @@ impl + Copy + Send + Sync> BulkConvolver { } } -impl + Copy + Send + Sync> Convolver for BulkConvolver +impl + Copy + Send + Sync> Convolver for BulkConvolver where Array2: Dot, Output = Array1>, { @@ -127,7 +127,7 @@ pub struct ConvolverFFT { impl ConvolverFFT where - T: DctNum + DualNum, + T: DctNum + DualNum, D::Larger: Dimension, D::Smaller: Dimension, ::Larger: Dimension, @@ -159,7 +159,7 @@ where impl ConvolverFFT where - T: DctNum + DualNum, + T: DctNum + DualNum, D::Larger: Dimension, ::Larger: Dimension, { @@ -278,7 +278,7 @@ where impl ConvolverFFT where - T: DctNum + DualNum, + T: DctNum + DualNum, D::Larger: Dimension, ::Larger: Dimension, { @@ -370,7 +370,7 @@ where impl Convolver for ConvolverFFT where - T: DctNum + DualNum, + T: DctNum + DualNum, D::Larger: Dimension, ::Larger: Dimension, { @@ -555,7 +555,7 @@ struct CurvilinearConvolver { impl CurvilinearConvolver where - T: DctNum + DualNum, + T: DctNum + DualNum, D::Larger: Dimension, D::Smaller: Dimension, ::Larger: Dimension, @@ -576,7 +576,7 @@ where impl Convolver for CurvilinearConvolver where - T: DctNum + DualNum, + T: DctNum + DualNum, D::Smaller: Dimension, D::Larger: Dimension, { diff --git a/crates/feos-dft/src/convolver/periodic_convolver.rs b/crates/feos-dft/src/convolver/periodic_convolver.rs index bd594e451..dfb12ff06 100644 --- a/crates/feos-dft/src/convolver/periodic_convolver.rs +++ b/crates/feos-dft/src/convolver/periodic_convolver.rs @@ -26,7 +26,7 @@ pub struct PeriodicConvolver { impl PeriodicConvolver where - T: FftNum + DualNum, + T: FftNum + DualNum, D::Larger: Dimension, ::Larger: Dimension, { @@ -223,7 +223,7 @@ impl PeriodicConvolver { impl Convolver for PeriodicConvolver where - T: FftNum + DualNum, + T: FftNum + DualNum, D::Larger: Dimension, ::Larger: Dimension, { diff --git a/crates/feos-dft/src/convolver/transform.rs b/crates/feos-dft/src/convolver/transform.rs index abe4549e3..30e736d0a 100644 --- a/crates/feos-dft/src/convolver/transform.rs +++ b/crates/feos-dft/src/convolver/transform.rs @@ -24,7 +24,7 @@ impl SinCosTransform { } } -pub(super) trait FourierTransform>: Send + Sync { +pub(super) trait FourierTransform>: Send + Sync { fn forward_transform(&self, f_r: ArrayView1, f_k: ArrayViewMut1, scalar: bool); fn back_transform(&self, f_k: ArrayViewMut1, f_r: ArrayViewMut1, scalar: bool); @@ -34,7 +34,7 @@ pub(super) struct CartesianTransform { dct: Arc>, } -impl + DctNum> CartesianTransform { +impl + DctNum> CartesianTransform { #[expect(clippy::new_ret_no_self)] pub(super) fn new(axis: &Axis) -> (Box>, Array1) { let (s, k) = Self::init(axis); @@ -104,7 +104,7 @@ impl + DctNum> CartesianTransform { } } -impl + DctNum> FourierTransform for CartesianTransform { +impl + DctNum> FourierTransform for CartesianTransform { fn forward_transform(&self, f_r: ArrayView1, mut f_k: ArrayViewMut1, scalar: bool) { if scalar { f_k.slice_mut(s![..-1]).assign(&f_r); @@ -130,7 +130,7 @@ pub(super) struct SphericalTransform { dct: Arc>, } -impl + DctNum> SphericalTransform { +impl + DctNum> SphericalTransform { #[expect(clippy::new_ret_no_self)] pub(super) fn new(axis: &Axis) -> (Box>, Array1) { let points = axis.grid.len(); @@ -189,7 +189,7 @@ impl + DctNum> SphericalTransform { } } -impl + DctNum> FourierTransform for SphericalTransform { +impl + DctNum> FourierTransform for SphericalTransform { fn forward_transform(&self, f_r: ArrayView1, mut f_k: ArrayViewMut1, scalar: bool) { if scalar { self.sine_transform(&f_r * &self.r_grid, f_k.view_mut(), false); @@ -231,7 +231,7 @@ pub(super) struct PolarTransform { l: f64, } -impl + DctNum> PolarTransform { +impl + DctNum> PolarTransform { #[expect(clippy::new_ret_no_self)] pub(super) fn new(axis: &Axis) -> (Box>, Array1) { let points = axis.grid.len(); @@ -318,7 +318,7 @@ impl + DctNum> PolarTransform { } } -impl + DctNum> FourierTransform for PolarTransform { +impl + DctNum> FourierTransform for PolarTransform { fn forward_transform(&self, f_r: ArrayView1, f_k: ArrayViewMut1, scalar: bool) { self.transform(f_r, f_k, scalar, &self.r_grid, &self.k_grid, self.l); } @@ -339,12 +339,13 @@ pub(super) struct NoTransform(); impl NoTransform { #[expect(clippy::new_ret_no_self)] - pub(super) fn new>() -> (Box>, Array1) { + pub(super) fn new>() -> (Box>, Array1) + { (Box::new(Self()), arr1(&[0.0])) } } -impl> FourierTransform for NoTransform { +impl> FourierTransform for NoTransform { fn forward_transform(&self, f: ArrayView1, mut f_k: ArrayViewMut1, _: bool) { f_k.assign(&f); } diff --git a/crates/feos-dft/src/functional.rs b/crates/feos-dft/src/functional.rs index 8bad99d4c..f584f2110 100644 --- a/crates/feos-dft/src/functional.rs +++ b/crates/feos-dft/src/functional.rs @@ -28,7 +28,7 @@ impl HelmholtzEnergyFunctionalDyn self.residual.molecule_shape() } - fn bond_lengths + Copy>(&self, temperature: N) -> UnGraph<(), N> { + fn bond_lengths + Copy>(&self, temperature: N) -> UnGraph<(), N> { self.residual.bond_lengths(temperature) } } @@ -58,7 +58,7 @@ pub trait HelmholtzEnergyFunctionalDyn: ResidualDyn { fn molecule_shape(&self) -> MoleculeShape<'_>; /// Overwrite this, if the functional consists of heterosegmented chains. - fn bond_lengths + Copy>(&self, _temperature: N) -> UnGraph<(), N> { + fn bond_lengths + Copy>(&self, _temperature: N) -> UnGraph<(), N> { Graph::with_capacity(0, 0) } } @@ -76,7 +76,7 @@ pub trait HelmholtzEnergyFunctional: Residual { fn molecule_shape(&self) -> MoleculeShape<'_>; /// Overwrite this, if the functional consists of heterosegmented chains. - fn bond_lengths + Copy>(&self, _temperature: N) -> UnGraph<(), N> { + fn bond_lengths + Copy>(&self, _temperature: N) -> UnGraph<(), N> { Graph::with_capacity(0, 0) } @@ -112,7 +112,7 @@ pub trait HelmholtzEnergyFunctional: Residual { /// Calculate the (residual) intrinsic functional derivative $\frac{\delta\mathcal{\beta F}}{\delta\rho_i(\mathbf{r})}$. #[expect(clippy::type_complexity)] - fn functional_derivative + Copy>( + fn functional_derivative + Copy>( &self, temperature: N, density: &Array, @@ -147,7 +147,7 @@ pub trait HelmholtzEnergyFunctional: Residual { } /// Calculate the bond integrals $I_{\alpha\alpha'}(\mathbf{r})$ - fn bond_integrals + Copy>( + fn bond_integrals + Copy>( &self, temperature: N, exponential: &Array, @@ -228,7 +228,10 @@ pub trait HelmholtzEnergyFunctional: Residual { i } - fn evaluate_bulk + Copy>(&self, state: &StateHD) -> Vec<(&'static str, D)> { + fn evaluate_bulk + Copy>( + &self, + state: &StateHD, + ) -> Vec<(&'static str, D)> { let mut res: Vec<_> = self .contributions() .map(|c| (c.name(), c.bulk_helmholtz_energy_density(state))) @@ -258,7 +261,7 @@ impl + Clone, F: HelmholtzEnergyFunctionalDyn + ResidualDyn F::molecule_shape(self.deref()) } - fn bond_lengths + Copy>(&self, temperature: N) -> UnGraph<(), N> { + fn bond_lengths + Copy>(&self, temperature: N) -> UnGraph<(), N> { F::bond_lengths(self.deref(), temperature) } } diff --git a/crates/feos-dft/src/functional_contribution.rs b/crates/feos-dft/src/functional_contribution.rs index 874df0ac1..ff9e6f9a6 100644 --- a/crates/feos-dft/src/functional_contribution.rs +++ b/crates/feos-dft/src/functional_contribution.rs @@ -11,10 +11,13 @@ pub trait FunctionalContribution: Sync + Send { fn name(&self) -> &'static str; /// Return the weight functions required in this contribution. - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo; + fn weight_functions + Copy>( + &self, + temperature: N, + ) -> WeightFunctionInfo; /// Overwrite this if the weight functions in pDGT are different than for DFT. - fn weight_functions_pdgt + Copy>( + fn weight_functions_pdgt + Copy>( &self, temperature: N, ) -> WeightFunctionInfo { @@ -22,13 +25,16 @@ pub trait FunctionalContribution: Sync + Send { } /// Return the Helmholtz energy density for the given temperature and weighted densities. - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, ) -> FeosResult>; - fn bulk_helmholtz_energy_density + Copy>(&self, state: &StateHD) -> N { + fn bulk_helmholtz_energy_density + Copy>( + &self, + state: &StateHD, + ) -> N { // calculate weight functions let weight_functions = self.weight_functions(state.temperature); @@ -46,7 +52,7 @@ pub trait FunctionalContribution: Sync + Send { .unwrap()[0] } - fn first_partial_derivatives + Copy>( + fn first_partial_derivatives + Copy>( &self, temperature: N, weighted_densities: Array2, diff --git a/crates/feos-dft/src/ideal_chain_contribution.rs b/crates/feos-dft/src/ideal_chain_contribution.rs index f054bbbc3..86c9f39e9 100644 --- a/crates/feos-dft/src/ideal_chain_contribution.rs +++ b/crates/feos-dft/src/ideal_chain_contribution.rs @@ -18,7 +18,7 @@ impl IdealChainContribution { } } - pub fn bulk_helmholtz_energy_density + Copy>( + pub fn bulk_helmholtz_energy_density + Copy>( &self, partial_density: &DVector, ) -> D { @@ -43,7 +43,7 @@ impl IdealChainContribution { where D: Dimension, D::Larger: Dimension, - N: DualNum, + N: DualNum, { let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0))); for (i, rhoi) in density.outer_iter().enumerate() { diff --git a/crates/feos-dft/src/profile/mod.rs b/crates/feos-dft/src/profile/mod.rs index 6497faa23..37f5e3aa9 100644 --- a/crates/feos-dft/src/profile/mod.rs +++ b/crates/feos-dft/src/profile/mod.rs @@ -180,7 +180,7 @@ impl DFTProfile where D::Larger: Dimension, { - fn integrate_reduced + Copy>(&self, mut profile: Array) -> N { + fn integrate_reduced + Copy>(&self, mut profile: Array) -> N { let (integration_weights, functional_determinant) = self.grid.integration_weights(); for (i, w) in integration_weights.into_iter().enumerate() { @@ -191,7 +191,7 @@ where profile.sum() * functional_determinant } - fn integrate_reduced_comp, N: DualNum + Copy>( + fn integrate_reduced_comp, N: DualNum + Copy>( &self, profile: &ArrayBase, ) -> Array1 { @@ -200,7 +200,10 @@ where }) } - pub(crate) fn integrate_reduced_segments, N: DualNum + Copy>( + pub(crate) fn integrate_reduced_segments< + S: Data, + N: DualNum + Copy, + >( &self, profile: &ArrayBase, ) -> DVector { diff --git a/crates/feos-dft/src/profile/properties.rs b/crates/feos-dft/src/profile/properties.rs index 2fe6a92a9..1c0427b28 100644 --- a/crates/feos-dft/src/profile/properties.rs +++ b/crates/feos-dft/src/profile/properties.rs @@ -82,7 +82,7 @@ where convolver: &dyn Convolver, ) -> FeosResult> where - N: DualNum + Copy, + N: DualNum + Copy, { let density_dual = density.mapv(N::from); let weighted_densities = convolver.weighted_densities(&density_dual); diff --git a/crates/feos-dft/src/weight_functions.rs b/crates/feos-dft/src/weight_functions.rs index e488799a8..912b21a1f 100644 --- a/crates/feos-dft/src/weight_functions.rs +++ b/crates/feos-dft/src/weight_functions.rs @@ -16,7 +16,7 @@ pub struct WeightFunction { pub shape: WeightFunctionShape, } -impl + Copy> WeightFunction { +impl + Copy> WeightFunction { /// Create a new weight function without prefactor pub fn new_unscaled(kernel_radius: DVector, shape: WeightFunctionShape) -> Self { Self { @@ -40,7 +40,7 @@ impl + Copy> WeightFunction { /// Calculates the value of the scalar weight function depending on its shape /// `k_abs` describes the absolute value of the Fourier variable - pub(crate) fn fft_scalar_weight_functions>( + pub(crate) fn fft_scalar_weight_functions>( &self, k_abs: &Array, lanczos: &Option>, @@ -90,7 +90,7 @@ impl + Copy> WeightFunction { /// Calculates the value of the vector weight function depending on its shape /// `k_abs` describes the absolute value of the Fourier variable /// `k` describes the (potentially multi-dimensional) Fourier variable - pub(crate) fn fft_vector_weight_functions>( + pub(crate) fn fft_vector_weight_functions>( &self, k_abs: &Array, k: &Array, @@ -277,7 +277,7 @@ impl WeightFunctionInfo { } } -impl + Copy> WeightFunctionInfo { +impl + Copy> WeightFunctionInfo { /// calculates the matrix of weight constants for this set of weighted densities pub fn weight_constants(&self, k: T, dimensions: usize) -> Array2 { let segments = self.component_index.len(); diff --git a/crates/feos/benches/dual_numbers.rs b/crates/feos/benches/dual_numbers.rs index eb2eff2e4..593da2cae 100644 --- a/crates/feos/benches/dual_numbers.rs +++ b/crates/feos/benches/dual_numbers.rs @@ -26,7 +26,7 @@ fn state_pcsaft(n: usize, eos: &PcSaft) -> State<&PcSaft> { } /// Residual Helmholtz energy given an equation of state and a StateHD. -fn a_res + Copy, E: Residual>((eos, state): (&E, &StateHD)) -> D { +fn a_res + Copy, E: Residual>((eos, state): (&E, &StateHD)) -> D { eos.reduced_residual_helmholtz_energy_density(state) } diff --git a/crates/feos/benches/dual_numbers_saftvrmie.rs b/crates/feos/benches/dual_numbers_saftvrmie.rs index 80f4da09d..a92698f59 100644 --- a/crates/feos/benches/dual_numbers_saftvrmie.rs +++ b/crates/feos/benches/dual_numbers_saftvrmie.rs @@ -30,11 +30,11 @@ fn state_saftvrmie(n: usize, eos: &SaftVRMie) -> State<&SaftVRMie> { } /// Residual Helmholtz energy given an equation of state and a StateHD. -fn a_res + Copy, E: Residual>((eos, state): (&E, &StateHD)) -> D { +fn a_res + Copy, E: Residual>((eos, state): (&E, &StateHD)) -> D { eos.reduced_residual_helmholtz_energy_density(state) } -fn d_hs + Copy>(inp: (&SaftVRMie, D)) -> D { +fn d_hs + Copy>(inp: (&SaftVRMie, D)) -> D { inp.0.params.hs_diameter(inp.1)[0] } diff --git a/crates/feos/src/association/dft.rs b/crates/feos/src/association/dft.rs index 9f2b11e87..b998fa5d3 100644 --- a/crates/feos/src/association/dft.rs +++ b/crates/feos/src/association/dft.rs @@ -15,7 +15,7 @@ impl Association { /// Uses the contact value of hard-sphere pair correlation function and model-specific /// implementations for the bonding volume. #[expect(clippy::too_many_arguments)] - fn yu_wu_association_strength + Copy>( + fn yu_wu_association_strength + Copy>( &self, parameters: &AssociationParameters, model: &A, @@ -98,7 +98,7 @@ where "Association" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let p = self.model; let r = p.hs_diameter(temperature) * N::from(0.5); let [_, _, _, c3] = p.geometry_coefficients(temperature); @@ -125,7 +125,7 @@ where ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, @@ -191,7 +191,7 @@ where } impl<'a, A: AssociationStrength> YuWuAssociationFunctional<'a, A> { - pub fn _helmholtz_energy_density + Copy, S: Data>( + pub fn _helmholtz_energy_density + Copy, S: Data>( &self, temperature: N, rho0: &Array2, @@ -268,7 +268,7 @@ impl<'a, A: AssociationStrength> YuWuAssociationFunctional<'a, A> { } #[expect(clippy::too_many_arguments)] - fn helmholtz_energy_density_ab_analytic + Copy, S: Data>( + fn helmholtz_energy_density_ab_analytic + Copy, S: Data>( &self, parameters: Option<&A::Record>, temperature: N, @@ -307,7 +307,7 @@ impl<'a, A: AssociationStrength> YuWuAssociationFunctional<'a, A> { } #[expect(clippy::too_many_arguments)] - fn helmholtz_energy_density_cc_analytic + Copy, S: Data>( + fn helmholtz_energy_density_cc_analytic + Copy, S: Data>( &self, parameters: Option<&A::Record>, temperature: N, diff --git a/crates/feos/src/association/mod.rs b/crates/feos/src/association/mod.rs index 72bd97a38..7e8f7034f 100644 --- a/crates/feos/src/association/mod.rs +++ b/crates/feos/src/association/mod.rs @@ -25,7 +25,7 @@ pub trait AssociationStrength: HardSphereProperties { type Record; /// Association strength excluding the contact value of the pair correlation function. - fn association_strength_ij + Copy>( + fn association_strength_ij + Copy>( &self, temperature: D, comp_i: usize, @@ -34,7 +34,7 @@ pub trait AssociationStrength: HardSphereProperties { ) -> D; /// Association strength matrix for all association sites. - fn association_strength + Copy>( + fn association_strength + Copy>( &self, state: &StateHD, diameter: &DVector, @@ -91,7 +91,7 @@ impl Association { } #[inline] - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, model: &A, parameters: &AssociationParameters, @@ -136,7 +136,7 @@ impl Association { } } - fn helmholtz_energy_density_ab_analytic + Copy>( + fn helmholtz_energy_density_ab_analytic + Copy>( &self, parameters: &AssociationParameters, state: &StateHD, @@ -158,7 +158,7 @@ impl Association { rhoa * (xa.ln() - xa * 0.5 + 0.5) + rhob * (xb.ln() - xb * 0.5 + 0.5) } - fn helmholtz_energy_density_cc_analytic + Copy>( + fn helmholtz_energy_density_cc_analytic + Copy>( &self, parameters: &AssociationParameters, state: &StateHD, @@ -176,7 +176,7 @@ impl Association { rhoc * (xc.ln() - xc * 0.5 + 0.5) } - fn helmholtz_energy_density_cross_association + Copy>( + fn helmholtz_energy_density_cross_association + Copy>( &self, rho: &DVector, delta_ab: &DMatrix, @@ -232,7 +232,7 @@ impl Association { Ok(rho.dot(&x_dual.map(f))) } - fn newton_step_cross_association + Copy>( + fn newton_step_cross_association + Copy>( x: &mut DVector, delta_ab: &DMatrix, delta_cc: &DMatrix, diff --git a/crates/feos/src/epcsaft/eos/born.rs b/crates/feos/src/epcsaft/eos/born.rs index edd633e0e..74109c7fd 100644 --- a/crates/feos/src/epcsaft/eos/born.rs +++ b/crates/feos/src/epcsaft/eos/born.rs @@ -8,7 +8,7 @@ use num_dual::DualNum; pub struct Born; impl Born { - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &ElectrolytePcSaftPars, state: &StateHD, diff --git a/crates/feos/src/epcsaft/eos/dispersion.rs b/crates/feos/src/epcsaft/eos/dispersion.rs index 374fc0c5f..0cdb12c3c 100644 --- a/crates/feos/src/epcsaft/eos/dispersion.rs +++ b/crates/feos/src/epcsaft/eos/dispersion.rs @@ -62,7 +62,7 @@ pub const B2: [f64; 7] = [ pub const T_REF: f64 = 298.15; impl ElectrolytePcSaftPars { - pub fn k_ij_t + Copy>(&self, temperature: D) -> DMatrix { + pub fn k_ij_t + Copy>(&self, temperature: D) -> DMatrix { let n = self.m.len(); let mut k_ij_t = DMatrix::zeros(n, n); @@ -79,7 +79,7 @@ impl ElectrolytePcSaftPars { k_ij_t } - pub fn epsilon_k_ij_t + Copy>(&self, temperature: D) -> DMatrix { + pub fn epsilon_k_ij_t + Copy>(&self, temperature: D) -> DMatrix { let k_ij_t = self.k_ij_t(temperature); let n = self.m.len(); @@ -97,7 +97,7 @@ impl ElectrolytePcSaftPars { pub struct Dispersion; impl Dispersion { - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &ElectrolytePcSaftPars, state: &StateHD, diff --git a/crates/feos/src/epcsaft/eos/hard_chain.rs b/crates/feos/src/epcsaft/eos/hard_chain.rs index 534aad267..c467a740b 100644 --- a/crates/feos/src/epcsaft/eos/hard_chain.rs +++ b/crates/feos/src/epcsaft/eos/hard_chain.rs @@ -8,7 +8,7 @@ pub struct HardChain; impl HardChain { #[inline] - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &ElectrolytePcSaftPars, state: &StateHD, diff --git a/crates/feos/src/epcsaft/eos/ionic.rs b/crates/feos/src/epcsaft/eos/ionic.rs index d2577d91c..56acf6301 100644 --- a/crates/feos/src/epcsaft/eos/ionic.rs +++ b/crates/feos/src/epcsaft/eos/ionic.rs @@ -12,7 +12,7 @@ const QE: f64 = 1.602176634e-19f64; const BOLTZMANN: f64 = 1.380649e-23; impl ElectrolytePcSaftPars { - pub fn bjerrum_length + Copy>( + pub fn bjerrum_length + Copy>( &self, state: &StateHD, epcsaft_variant: ElectrolytePcSaftVariants, @@ -36,7 +36,7 @@ impl ElectrolytePcSaftPars { pub struct Ionic; impl Ionic { - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &ElectrolytePcSaftPars, state: &StateHD, diff --git a/crates/feos/src/epcsaft/eos/mod.rs b/crates/feos/src/epcsaft/eos/mod.rs index 5a0a8c81f..dec36181f 100644 --- a/crates/feos/src/epcsaft/eos/mod.rs +++ b/crates/feos/src/epcsaft/eos/mod.rs @@ -97,7 +97,7 @@ impl ResidualDyn for ElectrolytePcSaft { self.parameters.pure.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { let msigma3 = self .params .m @@ -105,7 +105,7 @@ impl ResidualDyn for ElectrolytePcSaft { (msigma3.map(D::from).dot(molefracs) * FRAC_PI_6).recip() * self.options.max_eta } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { diff --git a/crates/feos/src/epcsaft/eos/permittivity.rs b/crates/feos/src/epcsaft/eos/permittivity.rs index abbc0ac90..0202f1957 100644 --- a/crates/feos/src/epcsaft/eos/permittivity.rs +++ b/crates/feos/src/epcsaft/eos/permittivity.rs @@ -20,11 +20,11 @@ pub enum PermittivityRecord { } #[derive(Clone)] -pub struct Permittivity> { +pub struct Permittivity> { pub permittivity: D, } -impl + Copy> Permittivity { +impl + Copy> Permittivity { pub fn new( state: &StateHD, parameters: &ElectrolytePcSaftPars, diff --git a/crates/feos/src/epcsaft/parameters.rs b/crates/feos/src/epcsaft/parameters.rs index 364de81e1..20d121d63 100644 --- a/crates/feos/src/epcsaft/parameters.rs +++ b/crates/feos/src/epcsaft/parameters.rs @@ -160,7 +160,7 @@ pub struct ElectrolytePcSaftPars { } impl ElectrolytePcSaftPars { - pub fn sigma_t + Copy>(&self, temperature: D) -> DVector { + pub fn sigma_t + Copy>(&self, temperature: D) -> DVector { let mut sigma_t = DVector::from_fn(self.sigma.len(), |i, _| D::from(self.sigma[i])); if let Some(i) = self.water_sigma_t_comp { @@ -171,7 +171,7 @@ impl ElectrolytePcSaftPars { sigma_t } - pub fn sigma_ij_t + Copy>(&self, temperature: D) -> DMatrix { + pub fn sigma_ij_t + Copy>(&self, temperature: D) -> DMatrix { let diameter = self.sigma_t(temperature); let n = diameter.len(); @@ -357,11 +357,11 @@ impl ElectrolytePcSaftPars { } impl HardSphereProperties for ElectrolytePcSaftPars { - fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { + fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { MonomerShape::NonSpherical(self.m.map(N::from)) } - fn hs_diameter + Copy>(&self, temperature: D) -> DVector { + fn hs_diameter + Copy>(&self, temperature: D) -> DVector { let sigma_t = self.sigma_t(temperature); let ti = temperature.recip() * -3.0; @@ -379,7 +379,7 @@ impl HardSphereProperties for ElectrolytePcSaftPars { impl AssociationStrength for ElectrolytePcSaftPars { type Record = ElectrolytePcSaftAssociationRecord; - fn association_strength_ij + Copy>( + fn association_strength_ij + Copy>( &self, temperature: D, comp_i: usize, diff --git a/crates/feos/src/gc_pcsaft/dft/dispersion.rs b/crates/feos/src/gc_pcsaft/dft/dispersion.rs index 25338e33c..d0bd82ac0 100644 --- a/crates/feos/src/gc_pcsaft/dft/dispersion.rs +++ b/crates/feos/src/gc_pcsaft/dft/dispersion.rs @@ -23,7 +23,7 @@ impl<'a> FunctionalContribution for AttractiveFunctional<'a> { "Attractive functional (GC)" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let p = &self.parameters; let d = p.hs_diameter(temperature); @@ -36,7 +36,7 @@ impl<'a> FunctionalContribution for AttractiveFunctional<'a> { ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, density: ArrayView2, diff --git a/crates/feos/src/gc_pcsaft/dft/hard_chain.rs b/crates/feos/src/gc_pcsaft/dft/hard_chain.rs index ddf0951a4..ccc33055c 100644 --- a/crates/feos/src/gc_pcsaft/dft/hard_chain.rs +++ b/crates/feos/src/gc_pcsaft/dft/hard_chain.rs @@ -22,7 +22,7 @@ impl<'a> FunctionalContribution for ChainFunctional<'a> { "Hard chain functional (GC)" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let p = &self.parameters; let d = p.hs_diameter(temperature); WeightFunctionInfo::new(p.component_index.clone(), true) @@ -44,7 +44,7 @@ impl<'a> FunctionalContribution for ChainFunctional<'a> { ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, diff --git a/crates/feos/src/gc_pcsaft/dft/mod.rs b/crates/feos/src/gc_pcsaft/dft/mod.rs index 18ba4c793..e30b8014a 100644 --- a/crates/feos/src/gc_pcsaft/dft/mod.rs +++ b/crates/feos/src/gc_pcsaft/dft/mod.rs @@ -78,7 +78,7 @@ impl ResidualDyn for GcPcSaftFunctional { self.parameters.molar_weight.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { let p = &self.params; let msigma3 = p.m.zip_map(&p.sigma, |m, s| m * s.powi(3) * m); let molefracs_segments: DVector<_> = p @@ -90,7 +90,7 @@ impl ResidualDyn for GcPcSaftFunctional { (msigma3.map(D::from).dot(&molefracs_segments) * FRAC_PI_6).recip() * self.options.max_eta } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { @@ -131,7 +131,7 @@ impl HelmholtzEnergyFunctionalDyn for GcPcSaftFunctional { contributions.into_iter() } - fn bond_lengths + Copy>(&self, temperature: N) -> UnGraph<(), N> { + fn bond_lengths + Copy>(&self, temperature: N) -> UnGraph<(), N> { // temperature dependent segment diameter let d = self.params.hs_diameter(temperature); @@ -154,12 +154,12 @@ impl Molarweight for GcPcSaftFunctional { } impl HardSphereProperties for GcPcSaftFunctionalParameters { - fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { + fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { let m = self.m.map(N::from); MonomerShape::Heterosegmented([m.clone(), m.clone(), m.clone(), m], &self.component_index) } - fn hs_diameter + Copy>(&self, temperature: D) -> DVector { + fn hs_diameter + Copy>(&self, temperature: D) -> DVector { let ti = temperature.recip() * -3.0; DVector::from_fn(self.sigma.len(), |i, _| { -((ti * self.epsilon_k[i]).exp() * 0.12 - 1.0) * self.sigma[i] @@ -170,7 +170,7 @@ impl HardSphereProperties for GcPcSaftFunctionalParameters { impl AssociationStrength for GcPcSaftFunctionalParameters { type Record = GcPcSaftAssociationRecord; - fn association_strength_ij + Copy>( + fn association_strength_ij + Copy>( &self, temperature: D, comp_i: usize, diff --git a/crates/feos/src/gc_pcsaft/eos/ad.rs b/crates/feos/src/gc_pcsaft/eos/ad.rs index 72b251eaa..c7f5d1d41 100644 --- a/crates/feos/src/gc_pcsaft/eos/ad.rs +++ b/crates/feos/src/gc_pcsaft/eos/ad.rs @@ -184,7 +184,7 @@ pub struct GcPcSaftADParameters { pub bonds: [Vec<([usize; 2], D)>; N], } -impl + Copy, const N: usize> GcPcSaftADParameters { +impl + Copy, const N: usize> GcPcSaftADParameters { pub fn re(&self) -> GcPcSaftADParameters { let Self { groups, bonds } = self; let groups = groups.map(|g| g.re()); @@ -195,7 +195,7 @@ impl + Copy, const N: usize> GcPcSaftADParameters { } } -impl + Copy, const N: usize> GcPcSaftADParameters { +impl + Copy, const N: usize> GcPcSaftADParameters { pub fn from_groups( group_map: [&HashMap<&'static str, D>; N], bond_map: [&HashMap<[&'static str; 2], D>; N], @@ -220,17 +220,19 @@ impl + Copy, const N: usize> GcPcSaftADParameters { #[derive(Clone)] pub struct GcPcSaftAD(pub GcPcSaftADParameters); -impl + Copy, const N: usize> Residual, D> for GcPcSaftAD { +impl + Copy, const N: usize> Residual, D> + for GcPcSaftAD +{ fn components(&self) -> usize { N } type Real = GcPcSaftAD; - type Lifted + Copy> = GcPcSaftAD; + type Lifted + Copy> = GcPcSaftAD; fn re(&self) -> Self::Real { GcPcSaftAD(self.0.re()) } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { let GcPcSaftADParameters { groups, bonds } = &self.0; let groups = groups.map(|x| D2::from_inner(&x)); let bonds = bonds @@ -416,7 +418,7 @@ impl + Copy, const N: usize> Residual, D> for GcPcSaftA } } -fn apply_group_count + Copy, const N: usize>( +fn apply_group_count + Copy, const N: usize>( groups: &SMatrix, x: &SVector, ) -> SMatrix { @@ -426,7 +428,7 @@ fn apply_group_count + Copy, const N: usize>( ms } -fn pair_integral + Copy>(mij1: D, mij2: D, eta: D, eps_ij_t: D) -> D { +fn pair_integral + Copy>(mij1: D, mij2: D, eta: D, eps_ij_t: D) -> D { let mut eta_i = D::one(); let mut j = D::zero(); for (ad, bd) in AD.into_iter().zip(BD) { @@ -438,7 +440,7 @@ fn pair_integral + Copy>(mij1: D, mij2: D, eta: D, eps_ij_t: D) j } -fn triplet_integral + Copy>(mij1: D, mij2: D, eta: D) -> D { +fn triplet_integral + Copy>(mij1: D, mij2: D, eta: D) -> D { let mut eta_i = D::one(); let mut j = D::zero(); for cd in CD { diff --git a/crates/feos/src/gc_pcsaft/eos/dispersion.rs b/crates/feos/src/gc_pcsaft/eos/dispersion.rs index 3f464507d..f9870624b 100644 --- a/crates/feos/src/gc_pcsaft/eos/dispersion.rs +++ b/crates/feos/src/gc_pcsaft/eos/dispersion.rs @@ -62,7 +62,7 @@ pub const B2: [f64; 7] = [ pub(super) struct Dispersion; impl Dispersion { - pub(super) fn helmholtz_energy_density + Copy>( + pub(super) fn helmholtz_energy_density + Copy>( &self, parameters: &GcPcSaftEosParameters, state: &StateHD, diff --git a/crates/feos/src/gc_pcsaft/eos/hard_chain.rs b/crates/feos/src/gc_pcsaft/eos/hard_chain.rs index f67dffda4..e9fec009b 100644 --- a/crates/feos/src/gc_pcsaft/eos/hard_chain.rs +++ b/crates/feos/src/gc_pcsaft/eos/hard_chain.rs @@ -6,7 +6,7 @@ use num_dual::*; pub(super) struct HardChain; impl HardChain { - pub(super) fn helmholtz_energy_density + Copy>( + pub(super) fn helmholtz_energy_density + Copy>( &self, parameters: &GcPcSaftEosParameters, state: &StateHD, diff --git a/crates/feos/src/gc_pcsaft/eos/mod.rs b/crates/feos/src/gc_pcsaft/eos/mod.rs index ae6acf960..3612164db 100644 --- a/crates/feos/src/gc_pcsaft/eos/mod.rs +++ b/crates/feos/src/gc_pcsaft/eos/mod.rs @@ -77,7 +77,7 @@ impl ResidualDyn for GcPcSaft { self.parameters.molar_weight.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { let p = &self.params; let molefracs_segments = DVector::from(p.component_index.clone()).map(|i| molefracs[i]); (p.m.component_mul(&p.sigma.map(|v| v.powi(3))) @@ -88,7 +88,7 @@ impl ResidualDyn for GcPcSaft { * self.options.max_eta } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &feos_core::StateHD, ) -> Vec<(&'static str, D)> { diff --git a/crates/feos/src/gc_pcsaft/eos/parameter.rs b/crates/feos/src/gc_pcsaft/eos/parameter.rs index ebf1ce5ba..b69dad0b8 100644 --- a/crates/feos/src/gc_pcsaft/eos/parameter.rs +++ b/crates/feos/src/gc_pcsaft/eos/parameter.rs @@ -110,12 +110,12 @@ impl GcPcSaftEosParameters { } impl HardSphereProperties for GcPcSaftEosParameters { - fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { + fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { let m = self.m.map(N::from); MonomerShape::Heterosegmented([m.clone(), m.clone(), m.clone(), m], &self.component_index) } - fn hs_diameter + Copy>(&self, temperature: D) -> DVector { + fn hs_diameter + Copy>(&self, temperature: D) -> DVector { let ti = temperature.recip() * -3.0; DVector::from_fn(self.sigma.len(), |i, _| { -((ti * self.epsilon_k[i]).exp() * 0.12 - 1.0) * self.sigma[i] @@ -126,7 +126,7 @@ impl HardSphereProperties for GcPcSaftEosParameters { impl AssociationStrength for GcPcSaftEosParameters { type Record = GcPcSaftAssociationRecord; - fn association_strength_ij + Copy>( + fn association_strength_ij + Copy>( &self, temperature: D, comp_i: usize, diff --git a/crates/feos/src/gc_pcsaft/eos/polar.rs b/crates/feos/src/gc_pcsaft/eos/polar.rs index 33073a77b..5f92afdca 100644 --- a/crates/feos/src/gc_pcsaft/eos/polar.rs +++ b/crates/feos/src/gc_pcsaft/eos/polar.rs @@ -31,7 +31,7 @@ pub const CD: [[f64; 3]; 4] = [ pub const PI_SQ_43: f64 = 4.0 * PI * FRAC_PI_3; -fn pair_integral_ij + Copy>(mij1: f64, mij2: f64, eta: D, eps_ij_t: D) -> D { +fn pair_integral_ij + Copy>(mij1: f64, mij2: f64, eta: D, eps_ij_t: D) -> D { let eta2 = eta * eta; let etas = [D::one(), eta, eta2, eta2 * eta, eta2 * eta2]; (0..AD.len()) @@ -43,7 +43,7 @@ fn pair_integral_ij + Copy>(mij1: f64, mij2: f64, eta: D, eps_ij .sum() } -fn triplet_integral_ijk + Copy>(mijk1: f64, mijk2: f64, eta: D) -> D { +fn triplet_integral_ijk + Copy>(mijk1: f64, mijk2: f64, eta: D) -> D { let eta2 = eta * eta; let etas = [D::one(), eta, eta2, eta2 * eta]; (0..CD.len()) @@ -121,7 +121,7 @@ impl Dipole { } impl Dipole { - pub(super) fn helmholtz_energy_density + Copy>( + pub(super) fn helmholtz_energy_density + Copy>( &self, parameters: &GcPcSaftEosParameters, state: &StateHD, diff --git a/crates/feos/src/hard_sphere/dft.rs b/crates/feos/src/hard_sphere/dft.rs index d48d6327a..9c654e01b 100644 --- a/crates/feos/src/hard_sphere/dft.rs +++ b/crates/feos/src/hard_sphere/dft.rs @@ -83,7 +83,7 @@ impl<'p, P: HardSphereProperties + Send + Sync> FunctionalContribution for FMTCo } } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let r = self.properties.hs_diameter(temperature) * N::from(0.5); let [c0, c1, c2, c3] = self.properties.geometry_coefficients(temperature); match (self.version, r.len()) { @@ -183,7 +183,7 @@ impl<'p, P: HardSphereProperties + Send + Sync> FunctionalContribution for FMTCo } } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, @@ -285,7 +285,7 @@ impl HardSphereProperties for HardSphereParameters { MonomerShape::Spherical(self.sigma.len()) } - fn hs_diameter>(&self, _: N) -> DVector { + fn hs_diameter>(&self, _: N) -> DVector { self.sigma.map(N::from) } } @@ -323,11 +323,11 @@ impl ResidualDyn for FMTFunctional { self.properties.sigma.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { molefracs.dot(&self.properties.sigma.map(D::from)).recip() * 1.2 } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { diff --git a/crates/feos/src/hard_sphere/mod.rs b/crates/feos/src/hard_sphere/mod.rs index 40d4936b4..a4a842768 100644 --- a/crates/feos/src/hard_sphere/mod.rs +++ b/crates/feos/src/hard_sphere/mod.rs @@ -27,10 +27,10 @@ pub enum MonomerShape<'a, D> { /// Properties of (generalized) hard sphere systems. pub trait HardSphereProperties { /// The [MonomerShape] used in the model. - fn monomer_shape + Copy>(&self, temperature: D) -> MonomerShape<'_, D>; + fn monomer_shape + Copy>(&self, temperature: D) -> MonomerShape<'_, D>; /// The temperature dependent hard-sphere diameters of every segment. - fn hs_diameter + Copy>(&self, temperature: D) -> DVector; + fn hs_diameter + Copy>(&self, temperature: D) -> DVector; /// For every segment, the index of the component that it is on. fn component_index(&self) -> Cow<'_, [usize]> { @@ -44,7 +44,7 @@ pub trait HardSphereProperties { } /// The geometry coefficients $C_{k,\alpha}$ for every segment. - fn geometry_coefficients + Copy>(&self, temperature: D) -> [DVector; 4] { + fn geometry_coefficients + Copy>(&self, temperature: D) -> [DVector; 4] { match self.monomer_shape(temperature) { MonomerShape::Spherical(n) => { let m = DVector::from_element(n, D::from(1.0)); @@ -56,7 +56,7 @@ pub trait HardSphereProperties { } /// The packing fractions $\zeta_k$. - fn zeta + Copy, const N: usize>( + fn zeta + Copy, const N: usize>( &self, temperature: D, partial_density: &DVector, @@ -94,7 +94,7 @@ impl HardSphere { /// Returns the Helmholtz energy, packing fractions, and temperature dependent diameters without redundant calculations. #[inline] pub fn helmholtz_energy_density_and_properties< - D: DualNum + Copy, + D: DualNum + Copy, P: HardSphereProperties, >( &self, @@ -126,7 +126,7 @@ impl HardSphere { } #[inline] - pub fn helmholtz_energy_density + Copy, P: HardSphereProperties>( + pub fn helmholtz_energy_density + Copy, P: HardSphereProperties>( &self, parameters: &P, state: &StateHD, diff --git a/crates/feos/src/ideal_gas/dippr.rs b/crates/feos/src/ideal_gas/dippr.rs index f976fbe11..7dce54223 100644 --- a/crates/feos/src/ideal_gas/dippr.rs +++ b/crates/feos/src/ideal_gas/dippr.rs @@ -55,7 +55,7 @@ impl Dippr { } } - fn c_p_integral + Copy>(&self, t: D) -> D { + fn c_p_integral + Copy>(&self, t: D) -> D { match self { Self::DIPPR100(coefs) => coefs .iter() @@ -79,7 +79,7 @@ impl Dippr { } } - fn c_p_t_integral + Copy>(&self, t: D) -> D { + fn c_p_t_integral + Copy>(&self, t: D) -> D { match self { Self::DIPPR100(coefs) => { coefs @@ -140,7 +140,7 @@ const RGAS: f64 = 8.31446261815324 * 1000.0; const T0: f64 = 298.15; impl IdealGas for Dippr { - fn ln_lambda3 + Copy>(&self, temperature: D) -> D { + fn ln_lambda3 + Copy>(&self, temperature: D) -> D { let t = temperature; let h = self.c_p_integral(t) - self.c_p_integral(T0); let s = self.c_p_t_integral(t) - self.c_p_t_integral(T0); @@ -152,16 +152,16 @@ impl IdealGas for Dippr { } } -impl + Copy> IdealGasAD for Dippr { +impl + Copy> IdealGasAD for Dippr { type Real = Self; - type Lifted + Copy> = Self; + type Lifted + Copy> = Self; fn re(&self) -> Self::Real { self.clone() } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { self.clone() } diff --git a/crates/feos/src/ideal_gas/joback.rs b/crates/feos/src/ideal_gas/joback.rs index e30c630b2..85a79097a 100644 --- a/crates/feos/src/ideal_gas/joback.rs +++ b/crates/feos/src/ideal_gas/joback.rs @@ -97,8 +97,11 @@ impl Joback { } } -impl + Copy> Joback { - fn ln_lambda3 + Copy + Mul>(&self, temperature: D2) -> D2 { +impl + Copy> Joback { + fn ln_lambda3 + Copy + Mul>( + &self, + temperature: D2, + ) -> D2 { let [a, b, c, d, e] = self.0; let t = temperature; let t2 = t * t; @@ -119,7 +122,7 @@ impl + Copy> Joback { } impl IdealGas for Joback { - fn ln_lambda3 + Copy>(&self, temperature: D) -> D { + fn ln_lambda3 + Copy>(&self, temperature: D) -> D { self.ln_lambda3(temperature) } @@ -128,13 +131,13 @@ impl IdealGas for Joback { } } -impl + Copy> IdealGasAD for Joback { +impl + Copy> IdealGasAD for Joback { type Real = Joback; - type Lifted + Copy> = Joback; + type Lifted + Copy> = Joback; fn re(&self) -> Self::Real { Joback(self.0.each_ref().map(D::re)) } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { Joback(self.0.each_ref().map(D2::from_inner)) } @@ -170,7 +173,7 @@ const GROUPS: [&str; 22] = [ "CH_pent", "CH_arom", "C_arom", "CH=O", ">C=O", "OCH3", "OCH2", "HCOO", "COO", "OH", "NH2", ]; -impl + Copy> Joback { +impl + Copy> Joback { pub fn from_groups(group_counts: [D; 22]) -> Self { let a: D = A.into_iter().zip(group_counts).map(|(a, g)| g * a).sum(); let b: D = B.into_iter().zip(group_counts).map(|(b, g)| g * b).sum(); @@ -361,20 +364,20 @@ pub mod test_ad { #[derive(Clone, Copy)] struct NoResidual; - impl + Copy> Residual for NoResidual { + impl + Copy> Residual for NoResidual { fn components(&self) -> usize { 1 } type Real = Self; - type Lifted + Copy> = Self; + type Lifted + Copy> = Self; fn re(&self) -> Self::Real { *self } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { *self } diff --git a/crates/feos/src/multiparameter/ideal_gas_function.rs b/crates/feos/src/multiparameter/ideal_gas_function.rs index fbd7571bb..8ceeb19c3 100644 --- a/crates/feos/src/multiparameter/ideal_gas_function.rs +++ b/crates/feos/src/multiparameter/ideal_gas_function.rs @@ -86,7 +86,7 @@ pub enum IdealGasFunction { } impl IdealGasFunction { - pub fn evaluate + Copy>(&self, delta: D, tau: D) -> D { + pub fn evaluate + Copy>(&self, delta: D, tau: D) -> D { match *self { IdealGasFunction::IdealGasHelmholtzLead { a1, a2 } => delta.ln() + a1 + tau * a2, IdealGasFunction::IdealGasHelmholtzLogTau { a } => tau.ln() * a, diff --git a/crates/feos/src/multiparameter/mod.rs b/crates/feos/src/multiparameter/mod.rs index fe30093ac..88290769a 100644 --- a/crates/feos/src/multiparameter/mod.rs +++ b/crates/feos/src/multiparameter/mod.rs @@ -83,12 +83,12 @@ impl ResidualDyn for MultiParameter { 1 } - fn compute_max_density + Copy>(&self, _: &DVector) -> D { + fn compute_max_density + Copy>(&self, _: &DVector) -> D { // Not sure what value works well here. This one is based on rho_c = 0.31*rho_max. D::from(6.02214076e-7 * self.rhoc / 0.31) } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { @@ -118,7 +118,7 @@ impl Subset for MultiParameter { } impl IdealGas for MultiParameterIdealGas { - fn ln_lambda3 + Copy>(&self, temperature: D) -> D { + fn ln_lambda3 + Copy>(&self, temperature: D) -> D { let tau = temperature.recip() * self.tc; // bit of a hack to convert from phi^0 into ln Lambda^3 let delta = D::from(E / (6.02214076e-7 * self.rhoc)); @@ -165,7 +165,7 @@ mod test { eos.terms .iter() .map(|f| f.evaluate(delta, tau)) - .sum::>() + .sum::>() }, &SVector::from([delta, tau]), ); @@ -191,7 +191,7 @@ mod test { eos.terms .iter() .map(|f| f.evaluate(delta, tau)) - .sum::>() + .sum::>() }, &SVector::from([delta, tau]), ); @@ -218,7 +218,7 @@ mod test { .terms .iter() .map(|r| r.evaluate(delta, tau)) - .sum::>() + .sum::>() }, &SVector::from([delta, tau]), ); @@ -245,7 +245,7 @@ mod test { .terms .iter() .map(|r| r.evaluate(delta, tau)) - .sum::>() + .sum::>() }, &SVector::from([delta, tau]), ); diff --git a/crates/feos/src/multiparameter/residual_function.rs b/crates/feos/src/multiparameter/residual_function.rs index d39d71062..3116c1db2 100644 --- a/crates/feos/src/multiparameter/residual_function.rs +++ b/crates/feos/src/multiparameter/residual_function.rs @@ -118,7 +118,7 @@ pub enum ResidualFunction { } impl ResidualFunction { - pub fn evaluate + Copy>(&self, delta: D, tau: D) -> D { + pub fn evaluate + Copy>(&self, delta: D, tau: D) -> D { match *self { ResidualFunction::ResidualHelmholtzPower { d, l, n, t } => { let mut pre = delta.powi(d) * tau.powf(t) * n; diff --git a/crates/feos/src/pcsaft/dft/dispersion.rs b/crates/feos/src/pcsaft/dft/dispersion.rs index ec8633599..d682308da 100644 --- a/crates/feos/src/pcsaft/dft/dispersion.rs +++ b/crates/feos/src/pcsaft/dft/dispersion.rs @@ -25,7 +25,7 @@ impl<'a> AttractiveFunctional<'a> { } } -fn att_weight_functions + Copy>( +fn att_weight_functions + Copy>( p: &PcSaftPars, psi: f64, temperature: N, @@ -43,18 +43,18 @@ impl<'a> FunctionalContribution for AttractiveFunctional<'a> { "Attractive functional" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { att_weight_functions(self.parameters, PSI_DFT, temperature) } - fn weight_functions_pdgt + Copy>( + fn weight_functions_pdgt + Copy>( &self, temperature: N, ) -> WeightFunctionInfo { att_weight_functions(self.parameters, PSI_PDGT, temperature) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, density: ArrayView2, diff --git a/crates/feos/src/pcsaft/dft/hard_chain.rs b/crates/feos/src/pcsaft/dft/hard_chain.rs index e179d4fcf..e52a48959 100644 --- a/crates/feos/src/pcsaft/dft/hard_chain.rs +++ b/crates/feos/src/pcsaft/dft/hard_chain.rs @@ -22,7 +22,7 @@ impl<'a> FunctionalContribution for ChainFunctional<'a> { "Hard chain functional" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let p = &self.parameters; let d = p.hs_diameter(temperature); WeightFunctionInfo::new(DVector::from_fn(d.len(), |i, _| i), true) @@ -48,7 +48,7 @@ impl<'a> FunctionalContribution for ChainFunctional<'a> { ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, diff --git a/crates/feos/src/pcsaft/dft/mod.rs b/crates/feos/src/pcsaft/dft/mod.rs index 52c16aa4a..4ca78d2ef 100644 --- a/crates/feos/src/pcsaft/dft/mod.rs +++ b/crates/feos/src/pcsaft/dft/mod.rs @@ -80,13 +80,13 @@ impl ResidualDyn for PcSaftFunctional { self.parameters.pure.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { let p = &self.params; let msigma3 = p.m.zip_map(&p.sigma, |m, s| m * s.powi(3)); (msigma3.map(D::from).dot(molefracs) * FRAC_PI_6).recip() * self.options.max_eta } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { diff --git a/crates/feos/src/pcsaft/dft/polar.rs b/crates/feos/src/pcsaft/dft/polar.rs index 58256e161..43cf2395d 100644 --- a/crates/feos/src/pcsaft/dft/polar.rs +++ b/crates/feos/src/pcsaft/dft/polar.rs @@ -8,7 +8,7 @@ use ndarray::*; use num_dual::DualNum; use std::f64::consts::{FRAC_PI_3, PI}; -pub(super) fn helmholtz_energy_density_polar + Copy>( +pub(super) fn helmholtz_energy_density_polar + Copy>( parameters: &PcSaftPars, temperature: N, density: ArrayView2, @@ -36,7 +36,7 @@ pub(super) fn helmholtz_energy_density_polar + Copy>( Ok(phi) } -pub fn pair_integral_ij + Copy>( +pub fn pair_integral_ij + Copy>( mij1: f64, mij2: f64, eta: &Array1, @@ -63,7 +63,7 @@ pub fn pair_integral_ij + Copy>( integral } -pub fn triplet_integral_ijk>( +pub fn triplet_integral_ijk>( mijk1: f64, mijk2: f64, eta: &Array1, @@ -78,7 +78,7 @@ pub fn triplet_integral_ijk>( integral } -fn triplet_integral_ijk_dq>( +fn triplet_integral_ijk_dq>( mijk: f64, eta: &Array1, c: &[[f64; 2]], @@ -91,7 +91,7 @@ fn triplet_integral_ijk_dq>( integral } -fn phi_polar_dipole + Copy>( +fn phi_polar_dipole + Copy>( p: &PcSaftPars, temperature: N, density: ArrayView2, @@ -179,7 +179,7 @@ fn phi_polar_dipole + Copy>( Ok(result) } -fn phi_polar_quadrupole + Copy>( +fn phi_polar_quadrupole + Copy>( p: &PcSaftPars, temperature: N, density: ArrayView2, @@ -267,7 +267,7 @@ fn phi_polar_quadrupole + Copy>( Ok(result) } -fn phi_polar_dipole_quadrupole + Copy>( +fn phi_polar_dipole_quadrupole + Copy>( p: &PcSaftPars, temperature: N, density: ArrayView2, diff --git a/crates/feos/src/pcsaft/dft/pure_saft_functional.rs b/crates/feos/src/pcsaft/dft/pure_saft_functional.rs index 05c26d0e4..a33e02b97 100644 --- a/crates/feos/src/pcsaft/dft/pure_saft_functional.rs +++ b/crates/feos/src/pcsaft/dft/pure_saft_functional.rs @@ -40,7 +40,7 @@ impl<'a> FunctionalContribution for PureFMTAssocFunctional<'a> { "Pure FMT+association" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let r = self.parameters.hs_diameter(temperature) * N::from(0.5); WeightFunctionInfo::new(dvector![0], false).extend( vec![ @@ -59,7 +59,7 @@ impl<'a> FunctionalContribution for PureFMTAssocFunctional<'a> { ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, @@ -150,7 +150,7 @@ impl<'a> FunctionalContribution for PureChainFunctional<'a> { "Pure chain" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let d = self.parameters.hs_diameter(temperature); WeightFunctionInfo::new(dvector![0], true) .add( @@ -167,7 +167,7 @@ impl<'a> FunctionalContribution for PureChainFunctional<'a> { ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, _: N, weighted_densities: ArrayView2, @@ -200,7 +200,7 @@ impl<'a> FunctionalContribution for PureAttFunctional<'a> { "Pure attractive" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let d = self.parameters.hs_diameter(temperature); const PSI: f64 = 1.3862; // Homosegmented DFT (Sauer2017) let psi = N::from(PSI); @@ -210,7 +210,7 @@ impl<'a> FunctionalContribution for PureAttFunctional<'a> { ) } - fn weight_functions_pdgt + Copy>( + fn weight_functions_pdgt + Copy>( &self, temperature: N, ) -> WeightFunctionInfo { @@ -223,7 +223,7 @@ impl<'a> FunctionalContribution for PureAttFunctional<'a> { ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, diff --git a/crates/feos/src/pcsaft/eos/dispersion.rs b/crates/feos/src/pcsaft/eos/dispersion.rs index 50fb3c1fc..cf75246c0 100644 --- a/crates/feos/src/pcsaft/eos/dispersion.rs +++ b/crates/feos/src/pcsaft/eos/dispersion.rs @@ -62,7 +62,7 @@ pub const B2: [f64; 7] = [ pub struct Dispersion; impl Dispersion { - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &PcSaftPars, state: &StateHD, diff --git a/crates/feos/src/pcsaft/eos/hard_chain.rs b/crates/feos/src/pcsaft/eos/hard_chain.rs index e894f695a..9f425edb2 100644 --- a/crates/feos/src/pcsaft/eos/hard_chain.rs +++ b/crates/feos/src/pcsaft/eos/hard_chain.rs @@ -7,7 +7,7 @@ pub struct HardChain; impl HardChain { #[inline] - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &PcSaftPars, state: &StateHD, diff --git a/crates/feos/src/pcsaft/eos/mod.rs b/crates/feos/src/pcsaft/eos/mod.rs index aa8e05d0c..2973451be 100644 --- a/crates/feos/src/pcsaft/eos/mod.rs +++ b/crates/feos/src/pcsaft/eos/mod.rs @@ -88,7 +88,7 @@ impl ResidualDyn for PcSaft { self.parameters.pure.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { let msigma3 = self .params .m @@ -96,7 +96,7 @@ impl ResidualDyn for PcSaft { (msigma3.map(D::from).dot(molefracs) * FRAC_PI_6).recip() * self.options.max_eta } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { @@ -167,11 +167,11 @@ impl Molarweight for PcSaft { } impl HardSphereProperties for PcSaftPars { - fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { + fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { MonomerShape::NonSpherical(self.m.map(N::from)) } - fn hs_diameter + Copy>(&self, temperature: D) -> DVector { + fn hs_diameter + Copy>(&self, temperature: D) -> DVector { let ti = temperature.recip() * -3.0; DVector::from_fn(self.sigma.len(), |i, _| { -((ti * self.epsilon_k[i]).exp() * 0.12 - 1.0) * self.sigma[i] @@ -182,7 +182,7 @@ impl HardSphereProperties for PcSaftPars { impl AssociationStrength for PcSaftPars { type Record = PcSaftAssociationRecord; - fn association_strength_ij + Copy>( + fn association_strength_ij + Copy>( &self, temperature: D, comp_i: usize, diff --git a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs index 160b3bd27..9d2bfba2f 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_binary.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_binary.rs @@ -20,7 +20,7 @@ impl PcSaftBinary { } impl ParametersAD for PcSaftBinary { - fn build + Copy>( + fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D, ) -> PcSaftBinary { PcSaftBinary::new( @@ -44,7 +44,7 @@ impl ParametersAD for PcSaftBinary { } impl ParametersAD for PcSaftBinary { - fn build + Copy>( + fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D, ) -> PcSaftBinary { PcSaftBinary::new( @@ -75,7 +75,7 @@ impl ParametersAD for PcSaftBinary { } } -fn hard_sphere + Copy>( +fn hard_sphere + Copy>( [m1, m2]: [D; 2], [x1, x2]: [D; 2], [d1, d2]: [D; 2], @@ -109,7 +109,7 @@ fn hard_sphere + Copy>( (hs, etas, zeta2, zeta3, frac_1mz3) } -fn hard_chain + Copy>( +fn hard_chain + Copy>( [m1, m2]: [D; 2], [d1, d2]: [D; 2], [rho1, rho2]: [D; 2], @@ -123,7 +123,7 @@ fn hard_chain + Copy>( } #[expect(clippy::too_many_arguments)] -fn dispersion + Copy>( +fn dispersion + Copy>( [m1, m2]: [D; 2], [sigma1, sigma2]: [D; 2], [epsilon_k1, epsilon_k2]: [D; 2], @@ -174,7 +174,7 @@ fn dispersion + Copy>( } #[expect(clippy::too_many_arguments)] -fn dipoles + Copy>( +fn dipoles + Copy>( [m1, m2]: [D; 2], [sigma1, sigma2]: [D; 2], [sigma11_3, sigma12_3, sigma22_3]: [D; 3], @@ -241,7 +241,7 @@ fn dipoles + Copy>( polar } -fn association + Copy>( +fn association + Copy>( assoc_params: [[D; 4]; 2], [sigma11_3, _, sigma22_3]: [D; 3], t_inv: D, @@ -328,7 +328,7 @@ fn association + Copy>( } #[expect(clippy::too_many_arguments)] -fn helmholtz_energy_density + Copy>( +fn helmholtz_energy_density + Copy>( temperature: D, rho: [D; 2], m: [D; 2], @@ -373,20 +373,20 @@ fn helmholtz_energy_density + Copy>( } } -impl + Copy> Residual for PcSaftBinary { +impl + Copy> Residual for PcSaftBinary { fn components(&self) -> usize { 2 } type Real = PcSaftBinary; - type Lifted + Copy> = PcSaftBinary; + type Lifted + Copy> = PcSaftBinary; fn re(&self) -> Self::Real { PcSaftBinary(( self.0.0.each_ref().map(|x| x.each_ref().map(D::re)), self.0.1.re(), )) } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { PcSaftBinary(( self.0 .0 @@ -430,20 +430,20 @@ impl + Copy> Residual for PcSaftBinary { } } -impl + Copy> Residual for PcSaftBinary { +impl + Copy> Residual for PcSaftBinary { fn components(&self) -> usize { 2 } type Real = PcSaftBinary; - type Lifted + Copy> = PcSaftBinary; + type Lifted + Copy> = PcSaftBinary; fn re(&self) -> Self::Real { PcSaftBinary(( self.0.0.each_ref().map(|x| x.each_ref().map(D::re)), self.0.1.re(), )) } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { PcSaftBinary(( self.0 .0 diff --git a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs index 46155f4cf..20f6673a8 100644 --- a/crates/feos/src/pcsaft/eos/pcsaft_pure.rs +++ b/crates/feos/src/pcsaft/eos/pcsaft_pure.rs @@ -11,9 +11,9 @@ const MAX_ETA: f64 = 0.5; /// Optimized implementation of PC-SAFT for a single component. #[derive(Clone, Copy)] -pub struct PcSaftPure + Copy, const N: usize>(pub [D; N]); +pub struct PcSaftPure + Copy, const N: usize>(pub [D; N]); -fn helmholtz_energy_density_non_assoc + Copy>( +fn helmholtz_energy_density_non_assoc + Copy>( m: D, sigma: D, epsilon_k: D, @@ -88,7 +88,7 @@ fn helmholtz_energy_density_non_assoc + Copy>( (hs + hc + disp + dipole, [eta, eta_m1]) } -fn helmholtz_energy_density + Copy>( +fn helmholtz_energy_density + Copy>( parameters: &[D; 8], temperature: D, density: D, @@ -112,17 +112,17 @@ fn helmholtz_energy_density + Copy>( non_assoc + assoc } -impl + Copy> Residual for PcSaftPure { +impl + Copy> Residual for PcSaftPure { fn components(&self) -> usize { 1 } type Real = PcSaftPure; - type Lifted + Copy> = PcSaftPure; + type Lifted + Copy> = PcSaftPure; fn re(&self) -> Self::Real { PcSaftPure(self.0.each_ref().map(D::re)) } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { PcSaftPure(self.0.each_ref().map(D2::from_inner)) } @@ -147,17 +147,17 @@ impl + Copy> Residual for PcSaftPure { } } -impl + Copy> Residual for PcSaftPure { +impl + Copy> Residual for PcSaftPure { fn components(&self) -> usize { 1 } type Real = PcSaftPure; - type Lifted + Copy> = PcSaftPure; + type Lifted + Copy> = PcSaftPure; fn re(&self) -> Self::Real { PcSaftPure(self.0.each_ref().map(D::re)) } - fn lift + Copy>(&self) -> Self::Lifted { + fn lift + Copy>(&self) -> Self::Lifted { PcSaftPure(self.0.each_ref().map(D2::from_inner)) } @@ -184,7 +184,7 @@ impl + Copy> Residual for PcSaftPure { } impl ParametersAD for PcSaftPure { - fn build + Copy>( + fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D, ) -> PcSaftPure { PcSaftPure([ @@ -197,7 +197,7 @@ impl ParametersAD for PcSaftPure { } impl ParametersAD for PcSaftPure { - fn build + Copy>( + fn build + Copy>( mut f: impl FnMut(&'static str, bool) -> D, ) -> PcSaftPure { PcSaftPure([ diff --git a/crates/feos/src/pcsaft/eos/polar.rs b/crates/feos/src/pcsaft/eos/polar.rs index 096bdc1c2..f8ad10849 100644 --- a/crates/feos/src/pcsaft/eos/polar.rs +++ b/crates/feos/src/pcsaft/eos/polar.rs @@ -125,7 +125,7 @@ impl MeanSegmentNumbers { } } -fn pair_integral_ij + Copy>( +fn pair_integral_ij + Copy>( mij1: f64, mij2: f64, etas: &[D], @@ -142,7 +142,7 @@ fn pair_integral_ij + Copy>( .sum() } -fn triplet_integral_ijk + Copy>( +fn triplet_integral_ijk + Copy>( mijk1: f64, mijk2: f64, etas: &[D], @@ -153,7 +153,7 @@ fn triplet_integral_ijk + Copy>( .sum() } -fn triplet_integral_ijk_dq + Copy>(mijk: f64, etas: &[D], c: &[[f64; 2]]) -> D { +fn triplet_integral_ijk_dq + Copy>(mijk: f64, etas: &[D], c: &[[f64; 2]]) -> D { (0..c.len()) .map(|i| etas[i] * (c[i][0] + mijk * c[i][1])) .sum() @@ -163,7 +163,7 @@ pub struct Dipole; impl Dipole { #[inline] - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &PcSaftPars, state: &StateHD, @@ -236,7 +236,7 @@ pub struct Quadrupole; impl Quadrupole { #[inline] - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &PcSaftPars, state: &StateHD, @@ -317,7 +317,7 @@ pub struct DipoleQuadrupole; impl DipoleQuadrupole { #[inline] - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &PcSaftPars, state: &StateHD, diff --git a/crates/feos/src/pets/dft/dispersion.rs b/crates/feos/src/pets/dft/dispersion.rs index 915be2be4..d25b08d9b 100644 --- a/crates/feos/src/pets/dft/dispersion.rs +++ b/crates/feos/src/pets/dft/dispersion.rs @@ -23,7 +23,7 @@ impl<'a> AttractiveFunctional<'a> { Self { parameters } } - fn att_weight_functions + Copy>( + fn att_weight_functions + Copy>( &self, psi: f64, temperature: N, @@ -41,18 +41,18 @@ impl<'a> FunctionalContribution for AttractiveFunctional<'a> { "Attractive functional" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { self.att_weight_functions(PSI_DFT, temperature) } - fn weight_functions_pdgt + Copy>( + fn weight_functions_pdgt + Copy>( &self, temperature: N, ) -> WeightFunctionInfo { self.att_weight_functions(PSI_PDGT, temperature) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, density: ArrayView2, diff --git a/crates/feos/src/pets/dft/pure_pets_functional.rs b/crates/feos/src/pets/dft/pure_pets_functional.rs index 14efe1b15..1e4b5f073 100644 --- a/crates/feos/src/pets/dft/pure_pets_functional.rs +++ b/crates/feos/src/pets/dft/pure_pets_functional.rs @@ -31,7 +31,7 @@ impl<'a> FunctionalContribution for PureFMTFunctional<'a> { "Pure FMT" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let r = self.parameters.hs_diameter(temperature) * N::from(0.5); WeightFunctionInfo::new(dvector![0], false).extend( vec![ @@ -46,7 +46,7 @@ impl<'a> FunctionalContribution for PureFMTFunctional<'a> { ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, @@ -123,7 +123,7 @@ impl<'a> FunctionalContribution for PureAttFunctional<'a> { "Pure attractive" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let d = self.parameters.hs_diameter(temperature); const PSI: f64 = 1.21; // Homosegmented DFT (Heier2018) WeightFunctionInfo::new(dvector![0], false).add( @@ -132,7 +132,7 @@ impl<'a> FunctionalContribution for PureAttFunctional<'a> { ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, diff --git a/crates/feos/src/pets/eos/dispersion.rs b/crates/feos/src/pets/eos/dispersion.rs index 2215a4225..b3f65c6aa 100644 --- a/crates/feos/src/pets/eos/dispersion.rs +++ b/crates/feos/src/pets/eos/dispersion.rs @@ -24,7 +24,7 @@ pub const B: [f64; 7] = [ ]; impl Pets { - pub fn dispersion_helmholtz_energy_density + Copy>( + pub fn dispersion_helmholtz_energy_density + Copy>( &self, state: &StateHD, ) -> D { diff --git a/crates/feos/src/pets/eos/mod.rs b/crates/feos/src/pets/eos/mod.rs index 5d9b9a666..5fc0c32a6 100644 --- a/crates/feos/src/pets/eos/mod.rs +++ b/crates/feos/src/pets/eos/mod.rs @@ -92,13 +92,13 @@ impl ResidualDyn for Pets { self.parameters.pure.len() } - fn compute_max_density + Copy>(&self, moles: &DVector) -> D { + fn compute_max_density + Copy>(&self, moles: &DVector) -> D { moles.sum() * self.options.max_eta / (self.sigma.map(|v| D::from(v.powi(3))).component_mul(moles)).sum() / FRAC_PI_6 } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &feos_core::StateHD, ) -> Vec<(&'static str, D)> { @@ -122,11 +122,11 @@ impl Molarweight for Pets { } impl HardSphereProperties for Pets { - fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { + fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { MonomerShape::Spherical(self.sigma.len()) } - fn hs_diameter + Copy>(&self, temperature: D) -> DVector { + fn hs_diameter + Copy>(&self, temperature: D) -> DVector { let ti = temperature.recip() * -3.052785558; DVector::from_fn(self.sigma.len(), |i, _| { -((ti * self.epsilon_k[i]).exp() * 0.127112544 - 1.0) * self.sigma[i] diff --git a/crates/feos/src/saftvrmie/eos/dispersion.rs b/crates/feos/src/saftvrmie/eos/dispersion.rs index cc35b6f22..eed119411 100644 --- a/crates/feos/src/saftvrmie/eos/dispersion.rs +++ b/crates/feos/src/saftvrmie/eos/dispersion.rs @@ -23,7 +23,7 @@ pub struct Properties { k0: [D; 4], } -impl + Copy + Zero> Properties { +impl + Copy + Zero> Properties { pub(super) fn new( parameters: &SaftVRMiePars, state: &StateHD, @@ -99,7 +99,7 @@ pub(super) const PHI: [[f64; 7]; 6] = [ ]; /// First, second and third order perturbations for dispersive interactions -pub fn helmholtz_energy_density_disp + Copy>( +pub fn helmholtz_energy_density_disp + Copy>( parameters: &SaftVRMiePars, properties: &Properties, state: &StateHD, @@ -195,7 +195,7 @@ pub fn helmholtz_energy_density_disp + Copy>( } /// Combine dispersion and chain contributions -pub fn helmholtz_energy_density_disp_chain + Copy>( +pub fn helmholtz_energy_density_disp_chain + Copy>( parameters: &SaftVRMiePars, properties: &Properties, state: &StateHD, @@ -334,7 +334,7 @@ pub fn helmholtz_energy_density_disp_chain + Copy>( } #[inline] -pub(super) fn zeta_eff + Copy>(zeta: D, lambda: f64) -> D { +pub(super) fn zeta_eff + Copy>(zeta: D, lambda: f64) -> D { let li = 1. / lambda; let li2 = li * li; let li3 = li * li2; @@ -350,14 +350,14 @@ pub(super) fn zeta_eff + Copy>(zeta: D, lambda: f64) -> D { /// Sutherland potential for mixtures (Eq. A 16) divided by 2 PI rho_s d_ij^3 epsilon_k_ij #[inline] -fn a1s_ij + Copy>(zeta_x: D, lambda: f64) -> D { +fn a1s_ij + Copy>(zeta_x: D, lambda: f64) -> D { let zeta_eff = zeta_eff(zeta_x, lambda); -(-zeta_eff * 0.5 + 1.0) / ((-zeta_eff + 1.0).powi(3) * (lambda - 3.0)) } /// Eq. A 12 of Lafitte divided by 2 PI rho_s d_ij^3 epsilon_k_ij #[inline] -fn b_ij + Copy>(zeta_x: D, x0: D, lambda: f64) -> D { +fn b_ij + Copy>(zeta_x: D, x0: D, lambda: f64) -> D { let x0_3ml = x0.powf(3.0 - lambda); let i = -(x0_3ml - 1.0) / (lambda - 3.0); let j = -(x0.powf(4.0 - lambda) * (lambda - 3.0) - x0_3ml * (lambda - 4.0) - 1.0) @@ -367,7 +367,7 @@ fn b_ij + Copy>(zeta_x: D, x0: D, lambda: f64) -> D { /// Calculates x0^l (a1s_ij + b_ij) without prefactor C #[inline] -fn a1s_b_ij + Copy>(zeta_x: D, x0: D, lambda: f64) -> D { +fn a1s_b_ij + Copy>(zeta_x: D, x0: D, lambda: f64) -> D { x0.powf(lambda) * (a1s_ij(zeta_x, lambda) + b_ij(zeta_x, x0, lambda)) } diff --git a/crates/feos/src/saftvrmie/eos/mod.rs b/crates/feos/src/saftvrmie/eos/mod.rs index 780f2fc37..1bdde781f 100644 --- a/crates/feos/src/saftvrmie/eos/mod.rs +++ b/crates/feos/src/saftvrmie/eos/mod.rs @@ -64,7 +64,7 @@ impl ResidualDyn for SaftVRMie { self.params.m.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { let msigma3 = self .params .m @@ -72,7 +72,7 @@ impl ResidualDyn for SaftVRMie { (msigma3.map(D::from).dot(molefracs) * FRAC_PI_6).recip() * self.options.max_eta } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { @@ -120,7 +120,7 @@ impl Molarweight for SaftVRMie { impl AssociationStrength for SaftVRMiePars { type Record = SaftVRMieAssociationRecord; - fn association_strength_ij + Copy>( + fn association_strength_ij + Copy>( &self, temperature: D, comp_i: usize, diff --git a/crates/feos/src/saftvrmie/parameters.rs b/crates/feos/src/saftvrmie/parameters.rs index b40cd524b..fe02875f3 100644 --- a/crates/feos/src/saftvrmie/parameters.rs +++ b/crates/feos/src/saftvrmie/parameters.rs @@ -170,7 +170,7 @@ impl SaftVRMiePars { impl SaftVRMiePars { #[inline] - pub fn hs_diameter_ij + Copy>( + pub fn hs_diameter_ij + Copy>( &self, i: usize, j: usize, @@ -197,7 +197,7 @@ impl SaftVRMiePars { /// /// Method of Aasen et al. /// Using starting value proposed in Clapeyron.jl (Andrés Riedemann) -fn lower_integratal_limit + Copy>(la: f64, lr: f64, c_eps_t: D) -> D { +fn lower_integratal_limit + Copy>(la: f64, lr: f64, c_eps_t: D) -> D { // initial value from repulsive contribution let k = (-c_eps_t.recip() * f64::EPSILON.ln()).ln(); let mut r = (-k / lr).exp(); @@ -219,7 +219,7 @@ fn lower_integratal_limit + Copy>(la: f64, lr: f64, c_eps_t: D) /// f is the function to find the root of. /// Here, f = -beta u_mie(r) - ln(EPS) #[inline] -fn mie_potential_halley + Copy>(r: D, la: f64, lr: f64, c_eps_t: D) -> [D; 3] { +fn mie_potential_halley + Copy>(r: D, la: f64, lr: f64, c_eps_t: D) -> [D; 3] { let ri = r.recip(); let plr = ri.powf(lr); let pla = ri.powf(la); @@ -237,17 +237,17 @@ fn mie_potential_halley + Copy>(r: D, la: f64, lr: f64, c_eps_t: /// Dimensionless Mie potential (divided by kT) #[inline] -fn beta_u_mie + Copy>(r: D, la: f64, lr: f64, sigma: f64, c_eps_t: D) -> D { +fn beta_u_mie + Copy>(r: D, la: f64, lr: f64, sigma: f64, c_eps_t: D) -> D { let ri = r.recip() * sigma; (ri.powf(lr) - ri.powf(la)) * c_eps_t } impl HardSphereProperties for SaftVRMiePars { - fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { + fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { MonomerShape::NonSpherical(self.m.map(N::from)) } - fn hs_diameter + Copy>(&self, temperature: D) -> DVector { + fn hs_diameter + Copy>(&self, temperature: D) -> DVector { let t_inv = temperature.recip(); DVector::from_fn(self.m.len(), |i, _| self.hs_diameter_ij(i, i, t_inv)) } diff --git a/crates/feos/src/saftvrqmie/dft/dispersion.rs b/crates/feos/src/saftvrqmie/dft/dispersion.rs index 220037cec..a5b2bfcfd 100644 --- a/crates/feos/src/saftvrqmie/dft/dispersion.rs +++ b/crates/feos/src/saftvrqmie/dft/dispersion.rs @@ -22,7 +22,7 @@ impl<'a> AttractiveFunctional<'a> { } } -fn att_weight_functions + Copy>( +fn att_weight_functions + Copy>( p: &SaftVRQMiePars, psi: f64, temperature: N, @@ -39,18 +39,18 @@ impl<'a> FunctionalContribution for AttractiveFunctional<'a> { "Attractive functional" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { att_weight_functions(self.parameters, PSI_DFT, temperature) } - fn weight_functions_pdgt + Copy>( + fn weight_functions_pdgt + Copy>( &self, temperature: N, ) -> WeightFunctionInfo { att_weight_functions(self.parameters, PSI_PDGT, temperature) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, density: ArrayView2, diff --git a/crates/feos/src/saftvrqmie/dft/mod.rs b/crates/feos/src/saftvrqmie/dft/mod.rs index 6cde777b4..5608a54dc 100644 --- a/crates/feos/src/saftvrqmie/dft/mod.rs +++ b/crates/feos/src/saftvrqmie/dft/mod.rs @@ -60,11 +60,11 @@ impl HelmholtzEnergyFunctionalDyn for SaftVRQMie { } impl HardSphereProperties for SaftVRQMiePars { - fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { + fn monomer_shape>(&self, _: N) -> MonomerShape<'_, N> { MonomerShape::Spherical(self.m.len()) } - fn hs_diameter + Copy>(&self, temperature: D) -> DVector { + fn hs_diameter + Copy>(&self, temperature: D) -> DVector { self.hs_diameter(temperature) } } diff --git a/crates/feos/src/saftvrqmie/dft/non_additive_hs.rs b/crates/feos/src/saftvrqmie/dft/non_additive_hs.rs index a7909ab35..29d931b6c 100644 --- a/crates/feos/src/saftvrqmie/dft/non_additive_hs.rs +++ b/crates/feos/src/saftvrqmie/dft/non_additive_hs.rs @@ -24,7 +24,7 @@ impl<'a> FunctionalContribution for NonAddHardSphereFunctional<'a> { "Non-additive hard-sphere functional" } - fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { + fn weight_functions + Copy>(&self, temperature: N) -> WeightFunctionInfo { let p = &self.parameters; let r = p.hs_diameter(temperature) * N::from(0.5); WeightFunctionInfo::new(DVector::from_fn(r.len(), |i, _| i), false) @@ -50,7 +50,7 @@ impl<'a> FunctionalContribution for NonAddHardSphereFunctional<'a> { ) } - fn helmholtz_energy_density + Copy>( + fn helmholtz_energy_density + Copy>( &self, temperature: N, weighted_densities: ArrayView2, @@ -143,7 +143,7 @@ impl<'a> FunctionalContribution for NonAddHardSphereFunctional<'a> { } } -pub fn non_additive_hs_energy_density + Copy>( +pub fn non_additive_hs_energy_density + Copy>( parameters: &SaftVRQMiePars, d_hs_ij: &Array2, d_hs_add_ij: &Array2, diff --git a/crates/feos/src/saftvrqmie/eos/dispersion.rs b/crates/feos/src/saftvrqmie/eos/dispersion.rs index f4bdfa12b..ae78efea7 100644 --- a/crates/feos/src/saftvrqmie/eos/dispersion.rs +++ b/crates/feos/src/saftvrqmie/eos/dispersion.rs @@ -31,11 +31,11 @@ const PHI: [[f64; 7]; 6] = [ ], ]; -pub struct Alpha> { +pub struct Alpha> { alpha_ij: DMatrix, } -impl + Copy> Alpha { +impl + Copy> Alpha { pub fn new( parameters: &SaftVRQMiePars, sigma_eff_ij: &DMatrix, @@ -90,7 +90,7 @@ impl + Copy> Alpha { pub struct Dispersion; impl Dispersion { - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &SaftVRQMiePars, state: &StateHD, @@ -148,7 +148,7 @@ impl Dispersion { #[cfg(feature = "dft")] #[expect(clippy::too_many_arguments)] -pub fn dispersion_energy_density + Copy>( +pub fn dispersion_energy_density + Copy>( parameters: &SaftVRQMiePars, d_hs_ij: &DMatrix, s_eff_ij: &DMatrix, @@ -184,7 +184,7 @@ pub fn dispersion_energy_density + Copy>( rho_s * (a1 * inv_t + a2 * inv_t.powi(2) + a3 * inv_t.powi(3)) } -fn zeta_saft_vrq_mie + Copy>( +fn zeta_saft_vrq_mie + Copy>( m: &DVector, x_s: &DVector, diameter: &DMatrix, @@ -199,7 +199,7 @@ fn zeta_saft_vrq_mie + Copy>( zeta * FRAC_PI_6 * rho_s } -fn first_order_perturbation + Copy>( +fn first_order_perturbation + Copy>( parameters: &SaftVRQMiePars, x_s: &DVector, zeta: D, @@ -239,7 +239,7 @@ fn first_order_perturbation + Copy>( } #[expect(clippy::too_many_arguments)] -fn first_order_perturbation_ij + Copy>( +fn first_order_perturbation_ij + Copy>( lambda_a: f64, lambda_r: f64, epsilon_k: f64, @@ -271,7 +271,7 @@ fn first_order_perturbation_ij + Copy>( a1_ij * c } -fn eta_eff + Copy>(lambda: f64, zeta: D) -> D { +fn eta_eff + Copy>(lambda: f64, zeta: D) -> D { let inv_lambda = DVector::from(vec![ 1.0, 1.0 / lambda, @@ -287,16 +287,16 @@ fn eta_eff + Copy>(lambda: f64, zeta: D) -> D { zeta * (zeta * (zeta * (zeta * c[3] + c[2]) + c[1]) + c[0]) } -fn sutherland + Copy>(lambda: f64, epsilon_k: f64, zeta: D, x0: D) -> D { +fn sutherland + Copy>(lambda: f64, epsilon_k: f64, zeta: D, x0: D) -> D { let ef = eta_eff(lambda, zeta); (-ef * 0.5 + 1.0) * -12.0 * x0.powf(lambda) * epsilon_k / (lambda - 3.0) / (-ef + 1.0).powi(3) } -fn ilambda>(lambda: f64, x0: D) -> D { +fn ilambda>(lambda: f64, x0: D) -> D { -(x0.powf(3.0 - lambda) - 1.0) / (lambda - 3.0) } -fn jlambda>(lambda: f64, x0: D) -> D { +fn jlambda>(lambda: f64, x0: D) -> D { -(x0.powf(4.0 - lambda) * (lambda - 3.0) - x0.powf(3.0 - lambda) * (lambda - 4.0) - 1.0) / ((lambda - 3.0) * (lambda - 4.0)) } @@ -305,7 +305,7 @@ fn jlambda>(lambda: f64, x0: D) -> D { /// B is divided by the packing fraction /// /// \author Morten Hammer, February 2018 -fn b + Copy>(lambda: f64, epsilon_k: f64, zeta: D, x0: D, x0_eff: D) -> D { +fn b + Copy>(lambda: f64, epsilon_k: f64, zeta: D, x0: D, x0_eff: D) -> D { let ilambda = ilambda(lambda, x0_eff); let jlambda = jlambda(lambda, x0_eff); let denum = (-zeta + 1.0).powi(3); @@ -316,7 +316,7 @@ fn b + Copy>(lambda: f64, epsilon_k: f64, zeta: D, x0: D, x0_eff } #[inline] -fn combine_sutherland_and_b + Copy>( +fn combine_sutherland_and_b + Copy>( lambda: f64, epsilon_k: f64, zeta: D, @@ -329,7 +329,7 @@ fn combine_sutherland_and_b + Copy>( } #[expect(clippy::too_many_arguments)] -fn second_order_perturbation + Copy>( +fn second_order_perturbation + Copy>( parameters: &SaftVRQMiePars, alpha: &Alpha, x_s: &DVector, @@ -390,7 +390,7 @@ fn quantum_prefactor_second_order(lambda: f64) -> f64 { } #[expect(clippy::too_many_arguments)] -fn second_order_perturbation_ij + Copy>( +fn second_order_perturbation_ij + Copy>( lambda_a: f64, lambda_r: f64, epsilon_k: f64, @@ -452,7 +452,7 @@ fn second_order_perturbation_ij + Copy>( a2_ij * 0.5 * epsilon_k * c.powi(2) } -fn third_order_perturbation + Copy>( +fn third_order_perturbation + Copy>( parameters: &SaftVRQMiePars, alpha: &Alpha, x_s: &DVector, @@ -471,7 +471,7 @@ fn third_order_perturbation + Copy>( a3 } -fn third_order_perturbation_ij + Copy>( +fn third_order_perturbation_ij + Copy>( i: usize, j: usize, epsilon_k_eff: D, diff --git a/crates/feos/src/saftvrqmie/eos/hard_sphere.rs b/crates/feos/src/saftvrqmie/eos/hard_sphere.rs index 066a19465..5242072e0 100644 --- a/crates/feos/src/saftvrqmie/eos/hard_sphere.rs +++ b/crates/feos/src/saftvrqmie/eos/hard_sphere.rs @@ -62,7 +62,7 @@ const W_K21: [f64; 21] = [ impl SaftVRQMiePars { #[inline] - pub fn hs_diameter + Copy>(&self, temperature: D) -> DVector { + pub fn hs_diameter + Copy>(&self, temperature: D) -> DVector { DVector::from_fn(self.m.len(), |i, _| -> D { let sigma_eff = self.calc_sigma_eff_ij(i, i, temperature); self.hs_diameter_ij(i, i, temperature, sigma_eff) @@ -70,7 +70,7 @@ impl SaftVRQMiePars { } #[inline] - pub fn hs_diameter_ij + Copy>( + pub fn hs_diameter_ij + Copy>( &self, i: usize, j: usize, @@ -89,7 +89,7 @@ impl SaftVRQMiePars { d_hs } - pub fn zero_integrand + Copy>( + pub fn zero_integrand + Copy>( &self, i: usize, j: usize, @@ -118,13 +118,13 @@ impl SaftVRQMiePars { } #[inline] - pub fn epsilon_k_eff + Copy>(&self, temperature: D) -> DVector { + pub fn epsilon_k_eff + Copy>(&self, temperature: D) -> DVector { DVector::from_fn(self.m.len(), |i, _| -> D { self.calc_epsilon_k_eff_ij(i, i, temperature) }) } - pub fn calc_epsilon_k_eff_ij + Copy>( + pub fn calc_epsilon_k_eff_ij + Copy>( &self, i: usize, j: usize, @@ -146,13 +146,13 @@ impl SaftVRQMiePars { } #[inline] - pub fn sigma_eff + Copy>(&self, temperature: D) -> DVector { + pub fn sigma_eff + Copy>(&self, temperature: D) -> DVector { DVector::from_fn(self.m.len(), |i, _| -> D { self.calc_sigma_eff_ij(i, i, temperature) }) } - pub fn calc_sigma_eff_ij + Copy>( + pub fn calc_sigma_eff_ij + Copy>( &self, i: usize, j: usize, @@ -174,12 +174,17 @@ impl SaftVRQMiePars { } #[inline] - pub fn quantum_d_ij>(&self, i: usize, j: usize, temperature: D) -> D { + pub fn quantum_d_ij>( + &self, + i: usize, + j: usize, + temperature: D, + ) -> D { quantum_d_mass(self.mass_ij[(i, j)], temperature) } /// Feynman-Hibbs corrected potential - pub fn qmie_potential_ij + Copy>( + pub fn qmie_potential_ij + Copy>( &self, i: usize, j: usize, @@ -232,14 +237,14 @@ impl SaftVRQMiePars { } #[inline] -pub fn quantum_d_mass>(mass: f64, temperature: D) -> D { +pub fn quantum_d_mass>(mass: f64, temperature: D) -> D { temperature.recip() / mass * D_QM_PREFACTOR } pub struct HardSphere; impl HardSphere { - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &SaftVRQMiePars, state: &StateHD, @@ -263,7 +268,7 @@ impl fmt::Display for HardSphere { } } -pub fn zeta + Copy>( +pub fn zeta + Copy>( m: &DVector, partial_density: &DVector, diameter: &DVector, @@ -279,7 +284,7 @@ pub fn zeta + Copy>( zeta } -pub fn zeta_23 + Copy>( +pub fn zeta_23 + Copy>( m: &DVector, molefracs: &DVector, diameter: &DVector, @@ -305,7 +310,7 @@ mod tests { #[test] fn test_quantum_d_mass() { let parameters = hydrogen_fh("1"); - let temperature = 26.7060; + let temperature = 26.7060_f64; let r = 3.5; let u0 = parameters.qmie_potential_ij(0, 0, r, temperature); let eps = 1.0e-5; @@ -320,7 +325,7 @@ mod tests { #[test] fn test_sigma_effective() { let parameters = hydrogen_fh("1"); - let temperature = 26.7060; + let temperature = 26.7060_f64; let sigma_eff = parameters.calc_sigma_eff_ij(0, 0, temperature); println!("{}", sigma_eff - 3.2540054024660556); assert!((sigma_eff - 3.2540054024660556).abs() < 5.0e-7) @@ -329,7 +334,7 @@ mod tests { #[test] fn test_eps_div_k_effective() { let parameters = hydrogen_fh("1"); - let temperature = 26.7060; + let temperature = 26.7060_f64; let epsilon_k_eff = parameters.calc_epsilon_k_eff_ij(0, 0, temperature); println!("{}", epsilon_k_eff - 21.654396207986697); assert!((epsilon_k_eff - 21.654396207986697).abs() < 1.0e-6) @@ -338,7 +343,7 @@ mod tests { #[test] fn test_zero_integrand() { let parameters = hydrogen_fh("1"); - let temperature = 26.706; + let temperature = 26.706_f64; let sigma_eff = parameters.calc_sigma_eff_ij(0, 0, temperature); let r0 = parameters.zero_integrand(0, 0, temperature, sigma_eff); println!("{}", r0 - 2.5265031901173732); diff --git a/crates/feos/src/saftvrqmie/eos/mod.rs b/crates/feos/src/saftvrqmie/eos/mod.rs index 9ec71818d..e77688b26 100644 --- a/crates/feos/src/saftvrqmie/eos/mod.rs +++ b/crates/feos/src/saftvrqmie/eos/mod.rs @@ -73,7 +73,7 @@ pub(crate) struct TemperatureDependentProperties { quantum_d_ij: DMatrix, } -impl + Copy> TemperatureDependentProperties { +impl + Copy> TemperatureDependentProperties { fn new(parameters: &SaftVRQMiePars, temperature: D) -> Self { let n = parameters.m.len(); let sigma_eff_ij = DMatrix::from_fn(n, n, |i, j| -> D { @@ -146,7 +146,7 @@ impl ResidualDyn for SaftVRQMie { self.parameters.pure.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { let msigma3 = self .params .m @@ -154,7 +154,7 @@ impl ResidualDyn for SaftVRQMie { (msigma3.map(D::from).dot(molefracs) * FRAC_PI_6).recip() * self.options.max_eta } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { diff --git a/crates/feos/src/saftvrqmie/eos/non_additive_hs.rs b/crates/feos/src/saftvrqmie/eos/non_additive_hs.rs index d2344123f..9029aca8c 100644 --- a/crates/feos/src/saftvrqmie/eos/non_additive_hs.rs +++ b/crates/feos/src/saftvrqmie/eos/non_additive_hs.rs @@ -9,7 +9,7 @@ use std::f64::consts::PI; pub struct NonAddHardSphere; impl NonAddHardSphere { - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &SaftVRQMiePars, state: &StateHD, @@ -27,7 +27,7 @@ impl NonAddHardSphere { } } -pub fn reduced_non_additive_hs_energy + Copy>( +pub fn reduced_non_additive_hs_energy + Copy>( parameters: &SaftVRQMiePars, d_hs_ij: &DMatrix, d_hs_add_ij: &DMatrix, diff --git a/crates/feos/src/uvtheory/eos/bh/attractive_perturbation.rs b/crates/feos/src/uvtheory/eos/bh/attractive_perturbation.rs index e55682c8a..b817306bf 100644 --- a/crates/feos/src/uvtheory/eos/bh/attractive_perturbation.rs +++ b/crates/feos/src/uvtheory/eos/bh/attractive_perturbation.rs @@ -40,7 +40,7 @@ pub(super) struct AttractivePerturbation; impl AttractivePerturbation { /// Helmholtz energy for attractive perturbation, eq. 52 - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &UVTheoryPars, state: &StateHD, @@ -74,11 +74,11 @@ impl AttractivePerturbation { } } -fn delta_b12u>(t_x: D, mean_field_constant_x: D, weighted_sigma3_ij: D) -> D { +fn delta_b12u>(t_x: D, mean_field_constant_x: D, weighted_sigma3_ij: D) -> D { -mean_field_constant_x / t_x * 2.0 * PI * weighted_sigma3_ij } -fn residual_virial_coefficient + Copy>( +fn residual_virial_coefficient + Copy>( p: &UVTheoryPars, x: &DVector, t: D, @@ -95,7 +95,7 @@ fn residual_virial_coefficient + Copy>( delta_b2bar } -fn correlation_integral_bh + Copy>( +fn correlation_integral_bh + Copy>( rho_x: D, mean_field_constant_x: D, rep_x: D, @@ -110,7 +110,7 @@ fn correlation_integral_bh + Copy>( /// U-fraction according to Barker-Henderson division. /// Eq. 15 -fn u_fraction_bh + Copy>(rep_x: D, reduced_density: D, one_fluid_beta: D) -> D { +fn u_fraction_bh + Copy>(rep_x: D, reduced_density: D, one_fluid_beta: D) -> D { let mut c = [D::zero(); 4]; let inv_rep = rep_x.recip(); for i in 0..4 { @@ -124,11 +124,11 @@ fn u_fraction_bh + Copy>(rep_x: D, reduced_density: D, one_fluid /// Activation function used for u-fraction according to Barker-Henderson division. /// Eq. 16 -fn activation + Copy>(c: D, one_fluid_beta: D) -> D { +fn activation + Copy>(c: D, one_fluid_beta: D) -> D { one_fluid_beta * c.sqrt() / (one_fluid_beta.powi(2) * c + 1.0).sqrt() } -fn one_fluid_properties + Copy>( +fn one_fluid_properties + Copy>( p: &UVTheoryPars, x: &DVector, t: D, @@ -164,7 +164,7 @@ fn one_fluid_properties + Copy>( ) } -fn coefficients_bh + Copy>(rep: D, att: D, d: D) -> [D; 3] { +fn coefficients_bh + Copy>(rep: D, att: D, d: D) -> [D; 3] { let c11 = d.powd(-rep + 6.0) * ((D::one() * 2.0f64).powd(-rep + 3.0) - d.powd(rep - 3.0)) / (-rep + 3.0) + (-d.powi(3) * 8.0 + 1.0) / 24.0; @@ -182,14 +182,14 @@ fn coefficients_bh + Copy>(rep: D, att: D, d: D) -> [D; 3] { [c1, c2, c3] } -fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64) -> D { +fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64) -> D { let rc = 5.0; let alpha = mean_field_constant(rep, att, rc); let yeff = y_eff(reduced_temperature, rep, att); -(yeff * (rc.powi(3) - 1.0) / 3.0 + reduced_temperature.recip() * alpha) * 2.0 * PI } -fn y_eff + Copy>(reduced_temperature: D, rep: f64, att: f64) -> D { +fn y_eff + Copy>(reduced_temperature: D, rep: f64, att: f64) -> D { // optimize: move this part to parameter initialization let rc = 5.0; let rs = 1.0; diff --git a/crates/feos/src/uvtheory/eos/bh/hard_sphere.rs b/crates/feos/src/uvtheory/eos/bh/hard_sphere.rs index 4b1dc97b8..27002073b 100644 --- a/crates/feos/src/uvtheory/eos/bh/hard_sphere.rs +++ b/crates/feos/src/uvtheory/eos/bh/hard_sphere.rs @@ -19,7 +19,7 @@ const BH_CONSTANTS_ETA_A: [[f64; 4]; 4] = [ /// Dimensionless Hard-sphere diameter according to Barker-Henderson division. /// Eq. S23 and S24. impl BarkerHenderson { - pub fn diameter_bh + Copy>( + pub fn diameter_bh + Copy>( parameters: &UVTheoryPars, temperature: D, ) -> DVector { @@ -39,7 +39,7 @@ impl BarkerHenderson { } } -pub(super) fn packing_fraction + Copy>( +pub(super) fn packing_fraction + Copy>( partial_density: &DVector, diameter: &DVector, ) -> D { @@ -48,7 +48,7 @@ pub(super) fn packing_fraction + Copy>( }) } -pub(super) fn packing_fraction_b + Copy>( +pub(super) fn packing_fraction_b + Copy>( parameters: &UVTheoryPars, diameter: &DVector, eta: D, @@ -68,7 +68,7 @@ pub(super) fn packing_fraction_b + Copy>( }) } -pub(super) fn packing_fraction_a + Copy>( +pub(super) fn packing_fraction_a + Copy>( parameters: &UVTheoryPars, diameter: &DVector, eta: D, diff --git a/crates/feos/src/uvtheory/eos/bh/mod.rs b/crates/feos/src/uvtheory/eos/bh/mod.rs index 3a678dd98..b34c8aa0c 100644 --- a/crates/feos/src/uvtheory/eos/bh/mod.rs +++ b/crates/feos/src/uvtheory/eos/bh/mod.rs @@ -12,7 +12,7 @@ mod reference_perturbation; pub struct BarkerHenderson; impl BarkerHenderson { - pub fn residual_helmholtz_energy_contributions + Copy>( + pub fn residual_helmholtz_energy_contributions + Copy>( &self, parameters: &UVTheoryPars, state: &StateHD, diff --git a/crates/feos/src/uvtheory/eos/bh/reference_perturbation.rs b/crates/feos/src/uvtheory/eos/bh/reference_perturbation.rs index 268642d3f..d6360d9f2 100644 --- a/crates/feos/src/uvtheory/eos/bh/reference_perturbation.rs +++ b/crates/feos/src/uvtheory/eos/bh/reference_perturbation.rs @@ -10,7 +10,7 @@ pub(super) struct ReferencePerturbation; impl ReferencePerturbation { /// Helmholtz energy for perturbation reference (Mayer-f), eq. 29 - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &UVTheoryPars, state: &StateHD, diff --git a/crates/feos/src/uvtheory/eos/mod.rs b/crates/feos/src/uvtheory/eos/mod.rs index 1957af6e8..c6d98cb86 100644 --- a/crates/feos/src/uvtheory/eos/mod.rs +++ b/crates/feos/src/uvtheory/eos/mod.rs @@ -70,12 +70,12 @@ impl ResidualDyn for UVTheory { self.parameters.pure.len() } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { let sigma3 = self.params.sigma.map(|v| v.powi(3)); (sigma3.map(D::from).dot(molefracs) * FRAC_PI_6).recip() * self.options.max_eta } - fn reduced_helmholtz_energy_density_contributions + Copy>( + fn reduced_helmholtz_energy_density_contributions + Copy>( &self, state: &feos_core::StateHD, ) -> Vec<(&'static str, D)> { diff --git a/crates/feos/src/uvtheory/eos/wca/attractive_perturbation.rs b/crates/feos/src/uvtheory/eos/wca/attractive_perturbation.rs index 1e7e8bc5a..ed6f96a51 100644 --- a/crates/feos/src/uvtheory/eos/wca/attractive_perturbation.rs +++ b/crates/feos/src/uvtheory/eos/wca/attractive_perturbation.rs @@ -72,7 +72,7 @@ pub struct AttractivePerturbation; impl AttractivePerturbation { /// Helmholtz energy for attractive perturbation, eq. 52 - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &UVTheoryPars, state: &StateHD, @@ -107,7 +107,7 @@ impl AttractivePerturbation { } // (S43) & (S53) -fn delta_b12u + Copy>( +fn delta_b12u + Copy>( t_x: D, mean_field_constant_x: D, weighted_sigma3_ij: D, @@ -120,7 +120,7 @@ fn delta_b12u + Copy>( * weighted_sigma3_ij } -fn residual_virial_coefficient + Copy>( +fn residual_virial_coefficient + Copy>( p: &UVTheoryPars, x: &DVector, t: D, @@ -144,7 +144,7 @@ fn residual_virial_coefficient + Copy>( delta_b2bar } -fn correlation_integral_wca + Copy>( +fn correlation_integral_wca + Copy>( rho_x: D, mean_field_constant_x: D, rep_x: D, @@ -162,13 +162,13 @@ fn correlation_integral_wca + Copy>( /// U-fraction according to Barker-Henderson division. /// Eq. 15 -fn u_fraction_wca + Copy>(rep_x: D, reduced_density: D) -> D { +fn u_fraction_wca + Copy>(rep_x: D, reduced_density: D) -> D { (reduced_density * CU_WCA[0] + reduced_density.powi(2) * (rep_x.recip() * CU_WCA[2] + CU_WCA[1])) .tanh() } -pub(super) fn one_fluid_properties + Copy>( +pub(super) fn one_fluid_properties + Copy>( p: &UVTheoryPars, x: &DVector, t: D, @@ -209,7 +209,7 @@ pub(super) fn one_fluid_properties + Copy>( } // Coefficients for IWCA from eq. (S55) -fn coefficients_wca + Copy>(rep: D, att: D, d: D) -> [D; 6] { +fn coefficients_wca + Copy>(rep: D, att: D, d: D) -> [D; 6] { let rep_inv = rep.recip(); let rs_x = (rep / att).powd((rep - att).recip()); let tau_x = -d + rs_x; @@ -241,7 +241,7 @@ fn coefficients_wca + Copy>(rep: D, att: D, d: D) -> [D; 6] { [c1, c2, c3, c4, c5, c6] } -fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64, q: D) -> D { +fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64, q: D) -> D { let rm = (rep / att).powf(1.0 / (rep - att)); // Check mixing rule!! let rc = 5.0; let alpha = mean_field_constant(rep, att, rc); @@ -253,7 +253,7 @@ fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64, * PI } -fn y_eff + Copy>(reduced_temperature: D, rep: f64, att: f64) -> D { +fn y_eff + Copy>(reduced_temperature: D, rep: f64, att: f64) -> D { // optimize: move this part to parameter initialization let rc = 5.0; let rs = (rep / att).powf(1.0 / (rep - att)); diff --git a/crates/feos/src/uvtheory/eos/wca/attractive_perturbation_uvb3.rs b/crates/feos/src/uvtheory/eos/wca/attractive_perturbation_uvb3.rs index b6f1772de..b6d36f9fb 100644 --- a/crates/feos/src/uvtheory/eos/wca/attractive_perturbation_uvb3.rs +++ b/crates/feos/src/uvtheory/eos/wca/attractive_perturbation_uvb3.rs @@ -85,7 +85,7 @@ pub(super) struct AttractivePerturbationB3; impl AttractivePerturbationB3 { /// Helmholtz energy for attractive perturbation - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &UVTheoryPars, state: &StateHD, @@ -128,7 +128,7 @@ impl AttractivePerturbationB3 { } } -fn delta_b12u + Copy>( +fn delta_b12u + Copy>( t_x: D, mean_field_constant_x: D, weighted_sigma3_ij: D, @@ -141,7 +141,7 @@ fn delta_b12u + Copy>( * weighted_sigma3_ij } -fn residual_virial_coefficient + Copy>( +fn residual_virial_coefficient + Copy>( p: &UVTheoryPars, x: &DVector, t: D, @@ -163,7 +163,7 @@ fn residual_virial_coefficient + Copy>( delta_b2bar } -fn residual_third_virial_coefficient + Copy>( +fn residual_third_virial_coefficient + Copy>( p: &UVTheoryPars, x: &DVector, t: D, @@ -190,7 +190,7 @@ fn residual_third_virial_coefficient + Copy>( } delta_b3bar } -fn correlation_integral_wca + Copy>( +fn correlation_integral_wca + Copy>( rho_x: D, mean_field_constant_x: D, rep_x: D, @@ -207,7 +207,7 @@ fn correlation_integral_wca + Copy>( } /// U-fraction with low temperature correction omega -fn u_fraction_wca + Copy>(rep_x: D, reduced_density: D, t_x: D) -> D { +fn u_fraction_wca + Copy>(rep_x: D, reduced_density: D, t_x: D) -> D { let omega = if t_x.re() < 175.0 { (-t_x * CU_WCA[5] * (reduced_density - CU_WCA[6]).powi(2)).exp() * ((t_x * CU_WCA[7]).tanh().recip() - 1.0).powi(2) @@ -221,7 +221,7 @@ fn u_fraction_wca + Copy>(rep_x: D, reduced_density: D, t_x: D) } // Coefficients for IWCA -fn coefficients_wca + Copy>(rep: D, att: D, d: D) -> [D; 6] { +fn coefficients_wca + Copy>(rep: D, att: D, d: D) -> [D; 6] { let rep_inv = rep.recip(); let rs_x = (rep / att).powd((rep - att).recip()); let tau_x = -d + rs_x; @@ -259,7 +259,7 @@ fn factorial(num: u64) -> u64 { (1..=num).product() } -fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64, q: D) -> D { +fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64, q: D) -> D { let rm = (rep / att).powd((rep - att).recip()); let beta = reduced_temperature.recip(); let b20 = q.powi(3) * 2.0 / 3.0 * PI; @@ -293,7 +293,7 @@ fn delta_b2 + Copy>(reduced_temperature: D, rep: f64, att: f64, (b20 - rm.powi(3) * 2.0 / 3.0 * PI - c1) * y - sum_beta * c2 - beta * c3 - beta.powi(2) * c4 } -fn delta_b31u + Copy>( +fn delta_b31u + Copy>( t_x: D, weighted_sigma3_ij: D, rm_x: D, @@ -311,7 +311,7 @@ fn delta_b31u + Copy>( t_x.recip() * 4.0 * mie_prefactor(rep_x, att_x) * PI * k1 * weighted_sigma3_ij.powi(2) } -fn delta_b3 + Copy>( +fn delta_b3 + Copy>( t_x: D, rm_x: f64, rep_x: f64, diff --git a/crates/feos/src/uvtheory/eos/wca/hard_sphere.rs b/crates/feos/src/uvtheory/eos/wca/hard_sphere.rs index 4b7bd916c..a985e2f31 100644 --- a/crates/feos/src/uvtheory/eos/wca/hard_sphere.rs +++ b/crates/feos/src/uvtheory/eos/wca/hard_sphere.rs @@ -49,7 +49,7 @@ pub(super) const WCA_CONSTANTS_ETA_B_UVB3: [[f64; 2]; 3] = [ /// Dimensionless Hard-sphere diameter according to Weeks-Chandler-Andersen division. impl WeeksChandlerAndersen { - pub fn diameter_wca + Copy>( + pub fn diameter_wca + Copy>( parameters: &UVTheoryPars, temperature: D, ) -> DVector { @@ -73,7 +73,7 @@ impl WeeksChandlerAndersen { } } -pub(super) fn dimensionless_diameter_q_wca + Copy>( +pub(super) fn dimensionless_diameter_q_wca + Copy>( t_x: D, rep_x: D, att_x: D, @@ -103,7 +103,7 @@ pub(super) fn dimensionless_diameter_q_wca + Copy>( * rs } -pub(super) fn packing_fraction + Copy>( +pub(super) fn packing_fraction + Copy>( partial_density: &DVector, diameter: &DVector, ) -> D { @@ -113,7 +113,7 @@ pub(super) fn packing_fraction + Copy>( } #[inline] -pub(super) fn dimensionless_length_scale + Copy>( +pub(super) fn dimensionless_length_scale + Copy>( parameters: &UVTheoryPars, temperature: D, ) -> DVector { @@ -133,7 +133,7 @@ pub(super) fn dimensionless_length_scale + Copy>( } #[inline] -pub(super) fn packing_fraction_b + Copy>( +pub(super) fn packing_fraction_b + Copy>( parameters: &UVTheoryPars, eta: D, temperature: D, @@ -155,7 +155,7 @@ pub(super) fn packing_fraction_b + Copy>( }) } -pub(super) fn packing_fraction_b_uvb3 + Copy>( +pub(super) fn packing_fraction_b_uvb3 + Copy>( parameters: &UVTheoryPars, eta: D, temperature: D, @@ -177,7 +177,7 @@ pub(super) fn packing_fraction_b_uvb3 + Copy>( }) } -pub(super) fn packing_fraction_a + Copy>( +pub(super) fn packing_fraction_a + Copy>( parameters: &UVTheoryPars, eta: D, temperature: D, @@ -206,7 +206,7 @@ pub(super) fn packing_fraction_a + Copy>( }) } -pub(super) fn packing_fraction_a_uvb3 + Copy>( +pub(super) fn packing_fraction_a_uvb3 + Copy>( parameters: &UVTheoryPars, eta: D, temperature: D, diff --git a/crates/feos/src/uvtheory/eos/wca/mod.rs b/crates/feos/src/uvtheory/eos/wca/mod.rs index bcd76477f..5a26810f3 100644 --- a/crates/feos/src/uvtheory/eos/wca/mod.rs +++ b/crates/feos/src/uvtheory/eos/wca/mod.rs @@ -17,7 +17,7 @@ use reference_perturbation_uvb3::ReferencePerturbationB3; pub struct WeeksChandlerAndersen; impl WeeksChandlerAndersen { - pub fn residual_helmholtz_energy_contributions + Copy>( + pub fn residual_helmholtz_energy_contributions + Copy>( &self, parameters: &UVTheoryPars, state: &StateHD, @@ -42,7 +42,7 @@ impl WeeksChandlerAndersen { pub struct WeeksChandlerAndersenB3; impl WeeksChandlerAndersenB3 { - pub fn residual_helmholtz_energy_contributions + Copy>( + pub fn residual_helmholtz_energy_contributions + Copy>( &self, parameters: &UVTheoryPars, state: &StateHD, diff --git a/crates/feos/src/uvtheory/eos/wca/reference_perturbation.rs b/crates/feos/src/uvtheory/eos/wca/reference_perturbation.rs index a60551d40..84a046a28 100644 --- a/crates/feos/src/uvtheory/eos/wca/reference_perturbation.rs +++ b/crates/feos/src/uvtheory/eos/wca/reference_perturbation.rs @@ -12,7 +12,7 @@ pub(super) struct ReferencePerturbation; impl ReferencePerturbation { /// Helmholtz energy for perturbation reference (Mayer-f), eq. 29 - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &UVTheoryPars, state: &StateHD, diff --git a/crates/feos/src/uvtheory/eos/wca/reference_perturbation_uvb3.rs b/crates/feos/src/uvtheory/eos/wca/reference_perturbation_uvb3.rs index 525cbceb4..b1391feae 100644 --- a/crates/feos/src/uvtheory/eos/wca/reference_perturbation_uvb3.rs +++ b/crates/feos/src/uvtheory/eos/wca/reference_perturbation_uvb3.rs @@ -13,7 +13,7 @@ pub(super) struct ReferencePerturbationB3; impl ReferencePerturbationB3 { /// Helmholtz energy for perturbation reference (Mayer-f), eq. 29 - pub fn helmholtz_energy_density + Copy>( + pub fn helmholtz_energy_density + Copy>( &self, parameters: &UVTheoryPars, state: &StateHD, diff --git a/crates/feos/src/uvtheory/parameters.rs b/crates/feos/src/uvtheory/parameters.rs index 102360b59..67eda3a22 100644 --- a/crates/feos/src/uvtheory/parameters.rs +++ b/crates/feos/src/uvtheory/parameters.rs @@ -34,12 +34,12 @@ const CD_BH: SMatrix = matrix![ 3.71527116894441E-03, 5.05384813757953E-03, 4.91003312452622E-02]; #[inline] -pub fn mie_prefactor + Copy>(rep: D, att: D) -> D { +pub fn mie_prefactor + Copy>(rep: D, att: D) -> D { rep / (rep - att) * (rep / att).powd(att / (rep - att)) } #[inline] -pub fn mean_field_constant + Copy>(rep: D, att: D, x: D) -> D { +pub fn mean_field_constant + Copy>(rep: D, att: D, x: D) -> D { mie_prefactor(rep, att) * (x.powd(-att + 3.0) / (att - 3.0) - x.powd(-rep + 3.0) / (rep - 3.0)) } @@ -123,11 +123,11 @@ fn bh_coefficients(rep: f64, att: f64) -> [f64; 5] { } impl HardSphereProperties for UVTheoryPars { - fn monomer_shape + Copy>(&self, _: D) -> MonomerShape<'_, D> { + fn monomer_shape + Copy>(&self, _: D) -> MonomerShape<'_, D> { MonomerShape::Spherical(self.sigma.len()) } - fn hs_diameter + Copy>(&self, temperature: D) -> DVector { + fn hs_diameter + Copy>(&self, temperature: D) -> DVector { match self.perturbation { Perturbation::BarkerHenderson => BarkerHenderson::diameter_bh(self, temperature), Perturbation::WeeksChandlerAndersen => { diff --git a/docs/rustguide/core/equation_of_state.rst b/docs/rustguide/core/equation_of_state.rst index 92ec9bacf..c087dfaab 100644 --- a/docs/rustguide/core/equation_of_state.rst +++ b/docs/rustguide/core/equation_of_state.rst @@ -69,7 +69,7 @@ The non-object-safe trait to do that is the ``HelmholtzEnergyDual`` trait: .. code-block:: rust // This trait cannot be made into a trait object - pub trait HelmholtzEnergyDual> { + pub trait HelmholtzEnergyDual> { fn helmholtz_energy(&self, state: &StateHD) -> D; } @@ -105,7 +105,7 @@ The residual Helmholtz energy is then computed as sum of all contributions: /// /// For simple equations of state (see e.g. `PengRobinson`) it might be /// easier to overwrite this function instead of implementing `residual`. - fn evaluate_residual>(&self, state: &StateHD) -> D + fn evaluate_residual>(&self, state: &StateHD) -> D where dyn HelmholtzEnergy: HelmholtzEnergyDual, { @@ -156,7 +156,7 @@ The ``IdealGasContribution`` supertrait is assembled from ``IdealGasContribution /// Ideal gas Helmholtz energy contribution that can /// be evaluated using generalized (hyper) dual numbers. - pub trait IdealGasContributionDual> { + pub trait IdealGasContributionDual> { /// The thermal de Broglie wavelength of each component in the form $\ln\left(\frac{\Lambda^3}{\AA^3}\right)$ fn de_broglie_wavelength(&self, temperature: D, components: usize) -> Array1; diff --git a/docs/rustguide/core/state.rst b/docs/rustguide/core/state.rst index 29703ab8a..bdd58b5a9 100644 --- a/docs/rustguide/core/state.rst +++ b/docs/rustguide/core/state.rst @@ -102,7 +102,7 @@ The residual entropy is defined as We need the first derivative of the residual Helmholtz energy with respect to temperature. The first derivative can be computed using a dual number consisting of a real part and *one non-real part* which is a ``Dual64`` struct. -Looking at the signature of the Helmholtz energy function, we see that it takes a ``StateHD`` as input (not a ``State``), where ``D`` is a generalized dual number (``D: DualNum``): +Looking at the signature of the Helmholtz energy function, we see that it takes a ``StateHD`` as input (not a ``State``), where ``D`` is a generalized dual number (``D: DualNum``): .. code-block:: rust @@ -121,7 +121,7 @@ Looking at the signature of the Helmholtz energy function, we see that it takes /// Properties are stored as generalized (hyper) dual numbers which allows /// for automatic differentiation. #[derive(Clone, Debug)] - pub struct StateHD> { + pub struct StateHD> { /// temperature in Kelvin pub temperature: D, /// volume in Angstrom^3 diff --git a/py-feos/src/ad/mod.rs b/py-feos/src/ad/mod.rs index c18525a68..fb257d559 100644 --- a/py-feos/src/ad/mod.rs +++ b/py-feos/src/ad/mod.rs @@ -482,8 +482,8 @@ macro_rules! impl_evaluate_gradients { Bound<'py, PyArray1>, ) where - $(R::Lifted>: Sync,)* - R::Lifted>: Sync + $(R::Lifted>: Sync,)* + R::Lifted>: Sync { let (value, grad, status) = if let Ok(pars) = parameters.extract::>() { diff --git a/py-feos/src/user_defined.rs b/py-feos/src/user_defined.rs index 12792059d..a429bd5d9 100644 --- a/py-feos/src/user_defined.rs +++ b/py-feos/src/user_defined.rs @@ -30,7 +30,7 @@ macro_rules! impl_ideal_gas { "Ideal gas (Python)" } - fn ln_lambda3 + Copy>(&self, temperature: D) -> D { + fn ln_lambda3 + Copy>(&self, temperature: D) -> D { let mut result = D::zero(); $( @@ -103,7 +103,7 @@ macro_rules! impl_residual { }) } - fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { + fn compute_max_density + Copy>(&self, molefracs: &DVector) -> D { let mut rho = D::zero(); $( @@ -123,7 +123,7 @@ macro_rules! impl_residual { panic!("compute_max_density: input data type not understood") } - fn reduced_helmholtz_energy_density_contributions + Copy + >( + fn reduced_helmholtz_energy_density_contributions + Copy + >( &self, state: &StateHD, ) -> Vec<(&'static str, D)> { @@ -250,7 +250,7 @@ macro_rules! impl_dual_state_helmholtz_energy { // No definition of dual number necessary for f64 state!(PyStateF, f64, f64); -dual_number!(PyReal64, Real, f64); +dual_number!(PyReal64, Real, f64); impl_dual_state_helmholtz_energy!(PyStateD, PyDual64, Dual64, f64); @@ -258,73 +258,63 @@ dual_number!(PyDualVec3, DualSVec64<3>, f64); impl_dual_state_helmholtz_energy!( PyStateDualDualVec3, PyDualDualVec3, - Dual, f64>, + Dual>, PyDualVec3 ); impl_dual_state_helmholtz_energy!(PyStateHD, PyHyperDual64, HyperDual64, f64); impl_dual_state_helmholtz_energy!(PyStateD2, PyDual2_64, Dual2_64, f64); impl_dual_state_helmholtz_energy!(PyStateD2Vec2, PyDual2SVec64_2, Dual2SVec64<2>, f64); impl_dual_state_helmholtz_energy!(PyStateD3, PyDual3_64, Dual3_64, f64); -impl_dual_state_helmholtz_energy!(PyStateHDD, PyHyperDualDual64, HyperDual, PyDual64); +impl_dual_state_helmholtz_energy!(PyStateHDD, PyHyperDualDual64, HyperDual, PyDual64); dual_number!(PyDualVec2, DualSVec64<2>, f64); impl_dual_state_helmholtz_energy!( PyStateHDDVec2, PyHyperDualVec2, - HyperDual, f64>, + HyperDual>, PyDualVec2 ); impl_dual_state_helmholtz_energy!( PyStateHDDVec3, PyHyperDualVec3, - HyperDual, f64>, + HyperDual>, PyDualVec3 ); -impl_dual_state_helmholtz_energy!( - PyStateD2D, - PyDual2Dual64, - Dual2, - PyDual64 -); -impl_dual_state_helmholtz_energy!( - PyStateD3D, - PyDual3Dual64, - Dual3, - PyDual64 -); +impl_dual_state_helmholtz_energy!(PyStateD2D, PyDual2Dual64, Dual2, PyDual64); +impl_dual_state_helmholtz_energy!(PyStateD3D, PyDual3Dual64, Dual3, PyDual64); impl_dual_state_helmholtz_energy!( PyStateD3DVec2, PyDual3DualVec2, - Dual3, f64>, + Dual3>, PyDualVec2 ); impl_dual_state_helmholtz_energy!( PyStateD3DVec3, PyDual3DualVec3, - Dual3, f64>, + Dual3>, PyDualVec3 ); impl_ideal_gas!( - PyReal64, Real; + PyReal64, Real; PyDual64, Dual64; PyDualDualVec3, - Dual, f64>; + Dual>; PyHyperDual64, HyperDual64; PyDual2_64, Dual2_64; PyDual3_64, Dual3_64; - PyHyperDualDual64, HyperDual; + PyHyperDualDual64, HyperDual; PyHyperDualVec2, - HyperDual, f64>; + HyperDual>; PyHyperDualVec3, - HyperDual, f64>; + HyperDual>; PyDual2Dual64, - Dual2; + Dual2; PyDual3Dual64, - Dual3; + Dual3; PyDual3DualVec2, - Dual3, f64>; + Dual3>; PyDual3DualVec3, - Dual3, f64> + Dual3> ); impl_residual!( @@ -332,28 +322,28 @@ impl_residual!( PyStateD, PyDual64, Dual64; PyStateDualDualVec3, PyDualDualVec3, - Dual, f64>; + Dual>; PyStateHD, PyHyperDual64, HyperDual64; PyStateD2, PyDual2_64, Dual2_64; PyStateD2Vec2, PyDual2SVec64_2, Dual2SVec64<2>; PyStateD3, PyDual3_64, Dual3_64; - PyStateHDD, PyHyperDualDual64, HyperDual; + PyStateHDD, PyHyperDualDual64, HyperDual; PyStateHDDVec2, PyHyperDualVec2, - HyperDual, f64>; + HyperDual>; PyStateHDDVec3, PyHyperDualVec3, - HyperDual, f64>; + HyperDual>; PyStateD2D, PyDual2Dual64, - Dual2; + Dual2; PyStateD3D, PyDual3Dual64, - Dual3; + Dual3; PyStateD3DVec2, PyDual3DualVec2, - Dual3, f64>; + Dual3>; PyStateD3DVec3, PyDual3DualVec3, - Dual3, f64> + Dual3> ); From 5af3237ee6ba135299cc5ca3ee9e51003476a422 Mon Sep 17 00:00:00 2001 From: Philipp Rehner <69816385+prehner@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:50:43 +0200 Subject: [PATCH 6/6] Fix calculation of bulk state when moles are specified (#382) --- crates/feos-dft/src/adsorption/pore.rs | 1 + crates/feos-dft/src/convolver/mod.rs | 61 +----- .../src/convolver/periodic_convolver.rs | 16 +- crates/feos-dft/src/geometry.rs | 14 +- crates/feos-dft/src/profile/mod.rs | 174 ++++++++++-------- crates/feos-dft/src/profile/properties.rs | 17 +- crates/feos-dft/src/solver.rs | 38 ++-- py-feos/src/dft/adsorption/pore.rs | 7 + py-feos/src/dft/profile.rs | 6 +- 9 files changed, 156 insertions(+), 178 deletions(-) diff --git a/crates/feos-dft/src/adsorption/pore.rs b/crates/feos-dft/src/adsorption/pore.rs index fc3cf93be..b5d37630c 100644 --- a/crates/feos-dft/src/adsorption/pore.rs +++ b/crates/feos-dft/src/adsorption/pore.rs @@ -162,6 +162,7 @@ where } pub fn update_bulk(mut self, bulk: &State) -> Self { + self.profile.specification = DFTSpecification::from_state(bulk); self.profile.bulk = bulk.clone(); self.grand_potential = None; self.interfacial_tension = None; diff --git a/crates/feos-dft/src/convolver/mod.rs b/crates/feos-dft/src/convolver/mod.rs index c047bcd0e..540ce4d97 100644 --- a/crates/feos-dft/src/convolver/mod.rs +++ b/crates/feos-dft/src/convolver/mod.rs @@ -1,10 +1,8 @@ use crate::geometry::{Axis, Geometry, Grid}; use crate::weight_functions::*; -use ndarray::linalg::Dot; use ndarray::prelude::*; use ndarray::{Axis as Axis_nd, RemoveAxis, Slice}; use num_dual::*; -use num_traits::Zero; use rustdct::DctNum; use std::ops::{AddAssign, MulAssign, SubAssign}; use std::sync::Arc; @@ -36,46 +34,6 @@ pub trait Convolver: Send + Sync { ) -> Array; } -pub(crate) struct BulkConvolver { - weight_constants: Vec>, -} - -impl + Copy + Send + Sync> BulkConvolver { - #[expect(clippy::new_ret_no_self)] - pub(crate) fn new(weight_functions: Vec>) -> Arc> { - let weight_constants = weight_functions - .into_iter() - .map(|w| w.weight_constants(Zero::zero(), 0)) - .collect(); - Arc::new(Self { weight_constants }) - } -} - -impl + Copy + Send + Sync> Convolver for BulkConvolver -where - Array2: Dot, Output = Array1>, -{ - fn convolve(&self, _: Array0, _: &WeightFunction) -> Array0 { - unreachable!() - } - - fn weighted_densities(&self, density: &Array1) -> Vec> { - self.weight_constants - .iter() - .map(|w| w.dot(density)) - .collect() - } - - fn functional_derivative(&self, partial_derivatives: &[Array1]) -> Array1 { - self.weight_constants - .iter() - .zip(partial_derivatives.iter()) - .map(|(w, pd)| pd.dot(w)) - .reduce(|a, b| a + b) - .unwrap() - } -} - /// Base structure to hold either information about the weight function through /// `WeightFunctionInfo` or the weight functions themselves via /// `FFTWeightFunctions`. @@ -125,11 +83,10 @@ pub struct ConvolverFFT { cartesian_transforms: Vec>, } -impl ConvolverFFT +impl ConvolverFFT where T: DctNum + DualNum, D::Larger: Dimension, - D::Smaller: Dimension, ::Larger: Dimension, { /// Create the appropriate FFT convolver for the given grid. @@ -139,9 +96,11 @@ where lanczos: Option, ) -> Arc> { match grid { + Grid::Bulk => PeriodicConvolver::new_0d(weight_functions), Grid::Polar(r) => CurvilinearConvolver::new(r, &[], weight_functions, lanczos), Grid::Spherical(r) => CurvilinearConvolver::new(r, &[], weight_functions, lanczos), Grid::Cartesian1(z) => Self::new(Some(z), &[], weight_functions, lanczos), + Grid::Periodical1(z) => PeriodicConvolver::new_1d(z, weight_functions, lanczos), Grid::Cylindrical { r, z } => { CurvilinearConvolver::new(r, &[z], weight_functions, lanczos) } @@ -553,11 +512,10 @@ struct CurvilinearConvolver { convolver_boundary: Arc>, } -impl CurvilinearConvolver +impl CurvilinearConvolver where T: DctNum + DualNum, D::Larger: Dimension, - D::Smaller: Dimension, ::Larger: Dimension, { #[expect(clippy::new_ret_no_self)] @@ -574,10 +532,9 @@ where } } -impl Convolver for CurvilinearConvolver +impl Convolver for CurvilinearConvolver where T: DctNum + DualNum, - D::Smaller: Dimension, D::Larger: Dimension, { fn convolve( @@ -587,9 +544,9 @@ where ) -> Array { // subtract boundary profile from full profile let profile_boundary = profile - .index_axis(Axis(0), profile.shape()[0] - 1) + .slice_axis(Axis(0), Slice::from(profile.shape()[0] - 1..)) .into_owned(); - for mut lane in profile.outer_iter_mut() { + for mut lane in profile.axis_chunks_iter_mut(Axis(0), 1) { lane.sub_assign(&profile_boundary); } @@ -597,14 +554,12 @@ where let mut result = self.convolver.convolve(profile, weight_function); // convolve boundary profile - let profile_boundary = profile_boundary.insert_axis(Axis(0)); let result_boundary = self .convolver_boundary .convolve(profile_boundary, weight_function); // Add boundary result back to full result - let result_boundary = result_boundary.index_axis(Axis(0), 0); - for mut lane in result.outer_iter_mut() { + for mut lane in result.axis_chunks_iter_mut(Axis(0), 1) { lane.add_assign(&result_boundary); } result diff --git a/crates/feos-dft/src/convolver/periodic_convolver.rs b/crates/feos-dft/src/convolver/periodic_convolver.rs index dfb12ff06..7678797a7 100644 --- a/crates/feos-dft/src/convolver/periodic_convolver.rs +++ b/crates/feos-dft/src/convolver/periodic_convolver.rs @@ -30,6 +30,18 @@ where D::Larger: Dimension, ::Larger: Dimension, { + pub fn new_0d(weight_functions: &[WeightFunctionInfo]) -> Arc> { + Self::new(&[], |_| {}, weight_functions, None) + } + + pub fn new_1d( + axis: &Axis, + weight_functions: &[WeightFunctionInfo], + lanczos: Option, + ) -> Arc> { + Self::new(&[axis], |_| {}, weight_functions, lanczos) + } + pub fn new_2d( axes: &[&Axis], angle: Angle, @@ -200,7 +212,7 @@ impl PeriodicConvolver { } fn forward_transform(&self, f: ArrayView) -> Array, D2> { - let offset = D2::NDIM.unwrap() - D::NDIM.unwrap(); + let offset = f.ndim() - self.k_abs.ndim(); let mut result = f.mapv(Complex::from); for (i, transform) in self.forward_transforms.iter().enumerate() { for r in result.lanes_mut(Axis_nd(i + offset)).into_iter() { @@ -211,7 +223,7 @@ impl PeriodicConvolver { } fn inverse_transform(&self, mut f: Array, D2>) -> Array { - let offset = D2::NDIM.unwrap() - D::NDIM.unwrap(); + let offset = f.ndim() - self.k_abs.ndim(); for (i, transform) in self.inverse_transforms.iter().enumerate() { for r in f.lanes_mut(Axis_nd(i + offset)).into_iter() { self.transform(transform, r); diff --git a/crates/feos-dft/src/geometry.rs b/crates/feos-dft/src/geometry.rs index e69409e9b..b4a14f5d1 100644 --- a/crates/feos-dft/src/geometry.rs +++ b/crates/feos-dft/src/geometry.rs @@ -6,7 +6,9 @@ use std::f64::consts::{FRAC_PI_3, PI}; /// Grids with up to three dimensions. #[derive(Clone)] pub enum Grid { + Bulk, Cartesian1(Axis), + Periodical1(Axis), Cartesian2(Axis, Axis), Periodical2(Axis, Axis, Angle), Cartesian3(Axis, Axis, Axis), @@ -27,7 +29,8 @@ impl Grid { pub fn axes(&self) -> Vec<&Axis> { match self { - Self::Cartesian1(x) => vec![x], + Self::Bulk => vec![], + Self::Cartesian1(x) | Self::Periodical1(x) => vec![x], Self::Cartesian2(x, y) | Self::Periodical2(x, y, _) => vec![x, y], Self::Cartesian3(x, y, z) | Self::Periodical3(x, y, z, _) => vec![x, y, z], Self::Spherical(r) | Self::Polar(r) => vec![r], @@ -37,7 +40,8 @@ impl Grid { pub fn axes_mut(&mut self) -> Vec<&mut Axis> { match self { - Self::Cartesian1(x) => vec![x], + Self::Bulk => vec![], + Self::Cartesian1(x) | Self::Periodical1(x) => vec![x], Self::Cartesian2(x, y) | Self::Periodical2(x, y, _) => vec![x, y], Self::Cartesian3(x, y, z) | Self::Periodical3(x, y, z, _) => vec![x, y, z], Self::Spherical(r) | Self::Polar(r) => vec![r], @@ -72,7 +76,11 @@ impl Grid { pub fn mesh(&self) -> Vec>> { match self { - Grid::Cartesian1(ax) | Grid::Spherical(ax) | Grid::Polar(ax) => { + Self::Bulk => vec![], + Grid::Cartesian1(ax) + | Self::Periodical1(ax) + | Grid::Spherical(ax) + | Grid::Polar(ax) => { vec![Length::from_reduced(ax.grid.clone()).into_dyn()] } Grid::Cartesian2(u, v) => mesh_2d(u, v, 90.0 * DEGREES), diff --git a/crates/feos-dft/src/profile/mod.rs b/crates/feos-dft/src/profile/mod.rs index 37f5e3aa9..6def8ded2 100644 --- a/crates/feos-dft/src/profile/mod.rs +++ b/crates/feos-dft/src/profile/mod.rs @@ -1,10 +1,10 @@ -use crate::convolver::{BulkConvolver, Convolver, ConvolverFFT}; +use crate::convolver::{Convolver, ConvolverFFT, PeriodicConvolver}; use crate::functional::HelmholtzEnergyFunctional; use crate::geometry::Grid; use crate::solver::{DFTSolver, DFTSolverLog}; use feos_core::{FeosError, FeosResult, ReferenceSystem, State}; use nalgebra::{DVector, Dyn, U1}; -use ndarray::{Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, RemoveAxis}; +use ndarray::{Array, Array1, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix0, arr1}; use num_dual::DualNum; use quantity::{_Volume, Density, Energy, Entropy, Length, Moles, Quantity, Temperature, Volume}; use std::ops::{Add, MulAssign}; @@ -22,7 +22,7 @@ const MAX_POTENTIAL: f64 = 50.0; #[derive(Clone)] pub enum DFTSpecification { /// DFT with specified chemical potential. - ChemicalPotential, + ChemicalPotential(Array1), /// DFT with specified number of particles. /// /// The solution is still a grand canonical density profile, but the chemical @@ -30,21 +30,42 @@ pub enum DFTSpecification { /// with the specified number of particles. Moles(Array1), /// DFT with specified total number of moles. - TotalMoles(f64), + TotalMoles(f64, Array1), } impl DFTSpecification { - fn calculate_bulk_density( - &self, - bulk_density: &Array1, - z: &Array1, - ) -> FeosResult> { + fn calculate_fugacity(&self, z: &Array1) -> FeosResult> { Ok(match self { - Self::ChemicalPotential => bulk_density.clone(), + Self::ChemicalPotential(fugacity) => fugacity.clone(), Self::Moles(moles) => moles / z, - Self::TotalMoles(total_moles) => bulk_density * *total_moles / (bulk_density * z).sum(), + Self::TotalMoles(total_moles, fugacity) => { + fugacity * *total_moles / (fugacity * z).sum() + } }) } + + pub fn from_state(state: &State) -> Self { + let component_index = state.eos.component_index().into_owned(); + let m = arr1(&state.eos.m()); + let partial_density = state.partial_density().into_reduced(); + let temperature = state.temperature.into_reduced(); + let bulk_density = component_index + .iter() + .map(|&i| partial_density[i]) + .collect(); + let bulk_convolver = + PeriodicConvolver::<_, Ix0>::new_0d(&state.eos.weight_functions(temperature)); + let (_, dfdrho_bulk) = state + .eos + .functional_derivative(temperature, &bulk_density, bulk_convolver.as_ref()) + .unwrap(); + let exp_dfdrho = (dfdrho_bulk / m).mapv(f64::exp); + let bonds = state + .eos + .bond_integrals(temperature, &exp_dfdrho, bulk_convolver.as_ref()); + let fugacity = bulk_density * exp_dfdrho * bonds; + Self::ChemicalPotential(fugacity) + } } #[derive(Clone)] @@ -80,10 +101,9 @@ impl DFTProfile { } } -impl DFTProfile +impl DFTProfile where D::Larger: Dimension, - D::Smaller: Dimension, ::Larger: Dimension, { /// Create a new density profile. @@ -147,7 +167,7 @@ where convolver, temperature: bulk.temperature, density, - specification: DFTSpecification::ChemicalPotential, + specification: DFTSpecification::from_state(bulk), external_potential, bulk: bulk.clone(), solver_log: None, @@ -167,7 +187,12 @@ where pub fn fix_total_moles(&mut self) { let rho = self.density.to_reduced(); let moles = self.integrate_reduced_comp(&rho).sum(); - self.specification = DFTSpecification::TotalMoles(moles); + let DFTSpecification::ChemicalPotential(fugacity) = + DFTSpecification::from_state(&self.bulk) + else { + unreachable!() + }; + self.specification = DFTSpecification::TotalMoles(moles, fugacity); } /// Return the external potential in SI units. @@ -293,37 +318,17 @@ where .weighted_densities(&self.density.to_reduced())) } - #[expect(clippy::type_complexity)] - pub fn residual(&self, log: bool) -> FeosResult<(Array, Array1, f64)> { - // Read from profile + pub fn residual(&self, log: bool) -> FeosResult<(Array, f64)> { let density = self.density.to_reduced(); - let partial_density = self.bulk.partial_density().into_reduced(); - let mut bulk_density = self - .bulk - .eos - .component_index() - .iter() - .map(|&i| partial_density[i]) - .collect(); - - let (res, res_bulk, res_norm, _, _) = - self.euler_lagrange_equation(&density, &mut bulk_density, log)?; - Ok((res, res_bulk, res_norm)) + let (res, res_norm, _, _) = self.euler_lagrange_equation(&density, log)?; + Ok((res, res_norm)) } #[expect(clippy::type_complexity)] - pub(crate) fn euler_lagrange_equation( + fn fugacity( &self, density: &Array, - bulk_density: &mut Array1, - log: bool, - ) -> FeosResult<( - Array, - Array1, - f64, - Array, - Array, - )> { + ) -> FeosResult<(Array, Array, Array1)> { // calculate reduced temperature let temperature = self.temperature.to_reduced(); @@ -336,21 +341,10 @@ where // calculate total functional derivative dfdrho += &self.external_potential; - // calculate bulk functional derivative - let bulk_convolver = BulkConvolver::new(self.bulk.eos.weight_functions(temperature)); - let (_, dfdrho_bulk) = self.bulk.eos.functional_derivative( - temperature, - bulk_density, - bulk_convolver.as_ref(), - )?; dfdrho .outer_iter_mut() - .zip(dfdrho_bulk) .zip(self.bulk.eos.m().iter()) - .for_each(|((mut df, df_b), &m)| { - df -= df_b; - df /= m - }); + .for_each(|(mut df, &m)| df /= m); // calculate bond integrals let exp_dfdrho = dfdrho.mapv(|x| (-x).exp()); @@ -358,22 +352,36 @@ where .bulk .eos .bond_integrals(temperature, &exp_dfdrho, self.convolver.as_ref()); - let mut rho_projected = &exp_dfdrho * bonds; + let z = &exp_dfdrho * bonds; - // calculate bulk density based on the given specification - let res_bulk = &*bulk_density - - self.specification.calculate_bulk_density( - bulk_density, - &self.integrate_reduced_comp(&rho_projected), - )?; - *bulk_density -= &res_bulk; + // calculate fugacity based on the given specification + let fugacity = self + .specification + .calculate_fugacity(&self.integrate_reduced_comp(&z))?; - // multiply bulk density + Ok((exp_dfdrho, z, fugacity)) + } + + #[expect(clippy::type_complexity)] + pub(crate) fn euler_lagrange_equation( + &self, + density: &Array, + log: bool, + ) -> FeosResult<( + Array, + f64, + Array, + Array, + )> { + // calculate functional derivatives and fugacity + let (exp_dfdrho, mut rho_projected, fugacity) = self.fugacity(density)?; + + // multiply fugacity rho_projected .outer_iter_mut() - .zip(bulk_density.iter()) - .for_each(|(mut x, &rho_b)| { - x *= rho_b; + .zip(fugacity.iter()) + .for_each(|(mut x, &f)| { + x *= f; }); // calculate residual @@ -394,7 +402,7 @@ where (density - &rho_projected).mapv(|x| x * x).sum().sqrt() / (res.len() as f64).sqrt(); if res_norm.is_finite() { - Ok((res, res_bulk, res_norm, exp_dfdrho, rho_projected)) + Ok((res, res_norm, exp_dfdrho, rho_projected)) } else { Err(FeosError::IterationFailed("Euler-Lagrange equation".into())) } @@ -405,25 +413,35 @@ where let solver = solver.cloned().unwrap_or_default(); // Read from profile - let component_index = self.bulk.eos.component_index().into_owned(); let mut density = self.density.to_reduced(); - let partial_density = self.bulk.partial_density().into_reduced(); - let mut bulk_density = component_index - .iter() - .map(|&i| partial_density[i]) - .collect(); // Call solver(s) - self.call_solver(&mut density, &mut bulk_density, &solver, debug)?; + self.call_solver(&mut density, &solver, debug)?; + + // Update bulk state + if !matches!(self.specification, DFTSpecification::ChemicalPotential(_)) { + // solve a bulk profile with the Newton solver + let mut bulk_profile = + DFTProfile::::new(Grid::Bulk, &self.bulk, None, None, None); + let (_, _, fugacity) = self.fugacity(&density)?; + bulk_profile.specification = DFTSpecification::ChemicalPotential(fugacity); + let solver = DFTSolver::new(None).newton(None, None, None, None); + bulk_profile.solve(Some(&solver), false)?; + + // create the state based on the results from the bulk profile + let component_index = self.bulk.eos.component_index(); + let mut partial_density = self.bulk.partial_density(); + bulk_profile + .density + .into_iter() + .enumerate() + .for_each(|(i, r)| partial_density.set(component_index[i], r)); + self.bulk = State::new_density(&self.bulk.eos, self.bulk.temperature, partial_density)?; + } // Update profile self.density = Density::from_reduced(density); - let mut partial_density = self.bulk.partial_density(); - bulk_density - .into_iter() - .enumerate() - .for_each(|(i, r)| partial_density.set(component_index[i], Density::from_reduced(r))); - self.bulk = State::new_density(&self.bulk.eos, self.bulk.temperature, partial_density)?; + Ok(()) } } diff --git a/crates/feos-dft/src/profile/properties.rs b/crates/feos-dft/src/profile/properties.rs index 1c0427b28..8e602b34b 100644 --- a/crates/feos-dft/src/profile/properties.rs +++ b/crates/feos-dft/src/profile/properties.rs @@ -1,11 +1,11 @@ #![allow(type_alias_bounds)] use super::DFTProfile; -use crate::convolver::{BulkConvolver, Convolver}; +use crate::convolver::{Convolver, PeriodicConvolver}; use crate::functional_contribution::FunctionalContribution; use crate::{ConvolverFFT, DFTSolverLog, HelmholtzEnergyFunctional, WeightFunctionInfo}; use feos_core::{Contributions, FeosResult, ReferenceSystem, Total, Verbosity}; use nalgebra::{DMatrix, DVector}; -use ndarray::{Array, Array1, Axis, Dimension, RemoveAxis}; +use ndarray::{Array, Array1, Axis, Dimension, Ix0, RemoveAxis}; use num_dual::{Dual64, DualNum}; use quantity::{ Density, Energy, Entropy, EntropyDensity, MolarEnergy, Moles, Pressure, Quantity, Temperature, @@ -298,17 +298,8 @@ where { fn density_derivative(&self, lhs: &Array) -> FeosResult> { let rho = self.density.to_reduced(); - let partial_density = self.bulk.partial_density().into_reduced(); - let mut rho_bulk = self - .bulk - .eos - .component_index() - .iter() - .map(|&i| partial_density[i]) - .collect(); - let second_partial_derivatives = self.second_partial_derivatives(&rho)?; - let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &mut rho_bulk, false)?; + let (_, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, false)?; let rhs = |x: &_| { let delta_functional_derivative = @@ -411,7 +402,7 @@ where .map(|&i| partial_density[i]) .collect(); let rho_bulk_dual = rho_bulk.mapv(Dual64::from); - let bulk_convolver = BulkConvolver::new(weight_functions); + let bulk_convolver = PeriodicConvolver::<_, Ix0>::new_0d(&weight_functions); let (_, dfdrho_bulk) = self.bulk .eos diff --git a/crates/feos-dft/src/solver.rs b/crates/feos-dft/src/solver.rs index 69d2e2757..2baba60fb 100644 --- a/crates/feos-dft/src/solver.rs +++ b/crates/feos-dft/src/solver.rs @@ -216,7 +216,6 @@ where pub(crate) fn call_solver( &mut self, rho: &mut Array, - rho_bulk: &mut Array1, solver: &DFTSolver, debug: bool, ) -> FeosResult<()> { @@ -225,13 +224,11 @@ where let mut log = DFTSolverLog::new(solver.verbosity); for algorithm in &solver.algorithms { let (conv, iter) = match algorithm { - DFTAlgorithm::PicardIteration(picard) => { - self.solve_picard(*picard, rho, rho_bulk, &mut log) - } + DFTAlgorithm::PicardIteration(picard) => self.solve_picard(*picard, rho, &mut log), DFTAlgorithm::AndersonMixing(anderson) => { - self.solve_anderson(*anderson, rho, rho_bulk, &mut log) + self.solve_anderson(*anderson, rho, &mut log) } - DFTAlgorithm::Newton(newton) => self.solve_newton(*newton, rho, rho_bulk, &mut log), + DFTAlgorithm::Newton(newton) => self.solve_newton(*newton, rho, &mut log), }?; converged = conv; iterations += iter; @@ -255,7 +252,6 @@ where &self, picard: PicardIteration, rho: &mut Array, - rho_bulk: &mut Array1, log: &mut DFTSolverLog, ) -> FeosResult<(bool, usize)> { let solver = if picard.log { @@ -266,8 +262,7 @@ where for k in 0..picard.max_iter { // calculate residual - let (res, _, res_norm, _, _) = - self.euler_lagrange_equation(&*rho, rho_bulk, picard.log)?; + let (res, res_norm, _, _) = self.euler_lagrange_equation(&*rho, picard.log)?; log.add_residual(solver, k, res_norm); // check for convergence @@ -276,10 +271,9 @@ where } // apply line search or constant damping - let damping_coefficient = picard.damping_coefficient.map_or_else( - || self.line_search(rho, &res, rho_bulk, res_norm, picard.log), - Ok, - )?; + let damping_coefficient = picard + .damping_coefficient + .map_or_else(|| self.line_search(rho, &res, res_norm, picard.log), Ok)?; // update solution if picard.log { @@ -295,7 +289,6 @@ where &self, rho: &Array, delta_rho: &Array, - rho_bulk: &mut Array1, res0: f64, logarithm: bool, ) -> FeosResult { @@ -311,9 +304,7 @@ where } else { rho + alpha * delta_rho }; - let Ok((_, _, res2, _, _)) = - self.euler_lagrange_equation(&rho_new, rho_bulk, logarithm) - else { + let Ok((_, res2, _, _)) = self.euler_lagrange_equation(&rho_new, logarithm) else { continue; }; if res2 > res0 { @@ -326,9 +317,7 @@ where } else { rho + 0.5 * alpha * delta_rho }; - let Ok((_, _, res1, _, _)) = - self.euler_lagrange_equation(&rho_new, rho_bulk, logarithm) - else { + let Ok((_, res1, _, _)) = self.euler_lagrange_equation(&rho_new, logarithm) else { continue; }; @@ -359,7 +348,6 @@ where &self, anderson: AndersonMixing, rho: &mut Array, - rho_bulk: &mut Array1, log: &mut DFTSolverLog, ) -> FeosResult<(bool, usize)> { let solver = if anderson.log { @@ -382,8 +370,7 @@ where let m = resm.len() + 1; // calculate residual - let (res, _, res_norm, _, _) = - self.euler_lagrange_equation(&*rho, rho_bulk, anderson.log)?; + let (res, res_norm, _, _) = self.euler_lagrange_equation(&*rho, anderson.log)?; log.add_residual(solver, k, res_norm); // check for convergence @@ -434,14 +421,13 @@ where &self, newton: Newton, rho: &mut Array, - rho_bulk: &mut Array1, log: &mut DFTSolverLog, ) -> FeosResult<(bool, usize)> { let solver = if newton.log { "Newton (log)" } else { "Newton" }; for k in 0..newton.max_iter { // calculate initial residual - let (res, _, res_norm, exp_dfdrho, rho_p) = - self.euler_lagrange_equation(rho, rho_bulk, newton.log)?; + let (res, res_norm, exp_dfdrho, rho_p) = + self.euler_lagrange_equation(rho, newton.log)?; log.add_residual(solver, k, res_norm); // check convergence diff --git a/py-feos/src/dft/adsorption/pore.rs b/py-feos/src/dft/adsorption/pore.rs index ab66684e2..d07c75b39 100644 --- a/py-feos/src/dft/adsorption/pore.rs +++ b/py-feos/src/dft/adsorption/pore.rs @@ -28,6 +28,13 @@ impl PyGrid { Self(Grid::Cartesian1(x)) } + /// Generate a 1D Cartesian grid with periodic boundary conditions on both sides. + #[staticmethod] + pub fn periodical_1d(n_points: usize, length: Length) -> Self { + let x = AxisDFT::new_cartesian(n_points, length, None); + Self(Grid::Periodical1(x)) + } + /// Generate a polar grid with radial axis. #[staticmethod] pub fn polar(n_points: usize, length: Length) -> Self { diff --git a/py-feos/src/dft/profile.rs b/py-feos/src/dft/profile.rs index 40104de89..e1acef783 100644 --- a/py-feos/src/dft/profile.rs +++ b/py-feos/src/dft/profile.rs @@ -18,9 +18,9 @@ macro_rules! impl_profile { &self, log: bool, py: Python<'py>, - ) -> PyResult<(Bound<'py, PyArrayDyn>, Bound<'py, PyArray1>, f64)> { - let (res_rho, res_mu, res_norm) = self.0.profile.residual(log).map_err(PyFeosError::from)?; - Ok((res_rho.view().into_dyn().to_pyarray(py), res_mu.view().to_pyarray(py), res_norm)) + ) -> PyResult<(Bound<'py, PyArrayDyn>, f64)> { + let (res_rho, res_norm) = self.0.profile.residual(log).map_err(PyFeosError::from)?; + Ok((res_rho.view().into_dyn().to_pyarray(py), res_norm)) } /// Solve the profile in-place. A non-default solver can be provided