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, SVector, U1, U2};
9use num_dual::{DualNum, DualStruct, Gradients, gradient, partial};
10use quantity::{Density, Pressure, 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<N, D>, N: Gradients, D: DualNum<f64> + Copy> PhaseEquilibrium<E, 2, N, D>
18where
19    DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
20{
21    /// Calculate a phase equilibrium for a pure component.
22    pub fn pure<TP: TemperatureOrPressure<D>>(
23        eos: &E,
24        temperature_or_pressure: TP,
25        initial_state: Option<&Self>,
26        options: SolverOptions,
27    ) -> FeosResult<Self> {
28        let (t, [rho_v, rho_l]) = if let Some(t) = temperature_or_pressure.temperature() {
29            let (_, rho) = Self::pure_t(eos, t, initial_state, options)?;
30            (t, rho)
31        } else if let Some(p) = temperature_or_pressure.pressure() {
32            Self::pure_p(eos, p, initial_state, options)?
33        } else {
34            unreachable!()
35        };
36        let x = E::pure_molefracs();
37        Ok(Self::two_phase(
38            State::new(eos, t, rho_v, &x)?,
39            State::new(eos, t, rho_l, x)?,
40        ))
41    }
42
43    /// Calculate a phase equilibrium for a pure component
44    /// and given temperature.
45    pub fn pure_t(
46        eos: &E,
47        temperature: Temperature<D>,
48        initial_state: Option<&Self>,
49        options: SolverOptions,
50    ) -> FeosResult<(Pressure<D>, [Density<D>; 2])> {
51        let eos_f64 = eos.re();
52        let t = temperature.into_reduced();
53
54        // First use given initial state if applicable
55        let mut vle = initial_state.and_then(|init| {
56            let vle = (
57                init.vapor()
58                    .pressure(Contributions::Total)
59                    .into_reduced()
60                    .re(),
61                [
62                    init.vapor().density.into_reduced().re(),
63                    init.liquid().density.into_reduced().re(),
64                ],
65            );
66            iterate_pure_t(&eos_f64, t.re(), vle, options).ok()
67        });
68
69        // Next try to initialize with an ideal gas assumption
70        vle = vle.or_else(|| {
71            _init_pure_ideal_gas(&eos_f64, temperature.re())
72                .and_then(|vle| iterate_pure_t(&eos_f64, t.re(), vle, options))
73                .ok()
74        });
75
76        // Finally use the spinodal to initialize the calculation
77        let (p, [rho_v, rho_l]) = vle.map_or_else(
78            || {
79                _init_pure_spinodal(&eos_f64, temperature.re())
80                    .and_then(|vle| iterate_pure_t(&eos_f64, t.re(), vle, options))
81            },
82            Ok,
83        )?;
84
85        // Implicit differentiation
86        let mut pressure = D::from(p);
87        let mut vapor_density = D::from(rho_v);
88        let mut liquid_density = D::from(rho_l);
89        let x = E::pure_molefracs();
90        for _ in 0..D::NDERIV {
91            let v_l = liquid_density.recip();
92            let v_v = vapor_density.recip();
93            let (a_l, p_l, dp_l) = eos.p_dpdrho(t, liquid_density, &x);
94            let (a_v, p_v, dp_v) = eos.p_dpdrho(t, vapor_density, &x);
95            pressure = -(a_l - a_v + t * (v_v / v_l).ln()) / (v_l - v_v);
96            liquid_density += (pressure - p_l) / dp_l;
97            vapor_density += (pressure - p_v) / dp_v;
98        }
99        Ok((
100            Pressure::from_reduced(pressure),
101            [
102                Density::from_reduced(vapor_density),
103                Density::from_reduced(liquid_density),
104            ],
105        ))
106    }
107}
108
109fn iterate_pure_t<E: Residual<N>, N: Dim>(
110    eos: &E,
111    temperature: f64,
112    (mut pressure, [mut vapor_density, mut liquid_density]): (f64, [f64; 2]),
113    options: SolverOptions,
114) -> FeosResult<(f64, [f64; 2])>
115where
116    DefaultAllocator: Allocator<N>,
117{
118    let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
119    let x = E::pure_molefracs();
120
121    log_iter!(
122        verbosity,
123        " iter |    residual    |     pressure     |    liquid density    |    vapor density     | Newton steps"
124    );
125    log_iter!(verbosity, "{:-<103}", "");
126    log_iter!(
127        verbosity,
128        " {:4} |                | {:12.8} | {:12.8} | {:12.8} |",
129        0,
130        Pressure::from_reduced(pressure),
131        Density::from_reduced(liquid_density),
132        Density::from_reduced(vapor_density)
133    );
134
135    for i in 1..=max_iter {
136        // calculate properties
137        let (a_l_res, p_l, p_rho_l) = eos.p_dpdrho(temperature, liquid_density, &x);
138        let (a_v_res, p_v, p_rho_v) = eos.p_dpdrho(temperature, vapor_density, &x);
139
140        // Estimate the new pressure
141        let v_v = vapor_density.recip();
142        let v_l = liquid_density.recip();
143        let delta_v = v_v - v_l;
144        let delta_a = a_v_res - a_l_res + 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_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<N, D>, N: Gradients, D: DualNum<f64> + Copy> PhaseEquilibrium<E, 2, N, D>
242where
243    DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
244{
245    /// Calculate a phase equilibrium for a pure component
246    /// and given pressure.
247    pub fn pure_p(
248        eos: &E,
249        pressure: Pressure<D>,
250        initial_state: Option<&Self>,
251        options: SolverOptions,
252    ) -> FeosResult<(Temperature<D>, [Density<D>; 2])> {
253        let eos_f64 = eos.re();
254        let p = pressure.into_reduced();
255
256        // Initialize the phase equilibrium
257        let vle = match initial_state {
258            Some(init) => (
259                init.vapor().temperature.into_reduced().re(),
260                [
261                    init.vapor().density.into_reduced().re(),
262                    init.liquid().density.into_reduced().re(),
263                ],
264            ),
265            None => init_pure_p(&eos_f64, pressure.re())?,
266        };
267        let (t, [rho_v, rho_l]) = iterate_pure_p(&eos_f64, p.re(), vle, options)?;
268
269        // Implicit differentiation
270        let mut temperature = D::from(t);
271        let mut vapor_density = D::from(rho_v);
272        let mut liquid_density = D::from(rho_l);
273        let x = E::pure_molefracs();
274        for _ in 0..D::NDERIV {
275            let v_l = liquid_density.recip();
276            let v_v = vapor_density.recip();
277            let (a_l, p_l, s_l, p_rho_l, p_t_l) =
278                eos.p_dpdrho_dpdt(temperature, liquid_density, &x);
279            let (a_v, p_v, s_v, p_rho_v, p_t_v) = eos.p_dpdrho_dpdt(temperature, vapor_density, &x);
280            let ln_rho = (v_l / v_v).ln();
281            let delta_t =
282                (p * (v_v - v_l) + (a_v - a_l + temperature * ln_rho)) / (s_v - s_l - ln_rho);
283            temperature += delta_t;
284            liquid_density += (p - p_l - p_t_l * delta_t) / p_rho_l;
285            vapor_density += (p - p_v - p_t_v * delta_t) / p_rho_v;
286        }
287        Ok((
288            Temperature::from_reduced(temperature),
289            [
290                Density::from_reduced(vapor_density),
291                Density::from_reduced(liquid_density),
292            ],
293        ))
294    }
295}
296
297/// Calculate a phase equilibrium for a pure component
298/// and given pressure.
299fn iterate_pure_p<E: Residual<N>, N: Dim>(
300    eos: &E,
301    pressure: f64,
302    (mut temperature, [mut vapor_density, mut liquid_density]): (f64, [f64; 2]),
303    options: SolverOptions,
304) -> FeosResult<(f64, [f64; 2])>
305where
306    DefaultAllocator: Allocator<N>,
307{
308    let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
309    let x = E::pure_molefracs();
310
311    log_iter!(
312        verbosity,
313        " iter |     residual     |   temperature   |    liquid density    |    vapor density     "
314    );
315    log_iter!(verbosity, "{:-<89}", "");
316    log_iter!(
317        verbosity,
318        " {:4} |                  | {:13.8} | {:12.8} | {:12.8}",
319        0,
320        Temperature::from_reduced(temperature),
321        Density::from_reduced(liquid_density),
322        Density::from_reduced(vapor_density)
323    );
324    for i in 1..=max_iter {
325        // calculate properties
326        let (a_l_res, p_l, s_l_res, p_rho_l, p_t_l) =
327            eos.p_dpdrho_dpdt(temperature, liquid_density, &x);
328        let (a_v_res, p_v, s_v_res, p_rho_v, p_t_v) =
329            eos.p_dpdrho_dpdt(temperature, vapor_density, &x);
330
331        // calculate the molar volumes
332        let v_l = liquid_density.recip();
333        let v_v = vapor_density.recip();
334
335        // estimate the temperature steps
336        let ln_rho = (v_l / v_v).ln();
337        let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + temperature * ln_rho))
338            / (s_v_res - s_l_res - ln_rho);
339        temperature += delta_t;
340
341        // calculate Newton steps for the densities and update state.
342        let rho_l = liquid_density + (pressure - p_l - p_t_l * delta_t) / p_rho_l;
343        let rho_v = vapor_density + (pressure - p_v - p_t_v * delta_t) / p_rho_v;
344
345        if rho_l.is_sign_negative() || rho_v.is_sign_negative() || delta_t.abs() > 1.0 {
346            // if densities are negative or the temperature step is large use density iteration instead
347            liquid_density = _density_iteration(
348                eos,
349                temperature,
350                pressure,
351                &x,
352                DensityInitialization::InitialDensity(liquid_density),
353            )?;
354            vapor_density = _density_iteration(
355                eos,
356                temperature,
357                pressure,
358                &x,
359                DensityInitialization::InitialDensity(vapor_density),
360            )?;
361        } else {
362            liquid_density = rho_l;
363            vapor_density = rho_v;
364        }
365
366        // check for trivial solution
367        if (vapor_density / liquid_density - 1.0).abs() < TRIVIAL_REL_DEVIATION {
368            return Err(FeosError::TrivialSolution);
369        }
370
371        // check for convergence
372        let res = delta_t.abs();
373        log_iter!(
374            verbosity,
375            " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}",
376            i,
377            res,
378            Temperature::from_reduced(temperature),
379            Density::from_reduced(liquid_density),
380            Density::from_reduced(vapor_density)
381        );
382        if res < temperature * tol {
383            log_result!(
384                verbosity,
385                "PhaseEquilibrium::pure_p: calculation converged in {} step(s)\n",
386                i
387            );
388            return Ok((temperature, [vapor_density, liquid_density]));
389        }
390    }
391    Err(FeosError::NotConverged("pure_p".to_owned()))
392}
393
394/// Initialize a new VLE for a pure substance for a given pressure.
395fn init_pure_p<E: Residual<N>, N: Gradients>(
396    eos: &E,
397    pressure: Pressure,
398) -> FeosResult<(f64, [f64; 2])>
399where
400    DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
401{
402    let trial_temperatures = [300.0, 500.0, 200.0];
403    let p = pressure.into_reduced();
404    let x = E::pure_molefracs();
405    let mut vle = None;
406    for t in trial_temperatures {
407        let liquid_density = _density_iteration(eos, t, p, &x, DensityInitialization::Liquid)?;
408        let vapor_density = _density_iteration(eos, t, p, &x, DensityInitialization::Vapor)?;
409        let _vle = (t, [vapor_density, liquid_density]);
410        if (vapor_density / liquid_density - 1.0).abs() >= TRIVIAL_REL_DEVIATION {
411            return Ok(_vle);
412        }
413        vle = Some(_vle);
414    }
415    let Some((t0, [mut rho_v, mut rho_l])) = vle else {
416        unreachable!()
417    };
418    let [mut t_v, mut t_l] = [t0, t0];
419
420    let cp = State::critical_point(eos, &x, None, None, SolverOptions::default())?;
421    let cp_density = cp.density.into_reduced();
422    if pressure > cp.pressure(Contributions::Total) {
423        return Err(FeosError::SuperCritical);
424    };
425
426    if rho_v < cp_density {
427        // reduce temperature of liquid phase...
428        for _ in 0..8 {
429            t_l *= SCALE_T_NEW;
430            rho_l = _density_iteration(eos, t_l, p, &x, DensityInitialization::Liquid)?;
431            if rho_l > cp_density {
432                break;
433            }
434        }
435    } else {
436        // ...or increase temperature of vapor phase
437        for _ in 0..8 {
438            t_v /= SCALE_T_NEW;
439            rho_v = _density_iteration(eos, t_v, p, &x, DensityInitialization::Vapor)?;
440            if rho_v < cp_density {
441                break;
442            }
443        }
444    }
445
446    // determine new temperatures and assign them to either the liquid or the vapor phase until
447    // both phases have the same temperature
448    for _ in 0..20 {
449        let h_s = |t, v| {
450            let (a_res, da_res) = gradient::<_, _, _, U2, _>(
451                partial(
452                    |t_v: SVector<_, _>, x| {
453                        let [[t, v]] = t_v.data.0;
454                        eos.lift().residual_helmholtz_energy(t, v, x)
455                    },
456                    &x,
457                ),
458                &SVector::from([t, v]),
459            );
460            let [[da_res_dt, da_res_dv]] = da_res.data.0;
461            (a_res - t * da_res_dt - v * da_res_dv + t, -da_res_dt)
462        };
463        let (h_l, s_l_res) = h_s(t_l, rho_l.recip());
464        let (h_v, s_v_res) = h_s(t_v, rho_v.recip());
465        let t = (h_v - h_l) / (s_v_res - s_l_res - (rho_v / rho_l).ln());
466        let trial_density = _density_iteration(eos, t, p, &x, DensityInitialization::Vapor)?;
467        if trial_density < cp_density {
468            rho_v = trial_density;
469            t_v = t;
470        }
471        let trial_density = _density_iteration(eos, t, p, &x, DensityInitialization::Liquid)?;
472        if trial_density > cp_density {
473            rho_l = trial_density;
474            t_l = t;
475        }
476        if t_l == t_v {
477            return Ok((t_l, [rho_v, rho_l]));
478        }
479    }
480    Err(FeosError::IterationFailed(
481        "new_init_p: could not find proper initial state".to_owned(),
482    ))
483}
484
485impl<E: Residual + Subset> PhaseEquilibrium<E, 2> {
486    /// Calculate the pure component vapor pressures of all
487    /// components in the system for the given temperature.
488    pub fn vapor_pressure(eos: &E, temperature: Temperature) -> Vec<Option<Pressure>> {
489        (0..eos.components())
490            .map(|i| {
491                let pure_eos = eos.subset(&[i]);
492                PhaseEquilibrium::pure_t(&pure_eos, temperature, None, SolverOptions::default())
493                    .map(|(p, _)| p)
494                    .ok()
495            })
496            .collect()
497    }
498
499    /// Calculate the pure component boiling temperatures of all
500    /// components in the system for the given pressure.
501    pub fn boiling_temperature(eos: &E, pressure: Pressure) -> Vec<Option<Temperature>> {
502        (0..eos.components())
503            .map(|i| {
504                let pure_eos = eos.subset(&[i]);
505                PhaseEquilibrium::pure_p(&pure_eos, pressure, None, SolverOptions::default())
506                    .map(|(t, _)| t)
507                    .ok()
508            })
509            .collect()
510    }
511
512    /// Calculate the pure component phase equilibria of all
513    /// components in the system.
514    pub fn vle_pure_comps<TP: TemperatureOrPressure>(
515        eos: &E,
516        temperature_or_pressure: TP,
517    ) -> Vec<Option<PhaseEquilibrium<E, 2>>> {
518        (0..eos.components())
519            .map(|i| {
520                let pure_eos = eos.subset(&[i]);
521                PhaseEquilibrium::pure(
522                    &pure_eos,
523                    temperature_or_pressure,
524                    None,
525                    SolverOptions::default(),
526                )
527                .and_then(|vle_pure| {
528                    let mut molefracs_vapor = DVector::zeros(eos.components());
529                    let mut molefracs_liquid = molefracs_vapor.clone();
530                    molefracs_vapor[i] = 1.0;
531                    molefracs_liquid[i] = 1.0;
532                    let vapor = State::new(
533                        eos,
534                        vle_pure.vapor().temperature,
535                        vle_pure.vapor().density,
536                        molefracs_vapor,
537                    )?;
538                    let liquid = State::new(
539                        eos,
540                        vle_pure.liquid().temperature,
541                        vle_pure.liquid().density,
542                        molefracs_liquid,
543                    )?;
544                    Ok(PhaseEquilibrium::two_phase(vapor, liquid))
545                })
546                .ok()
547            })
548            .collect()
549    }
550}