feos_core/phase_equilibria/
tp_flash.rs1use 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
13impl<E: Residual> PhaseEquilibrium<E, 2> {
15 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
37impl<E: Residual> State<E> {
39 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 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 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 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 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 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 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 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 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 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 let mut k_vec = Matrix4xX::zeros(self.vapor().eos.components());
194 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 let gibbs = self.total_gibbs_energy();
214
215 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 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 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 if let Some(nvc) = non_volatile_components.as_ref() {
263 nvc.iter().for_each(|&c| k[c] = 0.0);
264 }
265
266 *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 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 let mut beta = self.vapor_phase_fraction();
308 beta = rachford_rice(&feed_state.molefracs, k, Some(beta))?;
309
310 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 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 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 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 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}