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