Skip to main content

feos_core/phase_equilibria/
bubble_dew.rs

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
25/// Trait that enables functions to be generic over their input unit.
26pub 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
92// For some inexplicable reason this does not compile if the `Pressure` type is
93// used instead of the explicit unit. Maybe the type is too complicated for the
94// compiler?
95impl<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
137/// # Bubble and dew point calculations
138impl<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    /// Calculate a phase equilibrium for a given temperature
143    /// or pressure and composition of the liquid phase.
144    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    /// Calculate a phase equilibrium for a given temperature
164    /// or pressure and composition of the vapor phase.
165    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            // temperature is specified
233            let temperature_re = temperature_re.as_mut().ok_or(FeosError::Error(
234                "Temperature information is expected for bubble/dew calculation.".to_string(),
235            ))?;
236
237            // First use given initial pressure if applicable
238            if let Some(p) = pressure_re.as_mut() {
239                PhaseEquilibrium::iterate_bubble_dew(
240                    &eos_re,
241                    temperature_re,
242                    p,
243                    &molefracs_spec_re,
244                    molefracs_init,
245                    bubble,
246                    iterate_p,
247                    options,
248                )?
249            } else {
250                // Next try to initialize with an ideal gas assumption
251                let x2 = PhaseEquilibrium::starting_pressure_ideal_gas(
252                    &eos_re,
253                    *temperature_re,
254                    &molefracs_spec_re,
255                    bubble,
256                )
257                .and_then(|(p, x)| {
258                    let p = pressure_re.insert(p);
259                    PhaseEquilibrium::iterate_bubble_dew(
260                        &eos_re,
261                        temperature_re,
262                        p,
263                        &molefracs_spec_re,
264                        molefracs_init.or(Some(&x)),
265                        bubble,
266                        iterate_p,
267                        options,
268                    )
269                });
270
271                // Finally use the spinodal to initialize the calculation
272                x2.or_else(|_| {
273                    PhaseEquilibrium::starting_pressure_spinodal(
274                        &eos_re,
275                        *temperature_re,
276                        &molefracs_spec_re,
277                    )
278                    .and_then(|p| {
279                        let p = pressure_re.insert(p);
280                        PhaseEquilibrium::iterate_bubble_dew(
281                            &eos_re,
282                            temperature_re,
283                            p,
284                            &molefracs_spec_re,
285                            molefracs_init,
286                            bubble,
287                            iterate_p,
288                            options,
289                        )
290                    })
291                })?
292            }
293        } else {
294            // pressure is specified
295            let pressure_re = pressure_re.as_mut().ok_or(FeosError::Error(
296                "Pressure information is expected for bubble/dew calculation.".to_string(),
297            ))?;
298
299            let temperature_re = temperature_re
300                .as_mut()
301                .ok_or(FeosError::Error(
302                    "An initial temperature is required for the calculation of bubble/dew points at given pressure.".to_string()))?;
303            PhaseEquilibrium::iterate_bubble_dew(
304                &eos.re(),
305                temperature_re,
306                pressure_re,
307                &molefracs_spec_re,
308                molefracs_init,
309                bubble,
310                iterate_p,
311                options,
312            )?
313        };
314
315        // implicit differentiation
316        // unwraps here are safe
317        let (mut t, mut p) = if iterate_p {
318            (
319                temperature.unwrap().into_reduced(),
320                D::from(pressure_re.unwrap().into_reduced()),
321            )
322        } else {
323            (
324                D::from(temperature_re.unwrap().into_reduced()),
325                pressure.unwrap().into_reduced(),
326            )
327        };
328        let mut molar_volume = D::from(v1);
329        let mut rho2 = rho2.map(D::from);
330        for _ in 0..D::NDERIV {
331            if iterate_p {
332                Self::newton_step_t(
333                    eos,
334                    t,
335                    &molefracs_spec,
336                    &mut p,
337                    &mut molar_volume,
338                    &mut rho2,
339                    Verbosity::None,
340                )?
341            } else {
342                Self::newton_step_p(
343                    eos,
344                    &mut t,
345                    &molefracs_spec,
346                    p,
347                    &mut molar_volume,
348                    &mut rho2,
349                    Verbosity::None,
350                )?
351            };
352        }
353        let state1 = State::new(
354            eos,
355            Temperature::from_reduced(t),
356            Density::from_reduced(molar_volume.recip()),
357            molefracs_spec,
358        )?;
359        let rho2_total = rho2.sum();
360        let x2 = rho2 / rho2_total;
361        let state2 = State::new(
362            eos,
363            Temperature::from_reduced(t),
364            Density::from_reduced(rho2_total),
365            x2,
366        )?;
367
368        Ok(if bubble {
369            PhaseEquilibrium::with_vapor_phase_fraction(state2, state1, D::from(0.0), total_moles)
370        } else {
371            PhaseEquilibrium::with_vapor_phase_fraction(state1, state2, D::from(1.0), total_moles)
372        })
373    }
374
375    fn newton_step_t(
376        eos: &E,
377        temperature: D,
378        molefracs: &OVector<D, N>,
379        pressure: &mut D,
380        molar_volume: &mut D,
381        partial_density_other_phase: &mut OVector<D, N>,
382        verbosity: Verbosity,
383    ) -> FeosResult<f64> {
384        // calculate properties
385        let (p_1, mu_res_1, dp_1, dmu_1) = eos.dmu_drho(temperature, partial_density_other_phase);
386        let (p_2, mu_res_2, dp_2, dmu_2) = eos.dmu_dv(temperature, *molar_volume, molefracs);
387
388        // calculate residual
389        let n = molefracs.len();
390        let f = DVector::from_fn(n + 2, |i, _| {
391            if i == n {
392                p_1 - *pressure
393            } else if i == n + 1 {
394                p_2 - *pressure
395            } else {
396                mu_res_1[i] - mu_res_2[i]
397                    + (partial_density_other_phase[i] * *molar_volume / molefracs[i]).ln()
398                        * temperature
399            }
400        });
401
402        // calculate Jacobian
403        let jac = DMatrix::from_fn(n + 2, n + 2, |i, j| {
404            if i < n && j < n {
405                dmu_1[(i, j)]
406            } else if i < n && j == n {
407                -dmu_2[i]
408            } else if i == n && j < n {
409                dp_1[j]
410            } else if i == n + 1 && j == n {
411                dp_2
412            } else if i >= n && j == n + 1 {
413                -D::one()
414            } else {
415                D::zero()
416            }
417        });
418
419        // calculate Newton step
420        let dx = LU::<_, _, Dyn>::new(jac)?.solve(&f);
421
422        // apply Newton step
423        for i in 0..n {
424            partial_density_other_phase[i] -= dx[i];
425        }
426        *molar_volume -= dx[n];
427        *pressure -= dx[n + 1];
428
429        let error = f.map(|r| r.re()).norm();
430
431        let x = partial_density_other_phase.map(|r| r.re());
432        let x = &x / x.sum();
433        log_iteration(
434            verbosity,
435            Some(error),
436            Temperature::from_reduced(temperature.re()),
437            Pressure::from_reduced(pressure.re()),
438            x.as_slice(),
439            true,
440        );
441        Ok(error)
442    }
443
444    fn newton_step_p(
445        eos: &E,
446        temperature: &mut D,
447        molefracs: &OVector<D, N>,
448        pressure: D,
449        molar_volume: &mut D,
450        partial_density_other_phase: &mut OVector<D, N>,
451        verbosity: Verbosity,
452    ) -> FeosResult<f64> {
453        // calculate properties
454        let (p_1, mu_res_1, dp_1, dmu_1) = eos.dmu_drho(*temperature, partial_density_other_phase);
455        let (p_2, mu_res_2, dp_2, dmu_2) = eos.dmu_dv(*temperature, *molar_volume, molefracs);
456        let (dp_dt_1, dmu_res_dt_1) = eos.dmu_dt(*temperature, partial_density_other_phase);
457        let (dp_dt_2, dmu_res_dt_2) = eos.dmu_dt(*temperature, &(molefracs / *molar_volume));
458
459        // calculate residual
460        let n = molefracs.len();
461        let f = DVector::from_fn(n + 2, |i, _| {
462            if i == n {
463                p_1 - pressure
464            } else if i == n + 1 {
465                p_2 - pressure
466            } else {
467                mu_res_1[i] - mu_res_2[i]
468                    + (partial_density_other_phase[i] * *molar_volume / molefracs[i]).ln()
469                        * *temperature
470            }
471        });
472
473        // calculate Jacobian
474        let jac = DMatrix::from_fn(n + 2, n + 2, |i, j| {
475            if i < n && j < n {
476                dmu_1[(i, j)]
477            } else if i < n && j == n {
478                -dmu_2[i]
479            } else if i < n && j == n + 1 {
480                dmu_res_dt_1[i] - dmu_res_dt_2[i]
481                    + (partial_density_other_phase[i] * *molar_volume / molefracs[i]).ln()
482            } else if i == n && j < n {
483                dp_1[j]
484            } else if i == n && j == n + 1 {
485                dp_dt_1
486            } else if i == n + 1 && j == n {
487                dp_2
488            } else if i == n + 1 && j == n + 1 {
489                dp_dt_2
490            } else {
491                D::zero()
492            }
493        });
494
495        // calculate Newton step
496        let dx = LU::<_, _, Dyn>::new(jac)?.solve(&f);
497
498        // apply Newton step
499        for i in 0..n {
500            partial_density_other_phase[i] -= dx[i];
501        }
502        *molar_volume -= dx[n];
503        *temperature -= dx[n + 1];
504
505        let error = f.map(|r| r.re()).norm();
506
507        let x = partial_density_other_phase.map(|r| r.re());
508        let x = &x / x.sum();
509        log_iteration(
510            verbosity,
511            Some(error),
512            Temperature::from_reduced(temperature.re()),
513            Pressure::from_reduced(pressure.re()),
514            x.as_slice(),
515            true,
516        );
517        Ok(error)
518    }
519}
520
521/// # Bubble and dew point calculations
522impl<E: Residual<N>, N: Gradients> PhaseEquilibrium<E, 2, N>
523where
524    DefaultAllocator: Allocator<N> + Allocator<N, N> + Allocator<U1, N>,
525{
526    #[expect(clippy::too_many_arguments)]
527    fn iterate_bubble_dew(
528        eos: &E,
529        temperature: &mut Temperature,
530        pressure: &mut Pressure,
531        molefracs_spec: &OVector<f64, N>,
532        molefracs_init: Option<&OVector<f64, N>>,
533        bubble: bool,
534        iterate_p: bool,
535        options: (SolverOptions, SolverOptions),
536    ) -> FeosResult<(f64, OVector<f64, N>)> {
537        let [mut state1, mut state2] = if bubble {
538            Self::starting_x2_bubble(eos, *temperature, *pressure, molefracs_spec, molefracs_init)
539        } else {
540            Self::starting_x2_dew(eos, *temperature, *pressure, molefracs_spec, molefracs_init)
541        }?;
542        let (options_inner, options_outer) = options;
543
544        // initialize variables
545        let mut err_out = 1.0;
546        let mut k_out = 0;
547
548        if PhaseEquilibrium::is_trivial_solution(&state1, &state2) {
549            log_iter!(options_outer.verbosity, "Trivial solution encountered!");
550            return Err(FeosError::TrivialSolution);
551        }
552
553        log_iter!(
554            options_outer.verbosity,
555            "res outer loop | res inner loop |   temperature  |     pressure     | molefracs second phase",
556        );
557        log_iter!(options_outer.verbosity, "{:-<104}", "");
558        log_iteration(
559            options_outer.verbosity,
560            None,
561            *temperature,
562            *pressure,
563            state2.molefracs.as_slice(),
564            false,
565        );
566
567        // Outer loop for finding x2
568        for ko in 0..options_outer.max_iter.unwrap_or(MAX_ITER_OUTER) {
569            // Iso-Fugacity equation
570            err_out = if err_out > NEWTON_TOL {
571                // Inner loop for finding T or p
572                for _ in 0..options_inner.max_iter.unwrap_or(MAX_ITER_INNER) {
573                    let res = if iterate_p {
574                        Self::adjust_p(
575                            *temperature,
576                            pressure,
577                            &mut state1,
578                            &mut state2,
579                            options_inner.verbosity,
580                        )?
581                    } else {
582                        Self::adjust_t(
583                            temperature,
584                            *pressure,
585                            &mut state1,
586                            &mut state2,
587                            options_inner.verbosity,
588                        )?
589                    };
590                    if res < options_inner.tol.unwrap_or(TOL_INNER) {
591                        break;
592                    }
593                }
594                Self::adjust_x2(&state1, &mut state2, options_outer.verbosity)
595            } else {
596                let mut t = temperature.into_reduced();
597                let mut p = pressure.into_reduced();
598                let mut molar_volume = state1.molar_volume.into_reduced();
599                let mut rho2 = state2.partial_density().to_reduced();
600                let err = if iterate_p {
601                    Self::newton_step_t(
602                        &state1.eos,
603                        t,
604                        &state1.molefracs,
605                        &mut p,
606                        &mut molar_volume,
607                        &mut rho2,
608                        options_outer.verbosity,
609                    )?
610                } else {
611                    Self::newton_step_p(
612                        &state1.eos,
613                        &mut t,
614                        &state1.molefracs,
615                        p,
616                        &mut molar_volume,
617                        &mut rho2,
618                        options_outer.verbosity,
619                    )?
620                };
621                *temperature = Temperature::from_reduced(t);
622                *pressure = Pressure::from_reduced(p);
623                state1 = State::new(
624                    &state1.eos,
625                    *temperature,
626                    Density::from_reduced(molar_volume.recip()),
627                    molefracs_spec,
628                )?;
629                let density = rho2.sum();
630                state2 = State::new(
631                    &state2.eos,
632                    *temperature,
633                    Density::from_reduced(density),
634                    rho2 / density,
635                )?;
636                Ok(err)
637            }?;
638
639            if Self::is_trivial_solution(&state1, &state2) {
640                log_iter!(options_outer.verbosity, "Trivial solution encountered!");
641                return Err(FeosError::TrivialSolution);
642            }
643
644            if err_out < options_outer.tol.unwrap_or(TOL_OUTER) {
645                k_out = ko + 1;
646                break;
647            }
648        }
649
650        if err_out < options_outer.tol.unwrap_or(TOL_OUTER) {
651            log_result!(
652                options_outer.verbosity,
653                "Bubble/dew point: calculation converged in {} step(s)\n",
654                k_out
655            );
656            Ok((
657                state1.density.into_reduced().recip(),
658                state2.partial_density().to_reduced(),
659            ))
660        } else {
661            // not converged, return error
662            Err(FeosError::NotConverged(String::from(
663                "bubble-dew-iteration",
664            )))
665        }
666    }
667
668    fn adjust_p(
669        temperature: Temperature,
670        pressure: &mut Pressure,
671        state1: &mut State<E, N>,
672        state2: &mut State<E, N>,
673        verbosity: Verbosity,
674    ) -> FeosResult<f64> {
675        // calculate K = phi_1/phi_2 = x_2/x_1
676        let ln_phi_1 = state1.ln_phi();
677        let ln_phi_2 = state2.ln_phi();
678        let k = (&ln_phi_1 - &ln_phi_2).map(f64::exp);
679
680        // calculate residual
681        let xk = state1.molefracs.component_mul(&k);
682        let f = xk.sum() - 1.0;
683
684        // Derivative w.r.t. ln(pressure)
685        let ln_phi_1_dp = state1.dln_phi_dp();
686        let ln_phi_2_dp = state2.dln_phi_dp();
687        let df = ((ln_phi_1_dp - ln_phi_2_dp) * *pressure)
688            .into_value()
689            .component_mul(&xk)
690            .sum();
691        let mut lnpstep = -f / df;
692
693        // catch too big p-steps
694        lnpstep = lnpstep.clamp(-MAX_LNPSTEP, MAX_LNPSTEP);
695
696        // Update p
697        *pressure *= lnpstep.exp();
698
699        // update states with new temperature/pressure
700        Self::adjust_states(temperature, *pressure, state1, state2, None)?;
701
702        // log
703        log_iteration(
704            verbosity,
705            Some(f),
706            temperature,
707            *pressure,
708            state2.molefracs.as_slice(),
709            false,
710        );
711
712        Ok(f.abs())
713    }
714
715    fn adjust_t(
716        temperature: &mut Temperature,
717        pressure: Pressure,
718        state1: &mut State<E, N>,
719        state2: &mut State<E, N>,
720        verbosity: Verbosity,
721    ) -> FeosResult<f64> {
722        // calculate K = phi_1/phi_2 = x_2/x_1
723        let ln_phi_1 = state1.ln_phi();
724        let ln_phi_2 = state2.ln_phi();
725        let k = (&ln_phi_1 - &ln_phi_2).map(f64::exp);
726
727        // calculate residual
728        let f = state1.molefracs.dot(&k) - 1.0;
729
730        // Derivative w.r.t. temperature
731        let ln_phi_1_dt = state1.dln_phi_dt();
732        let ln_phi_2_dt = state2.dln_phi_dt();
733        let df = ((ln_phi_1_dt - ln_phi_2_dt)
734            .component_mul(&Dimensionless::new(state1.molefracs.component_mul(&k))))
735        .sum();
736        let mut tstep = -f / df;
737
738        // catch too big t-steps
739        if tstep < -Temperature::from_reduced(MAX_TSTEP) {
740            tstep = -Temperature::from_reduced(MAX_TSTEP);
741        } else if tstep > Temperature::from_reduced(MAX_TSTEP) {
742            tstep = Temperature::from_reduced(MAX_TSTEP);
743        }
744
745        // Update t
746        *temperature += tstep;
747
748        // update states with new temperature
749        Self::adjust_states(*temperature, pressure, state1, state2, None)?;
750
751        // log
752        log_iteration(
753            verbosity,
754            Some(f),
755            *temperature,
756            pressure,
757            state2.molefracs.as_slice(),
758            false,
759        );
760
761        Ok(f.abs())
762    }
763
764    fn starting_pressure_ideal_gas(
765        eos: &E,
766        temperature: Temperature,
767        molefracs_spec: &OVector<f64, N>,
768        bubble: bool,
769    ) -> FeosResult<(Pressure, OVector<f64, N>)> {
770        if bubble {
771            Self::starting_pressure_ideal_gas_bubble(eos, temperature, molefracs_spec)
772        } else {
773            Self::starting_pressure_ideal_gas_dew(eos, temperature, molefracs_spec)
774        }
775    }
776
777    pub(super) fn starting_pressure_ideal_gas_bubble(
778        eos: &E,
779        temperature: Temperature,
780        liquid_molefracs: &OVector<f64, N>,
781    ) -> FeosResult<(Pressure, OVector<f64, N>)> {
782        let density = 0.75 * Density::from_reduced(eos.compute_max_density(liquid_molefracs));
783        let liquid = State::new(eos, temperature, density, liquid_molefracs)?;
784        let v_l = liquid.partial_molar_volume();
785        let p_l = liquid.pressure(Contributions::Total);
786        let mu_l = liquid.residual_chemical_potential();
787        let k_i = liquid_molefracs.component_mul(
788            &((mu_l - v_l * p_l) / (RGAS * temperature))
789                .into_value()
790                .map(f64::exp),
791        );
792        let p = k_i.sum() * RGAS * temperature * density;
793        let y = &k_i / k_i.sum();
794        Ok((p, y))
795    }
796
797    fn starting_pressure_ideal_gas_dew(
798        eos: &E,
799        temperature: Temperature,
800        vapor_molefracs: &OVector<f64, N>,
801    ) -> FeosResult<(Pressure, OVector<f64, N>)> {
802        let mut p: Option<Pressure> = None;
803
804        let mut x = vapor_molefracs.clone();
805        for _ in 0..5 {
806            let density = Density::from_reduced(0.75 * eos.compute_max_density(&x));
807            let liquid = State::new(eos, temperature, density, x)?;
808            let v_l = liquid.partial_molar_volume();
809            let p_l = liquid.pressure(Contributions::Total);
810            let mu_l = liquid.residual_chemical_potential();
811            let k = vapor_molefracs.clone().component_div(
812                &((mu_l - v_l * p_l) / (RGAS * temperature))
813                    .into_value()
814                    .map(f64::exp),
815            );
816            let k_sum = k.sum();
817            let p_new = RGAS * temperature * density / k_sum;
818            x = k / k_sum;
819            if let Some(p_old) = p
820                && ((p_new - p_old) / p_old).into_value().abs() < 1e-5
821            {
822                p = Some(p_new);
823                break;
824            }
825            p = Some(p_new);
826        }
827        Ok((p.unwrap(), x))
828    }
829
830    pub(super) fn starting_pressure_spinodal(
831        eos: &E,
832        temperature: Temperature,
833        molefracs: &OVector<f64, N>,
834    ) -> FeosResult<Pressure> {
835        let [sp_v, sp_l] = State::spinodal(eos, temperature, molefracs, Default::default())?;
836        let pv = sp_v.pressure(Contributions::Total);
837        let pl = sp_l.pressure(Contributions::Total);
838        Ok(0.5 * (Pressure::from_reduced(0.0).max(pl) + pv))
839    }
840
841    fn starting_x2_bubble(
842        eos: &E,
843        temperature: Temperature,
844        pressure: Pressure,
845        liquid_molefracs: &OVector<f64, N>,
846        vapor_molefracs: Option<&OVector<f64, N>>,
847    ) -> FeosResult<[State<E, N>; 2]> {
848        let liquid_state =
849            State::new_npt(eos, temperature, pressure, liquid_molefracs, Some(Liquid))?;
850        let xv = match vapor_molefracs {
851            Some(xv) => xv.clone(),
852            None => liquid_state
853                .ln_phi()
854                .map(f64::exp)
855                .component_mul(liquid_molefracs),
856        };
857        let vapor_state = State::new_npt(eos, temperature, pressure, xv, Some(Vapor))?;
858        Ok([liquid_state, vapor_state])
859    }
860
861    fn starting_x2_dew(
862        eos: &E,
863        temperature: Temperature,
864        pressure: Pressure,
865        vapor_molefracs: &OVector<f64, N>,
866        liquid_molefracs: Option<&OVector<f64, N>>,
867    ) -> FeosResult<[State<E, N>; 2]> {
868        let vapor_state = State::new_npt(eos, temperature, pressure, vapor_molefracs, Some(Vapor))?;
869        let xl = match liquid_molefracs {
870            Some(xl) => xl.clone(),
871            None => {
872                let xl = vapor_state
873                    .ln_phi()
874                    .map(f64::exp)
875                    .component_mul(vapor_molefracs);
876                let liquid_state = State::new_npt(eos, temperature, pressure, xl, Some(Liquid))?;
877                (vapor_state.ln_phi() - liquid_state.ln_phi())
878                    .map(f64::exp)
879                    .component_mul(vapor_molefracs)
880            }
881        };
882        let liquid_state = State::new_npt(eos, temperature, pressure, xl, Some(Liquid))?;
883        Ok([vapor_state, liquid_state])
884    }
885
886    fn adjust_states(
887        temperature: Temperature,
888        pressure: Pressure,
889        state1: &mut State<E, N>,
890        state2: &mut State<E, N>,
891        molefracs_state2: Option<&OVector<f64, N>>,
892    ) -> FeosResult<()> {
893        *state1 = State::new_npt(
894            &state1.eos,
895            temperature,
896            pressure,
897            &state1.molefracs,
898            Some(InitialDensity(state1.density)),
899        )?;
900        *state2 = State::new_npt(
901            &state2.eos,
902            temperature,
903            pressure,
904            molefracs_state2.unwrap_or(&state2.molefracs),
905            Some(InitialDensity(state2.density)),
906        )?;
907        Ok(())
908    }
909
910    fn adjust_x2(
911        state1: &State<E, N>,
912        state2: &mut State<E, N>,
913        verbosity: Verbosity,
914    ) -> FeosResult<f64> {
915        let x1 = &state1.molefracs;
916        let ln_phi_1 = state1.ln_phi();
917        let ln_phi_2 = state2.ln_phi();
918        let k = (ln_phi_1 - ln_phi_2).map(f64::exp);
919        let kx1 = k.component_mul(x1);
920        let err_out = kx1
921            .component_div(&state2.molefracs)
922            .map(|e| (e - 1.0).abs())
923            .sum();
924        let x2 = &kx1 / kx1.sum();
925        log_iter!(
926            verbosity,
927            "{:<14.8e} | {:14} | {:14} | {:16} |",
928            err_out,
929            "",
930            "",
931            ""
932        );
933        *state2 = State::new_npt(
934            &state2.eos,
935            state2.temperature,
936            state2.pressure(Contributions::Total),
937            x2,
938            Some(InitialDensity(state2.density)),
939        )?;
940        Ok(err_out)
941    }
942}
943
944fn log_iteration(
945    verbosity: Verbosity,
946    error: Option<f64>,
947    temperature: Temperature,
948    pressure: Pressure,
949    x2: &[f64],
950    newton: bool,
951) {
952    let error = error.map_or_else(|| format!("{:14}", ""), |e| format!("{:<14.8e}", e.abs()));
953    log_iter!(
954        verbosity,
955        "{:14} | {} | {:12.8} | {:12.8} | {:.8?} {}",
956        "",
957        error,
958        temperature,
959        pressure,
960        x2,
961        if newton { "NEWTON" } else { "" }
962    );
963}