feos_core/phase_equilibria/
bubble_dew.rs

1use super::PhaseEquilibrium;
2use crate::equation_of_state::Residual;
3use crate::errors::{EosError, EosResult};
4use crate::state::{
5    Contributions,
6    DensityInitialization::{InitialDensity, Liquid, Vapor},
7    State, StateBuilder, TPSpec,
8};
9use crate::{ReferenceSystem, SolverOptions, Verbosity};
10use ndarray::*;
11use num_dual::linalg::{norm, LU};
12use quantity::{Density, Dimensionless, Moles, Pressure, Quantity, SIUnit, Temperature, RGAS};
13use std::fmt;
14use std::sync::Arc;
15use typenum::{N1, N2, P1, Z0};
16
17const MAX_ITER_INNER: usize = 5;
18const TOL_INNER: f64 = 1e-9;
19const MAX_ITER_OUTER: usize = 400;
20const TOL_OUTER: f64 = 1e-10;
21
22const MAX_TSTEP: f64 = 20.0;
23const MAX_LNPSTEP: f64 = 0.1;
24const NEWTON_TOL: f64 = 1e-3;
25
26/// Trait that enables functions to be generic over their input unit.
27pub trait TemperatureOrPressure: Copy + Into<TPSpec> {
28    type Other: fmt::Display + TemperatureOrPressure;
29
30    const IDENTIFIER: &'static str;
31
32    fn temperature_pressure(&self, tp_init: Self::Other) -> (Temperature, Pressure);
33
34    fn from_state<E: Residual>(state: &State<E>) -> Self::Other;
35
36    fn linspace(
37        &self,
38        start: Self::Other,
39        end: Self::Other,
40        n: usize,
41    ) -> (Temperature<Array1<f64>>, Pressure<Array1<f64>>);
42
43    fn bubble_dew_point<E: Residual>(
44        eos: &Arc<E>,
45        tp_spec: Self,
46        tp_init: Option<Self::Other>,
47        molefracs_spec: &Array1<f64>,
48        molefracs_init: Option<&Array1<f64>>,
49        bubble: bool,
50        options: (SolverOptions, SolverOptions),
51    ) -> EosResult<PhaseEquilibrium<E, 2>>;
52
53    fn adjust_t_p<E: Residual>(
54        tp_spec: Self,
55        var: &mut Self::Other,
56        state1: &mut State<E>,
57        state2: &mut State<E>,
58        verbosity: Verbosity,
59    ) -> EosResult<f64>;
60
61    fn newton_step<E: Residual>(
62        tp_spec: Self,
63        var: &mut Self::Other,
64        state1: &mut State<E>,
65        state2: &mut State<E>,
66        verbosity: Verbosity,
67    ) -> EosResult<f64>;
68}
69
70impl TemperatureOrPressure for Temperature {
71    type Other = Pressure;
72    const IDENTIFIER: &'static str = "temperature";
73
74    fn temperature_pressure(&self, tp_init: Self::Other) -> (Temperature, Pressure) {
75        (*self, tp_init)
76    }
77
78    fn from_state<E: Residual>(state: &State<E>) -> Self::Other {
79        state.pressure(Contributions::Total)
80    }
81
82    fn linspace(
83        &self,
84        start: Pressure,
85        end: Pressure,
86        n: usize,
87    ) -> (Temperature<Array1<f64>>, Pressure<Array1<f64>>) {
88        (
89            Temperature::linspace(*self, *self, n),
90            Pressure::linspace(start, end, n),
91        )
92    }
93
94    fn bubble_dew_point<E: Residual>(
95        eos: &Arc<E>,
96        temperature: Self,
97        p_init: Option<Pressure>,
98        molefracs_spec: &Array1<f64>,
99        molefracs_init: Option<&Array1<f64>>,
100        bubble: bool,
101        options: (SolverOptions, SolverOptions),
102    ) -> EosResult<PhaseEquilibrium<E, 2>> {
103        // First use given initial pressure if applicable
104        if let Some(p) = p_init {
105            return PhaseEquilibrium::iterate_bubble_dew(
106                eos,
107                temperature,
108                p,
109                molefracs_spec,
110                molefracs_init,
111                bubble,
112                options,
113            );
114        }
115
116        // Next try to initialize with an ideal gas assumption
117        let vle =
118            PhaseEquilibrium::starting_pressure_ideal_gas(eos, temperature, molefracs_spec, bubble)
119                .and_then(|(p, x)| {
120                    PhaseEquilibrium::iterate_bubble_dew(
121                        eos,
122                        temperature,
123                        p,
124                        molefracs_spec,
125                        molefracs_init.or(Some(&x)),
126                        bubble,
127                        options,
128                    )
129                });
130
131        // Finally use the spinodal to initialize the calculation
132        vle.or_else(|_| {
133            PhaseEquilibrium::iterate_bubble_dew(
134                eos,
135                temperature,
136                PhaseEquilibrium::starting_pressure_spinodal(eos, temperature, molefracs_spec)?,
137                molefracs_spec,
138                molefracs_init,
139                bubble,
140                options,
141            )
142        })
143    }
144
145    fn adjust_t_p<E: Residual>(
146        temperature: Temperature,
147        pressure: &mut Pressure,
148        state1: &mut State<E>,
149        state2: &mut State<E>,
150        verbosity: Verbosity,
151    ) -> EosResult<f64> {
152        // calculate K = phi_1/phi_2 = x_2/x_1
153        let ln_phi_1 = state1.ln_phi();
154        let ln_phi_2 = state2.ln_phi();
155        let k = (&ln_phi_1 - &ln_phi_2).mapv(f64::exp);
156
157        // calculate residual
158        let f = (&state1.molefracs * &k).sum() - 1.0;
159
160        // Derivative w.r.t. ln(pressure)
161        let ln_phi_1_dp = state1.dln_phi_dp();
162        let ln_phi_2_dp = state2.dln_phi_dp();
163        let df =
164            (((ln_phi_1_dp - ln_phi_2_dp) * *pressure).into_value() * &state1.molefracs * &k).sum();
165        let mut lnpstep = -f / df;
166
167        // catch too big p-steps
168        lnpstep = lnpstep.clamp(-MAX_LNPSTEP, MAX_LNPSTEP);
169
170        // Update p
171        *pressure *= lnpstep.exp();
172
173        // update states with new temperature/pressure
174        adjust_states(temperature, *pressure, state1, state2, None)?;
175
176        // log
177        log_iter!(
178            verbosity,
179            "{:14} | {:<14.8e} | {:12.8} | {:.8}",
180            "",
181            f.abs(),
182            pressure,
183            state2.molefracs
184        );
185
186        Ok(f.abs())
187    }
188
189    fn newton_step<E: Residual>(
190        _: Temperature,
191        pressure: &mut Pressure,
192        state1: &mut State<E>,
193        state2: &mut State<E>,
194        verbosity: Verbosity,
195    ) -> EosResult<f64> {
196        let dmu_drho_1 = (state1.dmu_dni(Contributions::Total) * state1.volume)
197            .to_reduced()
198            .dot(&state1.molefracs);
199        let dmu_drho_2 = (state2.dmu_dni(Contributions::Total) * state2.volume).to_reduced();
200        let dp_drho_1 = (state1.dp_dni(Contributions::Total) * state1.volume)
201            .to_reduced()
202            .dot(&state1.molefracs);
203        let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume).to_reduced();
204        let mu_1_res = state1.residual_chemical_potential().to_reduced();
205        let mu_2_res = state2.residual_chemical_potential().to_reduced();
206        let p_1 = state1.pressure(Contributions::Total).to_reduced();
207        let p_2 = state2.pressure(Contributions::Total).to_reduced();
208
209        // calculate residual
210        let dmu_ig = (RGAS * state1.temperature).to_reduced()
211            * (&state1.partial_density / &state2.partial_density)
212                .into_value()
213                .mapv(f64::ln);
214        let res = concatenate![Axis(0), mu_1_res - mu_2_res + dmu_ig, arr1(&[p_1 - p_2])];
215        let error = norm(&res);
216
217        // calculate Jacobian
218        let jacobian = concatenate![
219            Axis(1),
220            concatenate![Axis(0), -dmu_drho_2, -dp_drho_2.insert_axis(Axis(0))],
221            concatenate![
222                Axis(0),
223                dmu_drho_1.insert_axis(Axis(1)),
224                arr2(&[[dp_drho_1]])
225            ]
226        ];
227
228        // calculate Newton step
229        let dx = LU::new(jacobian)?.solve(&res);
230
231        // apply Newton step
232        let rho_l1 = state1.density - Density::from_reduced(dx[dx.len() - 1]);
233        let rho_l2 =
234            state2.partial_density.clone() - Density::from_reduced(dx.slice(s![0..-1]).to_owned());
235
236        // update states
237        *state1 = StateBuilder::new(&state1.eos)
238            .temperature(state1.temperature)
239            .density(rho_l1)
240            .molefracs(&state1.molefracs)
241            .build()?;
242        *state2 = StateBuilder::new(&state2.eos)
243            .temperature(state2.temperature)
244            .partial_density(&rho_l2)
245            .build()?;
246        *pressure = state1.pressure(Contributions::Total);
247        log_iter!(
248            verbosity,
249            "{:<14.8e} | {:14} | {:12.8} | {:.8} NEWTON",
250            error,
251            "",
252            pressure,
253            state2.molefracs
254        );
255        Ok(error)
256    }
257}
258
259// For some inexplicable reason this does not compile if the `Pressure` type is
260// used instead of the explicit unit. Maybe the type is too complicated for the
261// compiler?
262impl TemperatureOrPressure for Quantity<f64, SIUnit<N2, N1, P1, Z0, Z0, Z0, Z0>> {
263    type Other = Temperature;
264    const IDENTIFIER: &'static str = "pressure";
265
266    fn temperature_pressure(&self, tp_init: Self::Other) -> (Temperature, Pressure) {
267        (tp_init, *self)
268    }
269
270    fn from_state<E: Residual>(state: &State<E>) -> Self::Other {
271        state.temperature
272    }
273
274    fn linspace(
275        &self,
276        start: Temperature,
277        end: Temperature,
278        n: usize,
279    ) -> (Temperature<Array1<f64>>, Pressure<Array1<f64>>) {
280        (
281            Temperature::linspace(start, end, n),
282            Pressure::linspace(*self, *self, n),
283        )
284    }
285
286    fn bubble_dew_point<E: Residual>(
287        eos: &Arc<E>,
288        pressure: Self,
289        t_init: Option<Temperature>,
290        molefracs_spec: &Array1<f64>,
291        molefracs_init: Option<&Array1<f64>>,
292        bubble: bool,
293        options: (SolverOptions, SolverOptions),
294    ) -> EosResult<PhaseEquilibrium<E, 2>> {
295        let temperature = t_init.expect("An initial temperature is required for the calculation of bubble/dew points at given pressure!");
296        PhaseEquilibrium::iterate_bubble_dew(
297            eos,
298            pressure,
299            temperature,
300            molefracs_spec,
301            molefracs_init,
302            bubble,
303            options,
304        )
305    }
306
307    fn adjust_t_p<E: Residual>(
308        pressure: Pressure,
309        temperature: &mut Temperature,
310        state1: &mut State<E>,
311        state2: &mut State<E>,
312        verbosity: Verbosity,
313    ) -> EosResult<f64> {
314        // calculate K = phi_1/phi_2 = x_2/x_1
315        let ln_phi_1 = state1.ln_phi();
316        let ln_phi_2 = state2.ln_phi();
317        let k = (&ln_phi_1 - &ln_phi_2).mapv(f64::exp);
318
319        // calculate residual
320        let f = (&state1.molefracs * &k).sum() - 1.0;
321
322        // Derivative w.r.t. temperature
323        let ln_phi_1_dt = state1.dln_phi_dt();
324        let ln_phi_2_dt = state2.dln_phi_dt();
325        let df = ((ln_phi_1_dt - ln_phi_2_dt) * Dimensionless::new(&state1.molefracs * &k)).sum();
326        let mut tstep = -f / df;
327
328        // catch too big t-steps
329        if tstep < -Temperature::from_reduced(MAX_TSTEP) {
330            tstep = -Temperature::from_reduced(MAX_TSTEP);
331        } else if tstep > Temperature::from_reduced(MAX_TSTEP) {
332            tstep = Temperature::from_reduced(MAX_TSTEP);
333        }
334
335        // Update t
336        *temperature += tstep;
337
338        // update states with new temperature
339        adjust_states(*temperature, pressure, state1, state2, None)?;
340
341        // log
342        log_iter!(
343            verbosity,
344            "{:14} | {:<14.8e} | {:12.8} | {:.8}",
345            "",
346            f.abs(),
347            temperature,
348            state2.molefracs
349        );
350
351        Ok(f.abs())
352    }
353
354    fn newton_step<E: Residual>(
355        pressure: Pressure,
356        temperature: &mut Temperature,
357        state1: &mut State<E>,
358        state2: &mut State<E>,
359        verbosity: Verbosity,
360    ) -> EosResult<f64> {
361        let dmu_drho_1 = (state1.dmu_dni(Contributions::Total) * state1.volume)
362            .to_reduced()
363            .dot(&state1.molefracs);
364        let dmu_drho_2 = (state2.dmu_dni(Contributions::Total) * state2.volume).to_reduced();
365        let dmu_res_dt_1 = state1.dmu_res_dt().to_reduced();
366        let dmu_res_dt_2 = state2.dmu_res_dt().to_reduced();
367        let dp_drho_1 = (state1.dp_dni(Contributions::Total) * state1.volume)
368            .to_reduced()
369            .dot(&state1.molefracs);
370        let dp_dt_1 = state1.dp_dt(Contributions::Total).to_reduced();
371        let dp_dt_2 = state2.dp_dt(Contributions::Total).to_reduced();
372        let dp_drho_2 = (state2.dp_dni(Contributions::Total) * state2.volume).to_reduced();
373        let mu_1_res = state1.residual_chemical_potential().to_reduced();
374        let mu_2_res = state2.residual_chemical_potential().to_reduced();
375        let p_1 = state1.pressure(Contributions::Total).to_reduced();
376        let p_2 = state2.pressure(Contributions::Total).to_reduced();
377        let p = pressure.to_reduced();
378
379        // calculate residual
380        let delta_dmu_ig_dt = (&state1.partial_density / &state2.partial_density)
381            .into_value()
382            .mapv(f64::ln);
383        let delta_mu_ig = (RGAS * state1.temperature).to_reduced() * &delta_dmu_ig_dt;
384        let res = concatenate![
385            Axis(0),
386            mu_1_res - mu_2_res + delta_mu_ig,
387            arr1(&[p_1 - p]),
388            arr1(&[p_2 - p])
389        ];
390        let error = norm(&res);
391
392        // calculate Jacobian
393        let jacobian = concatenate![
394            Axis(1),
395            concatenate![
396                Axis(0),
397                -dmu_drho_2,
398                Array2::zeros((1, res.len() - 2)),
399                dp_drho_2.insert_axis(Axis(0))
400            ],
401            concatenate![
402                Axis(0),
403                dmu_drho_1.insert_axis(Axis(1)),
404                arr2(&[[dp_drho_1], [0.0]])
405            ],
406            concatenate![
407                Axis(0),
408                (dmu_res_dt_1 - dmu_res_dt_2 + delta_dmu_ig_dt).insert_axis(Axis(1)),
409                arr2(&[[dp_dt_1], [dp_dt_2]])
410            ]
411        ];
412
413        // calculate Newton step
414        let dx = LU::new(jacobian)?.solve(&res);
415
416        // apply Newton step
417        let rho_l1 = state1.density - Density::from_reduced(dx[dx.len() - 2]);
418        let rho_l2 =
419            state2.partial_density.clone() - Density::from_reduced(dx.slice(s![0..-2]).to_owned());
420        let t = state1.temperature - Temperature::from_reduced(dx[dx.len() - 1]);
421
422        // update states
423        *state1 = StateBuilder::new(&state1.eos)
424            .temperature(t)
425            .density(rho_l1)
426            .molefracs(&state1.molefracs)
427            .build()?;
428        *state2 = StateBuilder::new(&state2.eos)
429            .temperature(t)
430            .partial_density(&rho_l2)
431            .build()?;
432        *temperature = t;
433        log_iter!(
434            verbosity,
435            "{:<14.8e} | {:14} | {:12.8} | {:.8} NEWTON",
436            error,
437            "",
438            temperature,
439            state2.molefracs
440        );
441        Ok(error)
442    }
443}
444
445/// # Bubble and dew point calculations
446impl<E: Residual> PhaseEquilibrium<E, 2> {
447    /// Calculate a phase equilibrium for a given temperature
448    /// or pressure and composition of the liquid phase.
449    pub fn bubble_point<TP: TemperatureOrPressure>(
450        eos: &Arc<E>,
451        temperature_or_pressure: TP,
452        liquid_molefracs: &Array1<f64>,
453        tp_init: Option<TP::Other>,
454        vapor_molefracs: Option<&Array1<f64>>,
455        options: (SolverOptions, SolverOptions),
456    ) -> EosResult<Self> {
457        Self::bubble_dew_point(
458            eos,
459            temperature_or_pressure,
460            tp_init,
461            liquid_molefracs,
462            vapor_molefracs,
463            true,
464            options,
465        )
466    }
467
468    /// Calculate a phase equilibrium for a given temperature
469    /// or pressure and composition of the vapor phase.
470    pub fn dew_point<TP: TemperatureOrPressure>(
471        eos: &Arc<E>,
472        temperature_or_pressure: TP,
473        vapor_molefracs: &Array1<f64>,
474        tp_init: Option<TP::Other>,
475        liquid_molefracs: Option<&Array1<f64>>,
476        options: (SolverOptions, SolverOptions),
477    ) -> EosResult<Self> {
478        Self::bubble_dew_point(
479            eos,
480            temperature_or_pressure,
481            tp_init,
482            vapor_molefracs,
483            liquid_molefracs,
484            false,
485            options,
486        )
487    }
488
489    pub(super) fn bubble_dew_point<TP: TemperatureOrPressure>(
490        eos: &Arc<E>,
491        tp_spec: TP,
492        tp_init: Option<TP::Other>,
493        molefracs_spec: &Array1<f64>,
494        molefracs_init: Option<&Array1<f64>>,
495        bubble: bool,
496        options: (SolverOptions, SolverOptions),
497    ) -> EosResult<Self> {
498        TP::bubble_dew_point(
499            eos,
500            tp_spec,
501            tp_init,
502            molefracs_spec,
503            molefracs_init,
504            bubble,
505            options,
506        )
507    }
508
509    fn iterate_bubble_dew<TP: TemperatureOrPressure>(
510        eos: &Arc<E>,
511        tp_spec: TP,
512        tp_init: TP::Other,
513        molefracs_spec: &Array1<f64>,
514        molefracs_init: Option<&Array1<f64>>,
515        bubble: bool,
516        options: (SolverOptions, SolverOptions),
517    ) -> EosResult<Self> {
518        let (t, p) = tp_spec.temperature_pressure(tp_init);
519        let [state1, state2] = if bubble {
520            starting_x2_bubble(eos, t, p, molefracs_spec, molefracs_init)
521        } else {
522            starting_x2_dew(eos, t, p, molefracs_spec, molefracs_init)
523        }?;
524        bubble_dew(tp_spec, tp_init, state1, state2, bubble, options)
525    }
526
527    fn starting_pressure_ideal_gas(
528        eos: &Arc<E>,
529        temperature: Temperature,
530        molefracs_spec: &Array1<f64>,
531        bubble: bool,
532    ) -> EosResult<(Pressure, Array1<f64>)> {
533        if bubble {
534            Self::starting_pressure_ideal_gas_bubble(eos, temperature, molefracs_spec)
535        } else {
536            Self::starting_pressure_ideal_gas_dew(eos, temperature, molefracs_spec)
537        }
538    }
539
540    pub(super) fn starting_pressure_ideal_gas_bubble(
541        eos: &Arc<E>,
542        temperature: Temperature,
543        liquid_molefracs: &Array1<f64>,
544    ) -> EosResult<(Pressure, Array1<f64>)> {
545        let m = Moles::from_reduced(liquid_molefracs.to_owned());
546        let density = 0.75 * eos.max_density(Some(&m))?;
547        let liquid = State::new_nvt(eos, temperature, m.sum() / density, &m)?;
548        let v_l = liquid.partial_molar_volume();
549        let p_l = liquid.pressure(Contributions::Total);
550        let mu_l = liquid.residual_chemical_potential();
551        let p_i = (liquid_molefracs * temperature * density * RGAS)
552            * Dimensionless::new(
553                ((mu_l - p_l * v_l) / (RGAS * temperature))
554                    .into_value()
555                    .mapv(f64::exp),
556            );
557        let p = p_i.sum();
558        let y = (p_i / p).into_value();
559        Ok((p, y))
560    }
561
562    fn starting_pressure_ideal_gas_dew(
563        eos: &Arc<E>,
564        temperature: Temperature,
565        vapor_molefracs: &Array1<f64>,
566    ) -> EosResult<(Pressure, Array1<f64>)> {
567        let mut p: Option<Pressure> = None;
568
569        let mut x = vapor_molefracs.clone();
570        for _ in 0..5 {
571            let m = Moles::from_reduced(x);
572            let density = 0.75 * eos.max_density(Some(&m))?;
573            let liquid = State::new_nvt(eos, temperature, m.sum() / density, &m)?;
574            let v_l = liquid.partial_molar_volume();
575            let p_l = liquid.pressure(Contributions::Total);
576            let mu_l = liquid.residual_chemical_potential();
577            let k = vapor_molefracs
578                / ((mu_l - p_l * v_l) / (RGAS * temperature))
579                    .into_value()
580                    .mapv(f64::exp);
581            let p_new = RGAS * temperature * density / k.sum();
582            x = &k / k.sum();
583            if let Some(p_old) = p {
584                if ((p_new - p_old) / p_old).into_value().abs() < 1e-5 {
585                    p = Some(p_new);
586                    break;
587                }
588            }
589            p = Some(p_new);
590        }
591        Ok((p.unwrap(), x))
592    }
593
594    pub(super) fn starting_pressure_spinodal(
595        eos: &Arc<E>,
596        temperature: Temperature,
597        molefracs: &Array1<f64>,
598    ) -> EosResult<Pressure> {
599        let moles = Moles::from_reduced(molefracs.clone());
600        let [sp_v, sp_l] = State::spinodal(eos, temperature, Some(&moles), Default::default())?;
601        let pv = sp_v.pressure(Contributions::Total);
602        let pl = sp_l.pressure(Contributions::Total);
603        Ok(0.5 * (Pressure::from_reduced(0.0).max(pl) + pv))
604    }
605}
606
607fn starting_x2_bubble<E: Residual>(
608    eos: &Arc<E>,
609    temperature: Temperature,
610    pressure: Pressure,
611    liquid_molefracs: &Array1<f64>,
612    vapor_molefracs: Option<&Array1<f64>>,
613) -> EosResult<[State<E>; 2]> {
614    let liquid_state = State::new_npt(
615        eos,
616        temperature,
617        pressure,
618        &Moles::from_reduced(liquid_molefracs.clone()),
619        Liquid,
620    )?;
621    let xv = match vapor_molefracs {
622        Some(xv) => xv.clone(),
623        None => liquid_state.ln_phi().mapv(f64::exp) * liquid_molefracs,
624    };
625    let vapor_state = State::new_npt(eos, temperature, pressure, &Moles::from_reduced(xv), Vapor)?;
626    Ok([liquid_state, vapor_state])
627}
628
629fn starting_x2_dew<E: Residual>(
630    eos: &Arc<E>,
631    temperature: Temperature,
632    pressure: Pressure,
633    vapor_molefracs: &Array1<f64>,
634    liquid_molefracs: Option<&Array1<f64>>,
635) -> EosResult<[State<E>; 2]> {
636    let vapor_state = State::new_npt(
637        eos,
638        temperature,
639        pressure,
640        &Moles::from_reduced(vapor_molefracs.clone()),
641        Vapor,
642    )?;
643    let xl = match liquid_molefracs {
644        Some(xl) => xl.clone(),
645        None => {
646            let xl = vapor_state.ln_phi().mapv(f64::exp) * vapor_molefracs;
647            let liquid_state =
648                State::new_npt(eos, temperature, pressure, &Moles::from_reduced(xl), Liquid)?;
649            (vapor_state.ln_phi() - liquid_state.ln_phi()).mapv(f64::exp) * vapor_molefracs
650        }
651    };
652    let liquid_state =
653        State::new_npt(eos, temperature, pressure, &Moles::from_reduced(xl), Liquid)?;
654    Ok([vapor_state, liquid_state])
655}
656
657fn bubble_dew<E: Residual, TP: TemperatureOrPressure>(
658    tp_spec: TP,
659    mut var_tp: TP::Other,
660    mut state1: State<E>,
661    mut state2: State<E>,
662    bubble: bool,
663    options: (SolverOptions, SolverOptions),
664) -> EosResult<PhaseEquilibrium<E, 2>> {
665    let (options_inner, options_outer) = options;
666
667    // initialize variables
668    let mut err_out = 1.0;
669    let mut k_out = 0;
670
671    if PhaseEquilibrium::is_trivial_solution(&state1, &state2) {
672        log_iter!(options_outer.verbosity, "Trivial solution encountered!");
673        return Err(EosError::TrivialSolution);
674    }
675
676    log_iter!(
677        options_outer.verbosity,
678        "res outer loop | res inner loop | {:^16} | molefracs second phase",
679        TP::IDENTIFIER
680    );
681    log_iter!(options_outer.verbosity, "{:-<85}", "");
682    log_iter!(
683        options_outer.verbosity,
684        "{:14} | {:14} | {:12.8} | {:.8}",
685        "",
686        "",
687        var_tp,
688        state2.molefracs
689    );
690
691    // Outer loop for finding x2
692    for ko in 0..options_outer.max_iter.unwrap_or(MAX_ITER_OUTER) {
693        // Iso-Fugacity equation
694        err_out = if err_out > NEWTON_TOL {
695            // Inner loop for finding T or p
696            for _ in 0..options_inner.max_iter.unwrap_or(MAX_ITER_INNER) {
697                if TP::adjust_t_p(
698                    tp_spec,
699                    &mut var_tp,
700                    &mut state1,
701                    &mut state2,
702                    options_inner.verbosity,
703                )? < options_inner.tol.unwrap_or(TOL_INNER)
704                {
705                    break;
706                }
707            }
708            adjust_x2(&state1, &mut state2, options_outer.verbosity)
709        } else {
710            TP::newton_step(
711                tp_spec,
712                &mut var_tp,
713                &mut state1,
714                &mut state2,
715                options_outer.verbosity,
716            )
717        }?;
718
719        if PhaseEquilibrium::is_trivial_solution(&state1, &state2) {
720            log_iter!(options_outer.verbosity, "Trivial solution encountered!");
721            return Err(EosError::TrivialSolution);
722        }
723
724        if err_out < options_outer.tol.unwrap_or(TOL_OUTER) {
725            k_out = ko + 1;
726            break;
727        }
728    }
729
730    if err_out < options_outer.tol.unwrap_or(TOL_OUTER) {
731        log_result!(
732            options_outer.verbosity,
733            "Bubble/dew point: calculation converged in {} step(s)\n",
734            k_out
735        );
736        if bubble {
737            Ok(PhaseEquilibrium([state2, state1]))
738        } else {
739            Ok(PhaseEquilibrium([state1, state2]))
740        }
741    } else {
742        // not converged, return EosError
743        Err(EosError::NotConverged(String::from("bubble-dew-iteration")))
744    }
745}
746
747fn adjust_states<E: Residual>(
748    temperature: Temperature,
749    pressure: Pressure,
750    state1: &mut State<E>,
751    state2: &mut State<E>,
752    moles_state2: Option<&Moles<Array1<f64>>>,
753) -> EosResult<()> {
754    *state1 = State::new_npt(
755        &state1.eos,
756        temperature,
757        pressure,
758        &state1.moles,
759        InitialDensity(state1.density),
760    )?;
761    *state2 = State::new_npt(
762        &state2.eos,
763        temperature,
764        pressure,
765        moles_state2.unwrap_or(&state2.moles),
766        InitialDensity(state2.density),
767    )?;
768    Ok(())
769}
770
771fn adjust_x2<E: Residual>(
772    state1: &State<E>,
773    state2: &mut State<E>,
774    verbosity: Verbosity,
775) -> EosResult<f64> {
776    let x1 = &state1.molefracs;
777    let ln_phi_1 = state1.ln_phi();
778    let ln_phi_2 = state2.ln_phi();
779    let k = (ln_phi_1 - ln_phi_2).mapv(f64::exp);
780    let err_out = (&k * x1 / &state2.molefracs - 1.0).mapv(f64::abs).sum();
781    let x2 = (x1 * &k) / (&k * x1).sum();
782    log_iter!(verbosity, "{:<14.8e} | {:14} | {:16} |", err_out, "", "");
783    *state2 = State::new_npt(
784        &state2.eos,
785        state2.temperature,
786        state2.pressure(Contributions::Total),
787        &Moles::from_reduced(x2),
788        InitialDensity(state2.density),
789    )?;
790    Ok(err_out)
791}