From d0ced9e293e5cef62a73536d46a335e0061f2321 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 7 Jun 2022 10:55:29 +0200 Subject: [PATCH 1/5] Make Parameter trait more flexible --- Cargo.toml | 2 +- src/cubic.rs | 2 +- src/joback.rs | 31 ++-- src/parameter/chemical_record.rs | 267 ++++++++----------------------- src/parameter/identifier.rs | 27 ++-- src/parameter/mod.rs | 156 ++++++++++++++---- src/parameter/model_record.rs | 49 +++--- src/python/parameter.rs | 78 +++------ 8 files changed, 281 insertions(+), 331 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d679614..50933a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ thiserror = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" indexmap = "1.7" -either = "1.6" +conv = "0.3" numpy = { version = "0.16", optional = true } pyo3 = { version = "0.16", optional = true } diff --git a/src/cubic.rs b/src/cubic.rs index 5c01b1d..e741cec 100644 --- a/src/cubic.rs +++ b/src/cubic.rs @@ -101,7 +101,7 @@ impl PengRobinsonParameters { pc: pc[i], acentric_factor: acentric_factor[i], }; - let id = Identifier::new("1", None, None, None, None, None); + let id = Identifier::default(); PureRecord::new(id, molarweight[i], record, None) }) .collect(); diff --git a/src/joback.rs b/src/joback.rs index e136d7e..2640421 100644 --- a/src/joback.rs +++ b/src/joback.rs @@ -6,6 +6,7 @@ use crate::{ EosResult, EosUnit, EquationOfState, HelmholtzEnergy, IdealGasContribution, IdealGasContributionDual, }; +use conv::ValueInto; use ndarray::Array1; use num_dual::*; use quantity::QuantityScalar; @@ -45,21 +46,21 @@ impl fmt::Display for JobackRecord { /// Implementation of the combining rules as described in /// [Joback and Reid, 1987](https://doi.org/10.1080/00986448708960487). -impl FromSegments for JobackRecord { - fn from_segments(segments: &[(Self, f64)]) -> Self { +impl> FromSegments for JobackRecord { + fn from_segments(segments: &[(Self, T)]) -> Result { let mut a = -37.93; let mut b = 0.21; let mut c = -3.91e-4; let mut d = 2.06e-7; let mut e = 0.0; segments.iter().for_each(|(s, n)| { - a += s.a * *n; - b += s.b * *n; - c += s.c * *n; - d += s.d * *n; - e += s.e * *n; + a += s.a * (*n).value_into().unwrap(); + b += s.b * (*n).value_into().unwrap(); + c += s.c * (*n).value_into().unwrap(); + d += s.d * (*n).value_into().unwrap(); + e += s.e * (*n).value_into().unwrap(); }); - Self { a, b, c, d, e } + Ok(Self { a, b, c, d, e }) } } @@ -167,7 +168,7 @@ mod tests { use super::*; - #[derive(Deserialize, Clone)] + #[derive(Deserialize, Clone, Debug)] struct ModelRecord; #[test] @@ -213,7 +214,7 @@ mod tests { let segment_records: Vec> = serde_json::from_str(segments_json).expect("Unable to parse json."); let segments = ChemicalRecord::new( - Identifier::new("", None, None, None, None, None), + Identifier::default(), vec![ String::from("-Cl"), String::from("-Cl"), @@ -226,15 +227,15 @@ mod tests { ], None, ) - .segment_count(&segment_records)?; - assert_eq!(segments.get(&segment_records[0]), Some(&2.0)); - assert_eq!(segments.get(&segment_records[1]), Some(&4.0)); - assert_eq!(segments.get(&segment_records[2]), Some(&2.0)); + .segment_map(&segment_records)?; + assert_eq!(segments.get(&segment_records[0]), Some(&2)); + assert_eq!(segments.get(&segment_records[1]), Some(&4)); + assert_eq!(segments.get(&segment_records[2]), Some(&2)); let joback_segments: Vec<_> = segments .iter() .map(|(s, &n)| (s.ideal_gas_record.clone().unwrap(), n)) .collect(); - let jr = JobackRecord::from_segments(&joback_segments); + let jr = JobackRecord::from_segments(&joback_segments)?; assert_relative_eq!( jr.a, 33.3 * 2.0 - 2.14 * 4.0 - 8.25 * 2.0 - 37.93, diff --git a/src/parameter/chemical_record.rs b/src/parameter/chemical_record.rs index e91014e..f9cce54 100644 --- a/src/parameter/chemical_record.rs +++ b/src/parameter/chemical_record.rs @@ -1,9 +1,9 @@ use super::identifier::Identifier; use super::segment::SegmentRecord; use super::ParameterError; -use either::Either; +use conv::ValueInto; +use num_traits::NumAssign; use serde::{Deserialize, Serialize}; -use std::borrow::Cow; use std::collections::{HashMap, HashSet}; // Auxiliary structure used to deserialize chemical records without bond information. @@ -18,17 +18,10 @@ struct ChemicalRecordJSON { #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(from = "ChemicalRecordJSON")] #[serde(into = "ChemicalRecordJSON")] -pub enum ChemicalRecord { - List { - identifier: Identifier, - segments: Vec, - bonds: Vec<[usize; 2]>, - }, - Count { - identifier: Identifier, - segments: HashMap, - bonds: HashMap<[String; 2], f64>, - }, +pub struct ChemicalRecord { + pub identifier: Identifier, + pub segments: Vec, + pub bonds: Vec<[usize; 2]>, } impl From for ChemicalRecord { @@ -39,23 +32,10 @@ impl From for ChemicalRecord { impl From for ChemicalRecordJSON { fn from(record: ChemicalRecord) -> Self { - match record { - ChemicalRecord::List { - identifier, - segments, - bonds, - } => Self { - identifier, - segments, - bonds: Some(bonds), - }, - ChemicalRecord::Count { - identifier: _, - segments: _, - bonds: _, - } => panic!( - "Only chemical records with detailed structural information can be serialized." - ), + Self { + identifier: record.identifier, + segments: record.segments, + bonds: Some(record.bonds), } } } @@ -75,171 +55,73 @@ impl ChemicalRecord { .map(|x| [x.0, x.1]) .collect() }); - Self::List { + Self { identifier, segments, bonds, } } - /// Create a new `ChemicalRecord` from a segment count. - pub fn new_count( - identifier: Identifier, - segments: HashMap, - bonds: Option>, - ) -> ChemicalRecord { - let bonds = bonds.unwrap_or_default(); - Self::Count { - identifier, - segments, - bonds, + /// Count the number of occurences of each individual segment identifier in the + /// chemical record. + /// + /// The map contains the segment identifier as key and the count as value. + pub fn segment_count(&self) -> HashMap { + let mut counts = HashMap::with_capacity(self.segments.len()); + for si in &self.segments { + let entry = counts.entry(si.clone()).or_insert_with(|| T::zero()); + *entry += T::one(); } + counts } - pub fn segments(&self) -> Either<&Vec, &HashMap> { - match self { - Self::List { - identifier: _, - segments, - bonds: _, - } => Either::Left(segments), - Self::Count { - identifier: _, - segments, - bonds: _, - } => Either::Right(segments), + /// Count the number of occurences of bonds between each pair of segment identifiers + /// in the chemical record. + /// + /// The map contains the segment identifiers as key and the count as value. + pub fn bond_count(&self) -> HashMap<[String; 2], T> { + let mut bond_counts = HashMap::new(); + for b in &self.bonds { + let s1 = self.segments[b[0]].clone(); + let s2 = self.segments[b[1]].clone(); + let indices = if s1 > s2 { [s2, s1] } else { [s1, s2] }; + let entry = bond_counts.entry(indices).or_insert_with(|| T::zero()); + *entry += T::one(); } + bond_counts } +} - pub fn identifier(&self) -> &Identifier { - match self { - Self::List { - identifier, - segments: _, - bonds: _, - } => identifier, - Self::Count { - identifier, - segments: _, - bonds: _, - } => identifier, - } +impl std::fmt::Display for ChemicalRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ChemicalRecord(")?; + write!(f, "\n\tidentifier={},", self.identifier)?; + write!(f, "\n\tsegments={:?},", self.segments)?; + write!(f, "\n\tbonds={:?}\n)", self.bonds) } +} - /// Count the number of occurences of each individual segment in the - /// chemical record. - /// - /// The map contains the segment record as key and the count as (float) value. - pub fn segment_count( - &self, - segment_records: &[SegmentRecord], - ) -> Result, f64>, ParameterError> { - let segment_map = self.segment_map(segment_records)?; - Ok(match self.segments() { - Either::Left(segments) => { - let mut counts = HashMap::with_capacity(segments.len()); - for si in segments { - let key = segment_map.get(si).unwrap().clone(); - let entry = counts.entry(key).or_insert(0.0); - *entry += 1.0; - } - counts - } - Either::Right(segments) => segments - .iter() - .map(|(id, &c)| (segment_map[id].clone(), c)) - .collect(), - }) - } +/// Trait that enables parameter generation from generic molecular representations. +pub trait SegmentCount { + type Count: Copy + ValueInto; + fn identifier(&self) -> &Identifier; /// Count the number of occurences of each individual segment identifier in the /// chemical record. /// - /// The map contains the segment identifier as key and the count as (float) value. - pub fn segment_id_count(&self) -> Cow> { - match self.segments() { - Either::Left(segments) => { - let mut counts = HashMap::with_capacity(segments.len()); - for si in segments { - let entry = counts.entry(si.clone()).or_insert(0.0); - *entry += 1.0; - } - Cow::Owned(counts) - } - Either::Right(segments) => Cow::Borrowed(segments), - } - } - - /// Count the number of occurences of each individual segment identifier and each - /// pair of identifiers for every bond in the chemical record. - #[allow(clippy::type_complexity)] - pub fn segment_and_bond_count( - &self, - ) -> (Cow>, Cow>) { - match self { - Self::List { - identifier: _, - segments, - bonds, - } => { - let mut segment_counts = HashMap::with_capacity(segments.len()); - for si in segments { - let entry = segment_counts.entry(si.clone()).or_insert(0.0); - *entry += 1.0; - } - - let mut bond_counts = HashMap::new(); - for b in bonds { - let s1 = segments[b[0]].clone(); - let s2 = segments[b[1]].clone(); - let indices = if s1 > s2 { [s2, s1] } else { [s1, s2] }; - let entry = bond_counts.entry(indices).or_insert(0.0); - *entry += 1.0; - } - (Cow::Owned(segment_counts), Cow::Owned(bond_counts)) - } - Self::Count { - identifier: _, - segments, - bonds, - } => (Cow::Borrowed(segments), Cow::Borrowed(bonds)), - } - } - - /// Return the full segment and bond information for the molecule - /// if possible. - #[allow(clippy::type_complexity)] - pub fn segment_and_bond_list( - &self, - ) -> Result<(&Vec, &Vec<[usize; 2]>), ParameterError> { - match self { - Self::List { - identifier: _, - segments, - bonds, - } => Ok((segments, bonds)), - Self::Count { - identifier: _, - segments: _, - bonds: _, - } => Err(ParameterError::IncompatibleParameters( - "No detailed structural information available.".into(), - )), - } - } + /// The map contains the segment identifier as key and the count as value. + fn segment_count(&self) -> HashMap; - /// Build a HashMap from SegmentRecords for the segments. + /// Count the number of occurences of each individual segment in the + /// chemical record. /// - /// The map contains the segment identifier (String) as key - /// and the SegmentRecord as value. - pub fn segment_map( + /// The map contains the segment record as key and the count as value. + fn segment_map( &self, segment_records: &[SegmentRecord], - ) -> Result>, ParameterError> { - let queried: HashSet<_> = match self.segments() { - Either::Left(segments) => segments.iter().cloned().collect(), - Either::Right(segments) => segments.keys().cloned().collect(), - }; + ) -> Result, Self::Count>, ParameterError> { + let count = self.segment_count(); + let queried: HashSet<_> = count.keys().cloned().collect(); let mut segments: HashMap> = segment_records .iter() .map(|r| (r.identifier.clone(), r.clone())) @@ -250,36 +132,21 @@ impl ChemicalRecord { let msg = format!("{:?}", missing); return Err(ParameterError::ComponentsNotFound(msg)); }; - Ok(queried - .iter() - .map(|s| segments.remove_entry(s).unwrap()) + Ok(count + .into_iter() + .map(|(s, c)| (segments.remove(&s).unwrap(), c)) .collect()) } } -impl std::fmt::Display for ChemicalRecord { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::List { - identifier, - segments, - bonds, - } => { - write!(f, "ChemicalRecord(")?; - write!(f, "\n\tidentifier={},", identifier)?; - write!(f, "\n\tsegments={:?},", segments)?; - write!(f, "\n\tbonds={:?}\n)", bonds) - } - Self::Count { - identifier, - segments, - bonds, - } => { - write!(f, "ChemicalRecord(")?; - write!(f, "\n\tidentifier={},", identifier)?; - write!(f, "\n\tsegments={:?},", segments)?; - write!(f, "\n\tbonds={:?}\n)", bonds) - } - } +impl SegmentCount for ChemicalRecord { + type Count = usize; + + fn identifier(&self) -> &Identifier { + &self.identifier + } + + fn segment_count(&self) -> HashMap { + self.segment_count() } } diff --git a/src/parameter/identifier.rs b/src/parameter/identifier.rs index 4b81a8e..c7b01d6 100644 --- a/src/parameter/identifier.rs +++ b/src/parameter/identifier.rs @@ -36,7 +36,9 @@ impl TryFrom<&str> for IdentifierOption { #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct Identifier { /// CAS number - pub cas: String, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub cas: Option, /// Commonly used english name #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -67,7 +69,7 @@ impl Identifier { /// ```no_run /// # use feos_core::parameter::Identifier; /// let methanol = Identifier::new( - /// "67-56-1", + /// Some("67-56-1"), /// Some("methanol"), /// Some("methanol"), /// Some("CO"), @@ -75,7 +77,7 @@ impl Identifier { /// Some("CH4O") /// ); pub fn new( - cas: &str, + cas: Option<&str>, name: Option<&str>, iupac_name: Option<&str>, smiles: Option<&str>, @@ -83,7 +85,7 @@ impl Identifier { formula: Option<&str>, ) -> Identifier { Identifier { - cas: cas.to_owned(), + cas: cas.map(Into::into), name: name.map(Into::into), iupac_name: iupac_name.map(Into::into), smiles: smiles.map(Into::into), @@ -94,7 +96,7 @@ impl Identifier { pub fn as_string(&self, option: IdentifierOption) -> Option { match option { - IdentifierOption::Cas => Some(self.cas.clone()), + IdentifierOption::Cas => self.cas.clone(), IdentifierOption::Name => self.name.clone(), IdentifierOption::IupacName => self.iupac_name.clone(), IdentifierOption::Smiles => self.smiles.clone(), @@ -106,21 +108,24 @@ impl Identifier { impl std::fmt::Display for Identifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Identifier(cas={}", self.cas)?; + write!(f, "Identifier(")?; + if let Some(n) = &self.cas { + write!(f, "cas={}, ", n)?; + } if let Some(n) = &self.name { - write!(f, ", name={}", n)?; + write!(f, "name={}, ", n)?; } if let Some(n) = &self.iupac_name { - write!(f, ", iupac_name={}", n)?; + write!(f, "iupac_name={}, ", n)?; } if let Some(n) = &self.smiles { - write!(f, ", smiles={}", n)?; + write!(f, "smiles={}, ", n)?; } if let Some(n) = &self.inchi { - write!(f, ", inchi={}", n)?; + write!(f, "inchi={}, ", n)?; } if let Some(n) = &self.formula { - write!(f, ", formula={}", n)?; + write!(f, "formula={}", n)?; } write!(f, ")") } diff --git a/src/parameter/mod.rs b/src/parameter/mod.rs index a5a78e5..63aa59a 100644 --- a/src/parameter/mod.rs +++ b/src/parameter/mod.rs @@ -1,6 +1,6 @@ //! Structures and traits that can be used to build model parameters for equations of state. -use indexmap::IndexSet; +use indexmap::{IndexMap, IndexSet}; use ndarray::Array2; use serde::de::DeserializeOwned; use std::collections::HashMap; @@ -15,7 +15,7 @@ mod identifier; mod model_record; mod segment; -pub use chemical_record::ChemicalRecord; +pub use chemical_record::{ChemicalRecord, SegmentCount}; pub use identifier::{Identifier, IdentifierOption}; pub use model_record::{BinaryRecord, FromSegments, FromSegmentsBinary, PureRecord}; pub use segment::SegmentRecord; @@ -29,8 +29,8 @@ pub trait Parameter where Self: Sized, { - type Pure: Clone + DeserializeOwned + Default; - type IdealGas: Clone + DeserializeOwned + Default; + type Pure: Clone + DeserializeOwned; + type IdealGas: Clone + DeserializeOwned; type Binary: Clone + DeserializeOwned + Default; /// Creates parameters from records for pure substances and possibly binary parameters. @@ -187,23 +187,25 @@ where /// /// The [FromSegments] trait needs to be implemented for both the model record /// and the ideal gas record. - fn from_segments( - chemical_records: Vec, + fn from_segments( + chemical_records: Vec, segment_records: Vec>, binary_segment_records: Option>>, ) -> Result where - Self::Pure: FromSegments, - Self::IdealGas: FromSegments, - Self::Binary: FromSegmentsBinary + Default, + Self::Pure: FromSegments, + Self::IdealGas: FromSegments, + Self::Binary: FromSegmentsBinary, { // update the pure records with model and ideal gas records // calculated from the gc method let pure_records = chemical_records .iter() .map(|cr| { - cr.segment_count(&segment_records) - .map(|segments| PureRecord::from_segments(cr.identifier().clone(), segments)) + let segments = cr.segment_map(&segment_records); + segments.and_then(|segments| { + PureRecord::from_segments(cr.identifier().clone(), segments) + }) }) .collect::, _>>()?; @@ -216,9 +218,9 @@ where .collect(); // For every component: map: id -> count - let segment_id_counts: Vec<_> = chemical_records + let segment_counts: Vec<_> = chemical_records .iter() - .map(|cr| cr.segment_id_count()) + .map(|cr| cr.segment_count()) .collect(); // full matrix of binary records from the gc method. @@ -227,8 +229,8 @@ where let n = pure_records.len(); let binary_records = Array2::from_shape_fn([n, n], |(i, j)| { let mut vec = Vec::new(); - for (id1, &n1) in segment_id_counts[i].iter() { - for (id2, &n2) in segment_id_counts[j].iter() { + for (id1, &n1) in segment_counts[i].iter() { + for (id2, &n2) in segment_counts[j].iter() { let binary = binary_map .get(&(id1.clone(), id2.clone())) .or_else(|| binary_map.get(&(id2.clone(), id1.clone()))) @@ -237,7 +239,7 @@ where vec.push((binary, n1, n2)); } } - Self::Binary::from_segments_binary(&vec) + Self::Binary::from_segments_binary(&vec).unwrap() }); Ok(Self::from_records(pure_records, binary_records)) @@ -256,9 +258,9 @@ where ) -> Result where P: AsRef, - Self::Pure: FromSegments, - Self::IdealGas: FromSegments, - Self::Binary: FromSegmentsBinary, + Self::Pure: FromSegments, + Self::IdealGas: FromSegments, + Self::Binary: FromSegmentsBinary, { let queried: IndexSet = substances .iter() @@ -272,7 +274,7 @@ where .into_iter() .filter_map(|record| { record - .identifier() + .identifier .as_string(search_option) .map(|i| (i, record)) }) @@ -329,6 +331,106 @@ where } } +pub trait ParameterHetero: Sized { + type Chemical: Clone; + type Pure: Clone + DeserializeOwned; + type IdealGas: Clone + DeserializeOwned; + type Binary: Clone + DeserializeOwned; + + fn from_segments>( + chemical_records: Vec, + segment_records: Vec>, + binary_segment_records: Option>>, + ) -> Result; + + /// Return the original records that were used to construct the parameters. + #[allow(clippy::type_complexity)] + fn records( + &self, + ) -> ( + &[Self::Chemical], + &[SegmentRecord], + &Option>>, + ); + + fn from_json_segments

