Skip to content

Commit 5008b2b

Browse files
authored
Datasets and properties (#358)
1 parent daa26a7 commit 5008b2b

30 files changed

Lines changed: 2796 additions & 705 deletions

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Added the `Composition` trait to allow more flexibility in the creation of states and phase equilibria. [#330](https://github.com/feos-org/feos/pull/330)
1313
- Added `PhaseEquilibrium::ph_flash` and `PhaseEquilibrium::ps_flash`. [#338](https://github.com/feos-org/feos/pull/338)
1414
- Added getters for `vapor_phase_fraction`, `molar_enthalpy`, `molar_entropy`, `total_moles`, `enthalpy`, and `entropy` to `PhaseEquilibrium`. [#338](https://github.com/feos-org/feos/pull/338)
15+
- Added `PropertyAD` trait in `feos_core::ad` with one struct per property for uniform evaluation with or without parameter derivatives, including parallel variants. [#358](https://github.com/feos-org/feos/pull/358)
16+
- Added `feos_core::ad::dataset` module with `PureDataset` and `BinaryDataset` types, constructible from records, CSV files, or readers, for use in parameter fits. [#358](https://github.com/feos-org/feos/pull/358)
17+
- Exposed `Property`, `PureDataset`, and `BinaryDataset` in `py-feos`. [#358](https://github.com/feos-org/feos/pull/358)
1518

1619
### Changed
1720
- Removed any assumptions about the total number of moles in a `State` or `PhaseEquilibrium`. Evaluating extensive properties now returns a `Result`. [#330](https://github.com/feos-org/feos/pull/330)
1821
- Redesigned the `IdealGas` trait and added `IdealGasAD` in analogy to `ResidualDyn` and `Residual`. [#330](https://github.com/feos-org/feos/pull/330)
22+
- Replaced the `PropertiesAD` blanket-impl trait with per-property `PropertyAD` types. [#358](https://github.com/feos-org/feos/pull/358)
23+
- Replaced `ParametersAD::named_derivatives` with a `build` constructor and `seed_derivatives(&values, names)`. [#358](https://github.com/feos-org/feos/pull/358)
24+
- Removed the per-property `*_derivatives` free functions in Python in favour of static methods on the new `Property` class. [#358](https://github.com/feos-org/feos/pull/358)
1925

2026
### Removed
2127
- Removed the `StateBuilder` struct, because it is mostly obsolete with the addition of the `Composition` trait. [#330](https://github.com/feos-org/feos/pull/330)
2228

2329
### Packaging
2430
- Updated `quantity` dependency to 0.13 and removed the `typenum` dependency. [#328](https://github.com/feos-org/feos/pull/328)
31+
- Added `csv` as a `feos-core` dependency for the new dataset module. [#358](https://github.com/feos-org/feos/pull/358)
2532

2633
## [Unreleased]
2734
### Added

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ gauss-quad = "0.2"
4242
approx = "0.5"
4343
criterion = "0.8"
4444
paste = "1.0"
45+
csv = "1.0"
4546

4647
feos-core = { version = "0.9", path = "crates/feos-core" }
4748
feos-dft = { version = "0.9", path = "crates/feos-dft" }

‎crates/feos-core/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ serde = { workspace = true, features = ["derive"] }
2525
serde_json = { workspace = true, features = ["preserve_order"] }
2626
indexmap = { workspace = true, features = ["serde"] }
2727
rayon = { workspace = true, optional = true }
28+
csv = { workspace = true }
2829
itertools = { workspace = true }
2930

3031
[dev-dependencies]
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
use std::{io, path::Path};
2+
3+
use nalgebra::U2;
4+
use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
5+
use serde::{Deserialize, Serialize};
6+
7+
use crate::Residual;
8+
use crate::ad::Gradient;
9+
use crate::ad::properties::{BubblePointPressure, DewPointPressure, PropertyAD};
10+
11+
use 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)]
15+
pub struct BubblePointRecord {
16+
pub temperature_k: f64,
17+
pub liquid_molefrac_1: f64,
18+
pub bubble_pressure_pa: f64,
19+
}
20+
21+
impl 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)]
40+
pub struct DewPointRecord {
41+
pub temperature_k: f64,
42+
pub vapor_molefrac_1: f64,
43+
pub dew_pressure_pa: f64,
44+
}
45+
46+
impl 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.
67+
macro_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+
151+
binary_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)]
172+
pub struct BinaryDataset {
173+
property: BinaryProperty,
174+
storage: DatasetStorage,
175+
}
176+
177+
impl 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+
208+
impl 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+
234+
impl 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)]
249+
mod 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 = "\
260+
temperature_k,liquid_molefrac_1,bubble_pressure_pa
261+
300.0,0.3,500000.0
262+
320.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 = "\
280+
temperature_k,vapor_molefrac_1,dew_pressure_pa
281+
310.0,0.7,400000.0
282+
330.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+
}

0 commit comments

Comments
 (0)