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}
125pub trait GroupCount: Copy {
126    #[expect(clippy::type_complexity)]
127    fn into_groups(
128        chemical_record: ChemicalRecord,
129    ) -> (Identifier, Vec<(String, Self)>, Vec<([usize; 2], Self)>);
130
131    fn into_f64(self) -> f64;
132}
133
134impl GroupCount for f64 {
135    fn into_groups(
136        chemical_record: ChemicalRecord,
137    ) -> (Identifier, Vec<(String, f64)>, Vec<([usize; 2], f64)>) {
138        let mut group_counts = IndexMap::with_capacity(chemical_record.segments.len());
139        let segment_to_group: Vec<_> = chemical_record
140            .segments
141            .into_iter()
142            .map(|si| {
143                let entry = group_counts.entry(si);
144                let index = entry.index();
145                *entry.or_insert(0.0) += 1.0;
146                index
147            })
148            .collect();
149
150        let mut bond_counts: IndexMap<_, _> = (0..group_counts.len())
151            .array_combinations()
152            .chain((0..group_counts.len()).map(|i| [i, i]))
153            .map(|g| (g, 0.0))
154            .collect();
155        for [i, j] in chemical_record.bonds {
156            let [s1, s2] = [segment_to_group[i], segment_to_group[j]];
157            bond_counts.entry([s1, s2]).and_modify(|x| *x += 1.0);
158            if s1 != s2 {
159                bond_counts.entry([s2, s1]).and_modify(|x| *x += 1.0);
160            }
161        }
162        let group_counts = group_counts.into_iter().collect();
163        let bond_counts = bond_counts.into_iter().filter(|(_, c)| *c > 0.0).collect();
164
165        (chemical_record.identifier, group_counts, bond_counts)
166    }
167
168    fn into_f64(self) -> f64 {
169        self
170    }
171}
172
173impl GroupCount for () {
174    fn into_groups(
175        chemical_record: ChemicalRecord,
176    ) -> (Identifier, Vec<(String, ())>, Vec<([usize; 2], ())>) {
177        let segments = chemical_record
178            .segments
179            .into_iter()
180            .map(|s| (s, ()))
181            .collect();
182        let bonds = chemical_record.bonds.into_iter().map(|b| (b, ())).collect();
183        (chemical_record.identifier, segments, bonds)
184    }
185
186    fn into_f64(self) -> f64 {
187        1.0
188    }
189}