1use super::bubble_dew::TemperatureOrPressure;
2use super::{PhaseDiagram, PhaseEquilibrium};
3use crate::errors::{FeosError, FeosResult};
4use crate::state::{Contributions, DensityInitialization::Vapor, State};
5use crate::{ReferenceSystem, Residual, SolverOptions, Subset};
6use nalgebra::{DVector, dvector, matrix, stack, vector};
7use ndarray::{Array1, s};
8use num_dual::linalg::LU;
9use num_dual::{Dual64, DualNum, first_derivative, partial, partial2};
10use quantity::{Density, Moles, Pressure, RGAS, Temperature};
11
12const DEFAULT_POINTS: usize = 51;
13
14impl<E: Residual + Subset> PhaseDiagram<E, 2> {
15 pub fn binary_vle<TP: TemperatureOrPressure>(
22 eos: &E,
23 temperature_or_pressure: TP,
24 npoints: Option<usize>,
25 x_lle: Option<(f64, f64)>,
26 bubble_dew_options: (SolverOptions, SolverOptions),
27 ) -> FeosResult<Self> {
28 let npoints = npoints.unwrap_or(DEFAULT_POINTS);
29
30 let vle_sat = PhaseEquilibrium::vle_pure_comps(eos, temperature_or_pressure);
32 let vle_sat = [vle_sat[1].clone(), vle_sat[0].clone()];
33
34 if let Some(x_lle) = x_lle {
36 let [states1, states2] = Self::calculate_vlle(
37 eos,
38 temperature_or_pressure,
39 npoints,
40 x_lle,
41 vle_sat,
42 bubble_dew_options,
43 )?;
44
45 let states = states1
46 .into_iter()
47 .chain(states2.into_iter().rev())
48 .collect();
49 return Ok(Self { states });
50 }
51
52 let bubble = temperature_or_pressure.temperature().is_some();
54
55 let (x_lim, vle_lim, bubble) = match vle_sat {
57 [None, None] => return Err(FeosError::SuperCritical),
58 [Some(vle2), None] => {
59 let cp = State::critical_point_binary(
60 eos,
61 temperature_or_pressure,
62 None,
63 None,
64 None,
65 SolverOptions::default(),
66 )?;
67 let x_max = cp.molefracs[0];
68 let cp_vle = PhaseEquilibrium::single_phase(cp);
69 ([0.0, x_max], (vle2, cp_vle), bubble)
70 }
71 [None, Some(vle1)] => {
72 let cp = State::critical_point_binary(
73 eos,
74 temperature_or_pressure,
75 None,
76 None,
77 None,
78 SolverOptions::default(),
79 )?;
80 let x_min = cp.molefracs[0];
81 let cp_vle = PhaseEquilibrium::single_phase(cp);
82 ([1.0, x_min], (vle1, cp_vle), bubble)
83 }
84 [Some(vle2), Some(vle1)] => ([0.0, 1.0], (vle2, vle1), true),
85 };
86
87 let mut states = iterate_vle(
88 eos,
89 temperature_or_pressure,
90 &x_lim,
91 vle_lim.0,
92 Some(vle_lim.1),
93 npoints,
94 bubble,
95 bubble_dew_options,
96 );
97 if !bubble {
98 states = states.into_iter().rev().collect();
99 }
100 let states = check_for_vlle(temperature_or_pressure, states, npoints, bubble_dew_options)?;
101 Ok(Self { states })
102 }
103
104 fn calculate_vlle<TP: TemperatureOrPressure>(
105 eos: &E,
106 tp: TP,
107 npoints: usize,
108 x_lle: (f64, f64),
109 vle_sat: [Option<PhaseEquilibrium<E, 2>>; 2],
110 bubble_dew_options: (SolverOptions, SolverOptions),
111 ) -> FeosResult<[Vec<PhaseEquilibrium<E, 2>>; 2]> {
112 match vle_sat {
113 [Some(vle2), Some(vle1)] => {
114 let states1 = iterate_vle(
115 eos,
116 tp,
117 &[0.0, x_lle.0],
118 vle2,
119 None,
120 npoints / 2,
121 true,
122 bubble_dew_options,
123 );
124 let states2 = iterate_vle(
125 eos,
126 tp,
127 &[1.0, x_lle.1],
128 vle1,
129 None,
130 npoints - npoints / 2,
131 true,
132 bubble_dew_options,
133 );
134 Ok([states1, states2])
135 }
136 _ => Err(FeosError::SuperCritical),
137 }
138 }
139
140 pub fn lle<TP: TemperatureOrPressure>(
147 eos: &E,
148 temperature_or_pressure: TP,
149 feed: &Moles<DVector<f64>>,
150 min_tp: TP::Other,
151 max_tp: TP::Other,
152 npoints: Option<usize>,
153 ) -> FeosResult<Self> {
154 let npoints = npoints.unwrap_or(DEFAULT_POINTS);
155 let mut states = Vec::with_capacity(npoints);
156
157 let (t_vec, p_vec) = temperature_or_pressure.linspace(min_tp, max_tp, npoints);
158 let mut vle = None;
159 for i in 0..npoints {
160 let (t, p) = (t_vec.get(i), p_vec.get(i));
161 vle = PhaseEquilibrium::tp_flash(
162 eos,
163 t,
164 p,
165 feed,
166 vle.as_ref(),
167 SolverOptions::default(),
168 None,
169 )
170 .ok();
171 if let Some(vle) = vle.as_ref() {
172 states.push(vle.clone());
173 }
174 }
175 Ok(Self { states })
176 }
177}
178
179#[expect(clippy::too_many_arguments)]
180fn iterate_vle<E: Residual + Subset, TP: TemperatureOrPressure>(
181 eos: &E,
182 tp: TP,
183 x_lim: &[f64],
184 vle_0: PhaseEquilibrium<E, 2>,
185 vle_1: Option<PhaseEquilibrium<E, 2>>,
186 npoints: usize,
187 bubble: bool,
188 bubble_dew_options: (SolverOptions, SolverOptions),
189) -> Vec<PhaseEquilibrium<E, 2>> {
190 let mut vle_vec = Vec::with_capacity(npoints);
191
192 let x = Array1::linspace(x_lim[0], x_lim[1], npoints);
193 let x = if vle_1.is_some() {
194 x.slice(s![1..-1])
195 } else {
196 x.slice(s![1..])
197 };
198
199 let tp_0 = Some(TP::from_state(vle_0.vapor()));
200 let mut tp_old = tp_0;
201 let mut y_old = None;
202 vle_vec.push(vle_0);
203 for xi in x {
204 let vle = PhaseEquilibrium::bubble_dew_point(
205 eos,
206 tp,
207 dvector![*xi, 1.0 - xi],
208 tp_old,
209 y_old.as_ref(),
210 bubble,
211 bubble_dew_options,
212 );
213
214 if let Ok(vle) = vle {
215 y_old = Some(if bubble {
216 vle.vapor().molefracs.clone()
217 } else {
218 vle.liquid().molefracs.clone()
219 });
220 tp_old = Some(TP::from_state(vle.vapor()));
221 vle_vec.push(vle.clone());
222 } else {
223 y_old = None;
224 tp_old = tp_0;
225 }
226 }
227 if let Some(vle_1) = vle_1 {
228 vle_vec.push(vle_1);
229 }
230
231 vle_vec
232}
233
234fn check_for_vlle<E: Residual + Subset, TP: TemperatureOrPressure>(
235 tp: TP,
236 states: Vec<PhaseEquilibrium<E, 2>>,
237 npoints: usize,
238 bubble_dew_options: (SolverOptions, SolverOptions),
239) -> FeosResult<Vec<PhaseEquilibrium<E, 2>>> {
240 let n = states.len();
241 let p: Vec<_> = states
242 .iter()
243 .map(|s| s.vapor().pressure(Contributions::Total))
244 .collect();
245 let t: Vec<_> = states.iter().map(|s| s.vapor().temperature).collect();
246 let x: Vec<_> = states.iter().map(|s| s.liquid().molefracs[0]).collect();
247 let y: Vec<_> = states.iter().map(|s| s.vapor().molefracs[0]).collect();
248
249 if let Some(t) = tp.temperature()
251 && p[1] > p[0]
252 && p[n - 2] > p[n - 1]
253 {
254 let [mut i, mut j] = [0, n - 1];
255 while i != j {
256 if p[i] > p[j] {
257 j -= 1;
258 } else {
259 i += 1
260 }
261 if y[j] < y[i] {
262 let (xj, yj, pj) = if j >= n - 2 {
264 let k_inf = (states[n - 1].liquid().ln_phi() - states[n - 1].vapor().ln_phi())
266 .map(f64::exp)[1];
267 (
268 [1.0, 1.0 - 1.0 / k_inf],
269 [1.0, 0.0],
270 [p[n - 1], p[n - 1] * (2.0 - 1.0 / k_inf)],
271 )
272 } else {
273 ([x[j + 1], x[j]], [y[j + 1], y[j]], [p[j + 1], p[j]])
275 };
276 let (xi, yi, pi) = if i == 1 {
277 let k_inf =
279 (states[0].liquid().ln_phi() - states[0].vapor().ln_phi()).map(f64::exp)[0];
280 (
281 [0.0, 1.0 / k_inf],
282 [0.0, 1.0],
283 [p[0], p[0] * (2.0 - 1.0 / k_inf)],
284 )
285 } else {
286 ([x[i - 1], x[i]], [y[i - 1], y[i]], [p[i - 1], p[i]])
288 };
289 let a = matrix![yi[1] - yi[0], yj[0] - yj[1];
291 (pi[1] - pi[0]).into_reduced(), (pj[0] - pj[1]).into_reduced()];
292 let b = vector![yj[0] - yi[0], (pj[0] - pi[0]).into_reduced()];
293 let [[r, s]] = LU::new(a)?.solve(&b).data.0;
294 let (xi, xj, p) = (
295 xi[0] + r * (xi[1] - xi[0]),
296 xj[0] + s * (xj[1] - xj[0]),
297 pi[0] + r * (pi[1] - pi[0]),
298 );
299 let Ok(vlle) = PhaseEquilibrium::heteroazeotrope(
300 &states[0].liquid().eos,
301 t,
302 (xi, xj),
303 Some(p),
304 Default::default(),
305 bubble_dew_options,
306 ) else {
307 return Ok(states);
308 };
309 let x_hetero = (vlle.liquid1().molefracs[0], vlle.liquid2().molefracs[0]);
310 return Ok(PhaseDiagram::binary_vle(
311 &states[0].liquid().eos,
312 tp,
313 Some(npoints),
314 Some(x_hetero),
315 bubble_dew_options,
316 )
317 .map_or(states, |dia| dia.states));
318 }
319 }
320 } else if let Some(p) = tp.pressure()
321 && t[1] < t[0]
322 && t[n - 2] < t[n - 1]
323 {
324 let [mut i, mut j] = [0, n - 1];
325 while i != j {
326 if t[i] < t[j] {
327 j -= 1;
328 } else {
329 i += 1
330 }
331 if y[j] < y[i] {
332 let (xj, yj, tj) = if j == n - 2 {
334 let vle = &states[n - 1];
336 let k_inf = (vle.liquid().ln_phi() - vle.vapor().ln_phi()).map(f64::exp)[1];
337 let dh = vle.vapor().residual_molar_enthalpy()
338 - vle.liquid().residual_molar_enthalpy();
339 let dv = 1.0 / vle.vapor().density - 1.0 / vle.liquid().density;
340 let pdv_dh = (p * dv).convert_into(dh);
341 (
342 [1.0, 1.0 - 1.0 / k_inf],
343 [1.0, 0.0],
344 [t[n - 1], t[n - 1] * (1.0 - (k_inf - 1.0) / k_inf * pdv_dh)],
345 )
346 } else {
347 ([x[j + 1], x[j]], [y[j + 1], y[j]], [t[j + 1], t[j]])
349 };
350 let (xi, yi, ti) = if i == 1 {
351 let vle = &states[0];
353 let k_inf = (vle.liquid().ln_phi() - vle.vapor().ln_phi()).map(f64::exp)[0];
354 let dh = vle.vapor().residual_molar_enthalpy()
355 - vle.liquid().residual_molar_enthalpy();
356 let dv = 1.0 / vle.vapor().density - 1.0 / vle.liquid().density;
357 let pdv_dh = (p * dv).convert_into(dh);
358 (
359 [0.0, 1.0 / k_inf],
360 [0.0, 1.0],
361 [t[0], t[0] * (1.0 - (k_inf - 1.0) / k_inf * pdv_dh)],
362 )
363 } else {
364 ([x[i - 1], x[i]], [y[i - 1], y[i]], [t[i - 1], t[i]])
366 };
367 let a = matrix![yi[1] - yi[0], yj[0] - yj[1];
369 (ti[1] - ti[0]).into_reduced(), (tj[0] - tj[1]).into_reduced()];
370 let b = vector![yj[0] - yi[0], (tj[0] - ti[0]).into_reduced()];
371 let [[r, s]] = LU::new(a)?.solve(&b).data.0;
372 let (xi, xj, t) = (
373 xi[0] + r * (xi[1] - xi[0]),
374 xj[0] + s * (xj[1] - xj[0]),
375 ti[0] + r * (ti[1] - ti[0]),
376 );
377 let Ok(vlle) = PhaseEquilibrium::heteroazeotrope(
378 &states[0].liquid().eos,
379 p,
380 (xi, xj),
381 Some(t),
382 Default::default(),
383 bubble_dew_options,
384 ) else {
385 return Ok(states);
386 };
387 let x_hetero = (vlle.liquid1().molefracs[0], vlle.liquid2().molefracs[0]);
388 return Ok(PhaseDiagram::binary_vle(
389 &states[0].liquid().eos,
390 tp,
391 Some(npoints),
392 Some(x_hetero),
393 bubble_dew_options,
394 )
395 .map_or(states, |dia| dia.states));
396 }
397 }
398 }
399 Ok(states)
400}
401
402pub struct PhaseDiagramHetero<E> {
404 pub vle1: PhaseDiagram<E, 2>,
405 pub vle2: PhaseDiagram<E, 2>,
406 pub lle: Option<PhaseDiagram<E, 2>>,
407}
408
409impl<E: Residual + Subset> PhaseDiagram<E, 2> {
410 #[expect(clippy::too_many_arguments)]
416 pub fn binary_vlle<TP: TemperatureOrPressure>(
417 eos: &E,
418 temperature_or_pressure: TP,
419 x_lle: (f64, f64),
420 tp_lim_lle: Option<TP::Other>,
421 tp_init_vlle: Option<TP::Other>,
422 npoints_vle: Option<usize>,
423 npoints_lle: Option<usize>,
424 bubble_dew_options: (SolverOptions, SolverOptions),
425 ) -> FeosResult<PhaseDiagramHetero<E>> {
426 let npoints_vle = npoints_vle.unwrap_or(DEFAULT_POINTS);
427
428 let vle_sat = PhaseEquilibrium::vle_pure_comps(eos, temperature_or_pressure);
430 let vle_sat = [vle_sat[1].clone(), vle_sat[0].clone()];
431
432 let vlle = PhaseEquilibrium::heteroazeotrope(
434 eos,
435 temperature_or_pressure,
436 x_lle,
437 tp_init_vlle,
438 SolverOptions::default(),
439 bubble_dew_options,
440 )?;
441 let x_hetero = (vlle.liquid1().molefracs[0], vlle.liquid2().molefracs[0]);
442
443 let [dia1, dia2] = PhaseDiagram::calculate_vlle(
445 eos,
446 temperature_or_pressure,
447 npoints_vle,
448 x_hetero,
449 vle_sat,
450 bubble_dew_options,
451 )?;
452
453 let lle = tp_lim_lle
455 .map(|tp_lim| {
456 let tp_hetero = TP::from_state(vlle.vapor());
457 let x_feed = 0.5 * (x_hetero.0 + x_hetero.1);
458 let feed = Moles::from_reduced(dvector![x_feed, 1.0 - x_feed]);
459 PhaseDiagram::lle(
460 eos,
461 temperature_or_pressure,
462 &feed,
463 tp_lim,
464 tp_hetero,
465 npoints_lle,
466 )
467 })
468 .transpose()?;
469
470 Ok(PhaseDiagramHetero {
471 vle1: PhaseDiagram::new(dia1),
472 vle2: PhaseDiagram::new(dia2),
473 lle,
474 })
475 }
476}
477
478impl<E: Clone> PhaseDiagramHetero<E> {
479 pub fn vle(&self) -> PhaseDiagram<E, 2> {
480 PhaseDiagram::new(
481 self.vle1
482 .states
483 .iter()
484 .chain(self.vle2.states.iter().rev())
485 .cloned()
486 .collect(),
487 )
488 }
489}
490
491const MAX_ITER_HETERO: usize = 50;
492const TOL_HETERO: f64 = 1e-8;
493
494impl<E: Residual> PhaseEquilibrium<E, 3> {
496 pub fn heteroazeotrope<TP: TemperatureOrPressure>(
499 eos: &E,
500 temperature_or_pressure: TP,
501 x_init: (f64, f64),
502 tp_init: Option<TP::Other>,
503 options: SolverOptions,
504 bubble_dew_options: (SolverOptions, SolverOptions),
505 ) -> FeosResult<Self> {
506 let (temperature, pressure, iterate_p) =
507 temperature_or_pressure.temperature_pressure(tp_init);
508 if iterate_p {
509 PhaseEquilibrium::heteroazeotrope_t(
510 eos,
511 temperature.ok_or(FeosError::Error(
512 "Temperature information is expected for heteroazeotrope calculation."
513 .to_string(),
514 ))?,
515 x_init,
516 pressure,
517 options,
518 bubble_dew_options,
519 )
520 } else {
521 PhaseEquilibrium::heteroazeotrope_p(
522 eos,
523 pressure.ok_or(FeosError::Error(
524 "Pressure information is expected for heteroazeotrope calculation.".to_string(),
525 ))?,
526 x_init,
527 temperature,
528 options,
529 bubble_dew_options,
530 )
531 }
532 }
533
534 #[expect(clippy::toplevel_ref_arg)]
537 fn heteroazeotrope_t(
538 eos: &E,
539 temperature: Temperature,
540 x_init: (f64, f64),
541 p_init: Option<Pressure>,
542 options: SolverOptions,
543 bubble_dew_options: (SolverOptions, SolverOptions),
544 ) -> FeosResult<Self> {
545 let x1 = dvector![x_init.0, 1.0 - x_init.0];
547 let x2 = dvector![x_init.1, 1.0 - x_init.1];
548 let vle1 = PhaseEquilibrium::bubble_point(
549 eos,
550 temperature,
551 &x1,
552 p_init,
553 None,
554 bubble_dew_options,
555 )?;
556 let vle2 = PhaseEquilibrium::bubble_point(
557 eos,
558 temperature,
559 &x2,
560 p_init,
561 None,
562 bubble_dew_options,
563 )?;
564 let mut l1 = vle1.liquid().clone();
565 let mut l2 = vle2.liquid().clone();
566 let p0 = (vle1.vapor().pressure(Contributions::Total)
567 + vle2.vapor().pressure(Contributions::Total))
568 * 0.5;
569 let y0 = (&vle1.vapor().molefracs + &vle2.vapor().molefracs) * 0.5;
570 let mut v = State::new_npt(eos, temperature, p0, y0, Some(Vapor))?;
571
572 for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) {
573 let dmu_drho_l1 = (l1.n_dmu_dni(Contributions::Total) * l1.molar_volume).to_reduced();
575 let dmu_drho_l2 = (l2.n_dmu_dni(Contributions::Total) * l2.molar_volume).to_reduced();
576 let dmu_drho_v = (v.n_dmu_dni(Contributions::Total) * v.molar_volume).to_reduced();
577 let dp_drho_l1 = (l1.n_dp_dni(Contributions::Total) * l1.molar_volume)
578 .to_reduced()
579 .transpose();
580 let dp_drho_l2 = (l2.n_dp_dni(Contributions::Total) * l2.molar_volume)
581 .to_reduced()
582 .transpose();
583 let dp_drho_v = (v.n_dp_dni(Contributions::Total) * v.molar_volume)
584 .to_reduced()
585 .transpose();
586 let mu_l1_res = l1.residual_chemical_potential().to_reduced();
587 let mu_l2_res = l2.residual_chemical_potential().to_reduced();
588 let mu_v_res = v.residual_chemical_potential().to_reduced();
589 let p_l1 = l1.pressure(Contributions::Total).to_reduced();
590 let p_l2 = l2.pressure(Contributions::Total).to_reduced();
591 let p_v = v.pressure(Contributions::Total).to_reduced();
592
593 let delta_l1v_mu_ig = (RGAS * v.temperature).to_reduced()
595 * (l1
596 .partial_density()
597 .to_reduced()
598 .component_div(&v.partial_density().to_reduced()))
599 .map(f64::ln);
600 let delta_l2v_mu_ig = (RGAS * v.temperature).to_reduced()
601 * (l2
602 .partial_density()
603 .to_reduced()
604 .component_div(&v.partial_density().to_reduced()))
605 .map(f64::ln);
606 let res = stack![
607 mu_l1_res - &mu_v_res + delta_l1v_mu_ig;
608 mu_l2_res - &mu_v_res + delta_l2v_mu_ig;
609 vector![p_l1 - p_v];
610 vector![p_l2 - p_v]
611 ];
612
613 if res.norm() < options.tol.unwrap_or(TOL_HETERO) {
615 return Ok(Self::new(v, l1, l2));
616 }
617
618 let jacobian = stack![
620 dmu_drho_l1, 0 , -&dmu_drho_v;
621 0 , dmu_drho_l2, -dmu_drho_v;
622 dp_drho_l1 , 0 , -&dp_drho_v;
623 0 , dp_drho_l2 , -dp_drho_v
624 ];
625
626 let dx = LU::new(jacobian)?.solve(&res);
628
629 let rho_l1 =
631 &l1.partial_density() - &Density::from_reduced(dx.rows_range(0..2).into_owned());
632 let rho_l2 =
633 &l2.partial_density() - &Density::from_reduced(dx.rows_range(2..4).into_owned());
634 let rho_v =
635 &v.partial_density() - &Density::from_reduced(dx.rows_range(4..6).into_owned());
636
637 for i in 0..2 {
639 if rho_l1.get(i).is_sign_negative()
640 || rho_l2.get(i).is_sign_negative()
641 || rho_v.get(i).is_sign_negative()
642 {
643 return Err(FeosError::IterationFailed(String::from(
644 "PhaseEquilibrium::heteroazeotrope_t",
645 )));
646 }
647 }
648
649 l1 = State::new_density(eos, temperature, rho_l1)?;
651 l2 = State::new_density(eos, temperature, rho_l2)?;
652 v = State::new_density(eos, temperature, rho_v)?;
653 }
654 Err(FeosError::NotConverged(String::from(
655 "PhaseEquilibrium::heteroazeotrope_t",
656 )))
657 }
658
659 #[expect(clippy::toplevel_ref_arg)]
662 fn heteroazeotrope_p(
663 eos: &E,
664 pressure: Pressure,
665 x_init: (f64, f64),
666 t_init: Option<Temperature>,
667 options: SolverOptions,
668 bubble_dew_options: (SolverOptions, SolverOptions),
669 ) -> FeosResult<Self> {
670 let p = pressure.to_reduced();
671
672 let x1 = dvector![x_init.0, 1.0 - x_init.0];
674 let x2 = dvector![x_init.1, 1.0 - x_init.1];
675 let vle1 =
676 PhaseEquilibrium::bubble_point(eos, pressure, &x1, t_init, None, bubble_dew_options)?;
677 let vle2 =
678 PhaseEquilibrium::bubble_point(eos, pressure, &x2, t_init, None, bubble_dew_options)?;
679 let mut l1 = vle1.liquid().clone();
680 let mut l2 = vle2.liquid().clone();
681 let t0 = (vle1.vapor().temperature + vle2.vapor().temperature) * 0.5;
682 let y0 = (&vle1.vapor().molefracs + &vle2.vapor().molefracs) * 0.5;
683 let mut v = State::new_npt(eos, t0, pressure, y0, Some(Vapor))?;
684
685 for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) {
686 let dmu_drho_l1 = (l1.n_dmu_dni(Contributions::Total) * l1.molar_volume).to_reduced();
688 let dmu_drho_l2 = (l2.n_dmu_dni(Contributions::Total) * l2.molar_volume).to_reduced();
689 let dmu_drho_v = (v.n_dmu_dni(Contributions::Total) * v.molar_volume).to_reduced();
690 let dmu_res_dt_l1 = (l1.dmu_res_dt()).to_reduced();
691 let dmu_res_dt_l2 = (l2.dmu_res_dt()).to_reduced();
692 let dmu_res_dt_v = (v.dmu_res_dt()).to_reduced();
693 let dp_drho_l1 = (l1.n_dp_dni(Contributions::Total) * l1.molar_volume)
694 .to_reduced()
695 .transpose();
696 let dp_drho_l2 = (l2.n_dp_dni(Contributions::Total) * l2.molar_volume)
697 .to_reduced()
698 .transpose();
699 let dp_drho_v = (v.n_dp_dni(Contributions::Total) * v.molar_volume)
700 .to_reduced()
701 .transpose();
702 let dp_dt_l1 = (l1.dp_dt(Contributions::Total)).to_reduced();
703 let dp_dt_l2 = (l2.dp_dt(Contributions::Total)).to_reduced();
704 let dp_dt_v = (v.dp_dt(Contributions::Total)).to_reduced();
705 let mu_l1_res = l1.residual_chemical_potential().to_reduced();
706 let mu_l2_res = l2.residual_chemical_potential().to_reduced();
707 let mu_v_res = v.residual_chemical_potential().to_reduced();
708 let p_l1 = l1.pressure(Contributions::Total).to_reduced();
709 let p_l2 = l2.pressure(Contributions::Total).to_reduced();
710 let p_v = v.pressure(Contributions::Total).to_reduced();
711
712 let delta_l1v_dmu_ig_dt = l1
714 .partial_density()
715 .to_reduced()
716 .component_div(&v.partial_density().to_reduced())
717 .map(f64::ln);
718 let delta_l2v_dmu_ig_dt = l2
719 .partial_density()
720 .to_reduced()
721 .component_div(&v.partial_density().to_reduced())
722 .map(f64::ln);
723 let delta_l1v_mu_ig = (RGAS * v.temperature).to_reduced() * &delta_l1v_dmu_ig_dt;
724 let delta_l2v_mu_ig = (RGAS * v.temperature).to_reduced() * &delta_l2v_dmu_ig_dt;
725 let res = stack![
726 mu_l1_res - &mu_v_res + delta_l1v_mu_ig;
727 mu_l2_res - &mu_v_res + delta_l2v_mu_ig;
728 vector![p_l1 - p];
729 vector![p_l2 - p];
730 vector![p_v - p]
731 ];
732
733 if res.norm() < options.tol.unwrap_or(TOL_HETERO) {
735 return Ok(Self::new(v, l1, l2));
736 }
737
738 let jacobian = stack![
739 dmu_drho_l1, 0, -&dmu_drho_v, dmu_res_dt_l1 - &dmu_res_dt_v + delta_l1v_dmu_ig_dt;
740 0, dmu_drho_l2, -dmu_drho_v, dmu_res_dt_l2 - &dmu_res_dt_v + delta_l2v_dmu_ig_dt;
741 dp_drho_l1, 0, 0, vector![dp_dt_l1];
742 0, dp_drho_l2, 0, vector![dp_dt_l2];
743 0, 0, dp_drho_v, vector![dp_dt_v]
744 ];
745
746 let dx = LU::new(jacobian)?.solve(&res);
748
749 let rho_l1 =
751 l1.partial_density() - Density::from_reduced(dx.rows_range(0..2).into_owned());
752 let rho_l2 =
753 l2.partial_density() - Density::from_reduced(dx.rows_range(2..4).into_owned());
754 let rho_v =
755 v.partial_density() - Density::from_reduced(dx.rows_range(4..6).into_owned());
756 let t = v.temperature - Temperature::from_reduced(dx[6]);
757
758 for i in 0..2 {
760 if rho_l1.get(i).is_sign_negative()
761 || rho_l2.get(i).is_sign_negative()
762 || rho_v.get(i).is_sign_negative()
763 || t.is_sign_negative()
764 {
765 return Err(FeosError::IterationFailed(String::from(
766 "PhaseEquilibrium::heteroazeotrope_p",
767 )));
768 }
769 }
770
771 l1 = State::new_density(eos, t, rho_l1)?;
773 l2 = State::new_density(eos, t, rho_l2)?;
774 v = State::new_density(eos, t, rho_v)?;
775 }
776 Err(FeosError::NotConverged(String::from(
777 "PhaseEquilibrium::heteroazeotrope_p",
778 )))
779 }
780}
781
782impl<E: Residual + Subset> PhaseEquilibrium<E, 2> {
784 pub fn binary_azeotrope<TP: TemperatureOrPressure>(
787 eos: &E,
788 temperature_or_pressure: TP,
789 ) -> FeosResult<Option<Self>> {
790 let vle = Self::vle_pure_comps(eos, temperature_or_pressure);
792
793 let mut iter = vle.into_iter();
795 let (Some(vle1), Some(vle2), None) = (iter.next(), iter.next(), iter.next()) else {
796 return Err(FeosError::IncompatibleComponents(eos.components(), 2));
797 };
798
799 let (Some(vle1), Some(vle2)) = (vle1, vle2) else {
801 return Err(FeosError::SuperCritical);
802 };
803
804 let henry1 =
806 State::henrys_law_constant(eos, vle1.liquid().temperature, &vle1.liquid().molefracs)?
807 [0];
808 let psat1 = vle1.liquid().pressure(Contributions::Total);
809 let henry2 =
810 State::henrys_law_constant(eos, vle2.liquid().temperature, &vle2.liquid().molefracs)?
811 [0];
812 let psat2 = vle2.liquid().pressure(Contributions::Total);
813
814 let ln_alpha1 = henry2.convert_into(psat2).ln();
818 let ln_alpha2 = psat1.convert_into(henry1).ln();
819
820 if ln_alpha1 * ln_alpha2 > 0.0 {
822 return Ok(None);
823 }
824
825 let x0 = -ln_alpha1 / (ln_alpha2 - ln_alpha1);
827
828 let (temperature, pressure, iterate_t) = temperature_or_pressure.temperature_pressure(None);
830 (if iterate_t {
831 Self::iterate_azeotrope_t(eos, temperature.unwrap(), x0, 10, 1e-10)
832 } else {
833 let t_init = vle1.liquid().temperature.min(vle2.liquid().temperature);
834 Self::iterate_azeotrope_p(eos, pressure.unwrap(), x0, t_init, 10, 1e-10)
835 })
836 .map(Some)
837 }
838
839 fn iterate_azeotrope_t(
840 eos: &E,
841 temperature: Temperature,
842 x0: f64,
843 max_iter: usize,
844 tol: f64,
845 ) -> FeosResult<Self> {
846 let x = Self::azeotrope_newton(
847 partial(
848 |x: Dual64, &t: &Temperature<_>| {
849 PhaseEquilibrium::bubble_point(
850 &eos.lift(),
851 t,
852 &dvector![x, -x + 1.0],
853 None,
854 None,
855 Default::default(),
856 )
857 .map(|vle| {
858 (vle.vapor().molefracs[0]
859 / vle.liquid().molefracs[0]
860 / (vle.vapor().molefracs[1] / vle.liquid().molefracs[1]))
861 .ln()
862 })
863 },
864 &temperature,
865 ),
866 x0,
867 max_iter,
868 tol,
869 )?;
870 PhaseEquilibrium::bubble_point(eos, temperature, x, None, None, Default::default())
871 }
872
873 fn iterate_azeotrope_p(
874 eos: &E,
875 pressure: Pressure,
876 x0: f64,
877 t_init: Temperature,
878 max_iter: usize,
879 tol: f64,
880 ) -> FeosResult<Self> {
881 let x = Self::azeotrope_newton(
882 partial2(
883 |x: Dual64, &p: &Pressure<_>, &t_init| {
884 PhaseEquilibrium::bubble_point(
885 &eos.lift(),
886 p,
887 &dvector![x, -x + 1.0],
888 Some(t_init),
889 None,
890 Default::default(),
891 )
892 .map(|vle| {
893 (vle.vapor().molefracs[0]
894 / vle.liquid().molefracs[0]
895 / (vle.vapor().molefracs[1] / vle.liquid().molefracs[1]))
896 .ln()
897 })
898 },
899 &pressure,
900 &t_init,
901 ),
902 x0,
903 max_iter,
904 tol,
905 )?;
906 PhaseEquilibrium::bubble_point(eos, pressure, x, Some(t_init), None, Default::default())
907 }
908
909 fn azeotrope_newton<F: Fn(Dual64) -> FeosResult<Dual64>>(
910 f: F,
911 x0: f64,
912 max_iter: usize,
913 tol: f64,
914 ) -> FeosResult<f64> {
915 let mut x = x0;
916 for _ in 0..max_iter {
917 let (f, df) = first_derivative(&f, x)?;
918 x -= f / df;
919 if f.abs() < tol {
920 return Ok(x);
921 }
922 }
923 Err(FeosError::NotConverged("binary_azeotrope".into()))
924 }
925}