|
| 1 | +use super::{A0, A1, A2, AD, B0, B1, B2, BD, CD, MAX_ETA}; |
| 2 | +use crate::{ParametersAD, ResidualHelmholtzEnergy}; |
| 3 | +use nalgebra::SVector; |
| 4 | +use num_dual::{jacobian, DualNum, DualVec}; |
| 5 | +use std::f64::consts::{FRAC_PI_6, PI}; |
| 6 | + |
| 7 | +const PI_SQ_43: f64 = 4.0 / 3.0 * PI * PI; |
| 8 | + |
| 9 | +pub struct PcSaftBinary { |
| 10 | + parameters: [[f64; 8]; 2], |
| 11 | + kij: f64, |
| 12 | +} |
| 13 | + |
| 14 | +impl PcSaftBinary { |
| 15 | + pub fn new(parameters: [[f64; 8]; 2], kij: f64) -> Self { |
| 16 | + Self { parameters, kij } |
| 17 | + } |
| 18 | +} |
| 19 | + |
| 20 | +impl ParametersAD for PcSaftBinary { |
| 21 | + type Parameters<D: DualNum<f64> + Copy> = ([[D; 8]; 2], D); |
| 22 | + |
| 23 | + fn params<D: DualNum<f64> + Copy>(&self) -> Self::Parameters<D> { |
| 24 | + (self.parameters.map(|p| p.map(D::from)), D::from(self.kij)) |
| 25 | + } |
| 26 | + |
| 27 | + fn params_from_inner<D: DualNum<f64> + Copy, D2: DualNum<f64, Inner = D> + Copy>( |
| 28 | + &(parameters, kij): &Self::Parameters<D>, |
| 29 | + ) -> Self::Parameters<D2> { |
| 30 | + ( |
| 31 | + parameters.map(|p| p.map(D2::from_inner)), |
| 32 | + D2::from_inner(kij), |
| 33 | + ) |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +impl ResidualHelmholtzEnergy<2> for PcSaftBinary { |
| 38 | + const RESIDUAL: &str = "PC-SAFT (binary)"; |
| 39 | + |
| 40 | + fn compute_max_density(&self, molefracs: &SVector<f64, 2>) -> f64 { |
| 41 | + let [p1, p2] = self.parameters; |
| 42 | + let [x1, x2] = molefracs.data.0[0]; |
| 43 | + let [m1, sigma1, ..] = p1; |
| 44 | + let [m2, sigma2, ..] = p2; |
| 45 | + MAX_ETA / (FRAC_PI_6 * (m1 * sigma1.powi(3) * x1 + m2 * sigma2.powi(3) * x2)) |
| 46 | + } |
| 47 | + |
| 48 | + fn residual_helmholtz_energy_density<D: DualNum<f64> + Copy>( |
| 49 | + &([p1, p2], kij): &Self::Parameters<D>, |
| 50 | + temperature: D, |
| 51 | + partial_density: &SVector<D, 2>, |
| 52 | + ) -> D { |
| 53 | + let [m1, sigma1, epsilon_k1, mu1, kappa_ab1, epsilon_k_ab1, na1, nb1] = p1; |
| 54 | + let [m2, sigma2, epsilon_k2, mu2, kappa_ab2, epsilon_k_ab2, na2, nb2] = p2; |
| 55 | + |
| 56 | + // temperature dependent segment diameter |
| 57 | + let t_inv = temperature.recip(); |
| 58 | + let diameter1 = sigma1 * (-(epsilon_k1 * (-3.) * t_inv).exp() * 0.12 + 1.0); |
| 59 | + let diameter2 = sigma2 * (-(epsilon_k2 * (-3.) * t_inv).exp() * 0.12 + 1.0); |
| 60 | + |
| 61 | + // Packing fractions |
| 62 | + let [density1, density2] = partial_density.data.0[0]; |
| 63 | + let density = density1 + density2; |
| 64 | + let [x1, x2] = [density1 / density, density2 / density]; |
| 65 | + let zeta = [0, 1, 2, 3] |
| 66 | + .map(|k| (m1 * x1 * diameter1.powi(k) + m2 * x2 * diameter2.powi(k)) * FRAC_PI_6); |
| 67 | + let zeta_23 = zeta[2] / zeta[3]; |
| 68 | + let [zeta0, zeta1, zeta2, zeta3] = zeta.map(|z| z * density); |
| 69 | + let frac_1mz3 = (-zeta3 + 1.0).recip(); |
| 70 | + |
| 71 | + let eta = zeta3; |
| 72 | + let eta2 = eta * eta; |
| 73 | + let eta3 = eta2 * eta; |
| 74 | + let eta_m1 = (-eta + 1.0).recip(); |
| 75 | + let eta_m2 = eta_m1 * eta_m1; |
| 76 | + let etas = [ |
| 77 | + D::one(), |
| 78 | + eta, |
| 79 | + eta2, |
| 80 | + eta3, |
| 81 | + eta2 * eta2, |
| 82 | + eta2 * eta3, |
| 83 | + eta3 * eta3, |
| 84 | + ]; |
| 85 | + |
| 86 | + // hard sphere |
| 87 | + let hs = (zeta1 * zeta2 * frac_1mz3 * 3.0 |
| 88 | + + zeta2.powi(2) * frac_1mz3.powi(2) * zeta_23 |
| 89 | + + (zeta2 * zeta_23.powi(2) - zeta0) * (-zeta3).ln_1p()) |
| 90 | + / std::f64::consts::FRAC_PI_6; |
| 91 | + |
| 92 | + // hard chain |
| 93 | + let c = zeta2 * frac_1mz3 * frac_1mz3; |
| 94 | + let g_hs = [diameter1, diameter2] |
| 95 | + .map(|d| frac_1mz3 + d * c * 1.5 - (d * c).powi(2) * (zeta3 - 1.0) * 0.5); |
| 96 | + let hc = -(density1 * (m1 - 1.0) * g_hs[0].ln() + density2 * (m2 - 1.0) * g_hs[1].ln()); |
| 97 | + |
| 98 | + // binary interactions |
| 99 | + let m = x1 * m1 + x2 * m2; |
| 100 | + let epsilon_k11 = epsilon_k1 * t_inv; |
| 101 | + let epsilon_k12 = (epsilon_k1 * epsilon_k2).sqrt() * t_inv; |
| 102 | + let epsilon_k22 = epsilon_k2 * t_inv; |
| 103 | + let sigma11_3 = sigma1.powi(3); |
| 104 | + let sigma12_3 = ((sigma1 + sigma2) * 0.5).powi(3); |
| 105 | + let sigma22_3 = sigma2.powi(3); |
| 106 | + let d11 = density1 * density1 * m1 * m1 * epsilon_k11 * sigma11_3; |
| 107 | + let d12 = density1 * density2 * m1 * m2 * epsilon_k12 * sigma12_3 * (-kij + 1.0); |
| 108 | + let d22 = density2 * density2 * m2 * m2 * epsilon_k22 * sigma22_3; |
| 109 | + let rho1mix = d11 + d12 * 2.0 + d22; |
| 110 | + let rho2mix = |
| 111 | + d11 * epsilon_k11 + d12 * epsilon_k12 * (-kij + 1.0) * 2.0 + d22 * epsilon_k22; |
| 112 | + |
| 113 | + // I1, I2 and C1 |
| 114 | + let mm1 = (m - 1.0) / m; |
| 115 | + let mm2 = (m - 2.0) / m; |
| 116 | + let mut i1 = D::zero(); |
| 117 | + let mut i2 = D::zero(); |
| 118 | + for i in 0..7 { |
| 119 | + i1 += (mm1 * (mm2 * A2[i] + A1[i]) + A0[i]) * etas[i]; |
| 120 | + i2 += (mm1 * (mm2 * B2[i] + B1[i]) + B0[i]) * etas[i]; |
| 121 | + } |
| 122 | + let c1 = (m * (eta * 8.0 - eta2 * 2.0) * eta_m2 * eta_m2 + 1.0 |
| 123 | + - (m - 1.0) * (eta * 20.0 - eta2 * 27.0 + eta2 * eta * 12.0 - eta2 * eta2 * 2.0) |
| 124 | + / ((eta - 1.0) * (eta - 2.0)).powi(2)) |
| 125 | + .recip(); |
| 126 | + |
| 127 | + // dispersion |
| 128 | + let disp = (-rho1mix * i1 * 2.0 - rho2mix * m * c1 * i2) * PI; |
| 129 | + |
| 130 | + // dipoles |
| 131 | + let mu_term1 = mu1 * mu1 / (m1 * temperature * 1.380649e-4) * density1; |
| 132 | + let mu_term2 = mu2 * mu2 / (m2 * temperature * 1.380649e-4) * density2; |
| 133 | + let sigma111 = sigma11_3; |
| 134 | + let sigma112 = sigma1 * ((sigma1 + sigma2) * 0.5).powi(2); |
| 135 | + let sigma122 = sigma2 * ((sigma1 + sigma2) * 0.5).powi(2); |
| 136 | + let sigma222 = sigma22_3; |
| 137 | + |
| 138 | + let m11_dipole = if m1.re() > 2.0 { D::from(2.0) } else { m1 }; |
| 139 | + let m22_dipole = if m2.re() > 2.0 { D::from(2.0) } else { m2 }; |
| 140 | + let m12_dipole = (m11_dipole * m22_dipole).sqrt(); |
| 141 | + let [j2_11, j2_12, j2_22] = [ |
| 142 | + (m11_dipole, epsilon_k11), |
| 143 | + (m12_dipole, epsilon_k12), |
| 144 | + (m22_dipole, epsilon_k22), |
| 145 | + ] |
| 146 | + .map(|(m, e)| { |
| 147 | + let m1 = (m - 1.0) / m; |
| 148 | + let m2 = m1 * (m - 2.0) / m; |
| 149 | + let mut j2 = D::zero(); |
| 150 | + for i in 0..5 { |
| 151 | + let a = m2 * AD[i][2] + m1 * AD[i][1] + AD[i][0]; |
| 152 | + let b = m2 * BD[i][2] + m1 * BD[i][1] + BD[i][0]; |
| 153 | + j2 += (a + b * e) * etas[i]; |
| 154 | + } |
| 155 | + j2 |
| 156 | + }); |
| 157 | + let m112_dipole = (m11_dipole * m11_dipole * m22_dipole).cbrt(); |
| 158 | + let m122_dipole = (m11_dipole * m22_dipole * m22_dipole).cbrt(); |
| 159 | + let [j3_111, j3_112, j3_122, j3_222] = [m11_dipole, m112_dipole, m122_dipole, m22_dipole] |
| 160 | + .map(|m| { |
| 161 | + let m1 = (m - 1.0) / m; |
| 162 | + let m2 = m1 * (m - 2.0) / m; |
| 163 | + let mut j3 = D::zero(); |
| 164 | + for i in 0..4 { |
| 165 | + j3 += (m2 * CD[i][2] + m1 * CD[i][1] + CD[i][0]) * etas[i]; |
| 166 | + } |
| 167 | + j3 |
| 168 | + }); |
| 169 | + |
| 170 | + let phi2 = (mu_term1 * mu_term1 / sigma11_3 * j2_11 |
| 171 | + + mu_term1 * mu_term2 / sigma12_3 * j2_12 * 2.0 |
| 172 | + + mu_term2 * mu_term2 / sigma22_3 * j2_22) |
| 173 | + * (-PI); |
| 174 | + let phi3 = (mu_term1.powi(3) / sigma111 * j3_111 |
| 175 | + + mu_term1.powi(2) * mu_term2 / sigma112 * j3_112 * 3.0 |
| 176 | + + mu_term1 * mu_term2.powi(2) / sigma122 * j3_122 * 3.0 |
| 177 | + + mu_term2.powi(3) / sigma222 * j3_222) |
| 178 | + * (-PI_SQ_43); |
| 179 | + |
| 180 | + let mut polar = phi2 * phi2 / (phi2 - phi3); |
| 181 | + if polar.re().is_nan() { |
| 182 | + polar = phi2 |
| 183 | + } |
| 184 | + |
| 185 | + // association |
| 186 | + let d11 = diameter1 * 0.5; |
| 187 | + let d12 = diameter1 * diameter2 / (diameter1 + diameter2); |
| 188 | + let d22 = diameter2 * 0.5; |
| 189 | + let [k11, k12, k22] = [d11, d12, d22].map(|d| d * zeta2 * frac_1mz3); |
| 190 | + let s11 = sigma11_3 * kappa_ab1; |
| 191 | + let mut s12 = (sigma11_3 * sigma22_3 * kappa_ab1 * kappa_ab2).sqrt(); |
| 192 | + if s12.re() == 0.0 { |
| 193 | + s12 = D::zero(); |
| 194 | + } |
| 195 | + let s22 = sigma22_3 * kappa_ab2; |
| 196 | + let e11 = (epsilon_k_ab1 * t_inv).exp() - 1.0; |
| 197 | + let e12 = ((epsilon_k_ab1 + epsilon_k_ab2) * 0.5 * t_inv).exp() - 1.0; |
| 198 | + let e22 = (epsilon_k_ab2 * t_inv).exp() - 1.0; |
| 199 | + let d11 = frac_1mz3 * (k11 * (k11 * 2.0 + 3.0) + 1.0) * s11 * e11; |
| 200 | + let d12 = frac_1mz3 * (k12 * (k12 * 2.0 + 3.0) + 1.0) * s12 * e12; |
| 201 | + let d22 = frac_1mz3 * (k22 * (k22 * 2.0 + 3.0) + 1.0) * s22 * e22; |
| 202 | + let rhoa1 = density1 * na1; |
| 203 | + let rhob1 = density1 * nb1; |
| 204 | + let rhoa2 = density2 * na2; |
| 205 | + let rhob2 = density2 * nb2; |
| 206 | + |
| 207 | + let [mut xa1, mut xa2] = [D::from(0.2); 2]; |
| 208 | + for _ in 0..50 { |
| 209 | + let (g, j) = jacobian( |
| 210 | + |x| { |
| 211 | + let [xa1, xa2] = x.data.0[0]; |
| 212 | + let xb1_i = xa1 * DualVec::from_re(rhoa1 * d11) |
| 213 | + + xa2 * DualVec::from_re(rhoa2 * d12) |
| 214 | + + 1.0; |
| 215 | + let xb2_i = xa1 * DualVec::from_re(rhoa1 * d12) |
| 216 | + + xa2 * DualVec::from_re(rhoa2 * d22) |
| 217 | + + 1.0; |
| 218 | + |
| 219 | + let f1 = xa1 - 1.0 |
| 220 | + + xa1 / xb1_i * DualVec::from_re(rhob1 * d11) |
| 221 | + + xa1 / xb2_i * DualVec::from_re(rhob2 * d12); |
| 222 | + let f2 = xa2 - 1.0 |
| 223 | + + xa2 / xb1_i * DualVec::from_re(rhob1 * d12) |
| 224 | + + xa2 / xb2_i * DualVec::from_re(rhob2 * d22); |
| 225 | + |
| 226 | + SVector::from([f1, f2]) |
| 227 | + }, |
| 228 | + SVector::from([xa1, xa2]), |
| 229 | + ); |
| 230 | + |
| 231 | + let [g1, g2] = g.data.0[0]; |
| 232 | + let [[j11, j12], [j21, j22]] = j.data.0; |
| 233 | + let det = j11 * j22 - j12 * j21; |
| 234 | + |
| 235 | + let delta_xa1 = (j22 * g1 - j12 * g2) / det; |
| 236 | + let delta_xa2 = (-j21 * g1 + j11 * g2) / det; |
| 237 | + if delta_xa1.re() < xa1.re() * 0.8 { |
| 238 | + xa1 -= delta_xa1; |
| 239 | + } else { |
| 240 | + xa1 *= 0.2; |
| 241 | + } |
| 242 | + if delta_xa2.re() < xa2.re() * 0.8 { |
| 243 | + xa2 -= delta_xa2; |
| 244 | + } else { |
| 245 | + xa2 *= 0.2; |
| 246 | + } |
| 247 | + |
| 248 | + if g1.re().abs() < 1e-15 && g2.re().abs() < 1e-15 { |
| 249 | + break; |
| 250 | + } |
| 251 | + } |
| 252 | + |
| 253 | + let xb1 = (xa1 * rhoa1 * d11 + xa2 * rhoa2 * d12 + 1.0).recip(); |
| 254 | + let xb2 = (xa1 * rhoa1 * d12 + xa2 * rhoa2 * d22 + 1.0).recip(); |
| 255 | + let f = |x: D| x.ln() - x * 0.5 + 0.5; |
| 256 | + let assoc = rhoa1 * f(xa1) + rhoa2 * f(xa2) + rhob1 * f(xb1) + rhob2 * f(xb2); |
| 257 | + |
| 258 | + (hs + hc + disp + polar + assoc) * temperature |
| 259 | + } |
| 260 | +} |
| 261 | + |
| 262 | +#[cfg(test)] |
| 263 | +pub mod test { |
| 264 | + use super::{PcSaftBinary, ResidualHelmholtzEnergy}; |
| 265 | + use crate::eos::pcsaft::test::pcsaft_binary; |
| 266 | + use approx::assert_relative_eq; |
| 267 | + use feos_core::{Contributions::Total, EosResult, ReferenceSystem, State}; |
| 268 | + use nalgebra::SVector; |
| 269 | + use ndarray::arr1; |
| 270 | + use quantity::{KELVIN, KILO, METER, MOL}; |
| 271 | + |
| 272 | + #[test] |
| 273 | + fn test_pcsaft_binary() -> EosResult<()> { |
| 274 | + let (pcsaft, eos) = pcsaft_binary()?; |
| 275 | + let pcsaft = (pcsaft.parameters, pcsaft.kij); |
| 276 | + |
| 277 | + let temperature = 300.0 * KELVIN; |
| 278 | + let volume = 2.3 * METER * METER * METER; |
| 279 | + let moles = arr1(&[1.3, 2.5]) * KILO * MOL; |
| 280 | + |
| 281 | + let state = State::new_nvt(&eos, temperature, volume, &moles)?; |
| 282 | + let a_feos = state.residual_molar_helmholtz_energy(); |
| 283 | + let mu_feos = state.residual_chemical_potential(); |
| 284 | + let p_feos = state.pressure(Total); |
| 285 | + let s_feos = state.residual_molar_entropy(); |
| 286 | + let h_feos = state.residual_molar_enthalpy(); |
| 287 | + |
| 288 | + let total_moles = moles.sum(); |
| 289 | + let t = temperature.to_reduced(); |
| 290 | + let v = (volume / total_moles).to_reduced(); |
| 291 | + let x = SVector::from_fn(|i, _| moles.get(i).convert_into(total_moles)); |
| 292 | + let a_ad = PcSaftBinary::residual_molar_helmholtz_energy(&pcsaft, t, v, &x); |
| 293 | + let mu_ad = PcSaftBinary::residual_chemical_potential(&pcsaft, t, v, &x); |
| 294 | + let p_ad = PcSaftBinary::pressure(&pcsaft, t, v, &x); |
| 295 | + let s_ad = PcSaftBinary::residual_molar_entropy(&pcsaft, t, v, &x); |
| 296 | + let h_ad = PcSaftBinary::residual_molar_enthalpy(&pcsaft, t, v, &x); |
| 297 | + |
| 298 | + for (s, c) in state.residual_helmholtz_energy_contributions() { |
| 299 | + println!("{s:20} {}", (c / state.volume).to_reduced()); |
| 300 | + } |
| 301 | + |
| 302 | + println!("\nMolar Helmholtz energy:\n{}", a_feos.to_reduced(),); |
| 303 | + println!("{a_ad}"); |
| 304 | + assert_relative_eq!(a_feos.to_reduced(), a_ad, max_relative = 1e-14); |
| 305 | + |
| 306 | + println!("\nChemical potential:\n{}", mu_feos.get(0).to_reduced()); |
| 307 | + println!("{}", mu_ad[0]); |
| 308 | + assert_relative_eq!(mu_feos.get(0).to_reduced(), mu_ad[0], max_relative = 1e-14); |
| 309 | + |
| 310 | + println!("\nPressure:\n{}", p_feos.to_reduced()); |
| 311 | + println!("{p_ad}"); |
| 312 | + assert_relative_eq!(p_feos.to_reduced(), p_ad, max_relative = 1e-14); |
| 313 | + |
| 314 | + println!("\nMolar entropy:\n{}", s_feos.to_reduced()); |
| 315 | + println!("{s_ad}"); |
| 316 | + assert_relative_eq!(s_feos.to_reduced(), s_ad, max_relative = 1e-14); |
| 317 | + |
| 318 | + println!("\nMolar enthalpy:\n{}", h_feos.to_reduced()); |
| 319 | + println!("{h_ad}"); |
| 320 | + assert_relative_eq!(h_feos.to_reduced(), h_ad, max_relative = 1e-14); |
| 321 | + |
| 322 | + Ok(()) |
| 323 | + } |
| 324 | +} |
0 commit comments