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