feos/estimator/
binary_vle.rs

1use super::{DataSet, EstimatorError, Phase};
2use feos_core::{
3    Contributions, DensityInitialization, PhaseDiagram, PhaseEquilibrium, ReferenceSystem,
4    Residual, State, TemperatureOrPressure,
5};
6use itertools::izip;
7use ndarray::{arr1, s, Array1, ArrayView1, Axis};
8use quantity::{MolarEnergy, Moles, Pressure, Quantity, Temperature, _Dimensionless, PASCAL, RGAS};
9use std::fmt;
10use std::iter::FromIterator;
11use std::ops::Sub;
12use std::sync::Arc;
13
14/// Store experimental binary VLE data for the calculation of chemical potential residuals.
15#[derive(Clone)]
16pub struct BinaryVleChemicalPotential {
17    temperature: Temperature<Array1<f64>>,
18    pressure: Pressure<Array1<f64>>,
19    liquid_molefracs: Array1<f64>,
20    vapor_molefracs: Array1<f64>,
21    target: Array1<f64>,
22}
23
24impl BinaryVleChemicalPotential {
25    pub fn new(
26        temperature: Temperature<Array1<f64>>,
27        pressure: Pressure<Array1<f64>>,
28        liquid_molefracs: Array1<f64>,
29        vapor_molefracs: Array1<f64>,
30    ) -> Self {
31        let target = Array1::ones(temperature.len() * 2);
32        Self {
33            temperature,
34            pressure,
35            liquid_molefracs,
36            vapor_molefracs,
37            target,
38        }
39    }
40}
41
42impl<E: Residual> DataSet<E> for BinaryVleChemicalPotential {
43    fn target(&self) -> &Array1<f64> {
44        &self.target
45    }
46
47    fn target_str(&self) -> &str {
48        "chemical potential"
49    }
50
51    fn input_str(&self) -> Vec<&str> {
52        vec![
53            "temperature",
54            "pressure",
55            "liquid molefracs",
56            "vapor molefracs",
57        ]
58    }
59
60    fn predict(&self, eos: &Arc<E>) -> Result<Array1<f64>, EstimatorError> {
61        let mut prediction = Vec::new();
62        for (&xi, &yi, t, p) in izip!(
63            &self.liquid_molefracs,
64            &self.vapor_molefracs,
65            &self.temperature,
66            &self.pressure
67        ) {
68            let liquid_moles = Moles::from_reduced(arr1(&[xi, 1.0 - xi]));
69            let liquid = State::new_npt(eos, t, p, &liquid_moles, DensityInitialization::Liquid)?;
70            let mu_res_liquid = liquid.residual_chemical_potential();
71            let vapor_moles = Moles::from_reduced(arr1(&[yi, 1.0 - yi]));
72            let vapor = State::new_npt(eos, t, p, &vapor_moles, DensityInitialization::Vapor)?;
73            let mu_res_vapor = vapor.residual_chemical_potential();
74
75            let kt = RGAS * t;
76            let rho_frac = (&liquid.partial_density / &vapor.partial_density).into_value();
77            prediction.push(mu_res_liquid.get(0) - mu_res_vapor.get(0) + kt * rho_frac[0].ln());
78            prediction.push(mu_res_liquid.get(1) - mu_res_vapor.get(1) + kt * rho_frac[1].ln());
79        }
80        let prediction = (MolarEnergy::from_vec(prediction) / MolarEnergy::from_reduced(500.0))
81            .into_value()
82            + 1.0;
83        Ok(prediction)
84    }
85
86    // fn get_input(&self) -> HashMap<String, SIArray1> {
87    //     let mut m = HashMap::with_capacity(4);
88    //     m.insert("temperature".to_owned(), self.temperature.clone());
89    //     m.insert("pressure".to_owned(), self.pressure.clone());
90    //     m.insert(
91    //         "liquid_molefracs".to_owned(),
92    //         &self.liquid_molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(),
93    //     );
94    //     m.insert(
95    //         "vapor_molefracs".to_owned(),
96    //         &self.vapor_molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(),
97    //     );
98    //     m
99    // }
100}
101
102/// Store experimental binary VLE data for the calculation of pressure residuals.
103#[derive(Clone)]
104pub struct BinaryVlePressure {
105    target: Array1<f64>,
106    temperature: Temperature<Array1<f64>>,
107    pressure: Pressure<Array1<f64>>,
108    unit: Pressure,
109    molefracs: Array1<f64>,
110    phase: Phase,
111}
112
113impl BinaryVlePressure {
114    pub fn new(
115        temperature: Temperature<Array1<f64>>,
116        pressure: Pressure<Array1<f64>>,
117        molefracs: Array1<f64>,
118        phase: Phase,
119    ) -> Self {
120        let unit = PASCAL;
121        Self {
122            target: (&pressure / unit).into_value(),
123            temperature,
124            pressure,
125            unit,
126            molefracs,
127            phase,
128        }
129    }
130}
131
132impl<E: Residual> DataSet<E> for BinaryVlePressure {
133    fn target(&self) -> &Array1<f64> {
134        &self.target
135    }
136
137    fn target_str(&self) -> &str {
138        "pressure"
139    }
140
141    fn input_str(&self) -> Vec<&str> {
142        let mut vec = vec!["temperature", "pressure"];
143        vec.push(match self.phase {
144            Phase::Vapor => "vapor molefracs",
145            Phase::Liquid => "liquid molefracs",
146        });
147        vec
148    }
149
150    fn predict(&self, eos: &Arc<E>) -> Result<Array1<f64>, EstimatorError> {
151        let options = Default::default();
152        self.molefracs
153            .iter()
154            .enumerate()
155            .map(|(i, &xi)| {
156                let vle = (match self.phase {
157                    Phase::Vapor => PhaseEquilibrium::dew_point(
158                        eos,
159                        self.temperature.get(i),
160                        &arr1(&[xi, 1.0 - xi]),
161                        Some(self.pressure.get(i)),
162                        None,
163                        options,
164                    ),
165                    Phase::Liquid => PhaseEquilibrium::bubble_point(
166                        eos,
167                        self.temperature.get(i),
168                        &arr1(&[xi, 1.0 - xi]),
169                        Some(self.pressure.get(i)),
170                        None,
171                        options,
172                    ),
173                })?;
174
175                Ok(vle
176                    .vapor()
177                    .pressure(Contributions::Total)
178                    .convert_to(self.unit))
179            })
180            .collect()
181    }
182
183    // fn get_input(&self) -> HashMap<String, SIArray1> {
184    //     let mut m = HashMap::with_capacity(4);
185    //     m.insert("temperature".to_owned(), self.temperature.clone());
186    //     m.insert("pressure".to_owned(), self.pressure.clone());
187    //     m.insert(
188    //         (match self.phase {
189    //             Phase::Vapor => "vapor_molefracs",
190    //             Phase::Liquid => "liquid_molefracs",
191    //         })
192    //         .to_owned(),
193    //         &self.molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(),
194    //     );
195    //     m
196    // }
197}
198
199/// Store experimental binary phase diagrams for the calculation of distance residuals.
200#[derive(Clone)]
201pub struct BinaryPhaseDiagram<TP: TemperatureOrPressure, U> {
202    specification: TP,
203    temperature_or_pressure: Quantity<Array1<f64>, U>,
204    liquid_molefracs: Option<Array1<f64>>,
205    vapor_molefracs: Option<Array1<f64>>,
206    npoints: Option<usize>,
207    target: Array1<f64>,
208}
209
210impl<TP: TemperatureOrPressure, U> BinaryPhaseDiagram<TP, U> {
211    pub fn new(
212        specification: TP,
213        temperature_or_pressure: Quantity<Array1<f64>, U>,
214        liquid_molefracs: Option<Array1<f64>>,
215        vapor_molefracs: Option<Array1<f64>>,
216        npoints: Option<usize>,
217    ) -> Self {
218        let count = liquid_molefracs.as_ref().map_or(0, |x| 2 * x.len())
219            + vapor_molefracs.as_ref().map_or(0, |x| 2 * x.len());
220        let target = Array1::ones(count);
221        Self {
222            specification,
223            temperature_or_pressure,
224            liquid_molefracs,
225            vapor_molefracs,
226            npoints,
227            target,
228        }
229    }
230}
231
232impl<
233        TP: TemperatureOrPressure + Sync + Send + fmt::Display,
234        U: Copy + Sync + Send,
235        E: Residual,
236    > DataSet<E> for BinaryPhaseDiagram<TP, U>
237where
238    Quantity<Array1<f64>, U>: FromIterator<TP::Other>,
239    U: Sub<U, Output = _Dimensionless>,
240{
241    fn target(&self) -> &Array1<f64> {
242        &self.target
243    }
244
245    fn target_str(&self) -> &str {
246        "distance"
247    }
248
249    fn input_str(&self) -> Vec<&str> {
250        let mut vec = vec![TP::IDENTIFIER, TP::Other::IDENTIFIER];
251        if self.liquid_molefracs.is_some() {
252            vec.push("liquid molefracs")
253        }
254        if self.vapor_molefracs.is_some() {
255            vec.push("vapor molefracs")
256        }
257        vec
258    }
259
260    fn predict(&self, eos: &Arc<E>) -> Result<Array1<f64>, EstimatorError> {
261        let mut res = Vec::new();
262
263        let dia = PhaseDiagram::binary_vle(
264            eos,
265            self.specification,
266            self.npoints,
267            None,
268            Default::default(),
269        )?;
270        let x_liq = dia.liquid().molefracs();
271        let x_vec_liq = x_liq.index_axis(Axis(1), 0);
272        let x_vap = dia.vapor().molefracs();
273        let x_vec_vap = x_vap.index_axis(Axis(1), 0);
274        let tp_vec = dia.vapor().iter().map(|s| TP::from_state(s)).collect();
275        for (x_exp, x_vec) in [
276            (&self.liquid_molefracs, x_vec_liq),
277            (&self.vapor_molefracs, x_vec_vap),
278        ] {
279            if let Some(x_exp) = x_exp {
280                res.extend(predict_distance(
281                    x_vec,
282                    &tp_vec,
283                    x_exp,
284                    &self.temperature_or_pressure,
285                ));
286            }
287        }
288        Ok(Array1::from_vec(res))
289    }
290
291    // fn get_input(&self) -> HashMap<String, SIArray1> {
292    //     let mut m = HashMap::with_capacity(4);
293    //     if self
294    //         .specification
295    //         .has_unit(&SIUnit::reference_temperature())
296    //     {
297    //         m.insert(
298    //             "temperature".to_owned(),
299    //             SIArray1::from_vec(vec![self.specification]),
300    //         );
301    //         m.insert("pressure".to_owned(), self.temperature_or_pressure.clone());
302    //     } else {
303    //         m.insert(
304    //             "pressure".to_owned(),
305    //             SIArray1::from_vec(vec![self.specification]),
306    //         );
307    //         m.insert(
308    //             "temperature".to_owned(),
309    //             self.temperature_or_pressure.clone(),
310    //         );
311    //     };
312    //     if let Some(liquid_molefracs) = &self.liquid_molefracs {
313    //         m.insert(
314    //             "liquid_molefracs".to_owned(),
315    //             liquid_molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(),
316    //         );
317    //     }
318    //     if let Some(vapor_molefracs) = &self.vapor_molefracs {
319    //         m.insert(
320    //             "vapor_molefracs".to_owned(),
321    //             vapor_molefracs * SIUnit::reference_moles() / SIUnit::reference_moles(),
322    //         );
323    //     }
324    //     m
325    // }
326}
327
328fn predict_distance<U: Copy + Sub<U, Output = _Dimensionless>>(
329    x_vec: ArrayView1<f64>,
330    tp_vec: &Quantity<Array1<f64>, U>,
331    x_exp: &Array1<f64>,
332    tp_exp: &Quantity<Array1<f64>, U>,
333) -> Vec<f64> {
334    let mut res = Vec::new();
335    for (tp, &x) in tp_exp.into_iter().zip(x_exp.into_iter()) {
336        let y = 1.0;
337        let y_vec = (tp_vec / tp).into_value();
338        let dx = &x_vec.slice(s![1..]) - &x_vec.slice(s![..-1]);
339        let dy = &y_vec.slice(s![1..]) - &y_vec.slice(s![..-1]);
340        let x_vec = x_vec.slice(s![..-1]);
341        let y_vec = y_vec.slice(s![..-1]);
342
343        let t = ((x - &x_vec) * &dx + (y - &y_vec) * &dy) / (&dx * &dx + &dy * &dy);
344        let x0 = &t * dx + x_vec;
345        let y0 = &t * dy + y_vec;
346
347        let k = x0
348            .iter()
349            .zip(y0.iter())
350            .enumerate()
351            .filter(|(i, _)| 0.0 < t[*i] && t[*i] < 1.0)
352            .map(|(i, (xx, yy))| (i, (xx - x) * (xx - x) + (yy - y) * (yy - y)))
353            .reduce(|(k1, d1), (k2, d2)| if d1 < d2 { (k1, d1) } else { (k2, d2) });
354
355        let (x0, y0) = match k {
356            None => {
357                let point = t
358                    .iter()
359                    .zip(t.iter().skip(1))
360                    .enumerate()
361                    .find(|&(_, (t1, t2))| *t1 > 1.0 && *t2 < 0.0);
362                if let Some((point, _)) = point {
363                    (x_vec[point + 1], y_vec[point + 1])
364                } else {
365                    let n = t.len();
366                    if t[0].abs() < t[n - 1].abs() {
367                        (x_vec[0], y_vec[0])
368                    } else {
369                        (x_vec[n - 1], y_vec[n - 1])
370                    }
371                }
372            }
373            Some((k, _)) => (x0[k], y0[k]),
374        };
375        res.push(x0 - x + 1.0);
376        res.push(y0);
377    }
378    res
379}