1use super::PhaseEquilibrium;
2use crate::equation_of_state::Residual;
3use crate::errors::{FeosError, FeosResult};
4use crate::state::{Contributions, State};
5use crate::{Composition, DensityInitialization, ReferenceSystem, SolverOptions, Verbosity};
6use nalgebra::allocator::Allocator;
7use nalgebra::{DefaultAllocator, Dim, Matrix3, OVector, SVector, U1, U2, vector};
8use num_dual::{
9 Dual, Dual2Vec, DualNum, DualStruct, Gradients, first_derivative, implicit_derivative_sp,
10};
11use quantity::{MolarEnergy, MolarVolume, Pressure, RGAS, Temperature};
12
13const MAX_ITER_TP: usize = 400;
14const TOL_TP: f64 = 1e-8;
15
16impl<E: Residual<N>, N: Gradients> PhaseEquilibrium<E, 2, N>
18where
19 DefaultAllocator: Allocator<N> + Allocator<N, N>,
20{
21 pub fn tp_flash<X: Composition<f64, N>>(
27 eos: &E,
28 temperature: Temperature,
29 pressure: Pressure,
30 feed: X,
31 initial_state: Option<&PhaseEquilibrium<E, 2, N>>,
32 options: SolverOptions,
33 non_volatile_components: Option<Vec<usize>>,
34 ) -> FeosResult<Self> {
35 State::new_npt(eos, temperature, pressure, feed, None)?.tp_flash(
36 initial_state,
37 options,
38 non_volatile_components,
39 )
40 }
41}
42
43impl<E: Residual<U2, D>, D: DualNum<f64> + Copy> PhaseEquilibrium<E, 2, U2, D> {
44 pub fn tp_flash_binary<X: Composition<D, U2>>(
49 eos: &E,
50 temperature: Temperature<D>,
51 pressure: Pressure<D>,
52 feed: X,
53 options: SolverOptions,
54 ) -> FeosResult<Self> {
55 let (feed, total_moles) = feed.into_molefracs(eos)?;
56 let z = feed[0];
57 let vle_re = State::new_npt(&eos.re(), temperature.re(), pressure.re(), z.re(), None)?
58 .tp_flash(None, options, None)?;
59
60 let t = temperature.into_reduced();
64 let p = pressure.into_reduced();
65
66 let variables = SVector::from([
68 vle_re.liquid().density.into_reduced().recip(),
69 vle_re.vapor().density.into_reduced().recip(),
70 vle_re.liquid().molefracs[0],
71 vle_re.vapor().molefracs[0],
72 ]);
73
74 let [[v_l, v_v, x, y]] = implicit_derivative_sp(
78 |variables, &[t, p, z]: &[_; 3]| {
79 let [[v_l, v_v, x, y]] = variables.data.0;
80 let beta = (z - x) / (y - x);
81 let eos = eos.lift();
82 let molar_gibbs_energy = |x: Dual2Vec<_, _, _>, v| {
83 let molefracs = vector![x, -x + 1.0];
84 let a_res = eos.residual_helmholtz_energy(t, v, &molefracs);
85 let a_ig = (x * (x / v).ln() - (x - 1.0) * ((-x + 1.0) / v).ln() - 1.0) * t;
86 a_res + a_ig + v * p
87 };
88 molar_gibbs_energy(y, v_v) * beta - molar_gibbs_energy(x, v_l) * (beta - 1.0)
91 },
92 variables,
93 &[t, p, z],
94 )
95 .data
96 .0;
97 let beta = (z - x) / (y - x);
98 let state = |x: D, v| {
99 let density = MolarVolume::from_reduced(v).inv();
100 State::new(eos, temperature, density, x)
101 };
102 Ok(Self::with_vapor_phase_fraction(
103 state(y, v_v)?,
104 state(x, v_l)?,
105 beta,
106 total_moles,
107 ))
108 }
109}
110
111impl<E: Residual<N>, N: Gradients> State<E, N>
113where
114 DefaultAllocator: Allocator<N> + Allocator<N, N>,
115{
116 pub fn tp_flash(
123 &self,
124 initial_state: Option<&PhaseEquilibrium<E, 2, N>>,
125 options: SolverOptions,
126 non_volatile_components: Option<Vec<usize>>,
127 ) -> FeosResult<PhaseEquilibrium<E, 2, N>> {
128 if let Some(initial_state) = initial_state {
130 let mut init = initial_state.clone();
131 init.update_states(
132 self,
133 initial_state.vapor().molefracs.clone(),
134 initial_state.liquid().molefracs.clone(),
135 initial_state.vapor_phase_fraction(),
136 )?;
137 let vle = self.tp_flash_(init, options, non_volatile_components.clone());
138 if vle.is_ok() {
139 return vle;
140 }
141 }
142
143 let (init1, init2) = PhaseEquilibrium::vle_init_stability(self)?;
144 let vle = self.tp_flash_(init1, options, non_volatile_components.clone());
145 if vle.is_ok() {
146 return vle;
147 }
148
149 if let Some(init2) = init2 {
150 self.tp_flash_(init2, options, non_volatile_components)
151 } else {
152 vle
153 }
154 }
155
156 pub fn tp_flash_(
157 &self,
158 mut new_vle_state: PhaseEquilibrium<E, 2, N>,
159 options: SolverOptions,
160 non_volatile_components: Option<Vec<usize>>,
161 ) -> FeosResult<PhaseEquilibrium<E, 2, N>> {
162 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_TP, TOL_TP);
164
165 log_iter!(
166 verbosity,
167 " iter | residual | phase I mole fractions | phase II mole fractions "
168 );
169 log_iter!(verbosity, "{:-<77}", "");
170 log_iter!(
171 verbosity,
172 " {:4} | | {:10.8?} | {:10.8?}",
173 0,
174 new_vle_state.vapor().molefracs.as_slice(),
175 new_vle_state.liquid().molefracs.as_slice(),
176 );
177
178 let mut iter = 0;
179 if non_volatile_components.is_none() {
180 new_vle_state.successive_substitution(
182 self,
183 3,
184 &mut iter,
185 &mut None,
186 tol,
187 verbosity,
188 &non_volatile_components,
189 )?;
190
191 let tpd = [
193 self.tangent_plane_distance(new_vle_state.vapor()),
194 self.tangent_plane_distance(new_vle_state.liquid()),
195 ];
196 let b = new_vle_state.phase_fractions;
197 let dg = b[0] * tpd[0] + b[1] * tpd[1];
198
199 if tpd[0] < 0.0 && dg >= 0.0 {
201 let mut k = (self.ln_phi() - new_vle_state.vapor().ln_phi()).map(f64::exp);
202 if let Some(nvc) = non_volatile_components.as_ref() {
204 nvc.iter().for_each(|&c| k[c] = 0.0);
205 }
206 new_vle_state.rachford_rice_inplace(self, &k)?;
207 new_vle_state.successive_substitution(
208 self,
209 1,
210 &mut iter,
211 &mut None,
212 tol,
213 verbosity,
214 &non_volatile_components,
215 )?;
216 }
217
218 if tpd[1] < 0.0 && dg >= 0.0 {
220 let mut k = (new_vle_state.liquid().ln_phi() - self.ln_phi()).map(f64::exp);
221 if let Some(nvc) = non_volatile_components.as_ref() {
223 nvc.iter().for_each(|&c| k[c] = 0.0);
224 }
225 new_vle_state.rachford_rice_inplace(self, &k)?;
226 new_vle_state.successive_substitution(
227 self,
228 1,
229 &mut iter,
230 &mut None,
231 tol,
232 verbosity,
233 &non_volatile_components,
234 )?;
235 }
236 }
237
238 new_vle_state.accelerated_successive_substitution(
240 self,
241 &mut iter,
242 max_iter,
243 tol,
244 verbosity,
245 &non_volatile_components,
246 )?;
247
248 Ok(new_vle_state)
249 }
250
251 fn tangent_plane_distance(&self, trial_state: &State<E, N>) -> f64 {
252 let ln_phi_z = self.ln_phi();
253 let ln_phi_w = trial_state.ln_phi();
254 let z = &self.molefracs;
255 let w = &trial_state.molefracs;
256 w.dot(&(w.map(f64::ln) + ln_phi_w - z.map(f64::ln) - ln_phi_z))
257 }
258}
259
260impl<E: Residual<N>, N: Gradients> PhaseEquilibrium<E, 2, N>
261where
262 DefaultAllocator: Allocator<N> + Allocator<N, N>,
263{
264 fn accelerated_successive_substitution(
265 &mut self,
266 feed_state: &State<E, N>,
267 iter: &mut usize,
268 max_iter: usize,
269 tol: f64,
270 verbosity: Verbosity,
271 non_volatile_components: &Option<Vec<usize>>,
272 ) -> FeosResult<()> {
273 let (n, _) = feed_state.molefracs.shape_generic();
274 for _ in 0..max_iter {
275 let mut k_vec = std::array::repeat(OVector::zeros_generic(n, U1));
277 if self.successive_substitution(
278 feed_state,
279 5,
280 iter,
281 &mut Some(&mut k_vec),
282 tol,
283 verbosity,
284 non_volatile_components,
285 )? {
286 log_result!(
287 verbosity,
288 "Tp flash: calculation converged in {} step(s)\n",
289 iter
290 );
291 return Ok(());
292 }
293
294 let gibbs = self.molar_gibbs_energy();
296
297 let delta_vec = [
299 &k_vec[1] - &k_vec[0],
300 &k_vec[2] - &k_vec[1],
301 &k_vec[3] - &k_vec[2],
302 ];
303 let delta = Matrix3::from_fn(|i, j| delta_vec[i].dot(&delta_vec[j]));
304 let d = delta[(0, 1)] * delta[(0, 1)] - delta[(0, 0)] * delta[(1, 1)];
305 let a = (delta[(0, 2)] * delta[(0, 1)] - delta[(1, 2)] * delta[(0, 0)]) / d;
306 let b = (delta[(1, 2)] * delta[(0, 1)] - delta[(0, 2)] * delta[(1, 1)]) / d;
307
308 let mut k = (&k_vec[3]
309 + ((b * &delta_vec[1] + (a + b) * &delta_vec[2]) / (1.0 - a - b)))
310 .map(f64::exp);
311
312 if let Some(nvc) = non_volatile_components.as_ref() {
314 nvc.iter().for_each(|&c| k[c] = 0.0);
315 }
316 if !k.iter().all(|i| i.is_finite()) {
317 continue;
318 }
319
320 let mut trial_vle_state = self.clone();
322 trial_vle_state.rachford_rice_inplace(feed_state, &k)?;
323 if trial_vle_state.molar_gibbs_energy() < gibbs {
324 *self = trial_vle_state;
325 }
326 }
327 Err(FeosError::NotConverged("TP flash".to_owned()))
328 }
329
330 #[expect(clippy::too_many_arguments)]
331 fn successive_substitution(
332 &mut self,
333 feed_state: &State<E, N>,
334 iterations: usize,
335 iter: &mut usize,
336 k_vec: &mut Option<&mut [OVector<f64, N>; 4]>,
337 abs_tol: f64,
338 verbosity: Verbosity,
339 non_volatile_components: &Option<Vec<usize>>,
340 ) -> FeosResult<bool> {
341 for i in 0..iterations {
342 let ln_phi_v = self.vapor().ln_phi();
343 let ln_phi_l = self.liquid().ln_phi();
344 let mut k = (&ln_phi_l - &ln_phi_v).map(f64::exp);
345
346 if let Some(nvc) = non_volatile_components.as_ref() {
348 nvc.iter().for_each(|&c| k[c] = 0.0);
349 }
350
351 *iter += 1;
353 let mut res_vec = k.component_mul(&self.liquid().molefracs) - &self.vapor().molefracs;
354
355 if let Some(nvc) = non_volatile_components.as_ref() {
357 nvc.iter().for_each(|&c| res_vec[c] = 0.0);
358 }
359
360 let res = res_vec.norm();
361 log_iter!(
362 verbosity,
363 " {:4} | {:14.8e} | {:.8?} | {:.8?}",
364 iter,
365 res,
366 self.vapor().molefracs.as_slice(),
367 self.liquid().molefracs.as_slice(),
368 );
369 if res < abs_tol {
370 return Ok(true);
371 }
372
373 self.rachford_rice_inplace(feed_state, &k)?;
374 if let Some(k_vec) = k_vec
375 && i >= iterations - 3
376 {
377 k_vec[i + 3 - iterations] = k.map(|ki| if ki > 0.0 { ki.ln() } else { 0.0 });
378 }
379 }
380 Ok(false)
381 }
382
383 fn rachford_rice_inplace(
384 &mut self,
385 feed_state: &State<E, N>,
386 k: &OVector<f64, N>,
387 ) -> FeosResult<()> {
388 let (b, [v, l]) =
390 rachford_rice(&feed_state.molefracs, k, Some(self.vapor_phase_fraction()))?;
391 self.update_states(feed_state, v, l, b)
392 }
393
394 fn update_states(
395 &mut self,
396 feed_state: &State<E, N>,
397 vapor_molefracs: OVector<f64, N>,
398 liquid_molefracs: OVector<f64, N>,
399 beta: f64,
400 ) -> FeosResult<()> {
401 let vapor = State::new_npt(
402 &feed_state.eos,
403 feed_state.temperature,
404 feed_state.pressure(Contributions::Total),
405 vapor_molefracs,
406 Some(DensityInitialization::InitialDensity(self.vapor().density)),
407 )?;
408 let liquid = State::new_npt(
409 &feed_state.eos,
410 feed_state.temperature,
411 feed_state.pressure(Contributions::Total),
412 liquid_molefracs,
413 Some(DensityInitialization::InitialDensity(self.liquid().density)),
414 )?;
415
416 *self = Self::with_vapor_phase_fraction(vapor, liquid, beta, feed_state.total_moles);
417
418 Ok(())
419 }
420
421 fn vle_init_stability(feed_state: &State<E, N>) -> FeosResult<(Self, Option<Self>)> {
422 let mut stable_states = feed_state.stability_analysis(SolverOptions::default())?;
423 let state1 = stable_states.pop();
424 let state2 = stable_states.pop();
425 if let Some(s1) = state1 {
426 let init1 = if s1.density < feed_state.density {
427 Self::two_phase(s1.clone(), feed_state.clone())
428 } else {
429 Self::two_phase(feed_state.clone(), s1.clone())
430 };
431 let init2 = state2.map(|s2| {
432 if s1.density < s2.density {
433 Self::two_phase(s1.clone(), s2.clone())
434 } else {
435 Self::two_phase(s2.clone(), s1.clone())
436 }
437 });
438 Ok((init1, init2))
439 } else {
440 Err(FeosError::NoPhaseSplit)
441 }
442 }
443
444 fn molar_gibbs_energy(&self) -> MolarEnergy {
446 self.states
447 .iter()
448 .fold(MolarEnergy::from_reduced(0.0), |acc, s| {
449 let ln_rho_m1 = s.partial_density().to_reduced().map(|r| r.ln() - 1.0);
450 acc + s.residual_molar_helmholtz_energy()
451 + s.pressure(Contributions::Total) * s.molar_volume
452 + RGAS * s.temperature * s.molefracs.dot(&ln_rho_m1)
453 })
454 }
455}
456
457fn rachford_rice<N: Dim>(
458 feed: &OVector<f64, N>,
459 k: &OVector<f64, N>,
460 beta_in: Option<f64>,
461) -> FeosResult<(f64, [OVector<f64, N>; 2])>
462where
463 DefaultAllocator: Allocator<N>,
464{
465 const MAX_ITER: usize = 10;
466 const ABS_TOL: f64 = 1e-6;
467
468 let (mut beta_min, mut beta_max) = if feed.dot(k) > 1.0
470 && feed
471 .component_div(k)
472 .iter()
473 .filter(|x| !x.is_nan())
474 .sum::<f64>()
475 > 1.0
476 {
477 (0.0, 1.0)
478 } else {
479 return Err(FeosError::IterationFailed(String::from("rachford_rice")));
480 };
481
482 for (&k, &f) in k.iter().zip(feed.iter()) {
484 if k > 1.0 {
485 let b = (k * f - 1.0) / (k - 1.0);
486 if b > beta_min {
487 beta_min = b;
488 }
489 }
490 if k < 1.0 {
491 let b = (1.0 - f) / (1.0 - k);
492 if b < beta_max {
493 beta_max = b;
494 }
495 }
496 }
497
498 let mut beta = 0.5 * (beta_min + beta_max);
500 if let Some(b) = beta_in
501 && b > beta_min
502 && b < beta_max
503 {
504 beta = b;
505 }
506 let g = feed.dot(&k.map(|k| (k - 1.0) / (1.0 - beta + beta * k)));
507 if g > 0.0 {
508 beta_min = beta
509 } else {
510 beta_max = beta
511 }
512
513 for _ in 0..MAX_ITER {
515 let (g, dg) = first_derivative(
516 |beta| {
517 let frac = k.map(|k| (-beta + beta * k + 1.0).recip() * (k - 1.0));
518 feed.map(Dual::from).dot(&frac)
519 },
520 beta,
521 );
522 if g > 0.0 {
523 beta_min = beta;
524 } else {
525 beta_max = beta;
526 }
527
528 let dbeta = g / dg;
529 beta -= dbeta;
530
531 if beta < beta_min || beta > beta_max {
532 beta = 0.5 * (beta_min + beta_max);
533 }
534 if dbeta.abs() < ABS_TOL {
535 let v = feed.component_mul(&k.map(|k| beta * k / (1.0 - beta + beta * k)));
537 let l = feed.component_mul(&k.map(|k| (1.0 - beta) / (1.0 - beta + beta * k)));
538 return Ok((beta, [v, l]));
539 }
540 }
541
542 let v = feed.component_mul(&k.map(|k| beta * k / (1.0 - beta + beta * k)));
544 let l = feed.component_mul(&k.map(|k| (1.0 - beta) / (1.0 - beta + beta * k)));
545
546 Ok((beta, [v, l]))
547}