feos_core/phase_equilibria/
mod.rs1use crate::equation_of_state::Residual;
2use crate::errors::{EosError, EosResult};
3use crate::state::{DensityInitialization, State};
4use crate::{Contributions, ReferenceSystem};
5use ndarray::Array1;
6use quantity::{Dimensionless, Energy, Moles, Pressure, Temperature, RGAS};
7use std::fmt;
8use std::fmt::Write;
9use std::sync::Arc;
10
11mod bubble_dew;
12mod phase_diagram_binary;
13mod phase_diagram_pure;
14mod phase_envelope;
15mod stability_analysis;
16mod tp_flash;
17mod vle_pure;
18pub use bubble_dew::TemperatureOrPressure;
19pub use phase_diagram_binary::PhaseDiagramHetero;
20pub use phase_diagram_pure::PhaseDiagram;
21
22#[derive(Debug)]
35pub struct PhaseEquilibrium<E, const N: usize>([State<E>; N]);
36
37impl<E, const N: usize> Clone for PhaseEquilibrium<E, N> {
38    fn clone(&self) -> Self {
39        Self(self.0.clone())
40    }
41}
42
43impl<E: Residual, const N: usize> fmt::Display for PhaseEquilibrium<E, N> {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        for (i, s) in self.0.iter().enumerate() {
46            writeln!(f, "phase {}: {}", i, s)?;
47        }
48        Ok(())
49    }
50}
51
52impl<E: Residual, const N: usize> PhaseEquilibrium<E, N> {
53    pub fn _repr_markdown_(&self) -> String {
54        if self.0[0].eos.components() == 1 {
55            let mut res = "||temperature|density|\n|-|-|-|\n".to_string();
56            for (i, s) in self.0.iter().enumerate() {
57                writeln!(
58                    res,
59                    "|phase {}|{:.5}|{:.5}|",
60                    i + 1,
61                    s.temperature,
62                    s.density
63                )
64                .unwrap();
65            }
66            res
67        } else {
68            let mut res = "||temperature|density|molefracs|\n|-|-|-|-|\n".to_string();
69            for (i, s) in self.0.iter().enumerate() {
70                writeln!(
71                    res,
72                    "|phase {}|{:.5}|{:.5}|{:.5}|",
73                    i + 1,
74                    s.temperature,
75                    s.density,
76                    s.molefracs
77                )
78                .unwrap();
79            }
80            res
81        }
82    }
83}
84
85impl<E> PhaseEquilibrium<E, 2> {
86    pub fn vapor(&self) -> &State<E> {
87        &self.0[0]
88    }
89
90    pub fn liquid(&self) -> &State<E> {
91        &self.0[1]
92    }
93}
94
95impl<E> PhaseEquilibrium<E, 3> {
96    pub fn vapor(&self) -> &State<E> {
97        &self.0[0]
98    }
99
100    pub fn liquid1(&self) -> &State<E> {
101        &self.0[1]
102    }
103
104    pub fn liquid2(&self) -> &State<E> {
105        &self.0[2]
106    }
107}
108
109impl<E: Residual> PhaseEquilibrium<E, 2> {
110    pub(super) fn from_states(state1: State<E>, state2: State<E>) -> Self {
111        let (vapor, liquid) = if state1.density < state2.density {
112            (state1, state2)
113        } else {
114            (state2, state1)
115        };
116        Self([vapor, liquid])
117    }
118
119    pub fn new_npt(
126        eos: &Arc<E>,
127        temperature: Temperature,
128        pressure: Pressure,
129        vapor_moles: &Moles<Array1<f64>>,
130        liquid_moles: &Moles<Array1<f64>>,
131    ) -> EosResult<Self> {
132        let liquid = State::new_npt(
133            eos,
134            temperature,
135            pressure,
136            liquid_moles,
137            DensityInitialization::Liquid,
138        )?;
139        let vapor = State::new_npt(
140            eos,
141            temperature,
142            pressure,
143            vapor_moles,
144            DensityInitialization::Vapor,
145        )?;
146        Ok(Self([vapor, liquid]))
147    }
148
149    pub(super) fn vapor_phase_fraction(&self) -> f64 {
150        (self.vapor().total_moles / (self.vapor().total_moles + self.liquid().total_moles))
151            .into_value()
152    }
153}
154
155impl<E: Residual, const N: usize> PhaseEquilibrium<E, N> {
156    pub(super) fn update_pressure(
157        mut self,
158        temperature: Temperature,
159        pressure: Pressure,
160    ) -> EosResult<Self> {
161        for s in self.0.iter_mut() {
162            *s = State::new_npt(
163                &s.eos,
164                temperature,
165                pressure,
166                &s.moles,
167                DensityInitialization::InitialDensity(s.density),
168            )?;
169        }
170        Ok(self)
171    }
172
173    pub(super) fn update_moles(
174        &mut self,
175        pressure: Pressure,
176        moles: [&Moles<Array1<f64>>; N],
177    ) -> EosResult<()> {
178        for (i, s) in self.0.iter_mut().enumerate() {
179            *s = State::new_npt(
180                &s.eos,
181                s.temperature,
182                pressure,
183                moles[i],
184                DensityInitialization::InitialDensity(s.density),
185            )?;
186        }
187        Ok(())
188    }
189
190    pub(super) fn total_gibbs_energy(&self) -> Energy {
192        self.0.iter().fold(Energy::from_reduced(0.0), |acc, s| {
193            let ln_rho = s.partial_density.to_reduced().mapv(f64::ln);
194            acc + s.residual_helmholtz_energy()
195                + s.pressure(Contributions::Total) * s.volume
196                + RGAS * s.temperature * (s.moles.clone() * Dimensionless::new(ln_rho - 1.0)).sum()
197        })
198    }
199}
200
201const TRIVIAL_REL_DEVIATION: f64 = 1e-5;
202
203impl<E: Residual> PhaseEquilibrium<E, 2> {
205    pub(super) fn check_trivial_solution(self) -> EosResult<Self> {
206        if Self::is_trivial_solution(self.vapor(), self.liquid()) {
207            Err(EosError::TrivialSolution)
208        } else {
209            Ok(self)
210        }
211    }
212
213    pub fn is_trivial_solution(state1: &State<E>, state2: &State<E>) -> bool {
215        let rho1 = state1.partial_density.to_reduced();
216        let rho2 = state2.partial_density.to_reduced();
217
218        rho1.iter()
219            .zip(rho2.iter())
220            .fold(0.0, |acc, (&rho1, &rho2)| {
221                (rho2 / rho1 - 1.0).abs().max(acc)
222            })
223            < TRIVIAL_REL_DEVIATION
224    }
225}