Skip to main content

feos_core/phase_equilibria/
vle_pure.rs

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
16/// # Pure component phase equilibria
17impl<E: Residual> PhaseEquilibrium<E, 2> {
18    /// Calculate a phase equilibrium for a pure component.
19    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    /// Calculate a phase equilibrium for a pure component
43    /// and given temperature.
44    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        // First use given initial state if applicable
54        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        // Next try to initialize with an ideal gas assumption
69        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        // Finally use the spinodal to initialize the calculation
76        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        // Implicit differentiation
85        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        // calculate properties
136        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        // Estimate the new pressure
140        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 the pressure becomes negative, assume the gas phase is ideal. The
148        // resulting pressure is always positive.
149        if p_new.is_sign_negative() {
150            p_new = p_v * ((-delta_a - p_v / vapor_density) / temperature).exp();
151        }
152
153        // Improve the estimate by exploiting the almost ideal behavior of the gas phase
154        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        // Emergency brake if the implementation of the EOS is not safe.
168        if p_new.is_nan() {
169            return Err(FeosError::IterationFailed("pure_t".to_owned()));
170        }
171
172        // Calculate Newton steps for the densities and update state.
173        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        // Check for convergence
180        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    /// Calculate a phase equilibrium for a pure component
261    /// and given pressure.
262    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        // Initialize the phase equilibrium
271        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            // calculate the pressures and derivatives
293            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            // calculate the residual molar entropies (already cached)
299            let s_l_res = vle.liquid().residual_molar_entropy();
300            let s_v_res = vle.vapor().residual_molar_entropy();
301
302            // calculate the residual molar Helmholtz energies (already cached)
303            let a_l_res = vle.liquid().residual_molar_helmholtz_energy();
304            let a_v_res = vle.vapor().residual_molar_helmholtz_energy();
305
306            // calculate the molar volumes
307            let v_l = 1.0 / vle.liquid().density;
308            let v_v = 1.0 / vle.vapor().density;
309
310            // estimate the temperature steps
311            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            // calculate Newton steps for the densities and update state.
318            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                // if densities are negative or the temperature step is large use density iteration instead
326                vle = vle
327                    .update_pressure(t_new, pressure)?
328                    .check_trivial_solution()?;
329            } else {
330                // update state
331                vle = Self([
332                    State::new_pure(eos, t_new, rho_v)?,
333                    State::new_pure(eos, t_new, rho_l)?,
334                ]);
335            }
336
337            // check for convergence
338            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    /// Initialize a new VLE for a pure substance for a given pressure.
361    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    /// Calculate the pure component vapor pressures of all
437    /// components in the system for the given temperature.
438    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    /// Calculate the pure component boiling temperatures of all
450    /// components in the system for the given pressure.
451    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    /// Calculate the pure component phase equilibria of all
463    /// components in the system.
464    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}