feos_core/parameter/
model_record.rs

1use super::{AssociationRecord, BinaryAssociationRecord, Identifier, IdentifierOption};
2use crate::FeosResult;
3use crate::errors::FeosError;
4use indexmap::IndexSet;
5use num_traits::Zero;
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::fmt;
10use std::fs::File;
11use std::io::BufReader;
12use std::ops::Deref;
13use std::path::Path;
14
15/// A collection of parameters with an arbitrary identifier.
16#[derive(Serialize, Deserialize, Clone, Debug)]
17pub struct Record<I, M, A> {
18    pub identifier: I,
19    #[serde(skip_serializing_if = "f64::is_zero")]
20    #[serde(default)]
21    pub molarweight: f64,
22    #[serde(flatten)]
23    pub model_record: M,
24    #[serde(skip_serializing_if = "Vec::is_empty")]
25    #[serde(default = "Vec::new")]
26    pub association_sites: Vec<AssociationRecord<A>>,
27}
28
29/// A collection of parameters of a pure substance.
30pub type PureRecord<M, A> = Record<Identifier, M, A>;
31
32/// Parameters describing an individual segment of a molecule.
33pub type SegmentRecord<M, A> = Record<String, M, A>;
34
35impl<I, M, A> Record<I, M, A> {
36    /// Create a new `ModelRecord`.
37    pub fn new(identifier: I, molarweight: f64, model_record: M) -> Self {
38        Self::with_association(identifier, molarweight, model_record, vec![])
39    }
40
41    /// Create a new `ModelRecord` including association information.
42    pub fn with_association(
43        identifier: I,
44        molarweight: f64,
45        model_record: M,
46        association_sites: Vec<AssociationRecord<A>>,
47    ) -> Self {
48        Self {
49            identifier,
50            molarweight,
51            model_record,
52            association_sites,
53        }
54    }
55
56    /// Update the `PureRecord` from segment counts.
57    ///
58    /// The [FromSegments] trait needs to be implemented for the model record.
59    pub fn from_segments<S>(identifier: I, segments: S) -> FeosResult<Self>
60    where
61        M: FromSegments,
62        S: IntoIterator<Item = (SegmentRecord<M, A>, f64)>,
63    {
64        let mut molarweight = 0.0;
65        let mut model_segments = Vec::new();
66        let association_sites = segments
67            .into_iter()
68            .flat_map(|(s, n)| {
69                molarweight += s.molarweight * n;
70                model_segments.push((s.model_record, n));
71                s.association_sites.into_iter().map(move |record| {
72                    AssociationRecord::with_id(
73                        record.id,
74                        record.parameters,
75                        record.na * n,
76                        record.nb * n,
77                        record.nc * n,
78                    )
79                })
80            })
81            .collect();
82        let model_record = M::from_segments(&model_segments)?;
83
84        Ok(Self::with_association(
85            identifier,
86            molarweight,
87            model_record,
88            association_sites,
89        ))
90    }
91}
92
93impl<M, A> PureRecord<M, A> {
94    /// Create pure substance parameters from a json file.
95    pub fn from_json<P, S>(
96        substances: &[S],
97        file: P,
98        identifier_option: IdentifierOption,
99    ) -> FeosResult<Vec<Self>>
100    where
101        P: AsRef<Path>,
102        S: Deref<Target = str>,
103        M: DeserializeOwned,
104        A: DeserializeOwned,
105    {
106        // create list of substances
107        let mut queried: HashSet<&str> = substances.iter().map(|s| s.deref()).collect();
108        // raise error on duplicate detection
109        if queried.len() != substances.len() {
110            return Err(FeosError::IncompatibleParameters(
111                "A substance was defined more than once.".to_string(),
112            ));
113        }
114
115        let f = File::open(file)?;
116        let reader = BufReader::new(f);
117        // use stream in the future
118        let file_records: Vec<Self> = serde_json::from_reader(reader)?;
119        let mut records: HashMap<&str, Self> = HashMap::with_capacity(substances.len());
120
121        // build map, draining list of queried substances in the process
122        for record in file_records {
123            if let Some(id) = record.identifier.as_str(identifier_option) {
124                queried.take(id).map(|id| records.insert(id, record));
125            }
126            // all parameters parsed
127            if queried.is_empty() {
128                break;
129            }
130        }
131
132        // report missing parameters
133        if !queried.is_empty() {
134            return Err(FeosError::ComponentsNotFound(format!("{queried:?}")));
135        };
136
137        // collect into vec in correct order
138        Ok(substances
139            .iter()
140            .map(|s| records.remove(s.deref()).unwrap())
141            .collect())
142    }
143
144    /// Creates parameters from substance information stored in multiple json files.
145    pub fn from_multiple_json<P, S>(
146        input: &[(Vec<S>, P)],
147        identifier_option: IdentifierOption,
148    ) -> FeosResult<Vec<Self>>
149    where
150        P: AsRef<Path>,
151        S: Deref<Target = str>,
152        M: DeserializeOwned,
153        A: DeserializeOwned,
154    {
155        // total number of substances queried
156        let nsubstances = input
157            .iter()
158            .fold(0, |acc, (substances, _)| acc + substances.len());
159
160        // queried substances with removed duplicates
161        let queried: IndexSet<String> = input
162            .iter()
163            .flat_map(|(substances, _)| substances)
164            .map(|substance| substance.to_string())
165            .collect();
166
167        // check if there are duplicates
168        if queried.len() != nsubstances {
169            return Err(FeosError::IncompatibleParameters(
170                "A substance was defined more than once.".to_string(),
171            ));
172        }
173
174        let mut records: Vec<Self> = Vec::with_capacity(nsubstances);
175
176        // collect parameters from files into single map
177        for (substances, file) in input {
178            records.extend(Self::from_json(substances, file, identifier_option)?);
179        }
180
181        Ok(records)
182    }
183}
184
185impl<M, A> SegmentRecord<M, A> {
186    /// Read a list of `SegmentRecord`s from a JSON file.
187    pub fn from_json<P: AsRef<Path>>(file: P) -> FeosResult<Vec<Self>>
188    where
189        M: DeserializeOwned,
190        A: DeserializeOwned,
191    {
192        Ok(serde_json::from_reader(BufReader::new(File::open(file)?))?)
193    }
194}
195
196impl<M: Serialize, A: Serialize> fmt::Display for PureRecord<M, A> {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        let s = serde_json::to_string(self).unwrap().replace("\"", "");
199        let s = s.replace(",", ", ").replace(":", ": ");
200        write!(f, "PureRecord({})", &s[1..s.len() - 1])
201    }
202}
203
204impl<M: Serialize, A: Serialize> fmt::Display for SegmentRecord<M, A> {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        let s = serde_json::to_string(self).unwrap().replace("\"", "");
207        let s = s.replace(",", ", ").replace(":", ": ");
208        write!(f, "SegmentRecord({})", &s[1..s.len() - 1])
209    }
210}
211
212/// Trait for models that implement a homosegmented group contribution
213/// method
214pub trait FromSegments: Clone {
215    /// Constructs the record from a list of segment records with their
216    /// number of occurences.
217    fn from_segments(segments: &[(Self, f64)]) -> FeosResult<Self>;
218}
219
220/// Trait for models that implement a homosegmented group contribution
221/// method and have a combining rule for binary interaction parameters.
222pub trait FromSegmentsBinary: Clone {
223    /// Constructs the binary record from a list of segment records with
224    /// their number of occurences.
225    fn from_segments_binary(segments: &[(Self, f64, f64)]) -> FeosResult<Self>;
226}
227
228impl FromSegmentsBinary for () {
229    fn from_segments_binary(_: &[(Self, f64, f64)]) -> FeosResult<Self> {
230        Ok(())
231    }
232}
233
234/// A collection of parameters that model interactions between two substances.
235#[derive(Serialize, Deserialize, Clone, Debug)]
236pub struct BinaryRecord<I, B, A> {
237    /// Identifier of the first component
238    pub id1: I,
239    /// Identifier of the second component
240    pub id2: I,
241    /// Binary interaction parameter(s)
242    #[serde(flatten)]
243    pub model_record: Option<B>,
244    /// Binary association records
245    #[serde(skip_serializing_if = "Vec::is_empty")]
246    #[serde(default = "Vec::new")]
247    pub association_sites: Vec<BinaryAssociationRecord<A>>,
248}
249
250/// A collection of parameters that model interactions between two segments.
251pub type BinarySegmentRecord<M, A> = BinaryRecord<String, M, A>;
252
253impl<I, B, A> BinaryRecord<I, B, A> {
254    /// Crates a new `BinaryRecord`.
255    pub fn new(id1: I, id2: I, model_record: Option<B>) -> Self {
256        Self::with_association(id1, id2, model_record, vec![])
257    }
258
259    /// Crates a new `BinaryRecord` including association sites.
260    pub fn with_association(
261        id1: I,
262        id2: I,
263        model_record: Option<B>,
264        association_sites: Vec<BinaryAssociationRecord<A>>,
265    ) -> Self {
266        Self {
267            id1,
268            id2,
269            model_record,
270            association_sites,
271        }
272    }
273
274    /// Read a list of `BinaryRecord`s from a JSON file.
275    pub fn from_json<P: AsRef<Path>>(file: P) -> FeosResult<Vec<Self>>
276    where
277        I: DeserializeOwned,
278        B: DeserializeOwned,
279        A: DeserializeOwned,
280    {
281        Ok(serde_json::from_reader(BufReader::new(File::open(file)?))?)
282    }
283}
284
285impl<I: Serialize + Clone, B: Serialize + Clone, A: Serialize + Clone> fmt::Display
286    for BinaryRecord<I, B, A>
287{
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        let s = serde_json::to_string(self).unwrap().replace("\"", "");
290        let s = s.replace(",", ", ").replace(":", ": ");
291        write!(f, "BinaryRecord({})", &s[1..s.len() - 1])
292    }
293}
294
295#[cfg(test)]
296mod test {
297    use super::*;
298
299    #[derive(Serialize, Deserialize, Debug, Default, Clone)]
300    struct TestModelRecordSegments {
301        a: f64,
302    }
303
304    #[test]
305    fn deserialize() {
306        let r = r#"
307        {
308            "identifier": {
309                "cas": "123-4-5"
310            },
311            "molarweight": 16.0426,
312            "a": 0.1
313        }
314        "#;
315        let record: PureRecord<TestModelRecordSegments, ()> =
316            serde_json::from_str(r).expect("Unable to parse json.");
317        assert_eq!(record.identifier.cas, Some("123-4-5".into()))
318    }
319
320    #[test]
321    fn deserialize_list() {
322        let r = r#"
323        [
324            {
325                "identifier": {
326                    "cas": "1"
327                },
328                "molarweight": 1.0,
329                "a": 1.0
330            },
331            {
332                "identifier": {
333                    "cas": "2"
334                },
335                "molarweight": 2.0,
336                "a": 2.0
337            }
338        ]"#;
339        let records: Vec<PureRecord<TestModelRecordSegments, ()>> =
340            serde_json::from_str(r).expect("Unable to parse json.");
341        assert_eq!(records[0].identifier.cas, Some("1".into()));
342        assert_eq!(records[1].identifier.cas, Some("2".into()))
343    }
344}