1use crate::errors::{FeosError, FeosResult};
2use crate::phase_equilibria::PhaseEquilibrium;
3use crate::state::{
4 Contributions,
5 DensityInitialization::{InitialDensity, Liquid, Vapor},
6};
7use crate::{Composition, ReferenceSystem, Residual, SolverOptions, State, Verbosity};
8use nalgebra::allocator::Allocator;
9use nalgebra::{DMatrix, DVector, DefaultAllocator, Dim, Dyn, OVector, U1};
10#[cfg(feature = "ndarray")]
11use ndarray::Array1;
12use num_dual::linalg::LU;
13use num_dual::{DualNum, DualStruct, Gradients};
14use quantity::{Density, Dimensionless, Pressure, Quantity, RGAS, SIUnit, Temperature};
15
16const MAX_ITER_INNER: usize = 5;
17const TOL_INNER: f64 = 1e-9;
18const MAX_ITER_OUTER: usize = 400;
19const TOL_OUTER: f64 = 1e-10;
20
21const MAX_TSTEP: f64 = 20.0;
22const MAX_LNPSTEP: f64 = 0.1;
23const NEWTON_TOL: f64 = 1e-3;
24
25pub trait TemperatureOrPressure<D: DualNum<f64> + Copy = f64>: Copy {
27 type Other: Copy;
28
29 const IDENTIFIER: &'static str;
30
31 fn temperature(&self) -> Option<Temperature<D>>;
32 fn pressure(&self) -> Option<Pressure<D>>;
33
34 fn temperature_pressure(
35 &self,
36 tp_init: Option<Self::Other>,
37 ) -> (Option<Temperature<D>>, Option<Pressure<D>>, bool);
38
39 fn from_state<E: Residual<N, D>, N: Gradients>(state: &State<E, N, D>) -> Self::Other
40 where
41 DefaultAllocator: Allocator<N>;
42
43 #[cfg(feature = "ndarray")]
44 fn linspace(
45 &self,
46 start: Self::Other,
47 end: Self::Other,
48 n: usize,
49 ) -> (Temperature<Array1<f64>>, Pressure<Array1<f64>>);
50}
51
52impl<D: DualNum<f64> + Copy> TemperatureOrPressure<D> for Temperature<D> {
53 type Other = Pressure<D>;
54 const IDENTIFIER: &'static str = "temperature";
55
56 fn temperature(&self) -> Option<Temperature<D>> {
57 Some(*self)
58 }
59
60 fn pressure(&self) -> Option<Pressure<D>> {
61 None
62 }
63
64 fn temperature_pressure(
65 &self,
66 tp_init: Option<Self::Other>,
67 ) -> (Option<Temperature<D>>, Option<Pressure<D>>, bool) {
68 (Some(*self), tp_init, true)
69 }
70
71 fn from_state<E: Residual<N, D>, N: Gradients>(state: &State<E, N, D>) -> Self::Other
72 where
73 DefaultAllocator: Allocator<N>,
74 {
75 state.pressure(Contributions::Total)
76 }
77
78 #[cfg(feature = "ndarray")]
79 fn linspace(
80 &self,
81 start: Pressure<D>,
82 end: Pressure<D>,
83 n: usize,
84 ) -> (Temperature<Array1<f64>>, Pressure<Array1<f64>>) {
85 (
86 Temperature::linspace(self.re(), self.re(), n),
87 Pressure::linspace(start.re(), end.re(), n),
88 )
89 }
90}
91
92impl<D: DualNum<f64> + Copy> TemperatureOrPressure<D>
96 for Quantity<D, SIUnit<-2, -1, 1, 0, 0, 0, 0>>
97{
98 type Other = Temperature<D>;
99 const IDENTIFIER: &'static str = "pressure";
100
101 fn temperature(&self) -> Option<Temperature<D>> {
102 None
103 }
104
105 fn pressure(&self) -> Option<Pressure<D>> {
106 Some(*self)
107 }
108
109 fn temperature_pressure(
110 &self,
111 tp_init: Option<Self::Other>,
112 ) -> (Option<Temperature<D>>, Option<Pressure<D>>, bool) {
113 (tp_init, Some(*self), false)
114 }
115
116 fn from_state<E: Residual<N, D>, N: Dim>(state: &State<E, N, D>) -> Self::Other
117 where
118 DefaultAllocator: Allocator<N>,
119 {
120 state.temperature
121 }
122
123 #[cfg(feature = "ndarray")]
124 fn linspace(
125 &self,
126 start: Temperature<D>,
127 end: Temperature<D>,
128 n: usize,
129 ) -> (Temperature<Array1<f64>>, Pressure<Array1<f64>>) {
130 (
131 Temperature::linspace(start.re(), end.re(), n),
132 Pressure::linspace(self.re(), self.re(), n),
133 )
134 }
135}
136
137impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> PhaseEquilibrium<E, 2, N, D>
139where
140 DefaultAllocator: Allocator<N> + Allocator<N, N> + Allocator<U1, N>,
141{
142 pub fn bubble_point<TP: TemperatureOrPressure<D>, X: Composition<D, N>>(
145 eos: &E,
146 temperature_or_pressure: TP,
147 liquid_molefracs: X,
148 tp_init: Option<TP::Other>,
149 vapor_molefracs: Option<&OVector<f64, N>>,
150 options: (SolverOptions, SolverOptions),
151 ) -> FeosResult<Self> {
152 Self::bubble_dew_point(
153 eos,
154 temperature_or_pressure,
155 liquid_molefracs,
156 tp_init,
157 vapor_molefracs,
158 true,
159 options,
160 )
161 }
162
163 pub fn dew_point<TP: TemperatureOrPressure<D>, X: Composition<D, N>>(
166 eos: &E,
167 temperature_or_pressure: TP,
168 vapor_molefracs: X,
169 tp_init: Option<TP::Other>,
170 liquid_molefracs: Option<&OVector<f64, N>>,
171 options: (SolverOptions, SolverOptions),
172 ) -> FeosResult<Self> {
173 Self::bubble_dew_point(
174 eos,
175 temperature_or_pressure,
176 vapor_molefracs,
177 tp_init,
178 liquid_molefracs,
179 false,
180 options,
181 )
182 }
183
184 pub(super) fn bubble_dew_point<TP: TemperatureOrPressure<D>, X: Composition<D, N>>(
185 eos: &E,
186 temperature_or_pressure: TP,
187 vapor_molefracs: X,
188 tp_init: Option<TP::Other>,
189 liquid_molefracs: Option<&OVector<f64, N>>,
190 bubble: bool,
191 options: (SolverOptions, SolverOptions),
192 ) -> FeosResult<Self> {
193 if eos.components() == 1 {
194 let mut vle = Self::pure(eos, temperature_or_pressure, None, options.1)?;
195 if bubble {
196 vle.phase_fractions = [D::from(0.0), D::from(1.0)];
197 }
198 Ok(vle)
199 } else {
200 let (temperature, pressure, iterate_p) =
201 temperature_or_pressure.temperature_pressure(tp_init);
202 Self::bubble_dew_point_tp(
203 eos,
204 temperature,
205 pressure,
206 vapor_molefracs,
207 liquid_molefracs,
208 bubble,
209 iterate_p,
210 options,
211 )
212 }
213 }
214
215 #[expect(clippy::too_many_arguments)]
216 fn bubble_dew_point_tp<X: Composition<D, N>>(
217 eos: &E,
218 temperature: Option<Temperature<D>>,
219 pressure: Option<Pressure<D>>,
220 composition: X,
221 molefracs_init: Option<&OVector<f64, N>>,
222 bubble: bool,
223 iterate_p: bool,
224 options: (SolverOptions, SolverOptions),
225 ) -> FeosResult<Self> {
226 let eos_re = eos.re();
227 let mut temperature_re = temperature.map(|t| t.re());
228 let mut pressure_re = pressure.map(|p| p.re());
229 let (molefracs_spec, total_moles) = composition.into_molefracs(eos)?;
230 let molefracs_spec_re = molefracs_spec.map(|x| x.re());
231 let (v1, rho2) = if iterate_p {
232 let temperature_re = temperature_re.as_mut().unwrap();
234
235 if let Some(p) = pressure_re.as_mut() {
237 PhaseEquilibrium::iterate_bubble_dew(
238 &eos_re,
239 temperature_re,
240 p,
241 &molefracs_spec_re,
242 molefracs_init,
243 bubble,
244 iterate_p,
245 options,
246 )?
247 } else {
248 let x2 = PhaseEquilibrium::starting_pressure_ideal_gas(
250 &eos_re,
251 *temperature_re,
252 &molefracs_spec_re,
253 bubble,
254 )
255 .and_then(|(p, x)| {
256 pressure_re = Some(p);
257 PhaseEquilibrium::iterate_bubble_dew(
258 &eos_re,
259 temperature_re,
260 pressure_re.as_mut().unwrap(),
261 &molefracs_spec_re,
262 molefracs_init.or(Some(&x)),
263 bubble,
264 iterate_p,
265 options,
266 )
267 });
268
269 x2.or_else(|_| {
271 PhaseEquilibrium::starting_pressure_spinodal(
272 &eos_re,
273 *temperature_re,
274 &molefracs_spec_re,
275 )
276 .and_then(|p| {
277 pressure_re = Some(p);
278 PhaseEquilibrium::iterate_bubble_dew(
279 &eos_re,
280 temperature_re,
281 pressure_re.as_mut().unwrap(),
282 &molefracs_spec_re,
283 molefracs_init,
284 bubble,
285 iterate_p,
286 options,
287 )
288 })
289 })?
290 }
291 } else {
292 let pressure_re = pressure_re.as_mut().unwrap();
294
295 let temperature_re = temperature_re.as_mut().expect("An initial temperature is required for the calculation of bubble/dew points at given pressure!");
296 PhaseEquilibrium::iterate_bubble_dew(
297 &eos.re(),
298 temperature_re,
299 pressure_re,
300 &molefracs_spec_re,
301 molefracs_init,
302 bubble,
303 iterate_p,
304 options,
305 )?
306 };
307
308 let (mut t, mut p) = if iterate_p {
310 (
311 temperature.unwrap().into_reduced(),
312 D::from(pressure_re.unwrap().into_reduced()),
313 )
314 } else {
315 (
316 D::from(temperature_re.unwrap().into_reduced()),
317 pressure.unwrap().into_reduced(),
318 )
319 };
320 let mut molar_volume = D::from(v1);
321 let mut rho2 = rho2.map(D::from);
322 for _ in 0..D::NDERIV {
323 if iterate_p {
324 Self::newton_step_t(
325 eos,
326 t,
327 &molefracs_spec,
328 &mut p,
329 &mut molar_volume,
330 &mut rho2,
331 Verbosity::None,
332 )
333 } else {
334 Self::newton_step_p(
335 eos,
336 &mut t,
337 &molefracs_spec,
338 p,
339 &mut molar_volume,
340 &mut rho2,
341 Verbosity::None,
342 )
343 };
344 }
345 let state1 = State::new(
346 eos,
347 Temperature::from_reduced(t),
348 Density::from_reduced(molar_volume.recip()),
349 molefracs_spec,
350 )?;
351 let rho2_total = rho2.sum();
352 let x2 = rho2 / rho2_total;
353 let state2 = State::new(
354 eos,
355 Temperature::from_reduced(t),
356 Density::from_reduced(rho2_total),
357 x2,
358 )?;
359
360 Ok(if bubble {
361 PhaseEquilibrium::with_vapor_phase_fraction(state2, state1, D::from(0.0), total_moles)
362 } else {
363 PhaseEquilibrium::with_vapor_phase_fraction(state1, state2, D::from(1.0), total_moles)
364 })
365 }
366
367 fn newton_step_t(
368 eos: &E,
369 temperature: D,
370 molefracs: &OVector<D, N>,
371 pressure: &mut D,
372 molar_volume: &mut D,
373 partial_density_other_phase: &mut OVector<D, N>,
374 verbosity: Verbosity,
375 ) -> f64 {
376 let (p_1, mu_res_1, dp_1, dmu_1) = eos.dmu_drho(temperature, partial_density_other_phase);
378 let (p_2, mu_res_2, dp_2, dmu_2) = eos.dmu_dv(temperature, *molar_volume, molefracs);
379
380 let n = molefracs.len();
382 let f = DVector::from_fn(n + 2, |i, _| {
383 if i == n {
384 p_1 - *pressure
385 } else if i == n + 1 {
386 p_2 - *pressure
387 } else {
388 mu_res_1[i] - mu_res_2[i]
389 + (partial_density_other_phase[i] * *molar_volume / molefracs[i]).ln()
390 * temperature
391 }
392 });
393
394 let jac = DMatrix::from_fn(n + 2, n + 2, |i, j| {
396 if i < n && j < n {
397 dmu_1[(i, j)]
398 } else if i < n && j == n {
399 -dmu_2[i]
400 } else if i == n && j < n {
401 dp_1[j]
402 } else if i == n + 1 && j == n {
403 dp_2
404 } else if i >= n && j == n + 1 {
405 -D::one()
406 } else {
407 D::zero()
408 }
409 });
410
411 let dx = LU::<_, _, Dyn>::new(jac).unwrap().solve(&f);
413
414 for i in 0..n {
416 partial_density_other_phase[i] -= dx[i];
417 }
418 *molar_volume -= dx[n];
419 *pressure -= dx[n + 1];
420
421 let error = f.map(|r| r.re()).norm();
422
423 let x = partial_density_other_phase.map(|r| r.re());
424 let x = &x / x.sum();
425 log_iteration(
426 verbosity,
427 Some(error),
428 Temperature::from_reduced(temperature.re()),
429 Pressure::from_reduced(pressure.re()),
430 x.as_slice(),
431 true,
432 );
433 error
434 }
435
436 fn newton_step_p(
437 eos: &E,
438 temperature: &mut D,
439 molefracs: &OVector<D, N>,
440 pressure: D,
441 molar_volume: &mut D,
442 partial_density_other_phase: &mut OVector<D, N>,
443 verbosity: Verbosity,
444 ) -> f64 {
445 let (p_1, mu_res_1, dp_1, dmu_1) = eos.dmu_drho(*temperature, partial_density_other_phase);
447 let (p_2, mu_res_2, dp_2, dmu_2) = eos.dmu_dv(*temperature, *molar_volume, molefracs);
448 let (dp_dt_1, dmu_res_dt_1) = eos.dmu_dt(*temperature, partial_density_other_phase);
449 let (dp_dt_2, dmu_res_dt_2) = eos.dmu_dt(*temperature, &(molefracs / *molar_volume));
450
451 let n = molefracs.len();
453 let f = DVector::from_fn(n + 2, |i, _| {
454 if i == n {
455 p_1 - pressure
456 } else if i == n + 1 {
457 p_2 - pressure
458 } else {
459 mu_res_1[i] - mu_res_2[i]
460 + (partial_density_other_phase[i] * *molar_volume / molefracs[i]).ln()
461 * *temperature
462 }
463 });
464
465 let jac = DMatrix::from_fn(n + 2, n + 2, |i, j| {
467 if i < n && j < n {
468 dmu_1[(i, j)]
469 } else if i < n && j == n {
470 -dmu_2[i]
471 } else if i < n && j == n + 1 {
472 dmu_res_dt_1[i] - dmu_res_dt_2[i]
473 + (partial_density_other_phase[i] * *molar_volume / molefracs[i]).ln()
474 } else if i == n && j < n {
475 dp_1[j]
476 } else if i == n && j == n + 1 {
477 dp_dt_1
478 } else if i == n + 1 && j == n {
479 dp_2
480 } else if i == n + 1 && j == n + 1 {
481 dp_dt_2
482 } else {
483 D::zero()
484 }
485 });
486
487 let dx = LU::<_, _, Dyn>::new(jac).unwrap().solve(&f);
489
490 for i in 0..n {
492 partial_density_other_phase[i] -= dx[i];
493 }
494 *molar_volume -= dx[n];
495 *temperature -= dx[n + 1];
496
497 let error = f.map(|r| r.re()).norm();
498
499 let x = partial_density_other_phase.map(|r| r.re());
500 let x = &x / x.sum();
501 log_iteration(
502 verbosity,
503 Some(error),
504 Temperature::from_reduced(temperature.re()),
505 Pressure::from_reduced(pressure.re()),
506 x.as_slice(),
507 true,
508 );
509 error
510 }
511}
512
513impl<E: Residual<N>, N: Gradients> PhaseEquilibrium<E, 2, N>
515where
516 DefaultAllocator: Allocator<N> + Allocator<N, N> + Allocator<U1, N>,
517{
518 #[expect(clippy::too_many_arguments)]
519 fn iterate_bubble_dew(
520 eos: &E,
521 temperature: &mut Temperature,
522 pressure: &mut Pressure,
523 molefracs_spec: &OVector<f64, N>,
524 molefracs_init: Option<&OVector<f64, N>>,
525 bubble: bool,
526 iterate_p: bool,
527 options: (SolverOptions, SolverOptions),
528 ) -> FeosResult<(f64, OVector<f64, N>)> {
529 let [mut state1, mut state2] = if bubble {
530 Self::starting_x2_bubble(eos, *temperature, *pressure, molefracs_spec, molefracs_init)
531 } else {
532 Self::starting_x2_dew(eos, *temperature, *pressure, molefracs_spec, molefracs_init)
533 }?;
534 let (options_inner, options_outer) = options;
535
536 let mut err_out = 1.0;
538 let mut k_out = 0;
539
540 if PhaseEquilibrium::is_trivial_solution(&state1, &state2) {
541 log_iter!(options_outer.verbosity, "Trivial solution encountered!");
542 return Err(FeosError::TrivialSolution);
543 }
544
545 log_iter!(
546 options_outer.verbosity,
547 "res outer loop | res inner loop | temperature | pressure | molefracs second phase",
548 );
549 log_iter!(options_outer.verbosity, "{:-<104}", "");
550 log_iteration(
551 options_outer.verbosity,
552 None,
553 *temperature,
554 *pressure,
555 state2.molefracs.as_slice(),
556 false,
557 );
558
559 for ko in 0..options_outer.max_iter.unwrap_or(MAX_ITER_OUTER) {
561 err_out = if err_out > NEWTON_TOL {
563 for _ in 0..options_inner.max_iter.unwrap_or(MAX_ITER_INNER) {
565 let res = if iterate_p {
566 Self::adjust_p(
567 *temperature,
568 pressure,
569 &mut state1,
570 &mut state2,
571 options_inner.verbosity,
572 )?
573 } else {
574 Self::adjust_t(
575 temperature,
576 *pressure,
577 &mut state1,
578 &mut state2,
579 options_inner.verbosity,
580 )?
581 };
582 if res < options_inner.tol.unwrap_or(TOL_INNER) {
583 break;
584 }
585 }
586 Self::adjust_x2(&state1, &mut state2, options_outer.verbosity)
587 } else {
588 let mut t = temperature.into_reduced();
589 let mut p = pressure.into_reduced();
590 let mut molar_volume = state1.molar_volume.into_reduced();
591 let mut rho2 = state2.partial_density().to_reduced();
592 let err = if iterate_p {
593 Self::newton_step_t(
594 &state1.eos,
595 t,
596 &state1.molefracs,
597 &mut p,
598 &mut molar_volume,
599 &mut rho2,
600 options_outer.verbosity,
601 )
602 } else {
603 Self::newton_step_p(
604 &state1.eos,
605 &mut t,
606 &state1.molefracs,
607 p,
608 &mut molar_volume,
609 &mut rho2,
610 options_outer.verbosity,
611 )
612 };
613 *temperature = Temperature::from_reduced(t);
614 *pressure = Pressure::from_reduced(p);
615 state1 = State::new(
616 &state1.eos,
617 *temperature,
618 Density::from_reduced(molar_volume.recip()),
619 molefracs_spec,
620 )?;
621 let density = rho2.sum();
622 state2 = State::new(
623 &state2.eos,
624 *temperature,
625 Density::from_reduced(density),
626 rho2 / density,
627 )?;
628 Ok(err)
629 }?;
630
631 if Self::is_trivial_solution(&state1, &state2) {
632 log_iter!(options_outer.verbosity, "Trivial solution encountered!");
633 return Err(FeosError::TrivialSolution);
634 }
635
636 if err_out < options_outer.tol.unwrap_or(TOL_OUTER) {
637 k_out = ko + 1;
638 break;
639 }
640 }
641
642 if err_out < options_outer.tol.unwrap_or(TOL_OUTER) {
643 log_result!(
644 options_outer.verbosity,
645 "Bubble/dew point: calculation converged in {} step(s)\n",
646 k_out
647 );
648 Ok((
649 state1.density.into_reduced().recip(),
650 state2.partial_density().to_reduced(),
651 ))
652 } else {
653 Err(FeosError::NotConverged(String::from(
655 "bubble-dew-iteration",
656 )))
657 }
658 }
659
660 fn adjust_p(
661 temperature: Temperature,
662 pressure: &mut Pressure,
663 state1: &mut State<E, N>,
664 state2: &mut State<E, N>,
665 verbosity: Verbosity,
666 ) -> FeosResult<f64> {
667 let ln_phi_1 = state1.ln_phi();
669 let ln_phi_2 = state2.ln_phi();
670 let k = (&ln_phi_1 - &ln_phi_2).map(f64::exp);
671
672 let xk = state1.molefracs.component_mul(&k);
674 let f = xk.sum() - 1.0;
675
676 let ln_phi_1_dp = state1.dln_phi_dp();
678 let ln_phi_2_dp = state2.dln_phi_dp();
679 let df = ((ln_phi_1_dp - ln_phi_2_dp) * *pressure)
680 .into_value()
681 .component_mul(&xk)
682 .sum();
683 let mut lnpstep = -f / df;
684
685 lnpstep = lnpstep.clamp(-MAX_LNPSTEP, MAX_LNPSTEP);
687
688 *pressure *= lnpstep.exp();
690
691 Self::adjust_states(temperature, *pressure, state1, state2, None)?;
693
694 log_iteration(
696 verbosity,
697 Some(f),
698 temperature,
699 *pressure,
700 state2.molefracs.as_slice(),
701 false,
702 );
703
704 Ok(f.abs())
705 }
706
707 fn adjust_t(
708 temperature: &mut Temperature,
709 pressure: Pressure,
710 state1: &mut State<E, N>,
711 state2: &mut State<E, N>,
712 verbosity: Verbosity,
713 ) -> FeosResult<f64> {
714 let ln_phi_1 = state1.ln_phi();
716 let ln_phi_2 = state2.ln_phi();
717 let k = (&ln_phi_1 - &ln_phi_2).map(f64::exp);
718
719 let f = state1.molefracs.dot(&k) - 1.0;
721
722 let ln_phi_1_dt = state1.dln_phi_dt();
724 let ln_phi_2_dt = state2.dln_phi_dt();
725 let df = ((ln_phi_1_dt - ln_phi_2_dt)
726 .component_mul(&Dimensionless::new(state1.molefracs.component_mul(&k))))
727 .sum();
728 let mut tstep = -f / df;
729
730 if tstep < -Temperature::from_reduced(MAX_TSTEP) {
732 tstep = -Temperature::from_reduced(MAX_TSTEP);
733 } else if tstep > Temperature::from_reduced(MAX_TSTEP) {
734 tstep = Temperature::from_reduced(MAX_TSTEP);
735 }
736
737 *temperature += tstep;
739
740 Self::adjust_states(*temperature, pressure, state1, state2, None)?;
742
743 log_iteration(
745 verbosity,
746 Some(f),
747 *temperature,
748 pressure,
749 state2.molefracs.as_slice(),
750 false,
751 );
752
753 Ok(f.abs())
754 }
755
756 fn starting_pressure_ideal_gas(
757 eos: &E,
758 temperature: Temperature,
759 molefracs_spec: &OVector<f64, N>,
760 bubble: bool,
761 ) -> FeosResult<(Pressure, OVector<f64, N>)> {
762 if bubble {
763 Self::starting_pressure_ideal_gas_bubble(eos, temperature, molefracs_spec)
764 } else {
765 Self::starting_pressure_ideal_gas_dew(eos, temperature, molefracs_spec)
766 }
767 }
768
769 pub(super) fn starting_pressure_ideal_gas_bubble(
770 eos: &E,
771 temperature: Temperature,
772 liquid_molefracs: &OVector<f64, N>,
773 ) -> FeosResult<(Pressure, OVector<f64, N>)> {
774 let density = 0.75 * Density::from_reduced(eos.compute_max_density(liquid_molefracs));
775 let liquid = State::new(eos, temperature, density, liquid_molefracs)?;
776 let v_l = liquid.partial_molar_volume();
777 let p_l = liquid.pressure(Contributions::Total);
778 let mu_l = liquid.residual_chemical_potential();
779 let k_i = liquid_molefracs.component_mul(
780 &((mu_l - v_l * p_l) / (RGAS * temperature))
781 .into_value()
782 .map(f64::exp),
783 );
784 let p = k_i.sum() * RGAS * temperature * density;
785 let y = &k_i / k_i.sum();
786 Ok((p, y))
787 }
788
789 fn starting_pressure_ideal_gas_dew(
790 eos: &E,
791 temperature: Temperature,
792 vapor_molefracs: &OVector<f64, N>,
793 ) -> FeosResult<(Pressure, OVector<f64, N>)> {
794 let mut p: Option<Pressure> = None;
795
796 let mut x = vapor_molefracs.clone();
797 for _ in 0..5 {
798 let density = Density::from_reduced(0.75 * eos.compute_max_density(&x));
799 let liquid = State::new(eos, temperature, density, x)?;
800 let v_l = liquid.partial_molar_volume();
801 let p_l = liquid.pressure(Contributions::Total);
802 let mu_l = liquid.residual_chemical_potential();
803 let k = vapor_molefracs.clone().component_div(
804 &((mu_l - v_l * p_l) / (RGAS * temperature))
805 .into_value()
806 .map(f64::exp),
807 );
808 let k_sum = k.sum();
809 let p_new = RGAS * temperature * density / k_sum;
810 x = k / k_sum;
811 if let Some(p_old) = p
812 && ((p_new - p_old) / p_old).into_value().abs() < 1e-5
813 {
814 p = Some(p_new);
815 break;
816 }
817 p = Some(p_new);
818 }
819 Ok((p.unwrap(), x))
820 }
821
822 pub(super) fn starting_pressure_spinodal(
823 eos: &E,
824 temperature: Temperature,
825 molefracs: &OVector<f64, N>,
826 ) -> FeosResult<Pressure> {
827 let [sp_v, sp_l] = State::spinodal(eos, temperature, molefracs, Default::default())?;
828 let pv = sp_v.pressure(Contributions::Total);
829 let pl = sp_l.pressure(Contributions::Total);
830 Ok(0.5 * (Pressure::from_reduced(0.0).max(pl) + pv))
831 }
832
833 fn starting_x2_bubble(
834 eos: &E,
835 temperature: Temperature,
836 pressure: Pressure,
837 liquid_molefracs: &OVector<f64, N>,
838 vapor_molefracs: Option<&OVector<f64, N>>,
839 ) -> FeosResult<[State<E, N>; 2]> {
840 let liquid_state =
841 State::new_npt(eos, temperature, pressure, liquid_molefracs, Some(Liquid))?;
842 let xv = match vapor_molefracs {
843 Some(xv) => xv.clone(),
844 None => liquid_state
845 .ln_phi()
846 .map(f64::exp)
847 .component_mul(liquid_molefracs),
848 };
849 let vapor_state = State::new_npt(eos, temperature, pressure, xv, Some(Vapor))?;
850 Ok([liquid_state, vapor_state])
851 }
852
853 fn starting_x2_dew(
854 eos: &E,
855 temperature: Temperature,
856 pressure: Pressure,
857 vapor_molefracs: &OVector<f64, N>,
858 liquid_molefracs: Option<&OVector<f64, N>>,
859 ) -> FeosResult<[State<E, N>; 2]> {
860 let vapor_state = State::new_npt(eos, temperature, pressure, vapor_molefracs, Some(Vapor))?;
861 let xl = match liquid_molefracs {
862 Some(xl) => xl.clone(),
863 None => {
864 let xl = vapor_state
865 .ln_phi()
866 .map(f64::exp)
867 .component_mul(vapor_molefracs);
868 let liquid_state = State::new_npt(eos, temperature, pressure, xl, Some(Liquid))?;
869 (vapor_state.ln_phi() - liquid_state.ln_phi())
870 .map(f64::exp)
871 .component_mul(vapor_molefracs)
872 }
873 };
874 let liquid_state = State::new_npt(eos, temperature, pressure, xl, Some(Liquid))?;
875 Ok([vapor_state, liquid_state])
876 }
877
878 fn adjust_states(
879 temperature: Temperature,
880 pressure: Pressure,
881 state1: &mut State<E, N>,
882 state2: &mut State<E, N>,
883 molefracs_state2: Option<&OVector<f64, N>>,
884 ) -> FeosResult<()> {
885 *state1 = State::new_npt(
886 &state1.eos,
887 temperature,
888 pressure,
889 &state1.molefracs,
890 Some(InitialDensity(state1.density)),
891 )?;
892 *state2 = State::new_npt(
893 &state2.eos,
894 temperature,
895 pressure,
896 molefracs_state2.unwrap_or(&state2.molefracs),
897 Some(InitialDensity(state2.density)),
898 )?;
899 Ok(())
900 }
901
902 fn adjust_x2(
903 state1: &State<E, N>,
904 state2: &mut State<E, N>,
905 verbosity: Verbosity,
906 ) -> FeosResult<f64> {
907 let x1 = &state1.molefracs;
908 let ln_phi_1 = state1.ln_phi();
909 let ln_phi_2 = state2.ln_phi();
910 let k = (ln_phi_1 - ln_phi_2).map(f64::exp);
911 let kx1 = k.component_mul(x1);
912 let err_out = kx1
913 .component_div(&state2.molefracs)
914 .map(|e| (e - 1.0).abs())
915 .sum();
916 let x2 = &kx1 / kx1.sum();
917 log_iter!(
918 verbosity,
919 "{:<14.8e} | {:14} | {:14} | {:16} |",
920 err_out,
921 "",
922 "",
923 ""
924 );
925 *state2 = State::new_npt(
926 &state2.eos,
927 state2.temperature,
928 state2.pressure(Contributions::Total),
929 x2,
930 Some(InitialDensity(state2.density)),
931 )?;
932 Ok(err_out)
933 }
934}
935
936fn log_iteration(
937 verbosity: Verbosity,
938 error: Option<f64>,
939 temperature: Temperature,
940 pressure: Pressure,
941 x2: &[f64],
942 newton: bool,
943) {
944 let error = error.map_or_else(|| format!("{:14}", ""), |e| format!("{:<14.8e}", e.abs()));
945 log_iter!(
946 verbosity,
947 "{:14} | {} | {:12.8} | {:12.8} | {:.8?} {}",
948 "",
949 error,
950 temperature,
951 pressure,
952 x2,
953 if newton { "NEWTON" } else { "" }
954 );
955}