Skip to main content

feos_core/ad/dataset/
binary.rs

1use std::{io, path::Path};
2
3use nalgebra::U2;
4use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
5use serde::{Deserialize, Serialize};
6
7use crate::Residual;
8use crate::ad::Gradient;
9use crate::ad::properties::{BubblePointPressure, DewPointPressure, PropertyAD};
10
11use super::{Dataset, DatasetAD, DatasetRecord, DatasetStorage, ParametersAD};
12
13/// The pressure column doubles as the initial guess passed to the VLE solver.
14#[derive(Deserialize, Serialize)]
15pub struct BubblePointRecord {
16    pub temperature_k: f64,
17    pub liquid_molefrac_1: f64,
18    pub bubble_pressure_pa: f64,
19}
20
21impl DatasetRecord for BubblePointRecord {
22    const N_INPUTS: usize = 3;
23
24    fn input(&self, column: usize) -> f64 {
25        match column {
26            0 => self.temperature_k,
27            1 => self.liquid_molefrac_1,
28            2 => self.bubble_pressure_pa,
29            _ => unreachable!("invalid bubble point input column"),
30        }
31    }
32
33    fn target(&self) -> f64 {
34        self.bubble_pressure_pa
35    }
36}
37
38/// The pressure column doubles as the initial guess passed to the VLE solver.
39#[derive(Deserialize, Serialize)]
40pub struct DewPointRecord {
41    pub temperature_k: f64,
42    pub vapor_molefrac_1: f64,
43    pub dew_pressure_pa: f64,
44}
45
46impl DatasetRecord for DewPointRecord {
47    const N_INPUTS: usize = 3;
48
49    fn input(&self, column: usize) -> f64 {
50        match column {
51            0 => self.temperature_k,
52            1 => self.vapor_molefrac_1,
53            2 => self.dew_pressure_pa,
54            _ => unreachable!("invalid dew point input column"),
55        }
56    }
57
58    fn target(&self) -> f64 {
59        self.dew_pressure_pa
60    }
61}
62
63/// Expand a list of binary-mixture property entries into:
64/// - the [`BinaryProperty`] enum, metadata and dispatch methods,
65/// - constructors,
66/// - [`BinaryDataset::from_csv`] and [`BinaryDataset::from_reader`] match arms.
67macro_rules! binary_properties {
68    ($(
69        $variant:ident {
70            record:       $record:ty,
71            property:     $prop:ty,
72            default_name: $default:expr,
73            input_names:  $inputs:expr,
74            target_name:  $target:expr,
75            constructor:  $ctor:ident,
76        }
77    ),* $(,)?) => {
78        /// Binary-mixture properties.
79        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
80        pub enum BinaryProperty {
81            $($variant,)*
82        }
83
84        impl BinaryProperty {
85            pub fn default_name(self) -> &'static str {
86                match self { $(Self::$variant => $default,)* }
87            }
88
89            pub fn input_names(self) -> &'static [&'static str] {
90                match self { $(Self::$variant => $inputs,)* }
91            }
92
93            pub fn target_name(self) -> &'static str {
94                match self { $(Self::$variant => $target,)* }
95            }
96
97            fn evaluate_ad<T: ParametersAD<U2>, const P: usize>(
98                self,
99                names: [String; P],
100                parameters: &[f64],
101                inputs: ArrayView2<f64>,
102            ) -> (Array1<f64>, Array2<f64>, Array1<bool>)
103            where
104                T::Lifted<Gradient<P>>: Sync,
105            {
106                match self {
107                    $(Self::$variant => <$prop>::evaluate_parallel_derivatives::<T, P>(names, parameters, inputs),)*
108                }
109            }
110
111            fn evaluate<E>(self, eos: &E, inputs: ArrayView2<f64>) -> (Array1<f64>, Array1<bool>)
112            where
113                E: Residual + Sync,
114            {
115                match self {
116                    $(Self::$variant => <$prop>::evaluate_parallel(eos, inputs),)*
117                }
118            }
119        }
120
121        impl BinaryDataset {
122            $(
123                pub fn $ctor(records: Vec<$record>) -> Self {
124                    Self {
125                        property: BinaryProperty::$variant,
126                        storage: DatasetStorage::from_records(records),
127                    }
128                }
129            )*
130
131            pub fn from_csv(property: BinaryProperty, path: &Path) -> Result<Self, csv::Error> {
132                let storage = match property {
133                    $(BinaryProperty::$variant => DatasetStorage::from_csv::<$record>(path)?,)*
134                };
135                Ok(Self { property, storage })
136            }
137
138            pub fn from_reader(
139                property: BinaryProperty,
140                reader: impl io::Read,
141            ) -> Result<Self, csv::Error> {
142                let storage = match property {
143                    $(BinaryProperty::$variant => DatasetStorage::from_reader::<$record>(reader)?,)*
144                };
145                Ok(Self { property, storage })
146            }
147        }
148    };
149}
150
151binary_properties! {
152    BubblePointPressure {
153        record:       BubblePointRecord,
154        property:     BubblePointPressure,
155        default_name: "bubble point pressure",
156        input_names:  &["temperature_k", "liquid_molefrac_1"],
157        target_name:  "bubble_pressure_pa",
158        constructor:  bubble_point_pressure,
159    },
160    DewPointPressure {
161        record:       DewPointRecord,
162        property:     DewPointPressure,
163        default_name: "dew point pressure",
164        input_names:  &["temperature_k", "vapor_molefrac_1"],
165        target_name:  "dew_pressure_pa",
166        constructor:  dew_point_pressure,
167    },
168}
169
170/// Binary-mixture dataset: shared data storage plus a property tag.
171#[derive(Clone)]
172pub struct BinaryDataset {
173    property: BinaryProperty,
174    storage: DatasetStorage,
175}
176
177impl BinaryDataset {
178    pub fn with_name(mut self, name: impl Into<String>) -> Self {
179        self.storage.set_name(name.into());
180        self
181    }
182
183    pub fn property(&self) -> BinaryProperty {
184        self.property
185    }
186
187    pub fn inputs(&self) -> ArrayView2<'_, f64> {
188        self.storage.inputs()
189    }
190
191    pub fn target(&self) -> ArrayView1<'_, f64> {
192        self.storage.target()
193    }
194
195    pub fn name(&self) -> &str {
196        self.storage.name().unwrap_or(self.property.default_name())
197    }
198
199    pub fn input_names(&self) -> &'static [&'static str] {
200        self.property.input_names()
201    }
202
203    pub fn target_name(&self) -> &'static str {
204        self.property.target_name()
205    }
206}
207
208impl Dataset for BinaryDataset {
209    fn inputs(&self) -> ArrayView2<'_, f64> {
210        self.inputs()
211    }
212
213    fn target(&self) -> ArrayView1<'_, f64> {
214        self.target()
215    }
216
217    fn name(&self) -> &str {
218        self.name()
219    }
220
221    fn input_names(&self) -> &'static [&'static str] {
222        self.input_names()
223    }
224
225    fn target_name(&self) -> &'static str {
226        self.target_name()
227    }
228
229    fn evaluate<E: Residual + Sync>(&self, eos: &E) -> (Array1<f64>, Array1<bool>) {
230        self.property.evaluate(eos, self.inputs())
231    }
232}
233
234impl DatasetAD<2> for BinaryDataset {
235    fn evaluate_ad_const<T: ParametersAD<U2>, const P: usize>(
236        &self,
237        names: [String; P],
238        parameters: &[f64],
239        inputs: ArrayView2<f64>,
240    ) -> (Array1<f64>, Array2<f64>, Array1<bool>)
241    where
242        T::Lifted<Gradient<P>>: Sync,
243    {
244        self.property.evaluate_ad::<T, P>(names, parameters, inputs)
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use std::io::Cursor;
252
253    fn csv(s: &str) -> Cursor<&[u8]> {
254        Cursor::new(s.as_bytes())
255    }
256
257    #[test]
258    fn bubble_point_from_reader() {
259        let data = "\
260temperature_k,liquid_molefrac_1,bubble_pressure_pa
261300.0,0.3,500000.0
262320.0,0.5,800000.0
263";
264        let ds =
265            BinaryDataset::from_reader(BinaryProperty::BubblePointPressure, csv(data)).unwrap();
266
267        assert_eq!(ds.inputs().ncols(), 3);
268        assert_eq!(ds.inputs().nrows(), 2);
269        assert_eq!(ds.inputs()[[0, 0]], 300.0);
270        assert_eq!(ds.inputs()[[0, 1]], 0.3);
271        assert_eq!(ds.inputs()[[0, 2]], 500000.0);
272        assert_eq!(ds.target()[0], 500000.0);
273        assert_eq!(ds.target()[1], 800000.0);
274        assert_eq!(ds.name(), "bubble point pressure");
275    }
276
277    #[test]
278    fn dew_point_from_reader() {
279        let data = "\
280temperature_k,vapor_molefrac_1,dew_pressure_pa
281310.0,0.7,400000.0
282330.0,0.9,700000.0
283";
284        let ds = BinaryDataset::from_reader(BinaryProperty::DewPointPressure, csv(data)).unwrap();
285
286        assert_eq!(ds.inputs().ncols(), 3);
287        assert_eq!(ds.inputs()[[0, 0]], 310.0);
288        assert_eq!(ds.inputs()[[0, 1]], 0.7);
289        assert_eq!(ds.inputs()[[0, 2]], 400000.0);
290        assert_eq!(ds.target()[1], 700000.0);
291        assert_eq!(ds.name(), "dew point pressure");
292    }
293
294    #[test]
295    fn dew_point_pressure_doubles_as_initial_guess() {
296        let records = vec![DewPointRecord {
297            temperature_k: 310.0,
298            vapor_molefrac_1: 0.7,
299            dew_pressure_pa: 400000.0,
300        }];
301        let ds = BinaryDataset::dew_point_pressure(records);
302        assert_eq!(ds.inputs()[[0, 2]], ds.target()[0]);
303    }
304}