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().unwrap();
234
235            // First use given initial pressure if applicable
236            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                // Next try to initialize with an ideal gas assumption
249                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                // Finally use the spinodal to initialize the calculation
270                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            // pressure is specified
293            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        // implicit differentiation
309        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        // calculate properties
377        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        // calculate residual
381        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        // calculate Jacobian
395        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        // calculate Newton step
412        let dx = LU::<_, _, Dyn>::new(jac).unwrap().solve(&f);
413
414        // apply Newton step
415        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        // calculate properties
446        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        // calculate residual
452        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        // calculate Jacobian
466        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        // calculate Newton step
488        let dx = LU::<_, _, Dyn>::new(jac).unwrap().solve(&f);
489
490        // apply Newton step
491        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
513/// # Bubble and dew point calculations
514impl<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        // initialize variables
537        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        // Outer loop for finding x2
560        for ko in 0..options_outer.max_iter.unwrap_or(MAX_ITER_OUTER) {
561            // Iso-Fugacity equation
562            err_out = if err_out > NEWTON_TOL {
563                // Inner loop for finding T or p
564                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            // not converged, return error
654            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        // calculate K = phi_1/phi_2 = x_2/x_1
668        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        // calculate residual
673        let xk = state1.molefracs.component_mul(&k);
674        let f = xk.sum() - 1.0;
675
676        // Derivative w.r.t. ln(pressure)
677        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        // catch too big p-steps
686        lnpstep = lnpstep.clamp(-MAX_LNPSTEP, MAX_LNPSTEP);
687
688        // Update p
689        *pressure *= lnpstep.exp();
690
691        // update states with new temperature/pressure
692        Self::adjust_states(temperature, *pressure, state1, state2, None)?;
693
694        // log
695        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        // calculate K = phi_1/phi_2 = x_2/x_1
715        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        // calculate residual
720        let f = state1.molefracs.dot(&k) - 1.0;
721
722        // Derivative w.r.t. temperature
723        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        // catch too big t-steps
731        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        // Update t
738        *temperature += tstep;
739
740        // update states with new temperature
741        Self::adjust_states(*temperature, pressure, state1, state2, None)?;
742
743        // log
744        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}