feos_core/phase_equilibria/
tp_flash.rs1use 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
14impl<E: Residual> PhaseEquilibrium<E, 2> {
16 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
41impl<E: Residual> State<E> {
43 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 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 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 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 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 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 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 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 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 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 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 let gibbs = self.total_gibbs_energy();
217
218 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 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 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 if let Some(nvc) = non_volatile_components.as_ref() {
269 nvc.iter().for_each(|&c| k[c] = 0.0);
270 }
271
272 *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 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 let mut beta = self.vapor_phase_fraction();
315 beta = rachford_rice(&feed_state.molefracs, k, Some(beta))?;
316
317 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 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 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 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 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}