Skip to main content

feos_core/parameter/
mod.rs

1//! Structures and traits that can be used to build model parameters for equations of state.
2use crate::errors::*;
3use indexmap::{IndexMap, IndexSet};
4use itertools::Itertools;
5use nalgebra::{DMatrix, DVector, Scalar};
6use quantity::{GRAM, MOL, MolarWeight};
7use serde::Serialize;
8use serde::de::DeserializeOwned;
9use std::array;
10use std::collections::HashMap;
11use std::fs::File;
12use std::io::BufReader;
13use std::ops::Deref;
14use std::path::Path;
15
16mod association;
17mod chemical_record;
18#[cfg(feature = "rusqlite")]
19mod database;
20mod identifier;
21mod model_record;
22
23pub use association::{
24    AssociationParameters, AssociationRecord, AssociationSite, BinaryAssociationRecord,
25    CombiningRule,
26};
27pub use chemical_record::{ChemicalRecord, GroupCount};
28pub use identifier::{Identifier, IdentifierOption};
29pub use model_record::{
30    BinaryRecord, BinarySegmentRecord, FromSegments, FromSegmentsBinary, PureRecord, Record,
31    SegmentRecord,
32};
33
34/// Parameters for one specific pure component or segment/group.
35#[derive(Clone)]
36pub struct PureParameters<M, C> {
37    pub identifier: String,
38    pub model_record: M,
39    pub count: C,
40    pub component_index: usize,
41}
42
43impl<M: Clone> PureParameters<M, ()> {
44    fn from_pure_record(model_record: M, component_index: usize) -> Self {
45        Self {
46            identifier: "".into(),
47            model_record,
48            count: (),
49            component_index,
50        }
51    }
52}
53
54impl<M: Clone, C> PureParameters<M, C> {
55    fn from_segment_record<A>(
56        segment: &SegmentRecord<M, A>,
57        count: C,
58        component_index: usize,
59    ) -> Self {
60        Self {
61            identifier: segment.identifier.clone(),
62            model_record: segment.model_record.clone(),
63            count,
64            component_index,
65        }
66    }
67}
68
69/// Parameters for one specific binary interaction.
70#[derive(Clone, Copy)]
71pub struct BinaryParameters<B, C> {
72    pub id1: usize,
73    pub id2: usize,
74    pub model_record: B,
75    pub count: C,
76}
77
78impl<B, C> BinaryParameters<B, C> {
79    pub fn new(id1: usize, id2: usize, model_record: B, count: C) -> Self {
80        Self {
81            id1,
82            id2,
83            model_record,
84            count,
85        }
86    }
87}
88
89/// The most general representation of model parameters.
90///
91/// All parameter structs are type aliases of this struct.
92/// See [Parameters], [GcParameters] and [IdealGasParameters].
93pub struct GenericParameters<P, B, A, Bo, C, Data> {
94    pub pure: Vec<PureParameters<P, C>>,
95    pub binary: Vec<BinaryParameters<B, ()>>,
96    pub bonds: Vec<BinaryParameters<Bo, C>>,
97    pub association: AssociationParameters<A>,
98    pub molar_weight: MolarWeight<DVector<f64>>,
99    data: Data,
100}
101
102/// Representation of component-specific parameters (no bonds, no counts).
103pub type Parameters<P, B, A> =
104    GenericParameters<P, B, A, (), (), (Vec<PureRecord<P, A>>, Vec<BinaryRecord<usize, B, A>>)>;
105/// Representation of group-specific parameters.
106pub type GcParameters<P, B, A, Bo, C> = GenericParameters<
107    P,
108    B,
109    A,
110    Bo,
111    C,
112    (
113        Vec<ChemicalRecord>,
114        Vec<SegmentRecord<P, A>>,
115        Option<Vec<BinarySegmentRecord<B, A>>>,
116        Vec<BinarySegmentRecord<Bo, ()>>,
117    ),
118>;
119/// Representation of parameters for ideal gas models (no association, no binary interactions).
120pub type IdealGasParameters<I> = Parameters<I, (), ()>;
121
122impl<P, B, A, Bo, C, Data> GenericParameters<P, B, A, Bo, C, Data> {
123    pub fn collate<F, T: Scalar + Copy, const N: usize>(&self, f: F) -> [DVector<T>; N]
124    where
125        F: Fn(&P) -> [T; N],
126    {
127        array::from_fn(|i| {
128            DVector::from_vec(self.pure.iter().map(|pr| f(&pr.model_record)[i]).collect())
129        })
130    }
131
132    pub fn collate_binary<F, T: Scalar + Default + Copy, const N: usize>(
133        &self,
134        f: F,
135    ) -> [DMatrix<T>; N]
136    where
137        F: Fn(&B) -> [T; N],
138    {
139        array::from_fn(|i| {
140            let mut b_mat = DMatrix::from_element(self.pure.len(), self.pure.len(), T::default());
141            for br in &self.binary {
142                let b = f(&br.model_record)[i];
143                b_mat[(br.id1, br.id2)] = b;
144                b_mat[(br.id2, br.id1)] = b;
145            }
146            b_mat
147        })
148    }
149
150    pub fn collate_ab<F, T: Scalar + Copy, const N: usize>(&self, f: F) -> [DMatrix<Option<T>>; N]
151    where
152        F: Fn(&A) -> [T; N],
153    {
154        let a = &self.association;
155        array::from_fn(|i| {
156            let mut b_mat = DMatrix::from_element(a.sites_a.len(), a.sites_b.len(), None);
157            for br in &a.binary_ab {
158                b_mat[(br.id1, br.id2)] = Some(f(&br.model_record)[i]);
159            }
160            b_mat
161        })
162    }
163
164    pub fn collate_cc<F, T: Scalar + Copy, const N: usize>(&self, f: F) -> [DMatrix<Option<T>>; N]
165    where
166        F: Fn(&A) -> [T; N],
167    {
168        let a = &self.association;
169        array::from_fn(|i| {
170            let mut b_mat = DMatrix::from_element(a.sites_c.len(), a.sites_c.len(), None);
171            for br in &a.binary_cc {
172                b_mat[(br.id1, br.id2)] = Some(f(&br.model_record)[i]);
173            }
174            b_mat
175        })
176    }
177}
178
179impl<P: Clone, B: Clone, A: CombiningRule<P> + Clone> Parameters<P, B, A> {
180    pub fn new(
181        pure_records: Vec<PureRecord<P, A>>,
182        binary_records: Vec<BinaryRecord<usize, B, A>>,
183    ) -> FeosResult<Self> {
184        let association_parameters = AssociationParameters::new(&pure_records, &binary_records)?;
185        let (molar_weight, pure) = pure_records
186            .iter()
187            .enumerate()
188            .map(|(i, pr)| {
189                (
190                    pr.molarweight,
191                    PureParameters::from_pure_record(pr.model_record.clone(), i),
192                )
193            })
194            .unzip();
195        let binary = binary_records
196            .iter()
197            .filter_map(|br| {
198                br.model_record
199                    .clone()
200                    .map(|m| BinaryParameters::new(br.id1, br.id2, m, ()))
201            })
202            .collect();
203
204        Ok(Self {
205            pure,
206            binary,
207            bonds: vec![],
208            association: association_parameters,
209            molar_weight: DVector::from_vec(molar_weight) * (GRAM / MOL),
210            data: (pure_records, binary_records),
211        })
212    }
213
214    /// Creates parameters for a pure component from a pure record.
215    pub fn new_pure(pure_record: PureRecord<P, A>) -> FeosResult<Self> {
216        Self::new(vec![pure_record], vec![])
217    }
218
219    /// Creates parameters for a binary system from pure records and an optional
220    /// binary interaction parameter.
221    pub fn new_binary(
222        pure_records: [PureRecord<P, A>; 2],
223        binary_record: Option<B>,
224        binary_association_records: Vec<BinaryAssociationRecord<A>>,
225    ) -> FeosResult<Self> {
226        let binary_record = vec![BinaryRecord::with_association(
227            0,
228            1,
229            binary_record,
230            binary_association_records,
231        )];
232        Self::new(pure_records.to_vec(), binary_record)
233    }
234
235    /// Creates parameters from records for pure substances and possibly binary parameters.
236    pub fn from_records(
237        pure_records: Vec<PureRecord<P, A>>,
238        binary_records: Vec<BinaryRecord<Identifier, B, A>>,
239        identifier_option: IdentifierOption,
240    ) -> FeosResult<Self> {
241        let binary_records =
242            Self::binary_matrix_from_records(&pure_records, &binary_records, identifier_option)?;
243        Self::new(pure_records, binary_records)
244    }
245
246    /// Creates parameters from model records with default values for the molar weight,
247    /// identifiers, association sites, and binary interaction parameters.
248    pub fn from_model_records(model_records: Vec<P>) -> FeosResult<Self> {
249        let pure_records = model_records
250            .into_iter()
251            .map(|r| PureRecord::new(Default::default(), Default::default(), r))
252            .collect();
253        Self::new(pure_records, vec![])
254    }
255}
256
257impl<P: Clone, B: Clone, A: Clone> Parameters<P, B, A> {
258    /// Helper function to build matrix from list of records in correct order.
259    pub fn binary_matrix_from_records(
260        pure_records: &[PureRecord<P, A>],
261        binary_records: &[BinaryRecord<Identifier, B, A>],
262        identifier_option: IdentifierOption,
263    ) -> FeosResult<Vec<BinaryRecord<usize, B, A>>> {
264        // Build Hashmap (id, id) -> BinaryRecord
265        let binary_map: HashMap<_, _> = {
266            binary_records
267                .iter()
268                .filter_map(|br| {
269                    let id1 = br.id1.as_str(identifier_option);
270                    let id2 = br.id2.as_str(identifier_option);
271                    id1.and_then(|id1| {
272                        id2.map(|id2| ((id1, id2), (&br.model_record, &br.association_sites)))
273                    })
274                })
275                .collect()
276        };
277
278        // look up pure records in Hashmap
279        pure_records
280            .iter()
281            .enumerate()
282            .array_combinations()
283            .chain(
284                // Somehow there is no array_combinations_with_replacement in itertools
285                // ii combinations are not intuitive here, but potentially needed for
286                // molecules with multiple association sites.
287                pure_records
288                    .iter()
289                    .enumerate()
290                    .map(|(i, p)| [(i, p), (i, p)]),
291            )
292            .map(|[(i1, p1), (i2, p2)]| {
293                let Some(id1) = p1.identifier.as_str(identifier_option) else {
294                    return Err(FeosError::MissingParameters(format!(
295                        "No {} for pure record {} ({}).",
296                        identifier_option, i1, p1.identifier
297                    )));
298                };
299                let Some(id2) = p2.identifier.as_str(identifier_option) else {
300                    return Err(FeosError::MissingParameters(format!(
301                        "No {} for pure record {} ({}).",
302                        identifier_option, i2, p2.identifier
303                    )));
304                };
305                Ok([(i1, id1), (i2, id2)])
306            })
307            .filter_map(|x| {
308                x.map(|[(i1, id1), (i2, id2)]| {
309                    let records = if let Some(&(b, a)) = binary_map.get(&(id1, id2)) {
310                        Some((b, a.clone()))
311                    } else if let Some(&(b, a)) = binary_map.get(&(id2, id1)) {
312                        let a = a
313                            .iter()
314                            .cloned()
315                            .map(|a| BinaryAssociationRecord::with_id(a.id2, a.id1, a.parameters))
316                            .collect();
317                        Some((b, a))
318                    } else {
319                        None
320                    };
321                    records.map(|(b, a)| BinaryRecord::with_association(i1, i2, b.clone(), a))
322                })
323                .transpose()
324            })
325            .collect()
326    }
327}
328
329impl<P: Clone, B: Clone, A: CombiningRule<P> + Clone> Parameters<P, B, A> {
330    /// Creates parameters from substance information stored in json files.
331    pub fn from_json<F, S>(
332        substances: Vec<S>,
333        file_pure: F,
334        file_binary: Option<F>,
335        identifier_option: IdentifierOption,
336    ) -> FeosResult<Self>
337    where
338        F: AsRef<Path>,
339        S: Deref<Target = str>,
340        P: DeserializeOwned,
341        B: DeserializeOwned,
342        A: DeserializeOwned,
343    {
344        Self::from_multiple_json(&[(substances, file_pure)], file_binary, identifier_option)
345    }
346
347    /// Creates parameters from substance information stored in multiple json files.
348    pub fn from_multiple_json<F, S>(
349        input: &[(Vec<S>, F)],
350        file_binary: Option<F>,
351        identifier_option: IdentifierOption,
352    ) -> FeosResult<Self>
353    where
354        F: AsRef<Path>,
355        S: Deref<Target = str>,
356        P: DeserializeOwned,
357        B: DeserializeOwned,
358        A: DeserializeOwned,
359    {
360        let records = PureRecord::from_multiple_json(input, identifier_option)?;
361
362        let binary_records = if let Some(path) = file_binary {
363            BinaryRecord::from_json(path)?
364        } else {
365            Vec::new()
366        };
367
368        Self::from_records(records, binary_records, identifier_option)
369    }
370
371    /// Creates parameters from the molecular structure and segment information.
372    ///
373    /// The [FromSegments] trait needs to be implemented for the model record
374    /// and the binary interaction parameters.
375    pub fn from_segments(
376        chemical_records: Vec<ChemicalRecord>,
377        segment_records: &[SegmentRecord<P, A>],
378        binary_segment_records: Option<&[BinarySegmentRecord<B, A>]>,
379    ) -> FeosResult<Self>
380    where
381        P: FromSegments,
382        B: FromSegmentsBinary + Default,
383    {
384        let segment_map: HashMap<_, _> =
385            segment_records.iter().map(|s| (&s.identifier, s)).collect();
386
387        // Calculate the pure records from the GC method.
388        let (group_counts, pure_records): (Vec<_>, _) = chemical_records
389            .into_iter()
390            .map(|cr| {
391                let (identifier, group_counts, _) = GroupCount::into_groups(cr);
392                let groups = group_counts
393                    .iter()
394                    .map(|(s, c)| {
395                        segment_map.get(s).map(|&x| (x.clone(), *c)).ok_or_else(|| {
396                            FeosError::MissingParameters(format!("No segment record found for {s}"))
397                        })
398                    })
399                    .collect::<FeosResult<Vec<_>>>()?;
400                let pure_record = PureRecord::from_segments(identifier, groups)?;
401                Ok::<_, FeosError>((group_counts, pure_record))
402            })
403            .collect::<Result<Vec<_>, _>>()?
404            .into_iter()
405            .unzip();
406
407        // Map: (id1, id2) -> model_record
408        // empty, if no binary segment records are provided
409        let binary_map: HashMap<_, _> = binary_segment_records
410            .into_iter()
411            .flat_map(|seg| seg.iter())
412            .filter_map(|br| br.model_record.as_ref().map(|b| ((&br.id1, &br.id2), b)))
413            .collect();
414
415        // full matrix of binary records from the gc method.
416        // If a specific segment-segment interaction is not in the binary map,
417        // the default value is used.
418        let binary_records = group_counts
419            .iter()
420            .enumerate()
421            .array_combinations()
422            .map(|[(i, sc1), (j, sc2)]| {
423                let mut vec = Vec::new();
424                for (id1, n1) in sc1.iter() {
425                    for (id2, n2) in sc2.iter() {
426                        let binary = binary_map
427                            .get(&(id1, id2))
428                            .or_else(|| binary_map.get(&(id2, id1)))
429                            .copied()
430                            .cloned()
431                            .unwrap_or_default();
432                        vec.push((binary, *n1, *n2));
433                    }
434                }
435                B::from_segments_binary(&vec).map(|br| BinaryRecord::new(i, j, Some(br)))
436            })
437            .collect::<Result<_, _>>()?;
438
439        Self::new(pure_records, binary_records)
440    }
441
442    /// Creates parameters from segment information stored in json files.
443    ///
444    /// The [FromSegments] trait needs to be implemented for both the model record
445    /// and the ideal gas record.
446    pub fn from_json_segments<F>(
447        substances: &[&str],
448        file_pure: F,
449        file_segments: F,
450        file_binary: Option<F>,
451        identifier_option: IdentifierOption,
452    ) -> FeosResult<Self>
453    where
454        F: AsRef<Path>,
455        P: FromSegments + DeserializeOwned,
456        B: FromSegmentsBinary + DeserializeOwned + Default,
457        A: DeserializeOwned,
458    {
459        let queried: IndexSet<_> = substances.iter().copied().collect();
460
461        let file = File::open(file_pure)?;
462        let reader = BufReader::new(file);
463        let chemical_records: Vec<ChemicalRecord> = serde_json::from_reader(reader)?;
464        let mut record_map: HashMap<_, _> = chemical_records
465            .into_iter()
466            .filter_map(|record| {
467                record
468                    .identifier
469                    .as_str(identifier_option)
470                    .map(|i| i.to_owned())
471                    .map(|i| (i, record))
472            })
473            .collect();
474
475        // Compare queried components and available components
476        let available: IndexSet<_> = record_map
477            .keys()
478            .map(|identifier| identifier as &str)
479            .collect();
480        if !queried.is_subset(&available) {
481            let missing: Vec<_> = queried.difference(&available).cloned().collect();
482            let msg = format!("{missing:?}");
483            return Err(FeosError::ComponentsNotFound(msg));
484        };
485
486        // collect all pure records that were queried
487        let chemical_records: Vec<_> = queried
488            .into_iter()
489            .filter_map(|identifier| record_map.remove(identifier))
490            .collect();
491
492        // Read segment records
493        let segment_records: Vec<SegmentRecord<P, A>> = SegmentRecord::from_json(file_segments)?;
494
495        // Read binary records
496        let binary_records = file_binary
497            .map(|file_binary| {
498                let reader = BufReader::new(File::open(file_binary)?);
499                let binary_records: FeosResult<Vec<BinarySegmentRecord<B, A>>> =
500                    Ok(serde_json::from_reader(reader)?);
501                binary_records
502            })
503            .transpose()?;
504
505        Self::from_segments(
506            chemical_records,
507            &segment_records,
508            binary_records.as_deref(),
509        )
510    }
511
512    pub fn identifiers(&self) -> Vec<&Identifier> {
513        self.data.0.iter().map(|pr| &pr.identifier).collect()
514    }
515
516    /// Return a parameter set containing the subset of components specified in `component_list`.
517    ///
518    /// # Panics
519    ///
520    /// Panics if index in `component_list` is out of bounds
521    pub fn subset(&self, component_list: &[usize]) -> Self {
522        let (pure_records, binary_records) = &self.data;
523        let pure_records = component_list
524            .iter()
525            .map(|&i| pure_records[i].clone())
526            .collect();
527        let comp_map: HashMap<_, _> = component_list
528            .iter()
529            .enumerate()
530            .map(|(i, &c)| (c, i))
531            .collect();
532        let binary_records = binary_records
533            .iter()
534            .filter_map(|br| {
535                let id1 = comp_map.get(&br.id1);
536                let id2 = comp_map.get(&br.id2);
537                id1.and_then(|&id1| {
538                    id2.map(|&id2| BinaryRecord::new(id1, id2, br.model_record.clone()))
539                })
540            })
541            .collect();
542
543        Self::new(pure_records, binary_records).unwrap()
544    }
545
546    /// Return scalar pure parameters as map.
547    ///
548    /// This function is not particularly efficient and mostly relevant in situations
549    /// in which the type information is thrown away on purpose (i.e., FFI)
550    pub fn pure_parameters(&self) -> IndexMap<String, DVector<f64>>
551    where
552        P: Serialize,
553    {
554        let na: Vec<_> = self.association.sites_a.iter().map(|s| s.n).collect();
555        let nb: Vec<_> = self.association.sites_b.iter().map(|s| s.n).collect();
556        let nc: Vec<_> = self.association.sites_c.iter().map(|s| s.n).collect();
557        let pars: IndexSet<_> = self
558            .pure
559            .iter()
560            .flat_map(|p| to_map(&p.model_record).into_keys())
561            .collect();
562        pars.into_iter()
563            .map(|p| {
564                let [pars] = self.collate(|r| [to_map(r).get(&p).copied().unwrap_or_default()]);
565                (p, pars)
566            })
567            .chain(
568                na.iter()
569                    .any(|&n| n > 0.0)
570                    .then(|| ("na".into(), DVector::from(na))),
571            )
572            .chain(
573                nb.iter()
574                    .any(|&n| n > 0.0)
575                    .then(|| ("nb".into(), DVector::from(nb))),
576            )
577            .chain(
578                nc.iter()
579                    .any(|&n| n > 0.0)
580                    .then(|| ("nc".into(), DVector::from(nc))),
581            )
582            .collect()
583    }
584
585    /// Return scalar binary interaction parameters as map.
586    ///
587    /// This function is not particularly efficient and mostly relevant in situations
588    /// in which the type information is thrown away on purpose (i.e., FFI)
589    pub fn binary_parameters(&self) -> IndexMap<String, DMatrix<f64>>
590    where
591        B: Serialize,
592    {
593        let pars: IndexSet<String> = self
594            .binary
595            .iter()
596            .flat_map(|b| to_map(&b.model_record).into_keys())
597            .collect();
598        pars.into_iter()
599            .map(|p| {
600                let [pars] = self.collate_binary(|b| [to_map(b)[&p]]);
601                (p, pars)
602            })
603            .collect()
604    }
605
606    /// Return scalar association parameters (AB) as map.
607    ///
608    /// This function is not particularly efficient and mostly relevant in situations
609    /// in which the type information is thrown away on purpose (i.e., FFI)
610    pub fn association_parameters_ab(&self) -> IndexMap<String, DMatrix<f64>>
611    where
612        A: Serialize,
613    {
614        let pars: IndexSet<String> = self
615            .association
616            .binary_ab
617            .iter()
618            .flat_map(|a| to_map(&a.model_record).into_keys())
619            .collect();
620        pars.into_iter()
621            .map(|p| {
622                let [pars] = self.collate_ab(|a| [to_map(a).get(&p).copied().unwrap_or_default()]);
623                let pars = pars.map(|p| p.unwrap_or(f64::NAN));
624                (p, pars)
625            })
626            .collect()
627    }
628
629    /// Return scalar association parameters (CC) as map.
630    ///
631    /// This function is not particularly efficient and mostly relevant in situations
632    /// in which the type information is thrown away on purpose (i.e., FFI)
633    pub fn association_parameters_cc(&self) -> IndexMap<String, DMatrix<f64>>
634    where
635        A: Serialize,
636    {
637        let pars: IndexSet<String> = self
638            .association
639            .binary_cc
640            .iter()
641            .flat_map(|a| to_map(&a.model_record).into_keys())
642            .collect();
643        pars.into_iter()
644            .map(|mut p| {
645                let [pars] = self.collate_cc(|a| [to_map(a)[&p]]);
646                let pars = pars.map(|p| p.unwrap_or(f64::NAN));
647                if p.ends_with("ab") {
648                    let _ = p.split_off(p.len() - 2);
649                }
650                p.push_str("cc");
651                (p, pars)
652            })
653            .collect()
654    }
655}
656
657fn to_map<R: Serialize>(record: R) -> IndexMap<String, f64> {
658    serde_json::from_value(serde_json::to_value(&record).unwrap()).unwrap()
659}
660
661impl<P, B, A, Bo> GcParameters<P, B, A, Bo, f64> {
662    pub fn segment_counts(&self) -> DVector<f64> {
663        DVector::from_vec(self.pure.iter().map(|pr| pr.count).collect())
664    }
665}
666
667impl<P: Clone, B: Clone, A: CombiningRule<P> + Clone, Bo: Clone, C: GroupCount + Default>
668    GcParameters<P, B, A, Bo, C>
669{
670    pub fn from_segments_hetero(
671        chemical_records: Vec<ChemicalRecord>,
672        segment_records: &[SegmentRecord<P, A>],
673        binary_segment_records: Option<&[BinarySegmentRecord<B, A>]>,
674    ) -> FeosResult<Self>
675    where
676        Bo: Default,
677    {
678        let mut bond_records = Vec::new();
679        for s1 in segment_records.iter() {
680            for s2 in segment_records.iter() {
681                bond_records.push(BinarySegmentRecord::new(
682                    s1.identifier.clone(),
683                    s2.identifier.clone(),
684                    Some(Bo::default()),
685                ));
686            }
687        }
688        Self::from_segments_with_bonds(
689            chemical_records,
690            segment_records,
691            binary_segment_records,
692            &bond_records,
693        )
694    }
695
696    pub fn from_segments_with_bonds(
697        chemical_records: Vec<ChemicalRecord>,
698        segment_records: &[SegmentRecord<P, A>],
699        binary_segment_records: Option<&[BinarySegmentRecord<B, A>]>,
700        bond_records: &[BinarySegmentRecord<Bo, ()>],
701    ) -> FeosResult<Self> {
702        let segment_map: HashMap<_, _> =
703            segment_records.iter().map(|s| (&s.identifier, s)).collect();
704
705        let mut bond_records_map = HashMap::new();
706        for bond_record in bond_records {
707            bond_records_map.insert((&bond_record.id1, &bond_record.id2), bond_record);
708            bond_records_map.insert((&bond_record.id2, &bond_record.id1), bond_record);
709        }
710
711        let mut groups = Vec::new();
712        let mut association_sites = Vec::new();
713        let mut bonds = Vec::new();
714        let mut molar_weight: DVector<f64> = DVector::zeros(chemical_records.len());
715        for (i, cr) in chemical_records.iter().enumerate() {
716            let (_, group_counts, bond_counts) = C::into_groups(cr.clone());
717            let n = groups.len();
718            for (s, c) in &group_counts {
719                let Some(&segment) = segment_map.get(s) else {
720                    return Err(FeosError::MissingParameters(format!(
721                        "No segment record found for {s}"
722                    )));
723                };
724                molar_weight[i] += segment.molarweight * c.into_f64();
725                groups.push(PureParameters::from_segment_record(segment, *c, i));
726                association_sites.push(segment.association_sites.clone());
727            }
728            for ([a, b], c) in bond_counts {
729                let id1 = &group_counts[a].0;
730                let id2 = &group_counts[b].0;
731                let Some(&bond) = bond_records_map.get(&(id1, id2)) else {
732                    return Err(FeosError::MissingParameters(format!(
733                        "No bond record found for {id1}-{id2}"
734                    )));
735                };
736                let Some(bond) = bond.model_record.as_ref() else {
737                    return Err(FeosError::MissingParameters(format!(
738                        "No bond record found for {id1}-{id2}"
739                    )));
740                };
741                bonds.push(BinaryParameters::new(a + n, b + n, bond.clone(), c));
742            }
743        }
744
745        let mut binary_records = Vec::new();
746        let mut binary_association_records = Vec::new();
747        if let Some(binary_segment_records) = binary_segment_records {
748            let mut binary_segment_records_map = HashMap::new();
749            for binary_record in binary_segment_records {
750                binary_segment_records_map
751                    .insert((&binary_record.id1, &binary_record.id2), binary_record);
752                binary_segment_records_map
753                    .insert((&binary_record.id2, &binary_record.id1), binary_record);
754            }
755
756            for [(i1, s1), (i2, s2)] in groups.iter().enumerate().array_combinations() {
757                if s1.component_index != s2.component_index {
758                    let id1 = &s1.identifier;
759                    let id2 = &s2.identifier;
760                    if let Some(&br) = binary_segment_records_map.get(&(id1, id2)) {
761                        if let Some(br) = &br.model_record {
762                            binary_records.push(BinaryParameters::new(i1, i2, br.clone(), ()));
763                        }
764                        if !br.association_sites.is_empty() {
765                            binary_association_records.push(BinaryParameters::new(
766                                i1,
767                                i2,
768                                br.association_sites.clone(),
769                                (),
770                            ))
771                        }
772                    }
773                }
774            }
775        }
776
777        let association_parameters = AssociationParameters::new_hetero(
778            &groups,
779            &association_sites,
780            &binary_association_records,
781        )?;
782
783        Ok(Self {
784            pure: groups,
785            binary: binary_records,
786            bonds,
787            association: association_parameters,
788            molar_weight: molar_weight * (GRAM / MOL),
789            data: (
790                chemical_records,
791                segment_records.to_vec(),
792                binary_segment_records.map(|b| b.to_vec()),
793                bond_records.to_vec(),
794            ),
795        })
796    }
797
798    /// Creates parameters from segment information stored in json files.
799    ///
800    /// The [FromSegments] trait needs to be implemented for both the model record
801    /// and the ideal gas record.
802    pub fn from_json_segments_hetero<F>(
803        substances: &[&str],
804        file_pure: F,
805        file_segments: F,
806        file_binary: Option<F>,
807        identifier_option: IdentifierOption,
808    ) -> FeosResult<Self>
809    where
810        F: AsRef<Path>,
811        P: DeserializeOwned,
812        B: DeserializeOwned + Default,
813        A: DeserializeOwned,
814        Bo: Default,
815    {
816        let (chemical_records, segment_records, binary_records) = Self::read_json(
817            substances,
818            file_pure,
819            file_segments,
820            file_binary,
821            identifier_option,
822        )?;
823
824        Self::from_segments_hetero(
825            chemical_records,
826            &segment_records,
827            binary_records.as_deref(),
828        )
829    }
830
831    /// Creates parameters from segment information stored in json files.
832    ///
833    /// The [FromSegments] trait needs to be implemented for both the model record
834    /// and the ideal gas record.
835    pub fn from_json_segments_with_bonds<F>(
836        substances: &[&str],
837        file_pure: F,
838        file_segments: F,
839        file_binary: Option<F>,
840        file_bonds: F,
841        identifier_option: IdentifierOption,
842    ) -> FeosResult<Self>
843    where
844        F: AsRef<Path>,
845        P: DeserializeOwned,
846        B: DeserializeOwned + Default,
847        A: DeserializeOwned,
848        Bo: DeserializeOwned,
849    {
850        let (chemical_records, segment_records, binary_records) = Self::read_json(
851            substances,
852            file_pure,
853            file_segments,
854            file_binary,
855            identifier_option,
856        )?;
857
858        // Read bond records
859        let bond_records: Vec<_> = BinaryRecord::from_json(file_bonds)?;
860
861        Self::from_segments_with_bonds(
862            chemical_records,
863            &segment_records,
864            binary_records.as_deref(),
865            &bond_records,
866        )
867    }
868
869    #[expect(clippy::type_complexity)]
870    fn read_json<F>(
871        substances: &[&str],
872        file_pure: F,
873        file_segments: F,
874        file_binary: Option<F>,
875        identifier_option: IdentifierOption,
876    ) -> FeosResult<(
877        Vec<ChemicalRecord>,
878        Vec<SegmentRecord<P, A>>,
879        Option<Vec<BinarySegmentRecord<B, A>>>,
880    )>
881    where
882        F: AsRef<Path>,
883        P: DeserializeOwned,
884        B: DeserializeOwned + Default,
885        A: DeserializeOwned,
886    {
887        let queried: IndexSet<_> = substances.iter().copied().collect();
888
889        let file = File::open(file_pure)?;
890        let reader = BufReader::new(file);
891        let chemical_records: Vec<ChemicalRecord> = serde_json::from_reader(reader)?;
892        let mut record_map: HashMap<_, _> = chemical_records
893            .into_iter()
894            .filter_map(|record| {
895                record
896                    .identifier
897                    .as_str(identifier_option)
898                    .map(|i| i.to_owned())
899                    .map(|i| (i, record))
900            })
901            .collect();
902
903        // Compare queried components and available components
904        let available: IndexSet<_> = record_map
905            .keys()
906            .map(|identifier| identifier as &str)
907            .collect();
908        if !queried.is_subset(&available) {
909            let missing: Vec<_> = queried.difference(&available).cloned().collect();
910            let msg = format!("{missing:?}");
911            return Err(FeosError::ComponentsNotFound(msg));
912        };
913
914        // collect all pure records that were queried
915        let chemical_records = queried
916            .into_iter()
917            .filter_map(|identifier| record_map.remove(identifier))
918            .collect();
919
920        // Read segment records
921        let segment_records = SegmentRecord::from_json(file_segments)?;
922
923        // Read binary records
924        let binary_records = file_binary
925            .map(|file_binary| BinaryRecord::from_json(file_binary))
926            .transpose()?;
927
928        Ok((chemical_records, segment_records, binary_records))
929    }
930
931    pub fn component_index(&self) -> Vec<usize> {
932        self.pure.iter().map(|pr| pr.component_index).collect()
933    }
934
935    pub fn identifiers(&self) -> Vec<&Identifier> {
936        self.data.0.iter().map(|cr| &cr.identifier).collect()
937    }
938
939    /// Return a parameter set containing the subset of components specified in `component_list`.
940    ///
941    /// # Panics
942    ///
943    /// Panics if index in `component_list` is out of bounds
944    pub fn subset(&self, component_list: &[usize]) -> Self {
945        let (chemical_records, segment_records, binary_segment_records, bond_records) = &self.data;
946        let chemical_records = component_list
947            .iter()
948            .map(|&i| chemical_records[i].clone())
949            .collect();
950        Self::from_segments_with_bonds(
951            chemical_records,
952            segment_records,
953            binary_segment_records.as_deref(),
954            bond_records,
955        )
956        .unwrap()
957    }
958}