Skip to main content

feos_core/parameter/
chemical_record.rs

1use super::{Identifier, IdentifierOption};
2use crate::{FeosError, FeosResult};
3use indexmap::IndexMap;
4use itertools::Itertools;
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet};
7use std::fs::File;
8use std::io::BufReader;
9use std::ops::Deref;
10use std::path::Path;
11
12// Auxiliary structure used to deserialize chemical records without explicit bond information.
13#[derive(Serialize, Deserialize)]
14struct ChemicalRecordJSON {
15    identifier: Identifier,
16    segments: Vec<String>,
17    bonds: Option<Vec<[usize; 2]>>,
18}
19
20/// Chemical information of a substance.
21#[derive(Deserialize, Serialize, Debug, Clone)]
22#[serde(from = "ChemicalRecordJSON")]
23#[serde(into = "ChemicalRecordJSON")]
24pub struct ChemicalRecord {
25    pub identifier: Identifier,
26    pub segments: Vec<String>,
27    pub bonds: Vec<[usize; 2]>,
28}
29
30impl From<ChemicalRecordJSON> for ChemicalRecord {
31    fn from(record: ChemicalRecordJSON) -> Self {
32        Self::new(record.identifier, record.segments, record.bonds)
33    }
34}
35
36impl From<ChemicalRecord> for ChemicalRecordJSON {
37    fn from(record: ChemicalRecord) -> Self {
38        Self {
39            identifier: record.identifier,
40            segments: record.segments,
41            bonds: Some(record.bonds),
42        }
43    }
44}
45
46impl ChemicalRecord {
47    /// Create a new `ChemicalRecord`.
48    ///
49    /// If no bonds are given, the molecule is assumed to be linear.
50    pub fn new(
51        identifier: Identifier,
52        segments: Vec<String>,
53        bonds: Option<Vec<[usize; 2]>>,
54    ) -> ChemicalRecord {
55        let bonds = bonds.unwrap_or_else(|| {
56            (0..segments.len() - 1)
57                .zip(1..segments.len())
58                .map(|x| [x.0, x.1])
59                .collect()
60        });
61        Self {
62            identifier,
63            segments,
64            bonds,
65        }
66    }
67
68    /// Create chemical records from a json file.
69    pub fn from_json<P, S>(
70        substances: &[S],
71        file: P,
72        identifier_option: IdentifierOption,
73    ) -> FeosResult<Vec<Self>>
74    where
75        P: AsRef<Path>,
76        S: Deref<Target = str>,
77    {
78        // create list of substances
79        let mut queried: HashSet<&str> = substances.iter().map(|s| s.deref()).collect();
80        // raise error on duplicate detection
81        if queried.len() != substances.len() {
82            return Err(FeosError::IncompatibleParameters(
83                "A substance was defined more than once.".to_string(),
84            ));
85        }
86
87        let f = File::open(file)?;
88        let reader = BufReader::new(f);
89        // use stream in the future
90        let file_records: Vec<Self> = serde_json::from_reader(reader)?;
91        let mut records: HashMap<&str, Self> = HashMap::with_capacity(substances.len());
92
93        // build map, draining list of queried substances in the process
94        for record in file_records {
95            if let Some(id) = record.identifier.as_str(identifier_option) {
96                queried.take(id).map(|id| records.insert(id, record));
97            }
98            // all parameters parsed
99            if queried.is_empty() {
100                break;
101            }
102        }
103
104        // report missing parameters
105        if !queried.is_empty() {
106            return Err(FeosError::ComponentsNotFound(format!("{queried:?}")));
107        };
108
109        // collect into vec in correct order
110        Ok(substances
111            .iter()
112            .map(|s| records.remove(s.deref()).unwrap())
113            .collect())
114    }
115}
116
117impl std::fmt::Display for ChemicalRecord {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        write!(f, "ChemicalRecord(")?;
120        write!(f, "\n\tidentifier={},", self.identifier)?;
121        write!(f, "\n\tsegments={:?},", self.segments)?;
122        write!(f, "\n\tbonds={:?}\n)", self.bonds)
123    }
124}
125
126/// The type that is used to account for the multiplicity of groups.
127///
128/// In practice `f64` for group-based models and `()` for segment-based
129/// models.
130pub trait GroupCount: Copy {
131    #[expect(clippy::type_complexity)]
132    fn into_groups(
133        chemical_record: ChemicalRecord,
134    ) -> (Identifier, Vec<(String, Self)>, Vec<([usize; 2], Self)>);
135
136    fn into_f64(self) -> f64;
137}
138
139impl GroupCount for f64 {
140    fn into_groups(
141        chemical_record: ChemicalRecord,
142    ) -> (Identifier, Vec<(String, f64)>, Vec<([usize; 2], f64)>) {
143        let mut group_counts = IndexMap::with_capacity(chemical_record.segments.len());
144        let segment_to_group: Vec<_> = chemical_record
145            .segments
146            .into_iter()
147            .map(|si| {
148                let entry = group_counts.entry(si);
149                let index = entry.index();
150                *entry.or_insert(0.0) += 1.0;
151                index
152            })
153            .collect();
154
155        let mut bond_counts: IndexMap<_, _> = (0..group_counts.len())
156            .array_combinations()
157            .chain((0..group_counts.len()).map(|i| [i, i]))
158            .map(|g| (g, 0.0))
159            .collect();
160        for [i, j] in chemical_record.bonds {
161            let [s1, s2] = [segment_to_group[i], segment_to_group[j]];
162            bond_counts.entry([s1, s2]).and_modify(|x| *x += 1.0);
163            if s1 != s2 {
164                bond_counts.entry([s2, s1]).and_modify(|x| *x += 1.0);
165            }
166        }
167        let group_counts = group_counts.into_iter().collect();
168        let bond_counts = bond_counts.into_iter().filter(|(_, c)| *c > 0.0).collect();
169
170        (chemical_record.identifier, group_counts, bond_counts)
171    }
172
173    fn into_f64(self) -> f64 {
174        self
175    }
176}
177
178impl GroupCount for () {
179    fn into_groups(
180        chemical_record: ChemicalRecord,
181    ) -> (Identifier, Vec<(String, ())>, Vec<([usize; 2], ())>) {
182        let segments = chemical_record
183            .segments
184            .into_iter()
185            .map(|s| (s, ()))
186            .collect();
187        let bonds = chemical_record.bonds.into_iter().map(|b| (b, ())).collect();
188        (chemical_record.identifier, segments, bonds)
189    }
190
191    fn into_f64(self) -> f64 {
192        1.0
193    }
194}