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) -> 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).unwrap().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 states;
308 };
309 let x_hetero = (vlle.liquid1().molefracs[0], vlle.liquid2().molefracs[0]);
310 return 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).unwrap().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 states;
386 };
387 let x_hetero = (vlle.liquid1().molefracs[0], vlle.liquid2().molefracs[0]);
388 return 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 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.unwrap(),
512 x_init,
513 pressure,
514 options,
515 bubble_dew_options,
516 )
517 } else {
518 PhaseEquilibrium::heteroazeotrope_p(
519 eos,
520 pressure.unwrap(),
521 x_init,
522 temperature,
523 options,
524 bubble_dew_options,
525 )
526 }
527 }
528
529 #[expect(clippy::toplevel_ref_arg)]
532 fn heteroazeotrope_t(
533 eos: &E,
534 temperature: Temperature,
535 x_init: (f64, f64),
536 p_init: Option<Pressure>,
537 options: SolverOptions,
538 bubble_dew_options: (SolverOptions, SolverOptions),
539 ) -> FeosResult<Self> {
540 let x1 = dvector![x_init.0, 1.0 - x_init.0];
542 let x2 = dvector![x_init.1, 1.0 - x_init.1];
543 let vle1 = PhaseEquilibrium::bubble_point(
544 eos,
545 temperature,
546 &x1,
547 p_init,
548 None,
549 bubble_dew_options,
550 )?;
551 let vle2 = PhaseEquilibrium::bubble_point(
552 eos,
553 temperature,
554 &x2,
555 p_init,
556 None,
557 bubble_dew_options,
558 )?;
559 let mut l1 = vle1.liquid().clone();
560 let mut l2 = vle2.liquid().clone();
561 let p0 = (vle1.vapor().pressure(Contributions::Total)
562 + vle2.vapor().pressure(Contributions::Total))
563 * 0.5;
564 let y0 = (&vle1.vapor().molefracs + &vle2.vapor().molefracs) * 0.5;
565 let mut v = State::new_npt(eos, temperature, p0, y0, Some(Vapor))?;
566
567 for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) {
568 let dmu_drho_l1 = (l1.n_dmu_dni(Contributions::Total) * l1.molar_volume).to_reduced();
570 let dmu_drho_l2 = (l2.n_dmu_dni(Contributions::Total) * l2.molar_volume).to_reduced();
571 let dmu_drho_v = (v.n_dmu_dni(Contributions::Total) * v.molar_volume).to_reduced();
572 let dp_drho_l1 = (l1.n_dp_dni(Contributions::Total) * l1.molar_volume)
573 .to_reduced()
574 .transpose();
575 let dp_drho_l2 = (l2.n_dp_dni(Contributions::Total) * l2.molar_volume)
576 .to_reduced()
577 .transpose();
578 let dp_drho_v = (v.n_dp_dni(Contributions::Total) * v.molar_volume)
579 .to_reduced()
580 .transpose();
581 let mu_l1_res = l1.residual_chemical_potential().to_reduced();
582 let mu_l2_res = l2.residual_chemical_potential().to_reduced();
583 let mu_v_res = v.residual_chemical_potential().to_reduced();
584 let p_l1 = l1.pressure(Contributions::Total).to_reduced();
585 let p_l2 = l2.pressure(Contributions::Total).to_reduced();
586 let p_v = v.pressure(Contributions::Total).to_reduced();
587
588 let delta_l1v_mu_ig = (RGAS * v.temperature).to_reduced()
590 * (l1
591 .partial_density()
592 .to_reduced()
593 .component_div(&v.partial_density().to_reduced()))
594 .map(f64::ln);
595 let delta_l2v_mu_ig = (RGAS * v.temperature).to_reduced()
596 * (l2
597 .partial_density()
598 .to_reduced()
599 .component_div(&v.partial_density().to_reduced()))
600 .map(f64::ln);
601 let res = stack![
602 mu_l1_res - &mu_v_res + delta_l1v_mu_ig;
603 mu_l2_res - &mu_v_res + delta_l2v_mu_ig;
604 vector![p_l1 - p_v];
605 vector![p_l2 - p_v]
606 ];
607
608 if res.norm() < options.tol.unwrap_or(TOL_HETERO) {
610 return Ok(Self::new(v, l1, l2));
611 }
612
613 let jacobian = stack![
615 dmu_drho_l1, 0 , -&dmu_drho_v;
616 0 , dmu_drho_l2, -dmu_drho_v;
617 dp_drho_l1 , 0 , -&dp_drho_v;
618 0 , dp_drho_l2 , -dp_drho_v
619 ];
620
621 let dx = LU::new(jacobian)?.solve(&res);
623
624 let rho_l1 =
626 &l1.partial_density() - &Density::from_reduced(dx.rows_range(0..2).into_owned());
627 let rho_l2 =
628 &l2.partial_density() - &Density::from_reduced(dx.rows_range(2..4).into_owned());
629 let rho_v =
630 &v.partial_density() - &Density::from_reduced(dx.rows_range(4..6).into_owned());
631
632 for i in 0..2 {
634 if rho_l1.get(i).is_sign_negative()
635 || rho_l2.get(i).is_sign_negative()
636 || rho_v.get(i).is_sign_negative()
637 {
638 return Err(FeosError::IterationFailed(String::from(
639 "PhaseEquilibrium::heteroazeotrope_t",
640 )));
641 }
642 }
643
644 l1 = State::new_density(eos, temperature, rho_l1)?;
646 l2 = State::new_density(eos, temperature, rho_l2)?;
647 v = State::new_density(eos, temperature, rho_v)?;
648 }
649 Err(FeosError::NotConverged(String::from(
650 "PhaseEquilibrium::heteroazeotrope_t",
651 )))
652 }
653
654 #[expect(clippy::toplevel_ref_arg)]
657 fn heteroazeotrope_p(
658 eos: &E,
659 pressure: Pressure,
660 x_init: (f64, f64),
661 t_init: Option<Temperature>,
662 options: SolverOptions,
663 bubble_dew_options: (SolverOptions, SolverOptions),
664 ) -> FeosResult<Self> {
665 let p = pressure.to_reduced();
666
667 let x1 = dvector![x_init.0, 1.0 - x_init.0];
669 let x2 = dvector![x_init.1, 1.0 - x_init.1];
670 let vle1 =
671 PhaseEquilibrium::bubble_point(eos, pressure, &x1, t_init, None, bubble_dew_options)?;
672 let vle2 =
673 PhaseEquilibrium::bubble_point(eos, pressure, &x2, t_init, None, bubble_dew_options)?;
674 let mut l1 = vle1.liquid().clone();
675 let mut l2 = vle2.liquid().clone();
676 let t0 = (vle1.vapor().temperature + vle2.vapor().temperature) * 0.5;
677 let y0 = (&vle1.vapor().molefracs + &vle2.vapor().molefracs) * 0.5;
678 let mut v = State::new_npt(eos, t0, pressure, y0, Some(Vapor))?;
679
680 for _ in 0..options.max_iter.unwrap_or(MAX_ITER_HETERO) {
681 let dmu_drho_l1 = (l1.n_dmu_dni(Contributions::Total) * l1.molar_volume).to_reduced();
683 let dmu_drho_l2 = (l2.n_dmu_dni(Contributions::Total) * l2.molar_volume).to_reduced();
684 let dmu_drho_v = (v.n_dmu_dni(Contributions::Total) * v.molar_volume).to_reduced();
685 let dmu_res_dt_l1 = (l1.dmu_res_dt()).to_reduced();
686 let dmu_res_dt_l2 = (l2.dmu_res_dt()).to_reduced();
687 let dmu_res_dt_v = (v.dmu_res_dt()).to_reduced();
688 let dp_drho_l1 = (l1.n_dp_dni(Contributions::Total) * l1.molar_volume)
689 .to_reduced()
690 .transpose();
691 let dp_drho_l2 = (l2.n_dp_dni(Contributions::Total) * l2.molar_volume)
692 .to_reduced()
693 .transpose();
694 let dp_drho_v = (v.n_dp_dni(Contributions::Total) * v.molar_volume)
695 .to_reduced()
696 .transpose();
697 let dp_dt_l1 = (l1.dp_dt(Contributions::Total)).to_reduced();
698 let dp_dt_l2 = (l2.dp_dt(Contributions::Total)).to_reduced();
699 let dp_dt_v = (v.dp_dt(Contributions::Total)).to_reduced();
700 let mu_l1_res = l1.residual_chemical_potential().to_reduced();
701 let mu_l2_res = l2.residual_chemical_potential().to_reduced();
702 let mu_v_res = v.residual_chemical_potential().to_reduced();
703 let p_l1 = l1.pressure(Contributions::Total).to_reduced();
704 let p_l2 = l2.pressure(Contributions::Total).to_reduced();
705 let p_v = v.pressure(Contributions::Total).to_reduced();
706
707 let delta_l1v_dmu_ig_dt = l1
709 .partial_density()
710 .to_reduced()
711 .component_div(&v.partial_density().to_reduced())
712 .map(f64::ln);
713 let delta_l2v_dmu_ig_dt = l2
714 .partial_density()
715 .to_reduced()
716 .component_div(&v.partial_density().to_reduced())
717 .map(f64::ln);
718 let delta_l1v_mu_ig = (RGAS * v.temperature).to_reduced() * &delta_l1v_dmu_ig_dt;
719 let delta_l2v_mu_ig = (RGAS * v.temperature).to_reduced() * &delta_l2v_dmu_ig_dt;
720 let res = stack![
721 mu_l1_res - &mu_v_res + delta_l1v_mu_ig;
722 mu_l2_res - &mu_v_res + delta_l2v_mu_ig;
723 vector![p_l1 - p];
724 vector![p_l2 - p];
725 vector![p_v - p]
726 ];
727
728 if res.norm() < options.tol.unwrap_or(TOL_HETERO) {
730 return Ok(Self::new(v, l1, l2));
731 }
732
733 let jacobian = stack![
734 dmu_drho_l1, 0, -&dmu_drho_v, dmu_res_dt_l1 - &dmu_res_dt_v + delta_l1v_dmu_ig_dt;
735 0, dmu_drho_l2, -dmu_drho_v, dmu_res_dt_l2 - &dmu_res_dt_v + delta_l2v_dmu_ig_dt;
736 dp_drho_l1, 0, 0, vector![dp_dt_l1];
737 0, dp_drho_l2, 0, vector![dp_dt_l2];
738 0, 0, dp_drho_v, vector![dp_dt_v]
739 ];
740
741 let dx = LU::new(jacobian)?.solve(&res);
743
744 let rho_l1 =
746 l1.partial_density() - Density::from_reduced(dx.rows_range(0..2).into_owned());
747 let rho_l2 =
748 l2.partial_density() - Density::from_reduced(dx.rows_range(2..4).into_owned());
749 let rho_v =
750 v.partial_density() - Density::from_reduced(dx.rows_range(4..6).into_owned());
751 let t = v.temperature - Temperature::from_reduced(dx[6]);
752
753 for i in 0..2 {
755 if rho_l1.get(i).is_sign_negative()
756 || rho_l2.get(i).is_sign_negative()
757 || rho_v.get(i).is_sign_negative()
758 || t.is_sign_negative()
759 {
760 return Err(FeosError::IterationFailed(String::from(
761 "PhaseEquilibrium::heteroazeotrope_p",
762 )));
763 }
764 }
765
766 l1 = State::new_density(eos, t, rho_l1)?;
768 l2 = State::new_density(eos, t, rho_l2)?;
769 v = State::new_density(eos, t, rho_v)?;
770 }
771 Err(FeosError::NotConverged(String::from(
772 "PhaseEquilibrium::heteroazeotrope_p",
773 )))
774 }
775}
776
777impl<E: Residual + Subset> PhaseEquilibrium<E, 2> {
779 pub fn binary_azeotrope<TP: TemperatureOrPressure>(
782 eos: &E,
783 temperature_or_pressure: TP,
784 ) -> FeosResult<Option<Self>> {
785 let vle = Self::vle_pure_comps(eos, temperature_or_pressure);
787
788 let mut iter = vle.into_iter();
790 let (Some(vle1), Some(vle2), None) = (iter.next(), iter.next(), iter.next()) else {
791 return Err(FeosError::IncompatibleComponents(eos.components(), 2));
792 };
793
794 let (Some(vle1), Some(vle2)) = (vle1, vle2) else {
796 return Err(FeosError::SuperCritical);
797 };
798
799 let henry1 =
801 State::henrys_law_constant(eos, vle1.liquid().temperature, &vle1.liquid().molefracs)?
802 [0];
803 let psat1 = vle1.liquid().pressure(Contributions::Total);
804 let henry2 =
805 State::henrys_law_constant(eos, vle2.liquid().temperature, &vle2.liquid().molefracs)?
806 [0];
807 let psat2 = vle2.liquid().pressure(Contributions::Total);
808
809 let ln_alpha1 = henry2.convert_into(psat2).ln();
813 let ln_alpha2 = psat1.convert_into(henry1).ln();
814
815 if ln_alpha1 * ln_alpha2 > 0.0 {
817 return Ok(None);
818 }
819
820 let x0 = -ln_alpha1 / (ln_alpha2 - ln_alpha1);
822
823 let (temperature, pressure, iterate_t) = temperature_or_pressure.temperature_pressure(None);
825 (if iterate_t {
826 Self::iterate_azeotrope_t(eos, temperature.unwrap(), x0, 10, 1e-10)
827 } else {
828 let t_init = vle1.liquid().temperature.min(vle2.liquid().temperature);
829 Self::iterate_azeotrope_p(eos, pressure.unwrap(), x0, t_init, 10, 1e-10)
830 })
831 .map(Some)
832 }
833
834 fn iterate_azeotrope_t(
835 eos: &E,
836 temperature: Temperature,
837 x0: f64,
838 max_iter: usize,
839 tol: f64,
840 ) -> FeosResult<Self> {
841 let x = Self::azeotrope_newton(
842 partial(
843 |x: Dual64, &t: &Temperature<_>| {
844 PhaseEquilibrium::bubble_point(
845 &eos.lift(),
846 t,
847 &dvector![x, -x + 1.0],
848 None,
849 None,
850 Default::default(),
851 )
852 .map(|vle| {
853 (vle.vapor().molefracs[0]
854 / vle.liquid().molefracs[0]
855 / (vle.vapor().molefracs[1] / vle.liquid().molefracs[1]))
856 .ln()
857 })
858 },
859 &temperature,
860 ),
861 x0,
862 max_iter,
863 tol,
864 )?;
865 PhaseEquilibrium::bubble_point(eos, temperature, x, None, None, Default::default())
866 }
867
868 fn iterate_azeotrope_p(
869 eos: &E,
870 pressure: Pressure,
871 x0: f64,
872 t_init: Temperature,
873 max_iter: usize,
874 tol: f64,
875 ) -> FeosResult<Self> {
876 let x = Self::azeotrope_newton(
877 partial2(
878 |x: Dual64, &p: &Pressure<_>, &t_init| {
879 PhaseEquilibrium::bubble_point(
880 &eos.lift(),
881 p,
882 &dvector![x, -x + 1.0],
883 Some(t_init),
884 None,
885 Default::default(),
886 )
887 .map(|vle| {
888 (vle.vapor().molefracs[0]
889 / vle.liquid().molefracs[0]
890 / (vle.vapor().molefracs[1] / vle.liquid().molefracs[1]))
891 .ln()
892 })
893 },
894 &pressure,
895 &t_init,
896 ),
897 x0,
898 max_iter,
899 tol,
900 )?;
901 PhaseEquilibrium::bubble_point(eos, pressure, x, Some(t_init), None, Default::default())
902 }
903
904 fn azeotrope_newton<F: Fn(Dual64) -> FeosResult<Dual64>>(
905 f: F,
906 x0: f64,
907 max_iter: usize,
908 tol: f64,
909 ) -> FeosResult<f64> {
910 let mut x = x0;
911 for _ in 0..max_iter {
912 let (f, df) = first_derivative(&f, x)?;
913 x -= f / df;
914 if f.abs() < tol {
915 return Ok(x);
916 }
917 }
918 Err(FeosError::NotConverged("binary_azeotrope".into()))
919 }
920}