feos_core/phase_equilibria/
vle_pure.rs

1use super::PhaseEquilibrium;
2use crate::equation_of_state::Residual;
3use crate::errors::{EosError, EosResult};
4use crate::state::{Contributions, DensityInitialization, State, TPSpec};
5use crate::{ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity};
6use ndarray::{arr1, Array1};
7use quantity::{Moles, Pressure, Temperature, RGAS};
8use std::sync::Arc;
9
10const SCALE_T_NEW: f64 = 0.7;
11const MAX_ITER_PURE: usize = 50;
12const TOL_PURE: f64 = 1e-12;
13
14/// # Pure component phase equilibria
15impl<E: Residual> PhaseEquilibrium<E, 2> {
16    /// Calculate a phase equilibrium for a pure component.
17    pub fn pure<TP: TemperatureOrPressure>(
18        eos: &Arc<E>,
19        temperature_or_pressure: TP,
20        initial_state: Option<&PhaseEquilibrium<E, 2>>,
21        options: SolverOptions,
22    ) -> EosResult<Self> {
23        match temperature_or_pressure.into() {
24            TPSpec::Temperature(t) => Self::pure_t(eos, t, initial_state, options),
25            TPSpec::Pressure(p) => Self::pure_p(eos, p, initial_state, options),
26        }
27    }
28
29    /// Calculate a phase equilibrium for a pure component
30    /// and given temperature.
31    fn pure_t(
32        eos: &Arc<E>,
33        temperature: Temperature,
34        initial_state: Option<&PhaseEquilibrium<E, 2>>,
35        options: SolverOptions,
36    ) -> EosResult<Self> {
37        let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
38
39        // First use given initial state if applicable
40        let mut vle = initial_state.and_then(|init| {
41            Self::init_pure_state(init, temperature)
42                .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity))
43                .ok()
44        });
45
46        // Next try to initialize with an ideal gas assumption
47        vle = vle.or_else(|| {
48            Self::init_pure_ideal_gas(eos, temperature)
49                .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity))
50                .ok()
51        });
52
53        // Finally use the spinodal to initialize the calculation
54        vle.map_or_else(
55            || {
56                Self::init_pure_spinodal(eos, temperature)
57                    .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity))
58            },
59            Ok,
60        )
61    }
62
63    fn iterate_pure_t(self, max_iter: usize, tol: f64, verbosity: Verbosity) -> EosResult<Self> {
64        let mut p_old = self.vapor().pressure(Contributions::Total);
65        let [mut vapor, mut liquid] = self.0;
66
67        log_iter!(verbosity,
68            " iter |     residual      |     pressure     |    liquid density    |    vapor density     | Newton steps"
69        );
70        log_iter!(verbosity, "{:-<106}", "");
71        log_iter!(
72            verbosity,
73            " {:4} |                   | {:12.8} | {:12.8} | {:12.8} |",
74            0,
75            p_old,
76            liquid.density,
77            vapor.density
78        );
79
80        for i in 1..=max_iter {
81            // calculate the pressures and derivatives
82            let (p_l, p_rho_l) = liquid.p_dpdrho();
83            let (p_v, p_rho_v) = vapor.p_dpdrho();
84            // calculate the molar Helmholtz energies (already cached)
85            let a_l_res = liquid.residual_molar_helmholtz_energy();
86            let a_v_res = vapor.residual_molar_helmholtz_energy();
87
88            // Estimate the new pressure
89            let kt = RGAS * vapor.temperature;
90            let delta_v = 1.0 / vapor.density - 1.0 / liquid.density;
91            let delta_a =
92                a_v_res - a_l_res + kt * (vapor.density / liquid.density).into_value().ln();
93            let mut p_new = -delta_a / delta_v;
94
95            // If the pressure becomes negative, assume the gas phase is ideal. The
96            // resulting pressure is always positive.
97            if p_new.is_sign_negative() {
98                p_new = p_v
99                    * ((-delta_a - p_v * vapor.volume / vapor.total_moles) / kt)
100                        .into_value()
101                        .exp();
102            }
103
104            // Improve the estimate by exploiting the almost ideal behavior of the gas phase
105            let kt = RGAS * vapor.temperature;
106            let mut newton_iter = 0;
107            let newton_tol = p_old * delta_v * tol;
108            for _ in 0..20 {
109                let p_frac = (p_new / p_old).into_value();
110                let f = p_new * delta_v + delta_a + (p_frac.ln() + 1.0 - p_frac) * kt;
111                let df_dp = delta_v + (1.0 / p_new - 1.0 / p_old) * kt;
112                p_new -= f / df_dp;
113                newton_iter += 1;
114                if f.abs() < newton_tol {
115                    break;
116                }
117            }
118
119            // Emergency brake if the implementation of the EOS is not safe.
120            if p_new.is_nan() {
121                return Err(EosError::IterationFailed("pure_t".to_owned()));
122            }
123
124            // Calculate Newton steps for the densities and update state.
125            let rho_l = liquid.density + (p_new - p_l) / p_rho_l;
126            let rho_v = vapor.density + (p_new - p_v) / p_rho_v;
127            liquid = State::new_pure(&liquid.eos, liquid.temperature, rho_l)?;
128            vapor = State::new_pure(&vapor.eos, vapor.temperature, rho_v)?;
129            if Self::is_trivial_solution(&vapor, &liquid) {
130                return Err(EosError::TrivialSolution);
131            }
132
133            // Check for convergence
134            let res = (p_new - p_old).abs();
135            log_iter!(
136                verbosity,
137                " {:4} | {:14.8e} | {:12.8} | {:12.8} | {:12.8} | {}",
138                i,
139                res,
140                p_new,
141                liquid.density,
142                vapor.density,
143                newton_iter
144            );
145            if res < p_old * tol {
146                log_result!(
147                    verbosity,
148                    "PhaseEquilibrium::pure_t: calculation converged in {} step(s)\n",
149                    i
150                );
151                return Ok(Self([vapor, liquid]));
152            }
153            p_old = p_new;
154        }
155        Err(EosError::NotConverged("pure_t".to_owned()))
156    }
157
158    /// Calculate a phase equilibrium for a pure component
159    /// and given pressure.
160    fn pure_p(
161        eos: &Arc<E>,
162        pressure: Pressure,
163        initial_state: Option<&Self>,
164        options: SolverOptions,
165    ) -> EosResult<Self> {
166        let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
167
168        // Initialize the phase equilibrium
169        let mut vle = match initial_state {
170            Some(init) => init
171                .clone()
172                .update_pressure(init.vapor().temperature, pressure)?,
173            None => PhaseEquilibrium::init_pure_p(eos, pressure)?,
174        };
175
176        log_iter!(
177            verbosity,
178            " iter |     residual     |   temperature   |    liquid density    |    vapor density     "
179        );
180        log_iter!(verbosity, "{:-<89}", "");
181        log_iter!(
182            verbosity,
183            " {:4} |                  | {:13.8} | {:12.8} | {:12.8}",
184            0,
185            vle.vapor().temperature,
186            vle.liquid().density,
187            vle.vapor().density
188        );
189        for i in 1..=max_iter {
190            // calculate the pressures and derivatives
191            let (p_l, p_rho_l) = vle.liquid().p_dpdrho();
192            let (p_v, p_rho_v) = vle.vapor().p_dpdrho();
193            let p_t_l = vle.liquid().dp_dt(Contributions::Total);
194            let p_t_v = vle.vapor().dp_dt(Contributions::Total);
195
196            // calculate the residual molar entropies (already cached)
197            let s_l_res = vle.liquid().residual_molar_entropy();
198            let s_v_res = vle.vapor().residual_molar_entropy();
199
200            // calculate the residual molar Helmholtz energies (already cached)
201            let a_l_res = vle.liquid().residual_molar_helmholtz_energy();
202            let a_v_res = vle.vapor().residual_molar_helmholtz_energy();
203
204            // calculate the molar volumes
205            let v_l = 1.0 / vle.liquid().density;
206            let v_v = 1.0 / vle.vapor().density;
207
208            // estimate the temperature steps
209            let kt = RGAS * vle.vapor().temperature;
210            let ln_rho = (v_l / v_v).into_value().ln();
211            let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + kt * ln_rho))
212                / (s_v_res - s_l_res - RGAS * ln_rho);
213            let t_new = vle.vapor().temperature + delta_t;
214
215            // calculate Newton steps for the densities and update state.
216            let rho_l = vle.liquid().density + (pressure - p_l - p_t_l * delta_t) / p_rho_l;
217            let rho_v = vle.vapor().density + (pressure - p_v - p_t_v * delta_t) / p_rho_v;
218
219            if rho_l.is_sign_negative()
220                || rho_v.is_sign_negative()
221                || delta_t.abs() > Temperature::from_reduced(1.0)
222            {
223                // if densities are negative or the temperature step is large use density iteration instead
224                vle = vle
225                    .update_pressure(t_new, pressure)?
226                    .check_trivial_solution()?;
227            } else {
228                // update state
229                vle = Self([
230                    State::new_pure(eos, t_new, rho_v)?,
231                    State::new_pure(eos, t_new, rho_l)?,
232                ]);
233            }
234
235            // check for convergence
236            let res = delta_t.abs();
237            log_iter!(
238                verbosity,
239                " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}",
240                i,
241                res,
242                vle.vapor().temperature,
243                vle.liquid().density,
244                vle.vapor().density
245            );
246            if res < vle.vapor().temperature * tol {
247                log_result!(
248                    verbosity,
249                    "PhaseEquilibrium::pure_p: calculation converged in {} step(s)\n",
250                    i
251                );
252                return Ok(vle);
253            }
254        }
255        Err(EosError::NotConverged("pure_p".to_owned()))
256    }
257
258    fn init_pure_state(initial_state: &Self, temperature: Temperature) -> EosResult<Self> {
259        let vapor = initial_state.vapor().update_temperature(temperature)?;
260        let liquid = initial_state.liquid().update_temperature(temperature)?;
261        Ok(Self([vapor, liquid]))
262    }
263
264    fn init_pure_ideal_gas(eos: &Arc<E>, temperature: Temperature) -> EosResult<Self> {
265        let m = Moles::from_reduced(arr1(&[1.0]));
266        let p = Self::starting_pressure_ideal_gas_bubble(eos, temperature, &arr1(&[1.0]))?.0;
267        PhaseEquilibrium::new_npt(eos, temperature, p, &m, &m)?.check_trivial_solution()
268    }
269
270    fn init_pure_spinodal(eos: &Arc<E>, temperature: Temperature) -> EosResult<Self> {
271        let p = Self::starting_pressure_spinodal(eos, temperature, &arr1(&[1.0]))?;
272        let m = Moles::from_reduced(arr1(&[1.0]));
273        PhaseEquilibrium::new_npt(eos, temperature, p, &m, &m)
274    }
275
276    /// Initialize a new VLE for a pure substance for a given pressure.
277    fn init_pure_p(eos: &Arc<E>, pressure: Pressure) -> EosResult<Self> {
278        let trial_temperatures = [
279            Temperature::from_reduced(300.0),
280            Temperature::from_reduced(500.0),
281            Temperature::from_reduced(200.0),
282        ];
283        let m = Moles::from_reduced(arr1(&[1.0]));
284        let mut vle = None;
285        let mut t0 = Temperature::from_reduced(1.0);
286        for t in trial_temperatures.iter() {
287            t0 = *t;
288            let _vle = PhaseEquilibrium::new_npt(eos, *t, pressure, &m, &m)?;
289            if !Self::is_trivial_solution(_vle.vapor(), _vle.liquid()) {
290                return Ok(_vle);
291            }
292            vle = Some(_vle);
293        }
294
295        let cp = State::critical_point(eos, None, None, SolverOptions::default())?;
296        if pressure > cp.pressure(Contributions::Total) {
297            return Err(EosError::SuperCritical);
298        };
299        if let Some(mut e) = vle {
300            if e.vapor().density < cp.density {
301                for _ in 0..8 {
302                    t0 *= SCALE_T_NEW;
303                    e.0[1] = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Liquid)?;
304                    if e.liquid().density > cp.density {
305                        break;
306                    }
307                }
308            } else {
309                for _ in 0..8 {
310                    t0 /= SCALE_T_NEW;
311                    e.0[0] = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?;
312                    if e.vapor().density < cp.density {
313                        break;
314                    }
315                }
316            }
317
318            for _ in 0..20 {
319                let h = |s: &State<_>| s.residual_enthalpy() + s.total_moles * RGAS * s.temperature;
320                t0 = (h(e.vapor()) - h(e.liquid()))
321                    / (e.vapor().residual_entropy()
322                        - e.liquid().residual_entropy()
323                        - RGAS
324                            * e.vapor().total_moles
325                            * ((e.vapor().density / e.liquid().density).into_value().ln()));
326                let trial_state =
327                    State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?;
328                if trial_state.density < cp.density {
329                    e.0[0] = trial_state;
330                }
331                let trial_state =
332                    State::new_npt(eos, t0, pressure, &m, DensityInitialization::Liquid)?;
333                if trial_state.density > cp.density {
334                    e.0[1] = trial_state;
335                }
336                if e.liquid().temperature == e.vapor().temperature {
337                    return Ok(e);
338                }
339            }
340            Err(EosError::IterationFailed(
341                "new_init_p: could not find proper initial state".to_owned(),
342            ))
343        } else {
344            unreachable!()
345        }
346    }
347}
348
349impl<E: Residual> PhaseEquilibrium<E, 2> {
350    /// Calculate the pure component vapor pressures of all
351    /// components in the system for the given temperature.
352    pub fn vapor_pressure(eos: &Arc<E>, temperature: Temperature) -> Vec<Option<Pressure>> {
353        (0..eos.components())
354            .map(|i| {
355                let pure_eos = Arc::new(eos.subset(&[i]));
356                PhaseEquilibrium::pure_t(&pure_eos, temperature, None, SolverOptions::default())
357                    .map(|vle| vle.vapor().pressure(Contributions::Total))
358                    .ok()
359            })
360            .collect()
361    }
362
363    /// Calculate the pure component boiling temperatures of all
364    /// components in the system for the given pressure.
365    pub fn boiling_temperature(eos: &Arc<E>, pressure: Pressure) -> Vec<Option<Temperature>> {
366        (0..eos.components())
367            .map(|i| {
368                let pure_eos = Arc::new(eos.subset(&[i]));
369                PhaseEquilibrium::pure_p(&pure_eos, pressure, None, SolverOptions::default())
370                    .map(|vle| vle.vapor().temperature)
371                    .ok()
372            })
373            .collect()
374    }
375
376    /// Calculate the pure component phase equilibria of all
377    /// components in the system.
378    pub fn vle_pure_comps<TP: TemperatureOrPressure>(
379        eos: &Arc<E>,
380        temperature_or_pressure: TP,
381    ) -> Vec<Option<PhaseEquilibrium<E, 2>>> {
382        (0..eos.components())
383            .map(|i| {
384                let pure_eos = Arc::new(eos.subset(&[i]));
385                PhaseEquilibrium::pure(
386                    &pure_eos,
387                    temperature_or_pressure,
388                    None,
389                    SolverOptions::default(),
390                )
391                .ok()
392                .map(|vle_pure| {
393                    let mut moles_vapor = Moles::from_reduced(Array1::zeros(eos.components()));
394                    let mut moles_liquid = moles_vapor.clone();
395                    moles_vapor.set(i, vle_pure.vapor().total_moles);
396                    moles_liquid.set(i, vle_pure.liquid().total_moles);
397                    let vapor = State::new_nvt(
398                        eos,
399                        vle_pure.vapor().temperature,
400                        vle_pure.vapor().volume,
401                        &moles_vapor,
402                    )
403                    .unwrap();
404                    let liquid = State::new_nvt(
405                        eos,
406                        vle_pure.liquid().temperature,
407                        vle_pure.liquid().volume,
408                        &moles_liquid,
409                    )
410                    .unwrap();
411                    PhaseEquilibrium::from_states(vapor, liquid)
412                })
413            })
414            .collect()
415    }
416}