1use super::{PhaseEquilibrium, TRIVIAL_REL_DEVIATION};
2use crate::density_iteration::{_density_iteration, _pressure_spinodal};
3use crate::equation_of_state::{Residual, Subset};
4use crate::errors::{FeosError, FeosResult};
5use crate::state::{Contributions, DensityInitialization, State};
6use crate::{ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity};
7use nalgebra::allocator::Allocator;
8use nalgebra::{DVector, DefaultAllocator, Dim, dvector};
9use num_dual::{DualNum, DualStruct, Gradients};
10use quantity::{Density, Pressure, RGAS, Temperature};
11
12const SCALE_T_NEW: f64 = 0.7;
13const MAX_ITER_PURE: usize = 50;
14const TOL_PURE: f64 = 1e-12;
15
16impl<E: Residual> PhaseEquilibrium<E, 2> {
18 pub fn pure<TP: TemperatureOrPressure>(
20 eos: &E,
21 temperature_or_pressure: TP,
22 initial_state: Option<&Self>,
23 options: SolverOptions,
24 ) -> FeosResult<Self> {
25 if let Some(t) = temperature_or_pressure.temperature() {
26 let (_, rho) = Self::pure_t(eos, t, initial_state, options)?;
27 Ok(Self(rho.map(|r| {
28 State::new_intensive(eos, t, r, &dvector![1.0]).unwrap()
29 })))
30 } else if let Some(p) = temperature_or_pressure.pressure() {
31 Self::pure_p(eos, p, initial_state, options)
32 } else {
33 unreachable!()
34 }
35 }
36}
37
38impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> PhaseEquilibrium<E, 2, N, D>
39where
40 DefaultAllocator: Allocator<N>,
41{
42 pub fn pure_t(
45 eos: &E,
46 temperature: Temperature<D>,
47 initial_state: Option<&Self>,
48 options: SolverOptions,
49 ) -> FeosResult<(Pressure<D>, [Density<D>; 2])> {
50 let eos_f64 = eos.re();
51 let t = temperature.into_reduced();
52
53 let mut vle = initial_state.and_then(|init| {
55 let vle = (
56 init.vapor()
57 .pressure(Contributions::Total)
58 .into_reduced()
59 .re(),
60 [
61 init.vapor().density.into_reduced().re(),
62 init.liquid().density.into_reduced().re(),
63 ],
64 );
65 iterate_pure_t(&eos_f64, t.re(), vle, options).ok()
66 });
67
68 vle = vle.or_else(|| {
70 _init_pure_ideal_gas(&eos_f64, temperature.re())
71 .and_then(|vle| iterate_pure_t(&eos_f64, t.re(), vle, options))
72 .ok()
73 });
74
75 let (p, [rho_v, rho_l]) = vle.map_or_else(
77 || {
78 _init_pure_spinodal(&eos_f64, temperature.re())
79 .and_then(|vle| iterate_pure_t(&eos_f64, t.re(), vle, options))
80 },
81 Ok,
82 )?;
83
84 let mut pressure = D::from(p);
86 let mut vapor_density = D::from(rho_v);
87 let mut liquid_density = D::from(rho_l);
88 let x = E::pure_molefracs();
89 for _ in 0..D::NDERIV {
90 let v_l = liquid_density.recip();
91 let v_v = vapor_density.recip();
92 let (f_l, p_l, dp_l) = eos.p_dpdrho(t, liquid_density, &x);
93 let (f_v, p_v, dp_v) = eos.p_dpdrho(t, vapor_density, &x);
94 pressure = -(f_l * v_l - f_v * v_v + t * (v_v / v_l).ln()) / (v_l - v_v);
95 liquid_density += (pressure - p_l) / dp_l;
96 vapor_density += (pressure - p_v) / dp_v;
97 }
98 Ok((
99 Pressure::from_reduced(pressure),
100 [
101 Density::from_reduced(vapor_density),
102 Density::from_reduced(liquid_density),
103 ],
104 ))
105 }
106}
107
108fn iterate_pure_t<E: Residual<N>, N: Dim>(
109 eos: &E,
110 temperature: f64,
111 (mut pressure, [mut vapor_density, mut liquid_density]): (f64, [f64; 2]),
112 options: SolverOptions,
113) -> FeosResult<(f64, [f64; 2])>
114where
115 DefaultAllocator: Allocator<N>,
116{
117 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
118 let x = E::pure_molefracs();
119
120 log_iter!(
121 verbosity,
122 " iter | residual | pressure | liquid density | vapor density | Newton steps"
123 );
124 log_iter!(verbosity, "{:-<103}", "");
125 log_iter!(
126 verbosity,
127 " {:4} | | {:12.8} | {:12.8} | {:12.8} |",
128 0,
129 Pressure::from_reduced(pressure),
130 Density::from_reduced(liquid_density),
131 Density::from_reduced(vapor_density)
132 );
133
134 for i in 1..=max_iter {
135 let (f_l_res, p_l, p_rho_l) = eos.p_dpdrho(temperature, liquid_density, &x);
137 let (f_v_res, p_v, p_rho_v) = eos.p_dpdrho(temperature, vapor_density, &x);
138
139 let v_v = vapor_density.recip();
141 let v_l = liquid_density.recip();
142 let delta_v = v_v - v_l;
143 let delta_a =
144 f_v_res * v_v - f_l_res * v_l + temperature * (vapor_density / liquid_density).ln();
145 let mut p_new = -delta_a / delta_v;
146
147 if p_new.is_sign_negative() {
150 p_new = p_v * ((-delta_a - p_v / vapor_density) / temperature).exp();
151 }
152
153 let mut newton_iter = 0;
155 let newton_tol = pressure * delta_v * tol;
156 for _ in 0..20 {
157 let p_frac = p_new / pressure;
158 let f = p_new * delta_v + delta_a + (p_frac.ln() + 1.0 - p_frac) * temperature;
159 let df_dp = delta_v + (1.0 / p_new - 1.0 / pressure) * temperature;
160 p_new -= f / df_dp;
161 newton_iter += 1;
162 if f.abs() < newton_tol {
163 break;
164 }
165 }
166
167 if p_new.is_nan() {
169 return Err(FeosError::IterationFailed("pure_t".to_owned()));
170 }
171
172 liquid_density += (p_new - p_l) / p_rho_l;
174 vapor_density += (p_new - p_v) / p_rho_v;
175 if (vapor_density / liquid_density - 1.0).abs() < TRIVIAL_REL_DEVIATION {
176 return Err(FeosError::TrivialSolution);
177 }
178
179 let res = (p_new - pressure).abs();
181 log_iter!(
182 verbosity,
183 " {:4} | {:14.8e} | {:12.8} | {:12.8} | {:12.8} | {}",
184 i,
185 res,
186 Pressure::from_reduced(p_new),
187 Density::from_reduced(liquid_density),
188 Density::from_reduced(vapor_density),
189 newton_iter
190 );
191 if res < pressure * tol {
192 log_result!(
193 verbosity,
194 "PhaseEquilibrium::pure_t: calculation converged in {} step(s)\n",
195 i
196 );
197 return Ok((pressure, [vapor_density, liquid_density]));
198 }
199 pressure = p_new;
200 }
201 Err(FeosError::NotConverged("pure_t".to_owned()))
202}
203
204fn _init_pure_ideal_gas<E: Residual<N>, N: Dim>(
205 eos: &E,
206 temperature: Temperature,
207) -> FeosResult<(f64, [f64; 2])>
208where
209 DefaultAllocator: Allocator<N>,
210{
211 let x = E::pure_molefracs();
212 let v = (0.75 * eos.compute_max_density(&x)).recip();
213 let t = temperature.into_reduced();
214 let a_res = eos.residual_molar_helmholtz_energy(t, v, &x);
215 let p = t / v * (a_res / t - 1.0).exp();
216 let rho_v = p / t;
217 let rho_l = v.recip();
218 let rho_v = _density_iteration(eos, t, p, &x, DensityInitialization::InitialDensity(rho_v))?;
219 let rho_l = _density_iteration(eos, t, p, &x, DensityInitialization::InitialDensity(rho_l))?;
220 Ok((p, [rho_v, rho_l]))
221}
222
223fn _init_pure_spinodal<E: Residual<N>, N: Dim>(
224 eos: &E,
225 temperature: Temperature,
226) -> FeosResult<(f64, [f64; 2])>
227where
228 DefaultAllocator: Allocator<N>,
229{
230 let x = E::pure_molefracs();
231 let maxdensity = eos.compute_max_density(&x);
232 let t = temperature.into_reduced();
233 let (p_l, _) = _pressure_spinodal(eos, t, 0.8 * maxdensity, &x)?;
234 let (p_v, _) = _pressure_spinodal(eos, t, 0.001 * maxdensity, &x)?;
235 let p = 0.5 * (0.0_f64.max(p_l) + p_v);
236 let rho_l = _density_iteration(eos, t, p, &x, DensityInitialization::Liquid)?;
237 let rho_v = _density_iteration(eos, t, p, &x, DensityInitialization::Vapor)?;
238 Ok((p, [rho_v, rho_l]))
239}
240
241impl<E: Residual> PhaseEquilibrium<E, 2> {
242 fn new_pt(eos: &E, temperature: Temperature, pressure: Pressure) -> FeosResult<Self> {
243 let liquid = State::new_xpt(
244 eos,
245 temperature,
246 pressure,
247 &dvector![1.0],
248 Some(DensityInitialization::Liquid),
249 )?;
250 let vapor = State::new_xpt(
251 eos,
252 temperature,
253 pressure,
254 &dvector![1.0],
255 Some(DensityInitialization::Vapor),
256 )?;
257 Ok(Self([vapor, liquid]))
258 }
259
260 fn pure_p(
263 eos: &E,
264 pressure: Pressure,
265 initial_state: Option<&Self>,
266 options: SolverOptions,
267 ) -> FeosResult<Self> {
268 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
269
270 let mut vle = match initial_state {
272 Some(init) => init
273 .clone()
274 .update_pressure(init.vapor().temperature, pressure)?,
275 None => PhaseEquilibrium::init_pure_p(eos, pressure)?,
276 };
277
278 log_iter!(
279 verbosity,
280 " iter | residual | temperature | liquid density | vapor density "
281 );
282 log_iter!(verbosity, "{:-<89}", "");
283 log_iter!(
284 verbosity,
285 " {:4} | | {:13.8} | {:12.8} | {:12.8}",
286 0,
287 vle.vapor().temperature,
288 vle.liquid().density,
289 vle.vapor().density
290 );
291 for i in 1..=max_iter {
292 let (p_l, p_rho_l) = vle.liquid().p_dpdrho();
294 let (p_v, p_rho_v) = vle.vapor().p_dpdrho();
295 let p_t_l = vle.liquid().dp_dt(Contributions::Total);
296 let p_t_v = vle.vapor().dp_dt(Contributions::Total);
297
298 let s_l_res = vle.liquid().residual_molar_entropy();
300 let s_v_res = vle.vapor().residual_molar_entropy();
301
302 let a_l_res = vle.liquid().residual_molar_helmholtz_energy();
304 let a_v_res = vle.vapor().residual_molar_helmholtz_energy();
305
306 let v_l = 1.0 / vle.liquid().density;
308 let v_v = 1.0 / vle.vapor().density;
309
310 let kt = RGAS * vle.vapor().temperature;
312 let ln_rho = (v_l / v_v).into_value().ln();
313 let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + kt * ln_rho))
314 / (s_v_res - s_l_res - RGAS * ln_rho);
315 let t_new = vle.vapor().temperature + delta_t;
316
317 let rho_l = vle.liquid().density + (pressure - p_l - p_t_l * delta_t) / p_rho_l;
319 let rho_v = vle.vapor().density + (pressure - p_v - p_t_v * delta_t) / p_rho_v;
320
321 if rho_l.is_sign_negative()
322 || rho_v.is_sign_negative()
323 || delta_t.abs() > Temperature::from_reduced(1.0)
324 {
325 vle = vle
327 .update_pressure(t_new, pressure)?
328 .check_trivial_solution()?;
329 } else {
330 vle = Self([
332 State::new_pure(eos, t_new, rho_v)?,
333 State::new_pure(eos, t_new, rho_l)?,
334 ]);
335 }
336
337 let res = delta_t.abs();
339 log_iter!(
340 verbosity,
341 " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}",
342 i,
343 res,
344 vle.vapor().temperature,
345 vle.liquid().density,
346 vle.vapor().density
347 );
348 if res < vle.vapor().temperature * tol {
349 log_result!(
350 verbosity,
351 "PhaseEquilibrium::pure_p: calculation converged in {} step(s)\n",
352 i
353 );
354 return Ok(vle);
355 }
356 }
357 Err(FeosError::NotConverged("pure_p".to_owned()))
358 }
359
360 fn init_pure_p(eos: &E, pressure: Pressure) -> FeosResult<Self> {
362 let trial_temperatures = [
363 Temperature::from_reduced(300.0),
364 Temperature::from_reduced(500.0),
365 Temperature::from_reduced(200.0),
366 ];
367 let x = dvector![1.0];
368 let mut vle = None;
369 let mut t0 = Temperature::from_reduced(1.0);
370 for t in trial_temperatures.iter() {
371 t0 = *t;
372 let _vle = PhaseEquilibrium::new_pt(eos, *t, pressure)?;
373 if !Self::is_trivial_solution(_vle.vapor(), _vle.liquid()) {
374 return Ok(_vle);
375 }
376 vle = Some(_vle);
377 }
378
379 let cp = State::critical_point(eos, None, None, None, SolverOptions::default())?;
380 if pressure > cp.pressure(Contributions::Total) {
381 return Err(FeosError::SuperCritical);
382 };
383 if let Some(mut e) = vle {
384 if e.vapor().density < cp.density {
385 for _ in 0..8 {
386 t0 *= SCALE_T_NEW;
387 e.0[1] =
388 State::new_xpt(eos, t0, pressure, &x, Some(DensityInitialization::Liquid))?;
389 if e.liquid().density > cp.density {
390 break;
391 }
392 }
393 } else {
394 for _ in 0..8 {
395 t0 /= SCALE_T_NEW;
396 e.0[0] =
397 State::new_xpt(eos, t0, pressure, &x, Some(DensityInitialization::Vapor))?;
398 if e.vapor().density < cp.density {
399 break;
400 }
401 }
402 }
403
404 for _ in 0..20 {
405 let h = |s: &State<_>| s.residual_enthalpy() + s.total_moles * RGAS * s.temperature;
406 t0 = (h(e.vapor()) - h(e.liquid()))
407 / (e.vapor().residual_entropy()
408 - e.liquid().residual_entropy()
409 - RGAS
410 * e.vapor().total_moles
411 * ((e.vapor().density / e.liquid().density).into_value().ln()));
412 let trial_state =
413 State::new_xpt(eos, t0, pressure, &x, Some(DensityInitialization::Vapor))?;
414 if trial_state.density < cp.density {
415 e.0[0] = trial_state;
416 }
417 let trial_state =
418 State::new_xpt(eos, t0, pressure, &x, Some(DensityInitialization::Liquid))?;
419 if trial_state.density > cp.density {
420 e.0[1] = trial_state;
421 }
422 if e.liquid().temperature == e.vapor().temperature {
423 return Ok(e);
424 }
425 }
426 Err(FeosError::IterationFailed(
427 "new_init_p: could not find proper initial state".to_owned(),
428 ))
429 } else {
430 unreachable!()
431 }
432 }
433}
434
435impl<E: Residual + Subset> PhaseEquilibrium<E, 2> {
436 pub fn vapor_pressure(eos: &E, temperature: Temperature) -> Vec<Option<Pressure>> {
439 (0..eos.components())
440 .map(|i| {
441 let pure_eos = eos.subset(&[i]);
442 PhaseEquilibrium::pure_t(&pure_eos, temperature, None, SolverOptions::default())
443 .map(|(p, _)| p)
444 .ok()
445 })
446 .collect()
447 }
448
449 pub fn boiling_temperature(eos: &E, pressure: Pressure) -> Vec<Option<Temperature>> {
452 (0..eos.components())
453 .map(|i| {
454 let pure_eos = eos.subset(&[i]);
455 PhaseEquilibrium::pure_p(&pure_eos, pressure, None, SolverOptions::default())
456 .map(|vle| vle.vapor().temperature)
457 .ok()
458 })
459 .collect()
460 }
461
462 pub fn vle_pure_comps<TP: TemperatureOrPressure>(
465 eos: &E,
466 temperature_or_pressure: TP,
467 ) -> Vec<Option<PhaseEquilibrium<E, 2>>> {
468 (0..eos.components())
469 .map(|i| {
470 let pure_eos = eos.subset(&[i]);
471 PhaseEquilibrium::pure(
472 &pure_eos,
473 temperature_or_pressure,
474 None,
475 SolverOptions::default(),
476 )
477 .and_then(|vle_pure| {
478 let mut molefracs_vapor = DVector::zeros(eos.components());
479 let mut molefracs_liquid = molefracs_vapor.clone();
480 molefracs_vapor[i] = 1.0;
481 molefracs_liquid[i] = 1.0;
482 let vapor = State::new_intensive(
483 eos,
484 vle_pure.vapor().temperature,
485 vle_pure.vapor().density,
486 &molefracs_vapor,
487 )?;
488 let liquid = State::new_intensive(
489 eos,
490 vle_pure.liquid().temperature,
491 vle_pure.liquid().density,
492 &molefracs_liquid,
493 )?;
494 Ok(PhaseEquilibrium::from_states(vapor, liquid))
495 })
496 .ok()
497 })
498 .collect()
499 }
500}