Skip to main content

feos_core/ad/dataset/
mod.rs

1mod binary;
2mod pure;
3
4use std::{io, path::Path, sync::Arc};
5
6use nalgebra::Const;
7use ndarray::{Array1, Array2, ArrayView1, ArrayView2};
8use serde::de::DeserializeOwned;
9
10use crate::Residual;
11use crate::ad::Gradient;
12
13use super::ParametersAD;
14
15pub use binary::*;
16pub use pure::*;
17
18/// Shared numerical data for all datasets.
19struct DatasetData {
20    inputs: Array2<f64>,
21    target: Array1<f64>,
22}
23
24/// Shared representation for all datasets.
25#[derive(Clone)]
26struct DatasetStorage {
27    data: Arc<DatasetData>,
28    name: Option<String>,
29}
30
31impl DatasetStorage {
32    fn from_records<R: DatasetRecord>(records: Vec<R>) -> Self {
33        let n = records.len();
34        let inputs = Array2::from_shape_fn((n, R::N_INPUTS), |(i, j)| records[i].input(j));
35        let target = Array1::from_iter(records.iter().map(DatasetRecord::target));
36        Self {
37            data: Arc::new(DatasetData { inputs, target }),
38            name: None,
39        }
40    }
41
42    fn from_csv<R: DatasetRecord>(path: &Path) -> Result<Self, csv::Error> {
43        let records = csv::Reader::from_path(path)?
44            .deserialize()
45            .collect::<Result<Vec<R>, _>>()?;
46        Ok(Self::from_records(records))
47    }
48
49    fn from_reader<R: DatasetRecord>(reader: impl io::Read) -> Result<Self, csv::Error> {
50        let records = csv::Reader::from_reader(reader)
51            .deserialize()
52            .collect::<Result<Vec<R>, _>>()?;
53        Ok(Self::from_records(records))
54    }
55
56    fn inputs(&self) -> ArrayView2<'_, f64> {
57        self.data.inputs.view()
58    }
59
60    fn target(&self) -> ArrayView1<'_, f64> {
61        self.data.target.view()
62    }
63
64    fn name(&self) -> Option<&str> {
65        self.name.as_deref()
66    }
67
68    fn set_name(&mut self, name: String) {
69        self.name = Some(name);
70    }
71}
72
73/// A record that can be collected into a dataset.
74pub trait DatasetRecord: DeserializeOwned {
75    /// Number of columns for inputs.
76    const N_INPUTS: usize;
77
78    /// Value of EoS input column.
79    fn input(&self, column: usize) -> f64;
80
81    /// Target value.
82    fn target(&self) -> f64;
83}
84
85/// Dataset that can be evaluated by an equation of state.
86pub trait Dataset {
87    /// Inputs for EoS evaluation, shape `[n_points, k]`.
88    fn inputs(&self) -> ArrayView2<'_, f64>;
89
90    /// Target values, shape `[n_points]`.
91    fn target(&self) -> ArrayView1<'_, f64>;
92
93    /// Property name.
94    ///
95    /// Used for logging and diagnostics.
96    fn name(&self) -> &str;
97
98    /// Names of independent input columns.
99    fn input_names(&self) -> &'static [&'static str];
100
101    /// Name of the target property.
102    fn target_name(&self) -> &'static str;
103
104    /// Evaluate this dataset's property with an equation of state.
105    ///
106    /// Returns `(predicted, converged)`:
107    /// - `predicted`: shape `[n_points]`, in SI units; `NaN` where the
108    ///   underlying solver did not converge.
109    /// - `converged`: shape `[n_points]`.
110    fn evaluate<E: Residual + Sync>(&self, eos: &E) -> (Array1<f64>, Array1<bool>);
111}
112
113/// Build [`GRADIENT_SLOTS`] and [`DatasetAD`] trait from a single list of 'slots'.
114///
115/// For each slot in [`GRADIENT_SLOTS`] a compile-time constant `P` variant is generated.
116macro_rules! define_dataset_ad {
117    ($($p:literal),+ $(,)?) => {
118        /// Compile-time gradient slot supported by [`DatasetAD::evaluate_ad`].
119        pub const GRADIENT_SLOTS: &[usize] = &[$($p),+];
120
121        /// Dataset that supports parameter-gradient evaluation
122        /// for equations of state implementing [`ParametersAD<N>`].
123        pub trait DatasetAD<const N: usize>: Dataset {
124            /// Evaluate the property and its `P` parameter gradients.
125            fn evaluate_ad_const<T: ParametersAD<Const<N>>, const P: usize>(
126                &self,
127                names: [String; P],
128                parameters: &[f64],
129                inputs: ArrayView2<f64>,
130            ) -> (Array1<f64>, Array2<f64>, Array1<bool>)
131            where
132                T::Lifted<Gradient<P>>: Sync;
133
134            /// Evaluate the property and its parameter gradients at the given parameters.
135            ///
136            /// - `param_names`: names of the `P` parameters being differentiated.
137            /// - `params`: the full parameter vector. Only entries listed in `param_names` are seeded.
138            ///
139            /// This function dispatches the const-P methods at run-time.
140            fn evaluate_ad<T: ParametersAD<Const<N>>>(
141                &self,
142                param_names: &[String],
143                parameters: &[f64],
144            ) -> (Array1<f64>, Array2<f64>, Array1<bool>)
145            where
146                $(T::Lifted<Gradient<$p>>: Sync,)*
147            {
148                fn to_const<const P: usize>(names: &[String]) -> [String; P] {
149                    names.to_vec().try_into().expect("parameter count mismatch")
150                }
151
152                match param_names.len() {
153                    $(
154                        $p => self.evaluate_ad_const::<T, $p>(
155                            to_const(param_names),
156                            parameters,
157                            self.inputs().view(),
158                        ),
159                    )+
160                    p => unreachable!(
161                        "parameter count {p} is not a member of GRADIENT_SLOTS={:?}",
162                        GRADIENT_SLOTS,
163                    ),
164                }
165            }
166        }
167    };
168}
169
170// We define the number of slots here.
171//
172// Note: might be good to investigate whether a smaller list makes sense here.
173// LLVM vectorises across entries in DualSVec. We might see no perf. difference
174// when using e.g. 3 vs 4 slots (even if only 3 are needed by the user).
175// If the number of monomophised variants ever gets problematic, we could reduce it that way.
176define_dataset_ad!(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);