feos_core/phase_equilibria/
tp_flash.rs

1use super::PhaseEquilibrium;
2use crate::equation_of_state::Residual;
3use crate::errors::{EosError, EosResult};
4use crate::state::{Contributions, DensityInitialization, State};
5use crate::{SolverOptions, Verbosity};
6use ndarray::*;
7use num_dual::linalg::norm;
8use quantity::{Dimensionless, Moles, Pressure, Temperature};
9use std::sync::Arc;
10
11const MAX_ITER_TP: usize = 400;
12const TOL_TP: f64 = 1e-8;
13
14/// # Flash calculations
15impl<E: Residual> PhaseEquilibrium<E, 2> {
16    /// Perform a Tp-flash calculation. If no initial values are
17    /// given, the solution is initialized using a stability analysis.
18    ///
19    /// The algorithm can be use to calculate phase equilibria of systems
20    /// containing non-volatile components (e.g. ions).
21    pub fn tp_flash(
22        eos: &Arc<E>,
23        temperature: Temperature,
24        pressure: Pressure,
25        feed: &Moles<Array1<f64>>,
26        initial_state: Option<&PhaseEquilibrium<E, 2>>,
27        options: SolverOptions,
28        non_volatile_components: Option<Vec<usize>>,
29    ) -> EosResult<Self> {
30        State::new_npt(
31            eos,
32            temperature,
33            pressure,
34            feed,
35            DensityInitialization::None,
36        )?
37        .tp_flash(initial_state, options, non_volatile_components)
38    }
39}
40
41/// # Flash calculations
42impl<E: Residual> State<E> {
43    /// Perform a Tp-flash calculation using the [State] as feed.
44    /// If no initial values are given, the solution is initialized
45    /// using a stability analysis.
46    ///
47    /// The algorithm can be use to calculate phase equilibria of systems
48    /// containing non-volatile components (e.g. ions).
49    pub fn tp_flash(
50        &self,
51        initial_state: Option<&PhaseEquilibrium<E, 2>>,
52        options: SolverOptions,
53        non_volatile_components: Option<Vec<usize>>,
54    ) -> EosResult<PhaseEquilibrium<E, 2>> {
55        // initialization
56        if let Some(init) = initial_state {
57            let vle = self.tp_flash_(
58                init.clone()
59                    .update_pressure(self.temperature, self.pressure(Contributions::Total))?,
60                options,
61                non_volatile_components.clone(),
62            );
63            if vle.is_ok() {
64                return vle;
65            }
66        }
67
68        let (init1, init2) = PhaseEquilibrium::vle_init_stability(self)?;
69        let vle = self.tp_flash_(init1, options, non_volatile_components.clone());
70        if vle.is_ok() {
71            return vle;
72        }
73
74        if let Some(init2) = init2 {
75            self.tp_flash_(init2, options, non_volatile_components)
76        } else {
77            vle
78        }
79    }
80
81    pub fn tp_flash_(
82        &self,
83        mut new_vle_state: PhaseEquilibrium<E, 2>,
84        options: SolverOptions,
85        non_volatile_components: Option<Vec<usize>>,
86    ) -> EosResult<PhaseEquilibrium<E, 2>> {
87        // set options
88        let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_TP, TOL_TP);
89
90        log_iter!(
91            verbosity,
92            " iter |    residual    |  phase I mole fractions  |  phase II mole fractions  "
93        );
94        log_iter!(verbosity, "{:-<77}", "");
95        log_iter!(
96            verbosity,
97            " {:4} |                | {:10.8} | {:10.8}",
98            0,
99            new_vle_state.vapor().molefracs,
100            new_vle_state.liquid().molefracs,
101        );
102
103        let mut iter = 0;
104        if non_volatile_components.is_none() {
105            // 3 steps of successive substitution
106            new_vle_state.successive_substitution(
107                self,
108                3,
109                &mut iter,
110                &mut None,
111                tol,
112                verbosity,
113                &non_volatile_components,
114            )?;
115
116            // check convergence
117            let beta = new_vle_state.vapor_phase_fraction();
118            let tpd = [
119                self.tangent_plane_distance(new_vle_state.vapor()),
120                self.tangent_plane_distance(new_vle_state.liquid()),
121            ];
122            let dg = (1.0 - beta) * tpd[1] + beta * tpd[0];
123
124            // fix if only tpd[1] is positive
125            if tpd[0] < 0.0 && dg >= 0.0 {
126                let mut k = (self.ln_phi() - new_vle_state.vapor().ln_phi()).mapv(f64::exp);
127                // Set k = 0 for non-volatile components
128                if let Some(nvc) = non_volatile_components.as_ref() {
129                    nvc.iter().for_each(|&c| k[c] = 0.0);
130                }
131                new_vle_state.update_states(self, &k)?;
132                new_vle_state.successive_substitution(
133                    self,
134                    1,
135                    &mut iter,
136                    &mut None,
137                    tol,
138                    verbosity,
139                    &non_volatile_components,
140                )?;
141            }
142
143            // fix if only tpd[0] is positive
144            if tpd[1] < 0.0 && dg >= 0.0 {
145                let mut k = (new_vle_state.liquid().ln_phi() - self.ln_phi()).mapv(f64::exp);
146                // Set k = 0 for non-volatile components
147                if let Some(nvc) = non_volatile_components.as_ref() {
148                    nvc.iter().for_each(|&c| k[c] = 0.0);
149                }
150                new_vle_state.update_states(self, &k)?;
151                new_vle_state.successive_substitution(
152                    self,
153                    1,
154                    &mut iter,
155                    &mut None,
156                    tol,
157                    verbosity,
158                    &non_volatile_components,
159                )?;
160            }
161        }
162
163        //continue with accelerated successive subsitution
164        new_vle_state.accelerated_successive_substitution(
165            self,
166            &mut iter,
167            max_iter,
168            tol,
169            verbosity,
170            &non_volatile_components,
171        )?;
172
173        Ok(new_vle_state)
174    }
175
176    fn tangent_plane_distance(&self, trial_state: &State<E>) -> f64 {
177        let ln_phi_z = self.ln_phi();
178        let ln_phi_w = trial_state.ln_phi();
179        let z = &self.molefracs;
180        let w = &trial_state.molefracs;
181        (w * &(w.mapv(f64::ln) + ln_phi_w - z.mapv(f64::ln) - ln_phi_z)).sum()
182    }
183}
184
185impl<E: Residual> PhaseEquilibrium<E, 2> {
186    fn accelerated_successive_substitution(
187        &mut self,
188        feed_state: &State<E>,
189        iter: &mut usize,
190        max_iter: usize,
191        tol: f64,
192        verbosity: Verbosity,
193        non_volatile_components: &Option<Vec<usize>>,
194    ) -> EosResult<()> {
195        for _ in 0..max_iter {
196            // do 5 successive substitution steps and check for convergence
197            let mut k_vec = Array::zeros((4, self.vapor().eos.components()));
198            if self.successive_substitution(
199                feed_state,
200                5,
201                iter,
202                &mut Some(&mut k_vec),
203                tol,
204                verbosity,
205                non_volatile_components,
206            )? {
207                log_result!(
208                    verbosity,
209                    "Tp flash: calculation converged in {} step(s)\n",
210                    iter
211                );
212                return Ok(());
213            }
214
215            // calculate total Gibbs energy before the extrapolation
216            let gibbs = self.total_gibbs_energy();
217
218            // extrapolate K values
219            let delta_vec = &k_vec.slice(s![1.., ..]) - &k_vec.slice(s![..3, ..]);
220            let delta = Array::from_shape_fn((3, 3), |(i, j)| {
221                (&delta_vec.index_axis(Axis(0), i) * &delta_vec.index_axis(Axis(0), j)).sum()
222            });
223            let d = delta[(0, 1)] * delta[(0, 1)] - delta[(0, 0)] * delta[(1, 1)];
224            let a = (delta[(0, 2)] * delta[(0, 1)] - delta[(1, 2)] * delta[(0, 0)]) / d;
225            let b = (delta[(1, 2)] * delta[(0, 1)] - delta[(0, 2)] * delta[(1, 1)]) / d;
226
227            let mut k = (&k_vec.index_axis(Axis(0), 3)
228                + &((b * &delta_vec.index_axis(Axis(0), 1)
229                    + (a + b) * &delta_vec.index_axis(Axis(0), 2))
230                    / (1.0 - a - b)))
231                .mapv(f64::exp);
232
233            // Set k = 0 for non-volatile components
234            if let Some(nvc) = non_volatile_components.as_ref() {
235                nvc.iter().for_each(|&c| k[c] = 0.0);
236            }
237            if !k.iter().all(|i| i.is_finite()) {
238                continue;
239            }
240
241            // calculate new states
242            let mut trial_vle_state = self.clone();
243            trial_vle_state.update_states(feed_state, &k)?;
244            if trial_vle_state.total_gibbs_energy() < gibbs {
245                *self = trial_vle_state;
246            }
247        }
248        Err(EosError::NotConverged("TP flash".to_owned()))
249    }
250
251    #[expect(clippy::too_many_arguments)]
252    fn successive_substitution(
253        &mut self,
254        feed_state: &State<E>,
255        iterations: usize,
256        iter: &mut usize,
257        k_vec: &mut Option<&mut Array2<f64>>,
258        abs_tol: f64,
259        verbosity: Verbosity,
260        non_volatile_components: &Option<Vec<usize>>,
261    ) -> EosResult<bool> {
262        for i in 0..iterations {
263            let ln_phi_v = self.vapor().ln_phi();
264            let ln_phi_l = self.liquid().ln_phi();
265            let mut k = (&ln_phi_l - &ln_phi_v).mapv(f64::exp);
266
267            // Set k = 0 for non-volatile components
268            if let Some(nvc) = non_volatile_components.as_ref() {
269                nvc.iter().for_each(|&c| k[c] = 0.0);
270            }
271
272            // check for convergence
273            *iter += 1;
274            let mut res_vec = ln_phi_l - ln_phi_v
275                + (&self.liquid().molefracs / &self.vapor().molefracs).map(|&i| {
276                    if i > 0.0 {
277                        i.ln()
278                    } else {
279                        0.0
280                    }
281                });
282
283            // Set residuum to 0 for non-volatile components
284            if let Some(nvc) = non_volatile_components.as_ref() {
285                nvc.iter().for_each(|&c| res_vec[c] = 0.0);
286            }
287            let res = norm(&res_vec);
288            log_iter!(
289                verbosity,
290                " {:4} | {:14.8e} | {:.8} | {:.8}",
291                iter,
292                res,
293                self.vapor().molefracs,
294                self.liquid().molefracs,
295            );
296            if res < abs_tol {
297                return Ok(true);
298            }
299
300            self.update_states(feed_state, &k)?;
301            if let Some(k_vec) = k_vec {
302                if i >= iterations - 3 {
303                    k_vec
304                        .index_axis_mut(Axis(0), i + 3 - iterations)
305                        .assign(&k.map(|ki| if *ki > 0.0 { ki.ln() } else { 0.0 }));
306                }
307            }
308        }
309        Ok(false)
310    }
311
312    fn update_states(&mut self, feed_state: &State<E>, k: &Array1<f64>) -> EosResult<()> {
313        // calculate vapor phase fraction using Rachford-Rice algorithm
314        let mut beta = self.vapor_phase_fraction();
315        beta = rachford_rice(&feed_state.molefracs, k, Some(beta))?;
316
317        // update VLE
318        let v = feed_state.moles.clone() * Dimensionless::new(beta * k / (1.0 - beta + beta * k));
319        let l =
320            feed_state.moles.clone() * Dimensionless::new((1.0 - beta) / (1.0 - beta + beta * k));
321        self.update_moles(feed_state.pressure(Contributions::Total), [&v, &l])?;
322        Ok(())
323    }
324
325    fn vle_init_stability(feed_state: &State<E>) -> EosResult<(Self, Option<Self>)> {
326        let mut stable_states = feed_state.stability_analysis(SolverOptions::default())?;
327        let state1 = stable_states.pop();
328        let state2 = stable_states.pop();
329        if let Some(s1) = state1 {
330            let init1 = Self::from_states(s1.clone(), feed_state.clone());
331            if let Some(s2) = state2 {
332                Ok((Self::from_states(s1, s2), Some(init1)))
333            } else {
334                Ok((init1, None))
335            }
336        } else {
337            Err(EosError::NoPhaseSplit)
338        }
339    }
340}
341
342fn rachford_rice(feed: &Array1<f64>, k: &Array1<f64>, beta_in: Option<f64>) -> EosResult<f64> {
343    const MAX_ITER: usize = 10;
344    const ABS_TOL: f64 = 1e-6;
345
346    // check if solution exists
347    let (mut beta_min, mut beta_max) =
348        if (feed * k).sum() > 1.0 && (feed / k).iter().filter(|x| !x.is_nan()).sum::<f64>() > 1.0 {
349            (0.0, 1.0)
350        } else {
351            return Err(EosError::IterationFailed(String::from("rachford_rice")));
352        };
353
354    // look for tighter bounds
355    for (&k, &f) in k.iter().zip(feed.iter()) {
356        if k > 1.0 {
357            let b = (k * f - 1.0) / (k - 1.0);
358            if b > beta_min {
359                beta_min = b;
360            }
361        }
362        if k < 1.0 {
363            let b = (1.0 - f) / (1.0 - k);
364            if b < beta_max {
365                beta_max = b;
366            }
367        }
368    }
369
370    // initialize
371    let mut beta = 0.5 * (beta_min + beta_max);
372    if let Some(b) = beta_in {
373        if b > beta_min && b < beta_max {
374            beta = b;
375        }
376    }
377    let g = (feed * &(k - 1.0) / (1.0 - beta + beta * k)).sum();
378    if g > 0.0 {
379        beta_min = beta
380    } else {
381        beta_max = beta
382    }
383
384    // iterate
385    for _ in 0..MAX_ITER {
386        let frac = (k - 1.0) / (1.0 - beta + beta * k);
387        let g = (feed * &frac).sum();
388        let dg = -(feed * &frac * &frac).sum();
389        if g > 0.0 {
390            beta_min = beta;
391        } else {
392            beta_max = beta;
393        }
394
395        let dbeta = g / dg;
396        beta -= dbeta;
397
398        if beta < beta_min || beta > beta_max {
399            beta = 0.5 * (beta_min + beta_max);
400        }
401        if dbeta.abs() < ABS_TOL {
402            return Ok(beta);
403        }
404    }
405
406    Ok(beta)
407}