( + substances: &[&str], + file_pure: P, + file_segments: P, + file_binary: Option

, + search_option: IdentifierOption, + ) -> Result + where + P: AsRef, + ChemicalRecord: Into, + { + let queried: IndexSet = substances + .iter() + .map(|identifier| identifier.to_string()) + .collect(); + + let reader = BufReader::new(File::open(file_pure)?); + let chemical_records: Vec = serde_json::from_reader(reader)?; + let mut record_map: IndexMap<_, _> = chemical_records + .into_iter() + .filter_map(|record| { + record + .identifier + .as_string(search_option) + .map(|i| (i, record)) + }) + .collect(); + + // Compare queried components and available components + let available: IndexSet = record_map + .keys() + .map(|identifier| identifier.to_string()) + .collect(); + if !queried.is_subset(&available) { + let missing: Vec = queried.difference(&available).cloned().collect(); + return Err(ParameterError::ComponentsNotFound(format!("{:?}", missing))); + }; + + // Collect all pure records that were queried + let chemical_records: Vec<_> = queried + .iter() + .filter_map(|identifier| record_map.remove(&identifier.clone())) + .collect(); + + // Read segment records + let segment_records: Vec> = + SegmentRecord::from_json(file_segments)?; + + // Read binary records + let binary_records = file_binary + .map(|file_binary| { + let reader = BufReader::new(File::open(file_binary)?); + let binary_records: Result< + Vec>, + ParameterError, + > = Ok(serde_json::from_reader(reader)?); + binary_records + }) + .transpose()?; + + Self::from_segments(chemical_records, segment_records, binary_records) + } + + fn subset(&self, component_list: &[usize]) -> Self { + let (chemical_records, segment_records, binary_segment_records) = self.records(); + let chemical_records: Vec<_> = component_list + .iter() + .map(|&i| chemical_records[i].clone()) + .collect(); + Self::from_segments( + chemical_records, + segment_records.to_vec(), + binary_segment_records.clone(), + ) + .unwrap() + } +} + /// Error type for incomplete parameter information and IO problems. #[derive(Error, Debug)] pub enum ParameterError { @@ -447,8 +549,8 @@ mod test { ); let p = MyParameter::from_records(pure_records, binary_matrix); - assert_eq!(p.pure_records[0].identifier.cas, "123-4-5"); - assert_eq!(p.pure_records[1].identifier.cas, "678-9-1"); + assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); + assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); assert_eq!(p.binary_records[[0, 1]].b, 12.0) } @@ -500,8 +602,8 @@ mod test { ); let p = MyParameter::from_records(pure_records, binary_matrix); - assert_eq!(p.pure_records[0].identifier.cas, "123-4-5"); - assert_eq!(p.pure_records[1].identifier.cas, "678-9-1"); + assert_eq!(p.pure_records[0].identifier.cas, Some("123-4-5".into())); + assert_eq!(p.pure_records[1].identifier.cas, Some("678-9-1".into())); assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); assert_eq!(p.binary_records[[0, 1]].b, 0.0) } @@ -563,9 +665,9 @@ mod test { ); let p = MyParameter::from_records(pure_records, binary_matrix); - assert_eq!(p.pure_records[0].identifier.cas, "000-0-0"); - assert_eq!(p.pure_records[1].identifier.cas, "123-4-5"); - assert_eq!(p.pure_records[2].identifier.cas, "678-9-1"); + assert_eq!(p.pure_records[0].identifier.cas, Some("000-0-0".into())); + assert_eq!(p.pure_records[1].identifier.cas, Some("123-4-5".into())); + assert_eq!(p.pure_records[2].identifier.cas, Some("678-9-1".into())); assert_eq!(p.binary_records[[0, 1]], MyBinaryModel::default()); assert_eq!(p.binary_records[[1, 0]], MyBinaryModel::default()); assert_eq!(p.binary_records[[0, 2]], MyBinaryModel::default()); diff --git a/src/parameter/model_record.rs b/src/parameter/model_record.rs index 8aa4844..baa34a8 100644 --- a/src/parameter/model_record.rs +++ b/src/parameter/model_record.rs @@ -1,5 +1,7 @@ use super::identifier::Identifier; use super::segment::SegmentRecord; +use super::ParameterError; +use conv::ValueInto; use serde::{Deserialize, Serialize}; /// A collection of parameters of a pure substance. @@ -8,7 +10,7 @@ pub struct PureRecord { pub identifier: Identifier, pub molarweight: f64, pub model_record: M, - #[serde(default)] + #[serde(default = "Default::default")] #[serde(skip_serializing_if = "Option::is_none")] pub ideal_gas_record: Option, } @@ -33,26 +35,35 @@ impl PureRecord { /// /// The [FromSegments] trait needs to be implemented for both the model record /// and the ideal gas record. - pub fn from_segments(identifier: Identifier, segments: S) -> Self + pub fn from_segments(identifier: Identifier, segments: S) -> Result where - M: FromSegments, - I: FromSegments, - S: IntoIterator, f64)>, + T: Copy + ValueInto, + M: FromSegments, + I: FromSegments, + S: IntoIterator, T)>, { let mut molarweight = 0.0; let mut model_segments = Vec::new(); let mut ideal_gas_segments = Vec::new(); for (s, n) in segments { - molarweight += s.molarweight * n; + molarweight += s.molarweight * n.value_into().unwrap(); model_segments.push((s.model_record, n)); ideal_gas_segments.push(s.ideal_gas_record.map(|ig| (ig, n))); } - let model_record = M::from_segments(&model_segments); + let model_record = M::from_segments(&model_segments)?; let ideal_gas_segments: Option> = ideal_gas_segments.into_iter().collect(); - let ideal_gas_record = ideal_gas_segments.as_deref().map(I::from_segments); + let ideal_gas_record = ideal_gas_segments + .as_deref() + .map(I::from_segments) + .transpose()?; - Self::new(identifier, molarweight, model_record, ideal_gas_record) + Ok(Self::new( + identifier, + molarweight, + model_record, + ideal_gas_record, + )) } } @@ -75,18 +86,18 @@ where /// Trait for models that implement a homosegmented group contribution /// method -pub trait FromSegments: Clone { +pub trait FromSegments: Clone { /// Constructs the record from a list of segment records with their - /// number of occurences and possibly binary interaction parameters. - fn from_segments(segments: &[(Self, f64)]) -> Self; + /// number of occurences. + fn from_segments(segments: &[(Self, T)]) -> Result; } /// Trait for models that implement a homosegmented group contribution /// method and have a combining rule for binary interaction parameters. -pub trait FromSegmentsBinary: Clone { - /// Constructs the record from a list of segment records with their - /// number of occurences and possibly binary interaction parameters. - fn from_segments_binary(segments: &[(Self, f64, f64)]) -> Self; +pub trait FromSegmentsBinary: Clone { + /// Constructs the binary record from a list of segment records with + /// their number of occurences. + fn from_segments_binary(segments: &[(Self, T, T)]) -> Result; } /// A collection of parameters that model interactions between two @@ -151,7 +162,7 @@ mod test { "#; let record: PureRecord = serde_json::from_str(r).expect("Unable to parse json."); - assert_eq!(record.identifier.cas, "123-4-5") + assert_eq!(record.identifier.cas, Some("123-4-5".into())) } #[test] @@ -179,7 +190,7 @@ mod test { ]"#; let records: Vec> = serde_json::from_str(r).expect("Unable to parse json."); - assert_eq!(records[0].identifier.cas, "1"); - assert_eq!(records[1].identifier.cas, "2") + assert_eq!(records[0].identifier.cas, Some("1".into())); + assert_eq!(records[1].identifier.cas, Some("2".into())) } } diff --git a/src/python/parameter.rs b/src/python/parameter.rs index 1a85955..6816380 100644 --- a/src/python/parameter.rs +++ b/src/python/parameter.rs @@ -1,9 +1,7 @@ use crate::impl_json_handling; use crate::parameter::{BinaryRecord, ChemicalRecord, Identifier, ParameterError}; -use either::Either; -use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; -use std::collections::HashMap; impl From for PyErr { fn from(e: ParameterError) -> PyErr { @@ -15,7 +13,7 @@ impl From for PyErr { /// /// Parameters /// ---------- -/// cas : str +/// cas : str, optional /// CAS number. /// name : str, optional /// name @@ -33,14 +31,16 @@ impl From for PyErr { /// Identifier #[pyclass(name = "Identifier")] #[derive(Clone)] -#[pyo3(text_signature = "(cas, name=None, iupac_name=None, smiles=None, inchi=None, formula=None)")] +#[pyo3( + text_signature = "(cas=None, name=None, iupac_name=None, smiles=None, inchi=None, formula=None)" +)] pub struct PyIdentifier(pub Identifier); #[pymethods] impl PyIdentifier { #[new] fn new( - cas: &str, + cas: Option<&str>, name: Option<&str>, iupac_name: Option<&str>, smiles: Option<&str>, @@ -53,13 +53,13 @@ impl PyIdentifier { } #[getter] - fn get_cas(&self) -> String { + fn get_cas(&self) -> Option { self.0.cas.clone() } #[setter] fn set_cas(&mut self, cas: &str) { - self.0.cas = cas.to_string(); + self.0.cas = Some(cas.to_string()); } #[getter] @@ -144,63 +144,27 @@ pub struct PyChemicalRecord(pub ChemicalRecord); #[pymethods] impl PyChemicalRecord { #[new] - fn new(identifier: PyIdentifier, segments: &PyAny, bonds: Option<&PyAny>) -> PyResult { - if let Ok(segments) = segments.extract::>() { - let bonds = bonds - .map(|bonds| bonds.extract::>()) - .transpose()?; - Ok(Self(ChemicalRecord::new(identifier.0, segments, bonds))) - } else if let Ok(segments) = segments.extract::>() { - let bonds = bonds - .map(|bonds| bonds.extract::>()) - .transpose()?; - Ok(Self(ChemicalRecord::new_count( - identifier.0, - segments, - bonds, - ))) - } else { - Err(PyValueError::new_err( - "`segments` must either be a list or a dict of strings.", - )) - } + fn new( + identifier: PyIdentifier, + segments: Vec, + bonds: Option>, + ) -> Self { + Self(ChemicalRecord::new(identifier.0, segments, bonds)) } #[getter] fn get_identifier(&self) -> PyIdentifier { - PyIdentifier(self.0.identifier().clone()) + PyIdentifier(self.0.identifier.clone()) } #[getter] - fn get_segments(&self, py: Python) -> PyObject { - match &self.0.segments() { - Either::Left(segments) => segments.to_object(py), - Either::Right(segments) => segments.to_object(py), - } + fn get_segments(&self) -> Vec { + self.0.segments.clone() } #[getter] - fn get_bonds(&self, py: Python) -> PyObject { - match &self.0 { - ChemicalRecord::List { - identifier: _, - segments: _, - bonds, - } => bonds - .iter() - .map(|[a, b]| (a, b)) - .collect::>() - .to_object(py), - ChemicalRecord::Count { - identifier: _, - segments: _, - bonds, - } => bonds - .iter() - .map(|([a, b], c)| ((a, b), c)) - .collect::>() - .to_object(py), - } + fn get_bonds(&self) -> Vec<[usize; 2]> { + self.0.bonds.clone() } fn __repr__(&self) -> PyResult { @@ -275,7 +239,7 @@ macro_rules! impl_binary_record { #[getter] fn get_model_record(&self, py: Python) -> PyObject { - if let Ok(mr) = f64::try_from(self.0.model_record) { + if let Ok(mr) = f64::try_from(self.0.model_record.clone()) { mr.to_object(py) } else { $py_model_record(self.0.model_record.clone()).into_py(py) @@ -742,7 +706,7 @@ macro_rules! impl_parameter_from_segments { /// Parameters /// ---------- /// chemical_records : [ChemicalRecord] - /// A list of pure component parameters. + /// A list of pure component chemical records. /// segment_records : [SegmentRecord] /// A list of records containing the parameters of /// all individual segments. From dcfc9f217eba020a0d526d98b6fa797a6bdf9ab0 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 7 Jun 2022 11:40:40 +0200 Subject: [PATCH 2/5] small improvements --- src/joback.rs | 11 ++++++----- src/parameter/chemical_record.rs | 7 ++++--- src/parameter/identifier.rs | 27 +++++++++++++++++++-------- src/parameter/mod.rs | 30 ++++++++++++++++-------------- 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/joback.rs b/src/joback.rs index 2640421..1ec0902 100644 --- a/src/joback.rs +++ b/src/joback.rs @@ -54,11 +54,12 @@ impl> FromSegments for JobackRecord { let mut d = 2.06e-7; let mut e = 0.0; segments.iter().for_each(|(s, n)| { - a += s.a * (*n).value_into().unwrap(); - b += s.b * (*n).value_into().unwrap(); - c += s.c * (*n).value_into().unwrap(); - d += s.d * (*n).value_into().unwrap(); - e += s.e * (*n).value_into().unwrap(); + let n = (*n).value_into().unwrap(); + a += s.a * n; + b += s.b * n; + c += s.c * n; + d += s.d * n; + e += s.e * n; }); Ok(Self { a, b, c, d, e }) } diff --git a/src/parameter/chemical_record.rs b/src/parameter/chemical_record.rs index f9cce54..619dc70 100644 --- a/src/parameter/chemical_record.rs +++ b/src/parameter/chemical_record.rs @@ -6,7 +6,7 @@ use num_traits::NumAssign; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; -// Auxiliary structure used to deserialize chemical records without bond information. +// Auxiliary structure used to deserialize chemical records without explicit bond information. #[derive(Serialize, Deserialize)] struct ChemicalRecordJSON { identifier: Identifier, @@ -104,16 +104,17 @@ impl std::fmt::Display for ChemicalRecord { /// Trait that enables parameter generation from generic molecular representations. pub trait SegmentCount { type Count: Copy + ValueInto; + fn identifier(&self) -> &Identifier; /// Count the number of occurences of each individual segment identifier in the - /// chemical record. + /// molecule. /// /// The map contains the segment identifier as key and the count as value. fn segment_count(&self) -> HashMap; /// Count the number of occurences of each individual segment in the - /// chemical record. + /// molecule. /// /// The map contains the segment record as key and the count as value. fn segment_map( diff --git a/src/parameter/identifier.rs b/src/parameter/identifier.rs index c7b01d6..e06bf7c 100644 --- a/src/parameter/identifier.rs +++ b/src/parameter/identifier.rs @@ -108,26 +108,26 @@ impl Identifier { impl std::fmt::Display for Identifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Identifier(")?; + let mut ids = Vec::new(); if let Some(n) = &self.cas { - write!(f, "cas={}, ", n)?; + ids.push(format!("cas={}", n)); } if let Some(n) = &self.name { - write!(f, "name={}, ", n)?; + ids.push(format!("name={}", n)); } if let Some(n) = &self.iupac_name { - write!(f, "iupac_name={}, ", n)?; + ids.push(format!("iupac_name={}", n)); } if let Some(n) = &self.smiles { - write!(f, "smiles={}, ", n)?; + ids.push(format!("smiles={}", n)); } if let Some(n) = &self.inchi { - write!(f, "inchi={}, ", n)?; + ids.push(format!("inchi={}", n)); } if let Some(n) = &self.formula { - write!(f, "formula={}", n)?; + ids.push(format!("formula={}", n)); } - write!(f, ")") + write!(f, "Identifier({})", ids.join(", ")) } } @@ -143,3 +143,14 @@ impl Hash for Identifier { self.cas.hash(state); } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_fmt() { + let id = Identifier::new(None, Some("acetone"), None, Some("CC(=O)C"), None, None); + assert_eq!(id.to_string(), "Identifier(name=acetone, smiles=CC(=O)C)"); + } +} diff --git a/src/parameter/mod.rs b/src/parameter/mod.rs index 63aa59a..4eba9b1 100644 --- a/src/parameter/mod.rs +++ b/src/parameter/mod.rs @@ -202,8 +202,7 @@ where let pure_records = chemical_records .iter() .map(|cr| { - let segments = cr.segment_map(&segment_records); - segments.and_then(|segments| { + cr.segment_map(&segment_records).and_then(|segments| { PureRecord::from_segments(cr.identifier().clone(), segments) }) }) @@ -227,20 +226,23 @@ where // If a specific segment-segment interaction is not in the binary map, // the default value is used. let n = pure_records.len(); - let binary_records = Array2::from_shape_fn([n, n], |(i, j)| { - let mut vec = Vec::new(); - for (id1, &n1) in segment_counts[i].iter() { - for (id2, &n2) in segment_counts[j].iter() { - let binary = binary_map - .get(&(id1.clone(), id2.clone())) - .or_else(|| binary_map.get(&(id2.clone(), id1.clone()))) - .cloned() - .unwrap_or_default(); - vec.push((binary, n1, n2)); + let mut binary_records = Array2::default([n, n]); + for i in 0..n { + for j in 0..n { + let mut vec = Vec::new(); + for (id1, &n1) in segment_counts[i].iter() { + for (id2, &n2) in segment_counts[j].iter() { + let binary = binary_map + .get(&(id1.clone(), id2.clone())) + .or_else(|| binary_map.get(&(id2.clone(), id1.clone()))) + .cloned() + .unwrap_or_default(); + vec.push((binary, n1, n2)); + } } + binary_records[(i, j)] = Self::Binary::from_segments_binary(&vec)? } - Self::Binary::from_segments_binary(&vec).unwrap() - }); + } Ok(Self::from_records(pure_records, binary_records)) } From 9a18ce29a3c482a9abba32fddffd3ee2e456fdba Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 7 Jun 2022 11:45:46 +0200 Subject: [PATCH 3/5] Add docstrings for `ParameterHetero` --- src/parameter/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/parameter/mod.rs b/src/parameter/mod.rs index 4eba9b1..5f89385 100644 --- a/src/parameter/mod.rs +++ b/src/parameter/mod.rs @@ -318,6 +318,7 @@ where Self::from_segments(chemical_records, segment_records, binary_records) } + /// Return a parameter set containing the subset of components specified in `component_list`. fn subset(&self, component_list: &[usize]) -> Self { let (pure_records, binary_records) = self.records(); let pure_records = component_list @@ -333,12 +334,14 @@ where } } +/// Constructor methods for parameters for heterosegmented models. pub trait ParameterHetero: Sized { type Chemical: Clone; type Pure: Clone + DeserializeOwned; type IdealGas: Clone + DeserializeOwned; type Binary: Clone + DeserializeOwned; + /// Creates parameters from the molecular structure and segment information. fn from_segments>( chemical_records: Vec, segment_records: Vec>, @@ -355,6 +358,7 @@ pub trait ParameterHetero: Sized { &Option>>, ); + /// Creates parameters from segment information stored in json files. fn from_json_segments

( substances: &[&str], file_pure: P, @@ -418,6 +422,7 @@ pub trait ParameterHetero: Sized { Self::from_segments(chemical_records, segment_records, binary_records) } + /// Return a parameter set containing the subset of components specified in `component_list`. fn subset(&self, component_list: &[usize]) -> Self { let (chemical_records, segment_records, binary_segment_records) = self.records(); let chemical_records: Vec<_> = component_list From a084f949bb5006eda9ab0432aec621aa78bc79af Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 7 Jun 2022 13:05:59 +0200 Subject: [PATCH 4/5] Use Cow in `SegmentCount` --- src/parameter/chemical_record.rs | 21 ++++++++++++--------- src/parameter/mod.rs | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/parameter/chemical_record.rs b/src/parameter/chemical_record.rs index 619dc70..f498b1b 100644 --- a/src/parameter/chemical_record.rs +++ b/src/parameter/chemical_record.rs @@ -4,7 +4,10 @@ use super::ParameterError; use conv::ValueInto; use num_traits::NumAssign; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, +}; // Auxiliary structure used to deserialize chemical records without explicit bond information. #[derive(Serialize, Deserialize)] @@ -105,13 +108,13 @@ impl std::fmt::Display for ChemicalRecord { pub trait SegmentCount { type Count: Copy + ValueInto; - fn identifier(&self) -> &Identifier; + fn identifier(&self) -> Cow; /// Count the number of occurences of each individual segment identifier in the /// molecule. /// /// The map contains the segment identifier as key and the count as value. - fn segment_count(&self) -> HashMap; + fn segment_count(&self) -> Cow>; /// Count the number of occurences of each individual segment in the /// molecule. @@ -134,8 +137,8 @@ pub trait SegmentCount { return Err(ParameterError::ComponentsNotFound(msg)); }; Ok(count - .into_iter() - .map(|(s, c)| (segments.remove(&s).unwrap(), c)) + .iter() + .map(|(s, c)| (segments.remove(s).unwrap(), *c)) .collect()) } } @@ -143,11 +146,11 @@ pub trait SegmentCount { impl SegmentCount for ChemicalRecord { type Count = usize; - fn identifier(&self) -> &Identifier { - &self.identifier + fn identifier(&self) -> Cow { + Cow::Borrowed(&self.identifier) } - fn segment_count(&self) -> HashMap { - self.segment_count() + fn segment_count(&self) -> Cow> { + Cow::Owned(self.segment_count()) } } diff --git a/src/parameter/mod.rs b/src/parameter/mod.rs index 5f89385..bee40af 100644 --- a/src/parameter/mod.rs +++ b/src/parameter/mod.rs @@ -203,7 +203,7 @@ where .iter() .map(|cr| { cr.segment_map(&segment_records).and_then(|segments| { - PureRecord::from_segments(cr.identifier().clone(), segments) + PureRecord::from_segments(cr.identifier().into_owned(), segments) }) }) .collect::, _>>()?; From 2a7c69cec87c235d84c6c27df7d8edbcaca08655 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 7 Jun 2022 15:27:42 +0200 Subject: [PATCH 5/5] update CHANGELOG --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 947c4bc..dc05647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added - Added `pure_records` getter in the `impl_parameter` macro. [#54](https://github.com/feos-org/feos-core/pull/54) +- Implemented `Deref` and `IntoIterator` for `StateVec` for additional vector functionalities of the `StateVec`. [#55](https://github.com/feos-org/feos-core/pull/55) +- Added `StateVec.__len__` and `StateVec.__getitem__` to allow indexing and iterating over `StateVec`s in Python. [#55](https://github.com/feos-org/feos-core/pull/55) +- Added `SegmentCount` trait that allows the construction of parameter sets from arbitrary chemical records. [#56](https://github.com/feos-org/feos-core/pull/56) +- Added `ParameterHetero` trait to generically provide utility functions for parameter sets of heterosegmented Helmholtz energy models. [#56](https://github.com/feos-org/feos-core/pull/56) ### Changed - Changed datatype for binary parameters in interfaces of the `from_records` and `new_binary` methods for parameters to take either numpy arrays of `f64` or a list of `BinaryRecord` as input. [#54](https://github.com/feos-org/feos-core/pull/54) +- Modified `PhaseDiagram.to_dict` function in Python to account for pure components and mixtures. [#55](https://github.com/feos-org/feos-core/pull/55) +- Changed `StateVec` to a tuple struct. [#55](https://github.com/feos-org/feos-core/pull/55) +- Made `cas` field of `Identifier` optional. [#56](https://github.com/feos-org/feos-core/pull/56) +- Added type parameter to `FromSegments` and made its `from_segments` function fallible for more control over model limitations. [#56](https://github.com/feos-org/feos-core/pull/56) +- Reverted `ChemicalRecord` back to a struct that only contains the structural information (and not segment and bond counts). [#56](https://github.com/feos-org/feos-core/pull/56) ## [0.2.0] - 2022-04-12 ### Added