feos_core/phase_equilibria/
tp_flash.rs

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