diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb7d490f0..a083dc18e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: strategy: fail-fast: false matrix: - model: [pcsaft, epcsaft, gc_pcsaft, pets, uvtheory, saftvrqmie, saftvrmie] + model: [pcsaft, epcsaft, gc_pcsaft, pets, uvtheory, saftvrqmie, saftvrmie, cubic] steps: - uses: actions/checkout@v4 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..1c2fda565 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/feos_cubics.iml b/.idea/feos_cubics.iml new file mode 100644 index 000000000..eeff0533e --- /dev/null +++ b/.idea/feos_cubics.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..56956f14d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..c8397c94c --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index aedef87ae..c26a873c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ serde_json = "1.0" indexmap = "2.0" itertools = "0.14" typenum = "1.16" +enum_dispatch = "0.3.13" rayon = "1.5" petgraph = "0.8" rustdct = "0.7" diff --git a/crates/feos-core/src/parameter/segment.rs b/crates/feos-core/src/parameter/segment.rs new file mode 100644 index 000000000..e69de29bb diff --git a/crates/feos/Cargo.toml b/crates/feos/Cargo.toml index 319e98fb1..e3e7dc0bf 100644 --- a/crates/feos/Cargo.toml +++ b/crates/feos/Cargo.toml @@ -13,7 +13,7 @@ description = "FeOs - A framework for equations of state and classical density f [dependencies] quantity = { workspace = true } num-dual = { workspace = true } -ndarray = { workspace = true } +ndarray = { workspace = true } # updated without this feature?, features = ["serde"] } petgraph = { workspace = true, optional = true } thiserror = { workspace = true } num-traits = { workspace = true } @@ -23,6 +23,7 @@ rayon = { workspace = true, optional = true } itertools = { workspace = true } typenum = { workspace = true } arrayvec = { workspace = true, features = ["serde"] } +enum_dispatch = {workspace = true, optional = true} feos-core = { workspace = true } feos-derive = { workspace = true } @@ -46,6 +47,7 @@ pets = [] saftvrqmie = [] saftvrmie = ["association"] rayon = ["dep:rayon", "ndarray/rayon", "feos-core/rayon", "feos-dft?/rayon"] +cubic = ["enum_dispatch"] all_models = [ "dft", "pcsaft", @@ -55,6 +57,7 @@ all_models = [ "pets", "saftvrqmie", "saftvrmie", + "cubic" ] [[bench]] diff --git a/crates/feos/src/cubic/alpha/mathias_copeman.rs b/crates/feos/src/cubic/alpha/mathias_copeman.rs new file mode 100644 index 000000000..fee29731b --- /dev/null +++ b/crates/feos/src/cubic/alpha/mathias_copeman.rs @@ -0,0 +1,49 @@ +use super::AlphaFunction; +use crate::cubic::parameters::CubicParameters; +use feos_core::{FeosError, FeosResult}; +use ndarray::{Array1, Zip}; +use num_dual::DualNum; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct MathiasCopeman(pub Vec<[f64; 3]>); + +impl AlphaFunction for MathiasCopeman { + #[inline] + fn alpha + Copy>( + &self, + _: &Array1, + reduced_temperature: &Array1, + ) -> Array1 { + Zip::from(reduced_temperature) + .and(&self.0) + .map_collect(|tr, c| { + let trsq = -tr.sqrt() + 1.0; + let a1 = trsq + c[0] + 1.0; + let a2 = match tr { + tr if tr.re() < 1.0 => trsq * (c[1] + c[2]), + _ => D::zero(), + }; + (a1 + a2).powi(2) + }) + } + + fn validate(&self, parameters: &Arc) -> FeosResult<()> { + if self.0.len() == parameters.tc.len() { + Ok(()) + } else { + Err(FeosError::IncompatibleParameters( + format!( + "Mathias Copeman alpha function was initialized for {} components, but the equation of state contains {}.", + self.0.len(), parameters.tc.len() + ) + ).into()) + } + } + + fn subset(&self, component_list: &[usize]) -> Self { + let mi = component_list.iter().map(|&i| self.0[i]).collect(); + Self(mi) + } +} diff --git a/crates/feos/src/cubic/alpha/mod.rs b/crates/feos/src/cubic/alpha/mod.rs new file mode 100644 index 000000000..f44afa1d3 --- /dev/null +++ b/crates/feos/src/cubic/alpha/mod.rs @@ -0,0 +1,45 @@ +use super::parameters::CubicParameters; +use enum_dispatch::enum_dispatch; +use feos_core::FeosResult; +pub use mathias_copeman::MathiasCopeman; +use ndarray::{Array1, ScalarOperand}; +use num_dual::DualNum; +pub use soave::{ + PengRobinson1976, PengRobinson1978, PengRobinson2019, RedlichKwong1972, RedlichKwong2019, Soave, +}; +use std::sync::Arc; +pub use twu::{GeneralizedTwu, Twu}; + +mod mathias_copeman; +mod soave; +mod twu; + +#[enum_dispatch] +pub trait AlphaFunction { + fn alpha + Copy + ScalarOperand>( + &self, + acentric_factor: &Array1, + reduced_temperature: &Array1, + ) -> Array1; + + /// Check for validity of alpha function against parameters, e.g. + /// to assert that the number of components match. + fn validate(&self, parameters: &Arc) -> FeosResult<()>; + + /// Generate the alpha function for a subset of components. + fn subset(&self, component_list: &[usize]) -> Self; +} + +#[enum_dispatch(AlphaFunction)] +#[derive(Debug, Clone)] +pub enum Alpha { + Soave, + PengRobinson1976, + PengRobinson1978, + PengRobinson2019, + RedlichKwong1972, + RedlichKwong2019, + MathiasCopeman, + GeneralizedTwu, + Twu, +} diff --git a/crates/feos/src/cubic/alpha/soave.rs b/crates/feos/src/cubic/alpha/soave.rs new file mode 100644 index 000000000..52d3c95ae --- /dev/null +++ b/crates/feos/src/cubic/alpha/soave.rs @@ -0,0 +1,170 @@ +use super::AlphaFunction; +use crate::cubic::parameters::CubicParameters; +use feos_core::FeosResult; +use ndarray::{Array1, Zip}; +use num_dual::DualNum; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +/// Generic version of Soave's function using 3rd order polynomial. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Soave { + /// coefficients for m-polynomial + mi: Array1, +} + +impl Soave { + pub fn new(mi: Array1) -> Self { + Soave { mi } + } +} + +impl AlphaFunction for Soave { + #[inline] + fn alpha>( + &self, + acentric_factor: &Array1, + reduced_temperature: &Array1, + ) -> Array1 { + let m = self + .mi + .iter() + .enumerate() + .fold(Array1::zeros(acentric_factor.len()), |m, (i, &mi)| { + &m + acentric_factor.mapv(|w| w.powi(i as i32)) * mi + }); + ((-reduced_temperature.mapv(|t| t.sqrt()) + 1.0) * m + 1.0).mapv(|a| a.powi(2)) + } + + fn validate(&self, _: &Arc) -> FeosResult<()> { + Ok(()) + } + + fn subset(&self, _: &[usize]) -> Self { + self.clone() + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct RedlichKwong1972; + +impl AlphaFunction for RedlichKwong1972 { + #[inline] + fn alpha>( + &self, + acentric_factor: &Array1, + reduced_temperature: &Array1, + ) -> Array1 { + let m = acentric_factor.mapv(|w| 0.48 + w * (1.574 - w * 0.176)); + ((-reduced_temperature.mapv(|t| t.sqrt()) + 1.0) * m + 1.0).mapv(|a| a.powi(2)) + } + + fn validate(&self, _: &Arc) -> FeosResult<()> { + Ok(()) + } + + fn subset(&self, _: &[usize]) -> Self { + Self + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PengRobinson1976; + +impl AlphaFunction for PengRobinson1976 { + #[inline] + fn alpha>( + &self, + acentric_factor: &Array1, + reduced_temperature: &Array1, + ) -> Array1 { + let m = acentric_factor.mapv(|w| 0.37464 + w * (1.54226 - w * 0.26992)); + ((-reduced_temperature.mapv(|t| t.sqrt()) + 1.0) * m + 1.0).mapv(|a| a.powi(2)) + } + + fn validate(&self, _: &Arc) -> FeosResult<()> { + Ok(()) + } + + fn subset(&self, _: &[usize]) -> Self { + Self + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PengRobinson1978; + +impl AlphaFunction for PengRobinson1978 { + #[inline] + fn alpha + Copy>( + &self, + acentric_factor: &Array1, + reduced_temperature: &Array1, + ) -> Array1 { + Zip::from(acentric_factor) + .and(reduced_temperature) + .map_collect(|&w, &tr| { + let m = if w <= 0.491 { + 0.37464 + w * (1.54226 - w * 0.26992) + } else { + // use higher-order polynomial if w > w(n-decane) + 0.379642 + w * (1.48503 + w * (-0.164423 + w * 0.016666)) + }; + ((-tr.sqrt() + 1.0) * m + 1.0).powi(2) + }) + } + + fn validate(&self, _: &Arc) -> FeosResult<()> { + Ok(()) + } + + fn subset(&self, _: &[usize]) -> Self { + Self + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct RedlichKwong2019; + +impl AlphaFunction for RedlichKwong2019 { + #[inline] + fn alpha>( + &self, + acentric_factor: &Array1, + reduced_temperature: &Array1, + ) -> Array1 { + let m = acentric_factor.mapv(|w| 0.481 + w * (1.5963 + w * (-0.2963 + w * 0.1223))); + ((-reduced_temperature.mapv(|t| t.sqrt()) + 1.0) * m + 1.0).mapv(|a| a.powi(2)) + } + + fn validate(&self, _: &Arc) -> FeosResult<()> { + Ok(()) + } + + fn subset(&self, _: &[usize]) -> Self { + Self + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PengRobinson2019; + +impl AlphaFunction for PengRobinson2019 { + #[inline] + fn alpha>( + &self, + acentric_factor: &Array1, + reduced_temperature: &Array1, + ) -> Array1 { + let m = acentric_factor.mapv(|w| 0.3919 + w * (1.4996 + w * (-0.2721 + w * 0.1063))); + ((-reduced_temperature.mapv(|t| t.sqrt()) + 1.0) * m + 1.0).mapv(|a| a.powi(2)) + } + + fn validate(&self, _: &Arc) -> FeosResult<()> { + Ok(()) + } + + fn subset(&self, _: &[usize]) -> Self { + Self + } +} diff --git a/crates/feos/src/cubic/alpha/twu.rs b/crates/feos/src/cubic/alpha/twu.rs new file mode 100644 index 000000000..198e69c8f --- /dev/null +++ b/crates/feos/src/cubic/alpha/twu.rs @@ -0,0 +1,124 @@ +use super::AlphaFunction; +use crate::cubic::parameters::CubicParameters; +use feos_core::{FeosError, FeosResult}; +use itertools::izip; +use ndarray::{Array1, Zip}; +use num_dual::DualNum; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +/// Generalized version of the Twu alpha function (1995). +/// +/// Different parameters are used for sub- and supercritical conditions. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct GeneralizedTwu([[[f64; 3]; 2]; 2]); + +impl GeneralizedTwu { + pub fn redlich_kwong() -> Self { + GeneralizedTwu([ + [ + [2.496441 * (0.919422 - 1.0), 0.141599, 2.496441 * 0.919422], + [3.291790 * (0.799457 - 1.0), 0.500315, 3.291790 * 0.799457], + ], + [ + [-0.2 * (6.500018 - 1.0), 0.441411, -0.2 * 6.500018], + [-8.0 * (1.289098 - 1.0), 0.032580, -8.0 * 1.289098], + ], + ]) + } + + pub fn peng_robinson() -> Self { + GeneralizedTwu([ + [ + [1.948150 * (0.911807 - 1.0), 0.125283, 1.948150 * 0.911807], + [2.812520 * (0.784054 - 1.0), 0.511614, 2.812520 * 0.784054], + ], + [ + [-0.2 * (4.963070 - 1.0), 0.401219, -0.2 * 4.963070], + [-0.8 * (1.248089 - 1.0), 0.024955, -0.8 * 1.248089], + ], + ]) + } +} + +impl AlphaFunction for GeneralizedTwu { + #[inline] + fn alpha + Copy>( + &self, + acentric_factor: &Array1, + reduced_temperature: &Array1, + ) -> Array1 { + Zip::from(acentric_factor) + .and(reduced_temperature) + .map_collect(|&w, &tr| { + if tr.re() <= 1.0 { + let [nm_m1_0, l0, nm0] = self.0[0][0]; + let [nm_m1_1, l1, nm1] = self.0[0][1]; + let a0 = tr.powf(nm_m1_0) * ((-tr.powf(nm0) + 1.0) * l0).exp(); + let a1 = tr.powf(nm_m1_1) * ((-tr.powf(nm1) + 1.0) * l1).exp(); + a0 + (a1 - a0) * w + } else { + let [nm_m1_0, l0, nm0] = self.0[1][0]; + let [nm_m1_1, l1, nm1] = self.0[1][1]; + let a0 = tr.powf(nm_m1_0) * ((-tr.powf(nm0) + 1.0) * l0).exp(); + let a1 = tr.powf(nm_m1_1) * ((-tr.powf(nm1) + 1.0) * l1).exp(); + a0 + (a1 - a0) * w + } + }) + } + + fn validate(&self, _: &Arc) -> FeosResult<()> { + Ok(()) + } + + fn subset(&self, _: &[usize]) -> Self { + self.clone() + } +} + +/// Generalized version of the Twu alpha function (1995). +/// +/// Different parameters are used for sub- and supercritical conditions. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Twu(Vec<[f64; 3]>); + +impl Twu { + pub fn new(l: Vec, m: Vec, n: Option>) -> Self { + let _n = n.unwrap_or(vec![2.0; l.len()]); + let input = izip!(l, m, _n) + .map(|(l, m, n)| [n * (m - 1.0), l, n * m]) + .collect(); + Self(input) + } +} + +impl AlphaFunction for Twu { + #[inline] + fn alpha + Copy>( + &self, + _: &Array1, + reduced_temperature: &Array1, + ) -> Array1 { + izip!(reduced_temperature, &self.0) + .map(|(tr, &[nmm1, l, nm])| tr.powf(nmm1) * ((-tr.powf(nm) + 1.0) * l).exp()) + .collect() + } + + fn validate(&self, parameters: &Arc) -> FeosResult<()> { + if self.0.len() == parameters.tc.len() { + Ok(()) + } else { + Err(FeosError::IncompatibleParameters( + format!( + "Twu alpha function was initialized for {} components, but the equation of state contains {}.", + self.0.len(), parameters.tc.len() + ) + ).into()) + } + } + + fn subset(&self, component_list: &[usize]) -> Self { + let c = component_list.iter().map(|&i| self.0[i]).collect(); + Self(c) + } +} diff --git a/crates/feos/src/cubic/mixing_rules.rs b/crates/feos/src/cubic/mixing_rules.rs new file mode 100644 index 000000000..bde1f7748 --- /dev/null +++ b/crates/feos/src/cubic/mixing_rules.rs @@ -0,0 +1,62 @@ +use super::{Cubic, alpha::AlphaFunction}; +use enum_dispatch::enum_dispatch; +use feos_core::StateHD; +use ndarray::ScalarOperand; +use num_dual::DualNum; + +/// Parameters of cubics +pub struct MixtureParameters { + /// attractive contribution divided by R + pub a: D, + /// repulsive contribution + pub b: D, + /// volume translation + pub c: D, +} + +#[enum_dispatch] +pub trait MixingRuleFunction { + fn apply + Copy + ScalarOperand>( + &self, + cubic: &Cubic, + state: &StateHD, + ) -> MixtureParameters; +} + +/// Quadratic summation over a and b. +#[derive(Debug, Clone)] +pub struct Quadratic; + +impl MixingRuleFunction for Quadratic { + fn apply + Copy + ScalarOperand>( + &self, + cubic: &Cubic, + state: &StateHD, + ) -> MixtureParameters { + let p = &cubic.parameters; + let pc = &cubic.critical_parameters; + let n = p.tc.len(); + let tr = p.tc.mapv(|tc| state.temperature / tc); + let at = cubic.options.alpha.alpha(&p.acentric_factor, &tr) * &pc.ac; + let mut a = D::zero(); + let mut b = D::zero(); + for i in 0..n { + let xi = state.molefracs[i]; + let ai = at[i]; + let bi = pc.bc[i]; + a += xi * xi * ai; + b += xi * xi * bi; + for j in i + 1..n { + a += xi * state.molefracs[j] * (ai * at[j]).sqrt() * (1.0 - p.k_ij[[i, j]]) * 2.0; + b += xi * state.molefracs[j] * (bi + pc.bc[j]) * 0.5 * (1.0 - p.l_ij[[i, j]]) * 2.0; + } + } + MixtureParameters { a, b, c: D::zero() } + } +} + +#[enum_dispatch(MixingRuleFunction)] +#[derive(Debug, Clone)] +pub enum MixingRule { + Quadratic, +} diff --git a/crates/feos/src/cubic/mod.rs b/crates/feos/src/cubic/mod.rs new file mode 100644 index 000000000..a8247a7a5 --- /dev/null +++ b/crates/feos/src/cubic/mod.rs @@ -0,0 +1,396 @@ +use alpha::{Alpha, AlphaFunction, PengRobinson1976, RedlichKwong1972}; +use feos_core::FeosResult; +use feos_core::parameter::Parameters; +use feos_core::{Components, Residual}; +use feos_core::{Molarweight, StateHD}; +use mixing_rules::{MixingRule, MixingRuleFunction, MixtureParameters, Quadratic}; +use ndarray::{Array1, ScalarOperand, Zip}; +use num_dual::DualNum; +use parameters::CubicParameters; +use quantity::{GRAM, MOL, MolarWeight}; +use std::f64::consts::SQRT_2; +use std::fmt; +use std::sync::Arc; + +mod alpha; +mod mixing_rules; +mod parameters; + +const KB_A3: f64 = 13806490.0; + +#[derive(Debug, Clone, Copy)] +pub struct Delta { + d1: f64, + d2: f64, + d12: f64, +} + +impl From<(f64, f64)> for Delta { + fn from(value: (f64, f64)) -> Self { + Delta { + d1: value.0, + d2: value.1, + d12: value.0 - value.1, + } + } +} + +impl Delta { + // Calculate universal critical constants from universal cubic parameters. + // + // See https://doi.org/10.1016/j.fluid.2012.05.008 + fn critical_constants(&self) -> (f64, f64) { + let (r1, r2) = (-self.d1, -self.d2); + let eta_c = 1.0 + / (((1.0 - r1) * (1.0 - r2).powi(2)).cbrt() + + ((1.0 - r2) * (1.0 - r1).powi(2)).cbrt() + + 1.0); + let omega_a = (1.0 - eta_c * r1) * (1.0 - eta_c * r2) / (1.0 - eta_c) + * (2.0 - eta_c * (r1 + r2)) + / (3.0 - eta_c * (1.0 + r1 + r2)).powi(2); + let omega_b = eta_c / (3.0 - eta_c * (1.0 + r1 + r2)); + (omega_a, omega_b) + } +} + +/// Parameters processed using model constants and substance critial data. +#[derive(Debug)] +pub struct CriticalParameters { + ac: Array1, + bc: Array1, + omega_a: f64, + omega_b: f64, +} + +impl CriticalParameters { + fn new(p: &Arc, delta: &Delta) -> Self { + let (omega_a, omega_b) = delta.critical_constants(); + let ac = omega_a * &p.tc.mapv(|tc| tc.powi(2)) * KB_A3 / &p.pc; + let bc = omega_b * &p.tc * KB_A3 / &p.pc; + Self { + ac, + bc, + omega_a, + omega_b, + } + } + + fn subset(&self, component_list: &[usize]) -> Self { + let n = component_list.len(); + let mut ac = Array1::zeros(n); + let mut bc = Array1::zeros(n); + Zip::from(&mut ac) + .and(&mut bc) + .and(component_list) + .for_each(|a, b, &i| { + *a = self.ac[i]; + *b = self.bc[i]; + }); + Self { + ac, + bc, + omega_a: self.omega_a, + omega_b: self.omega_b, + } + } +} + +#[derive(Debug, Clone)] +pub struct CubicOptions { + pub(crate) alpha: Alpha, + pub(crate) mixing: MixingRule, + pub(crate) delta: Delta, +} + +impl CubicOptions { + fn subset(&self, component_list: &[usize]) -> Self { + Self { + alpha: self.alpha.subset(component_list), + mixing: self.mixing.clone(), + delta: self.delta.clone(), + } + } +} + +/// A generic cubic equation of state. +pub struct Cubic { + /// Parameters + pub parameters: Arc, + pub options: CubicOptions, + /// processed parameters using model and substance critical data + pub critical_parameters: CriticalParameters, +} + +impl Cubic { + /// Generic cubic equation of state with adjustable universal constants. + pub fn new(parameters: Arc, options: CubicOptions) -> FeosResult { + let p = CriticalParameters::new(¶meters, &options.delta); + options.alpha.validate(¶meters)?; + Ok(Self { + parameters, + options, + critical_parameters: p, + }) + } + + /// Peng Robinson equation of state. + /// + /// Universal constants: + /// - $\delta_1 = 1 + \sqrt{2}$ + /// - $\delta_2 = 1 - \sqrt{2}$ + /// + /// If no options are supplied, the following is used: + /// - alpha function: Peng Robinson (1976) + /// - mixing rules: quadratic mixing + pub fn peng_robinson( + parameters: Arc, + alpha: Option, + mixing: Option, + ) -> FeosResult { + let delta: Delta = (1.0 + SQRT_2, 1.0 - SQRT_2).into(); + let p = CriticalParameters::new(¶meters, &delta); + let options = CubicOptions { + alpha: alpha.unwrap_or(PengRobinson1976.into()), + mixing: mixing.unwrap_or(Quadratic.into()), + delta, + }; + options.alpha.validate(¶meters)?; + Ok(Self { + parameters, + options, + critical_parameters: p, + }) + } + + /// Create equation of state of (Suave) Redlich Kwong. + /// + /// Universal constants: + /// - $\delta_1 = 1$ + /// - $\delta_2 = 0$ + /// + /// If no options are supplied, the following is used: + /// - alpha function: Soave (1972) + /// - mixing rules: quadratic mixing + pub fn redlich_kwong( + parameters: Arc, + alpha: Option, + mixing: Option, + ) -> FeosResult { + let delta: Delta = (1.0, 0.0).into(); + let p = CriticalParameters::new(¶meters, &delta); + let options = CubicOptions { + alpha: alpha.unwrap_or(RedlichKwong1972.into()), + mixing: mixing.unwrap_or(Quadratic.into()), + delta, + }; + options.alpha.validate(¶meters)?; + Ok(Self { + parameters, + options, + critical_parameters: p, + }) + } +} + +impl fmt::Display for Cubic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "cubic") + } +} + +impl Components for Cubic { + fn components(&self) -> usize { + self.parameters.tc.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { + Self { + parameters: Arc::new(self.parameters.subset(component_list)), + options: self.options.subset(component_list), + critical_parameters: self.critical_parameters.subset(component_list), + } + } +} + +impl Residual for Cubic { + fn compute_max_density(&self, moles: &Array1) -> f64 { + let b = (moles * &self.critical_parameters.bc).sum() / moles.sum(); + 0.9 / b + } + + fn residual_helmholtz_energy + Copy + ScalarOperand>( + &self, + state: &StateHD, + ) -> D { + let MixtureParameters { a, b, c: _ } = self.options.mixing.apply(self, state); + let n = state.moles.sum(); + let v = state.volume; + let bn = b * n; + n * ((v / (v - bn)).ln() + - a / (b * self.options.delta.d12 * state.temperature) + * ((v + bn * self.options.delta.d1) / (v + bn * self.options.delta.d2)).ln()) + } + + fn residual_helmholtz_energy_contributions + Copy + ScalarOperand>( + &self, + state: &StateHD, + ) -> Vec<(String, D)> { + vec![("cubic".to_string(), self.residual_helmholtz_energy(state))] + } +} + +impl Molarweight for Cubic { + fn molar_weight(&self) -> MolarWeight> { + &self.parameters.molarweight * (GRAM / MOL) + } +} + +#[cfg(test)] +mod tests { + // general import + use super::{ + Cubic, PengRobinson1976, + alpha::Alpha, + mixing_rules::{MixingRule, Quadratic}, + parameters::{CubicParameters, CubicRecord}, + }; + use feos_core::{ + Contributions::{IdealGas, Total}, + StateBuilder, StateHD, + cubic::{PengRobinson, PengRobinsonParameters, PengRobinsonRecord}, + parameter::{Identifier, Parameter, PureRecord}, + }; + use ndarray::arr1; + use quantity::*; + use std::sync::Arc; + use typenum::P3; + + use super::*; + + #[test] + fn a_res() { + println!("PengRobinson Propane Residual Helmholtz Free Energy Test"); + + let pc = 4.21e6; // Pa + let tc = 369.83; // K + let mw = 44.1; // g/mol + let omega = 0.153; // dimensionless + + // Create the newly implemented PR record + let propane_implemented = PureRecord::new( + Identifier::new(None, Some("propane"), None, None, None, None), + mw, + CubicRecord::new(tc, pc, omega), + ); + + let parameters_implemented = + Arc::new(CubicParameters::new_pure(propane_implemented).unwrap()); + + let eos_implemented = Cubic::peng_robinson( + parameters_implemented, + Some(Alpha::PengRobinson1976(PengRobinson1976)), + Some(MixingRule::Quadratic(Quadratic)), + ) + .unwrap(); + + dbg!(&eos_implemented.critical_parameters); + + // Create the original PR record for comparison from feos-core. + let propane_compare = PureRecord::new( + Identifier::new(None, Some("propane"), None, None, None, None), + mw, + PengRobinsonRecord::new(tc, pc, omega), + ); + + let parameters_compare = PengRobinsonParameters::new_pure(propane_compare).unwrap(); + let eos_compare = Arc::new(PengRobinson::new(Arc::new(parameters_compare))); + + // Test state. Residual Helmholtz energy of both eos will be used to test. + let state = StateHD::new(300.0, 1.0e5, arr1(&[5.0])); + + dbg!(&state); + + assert_eq!( + eos_implemented.residual_helmholtz_energy(&state), + eos_compare.residual_helmholtz_energy(&state) + ) + } + + #[test] + fn pressure() { + let pc = 4.21e6; // Pa + let tc = 369.83; // K + let mw = 44.1; // g/mol + let omega = 0.153; // dimensionless + + // Create the newly implemented PR record + let propane_implemented = PureRecord::new( + Identifier::new(None, Some("propane"), None, None, None, None), + mw, + CubicRecord::new(tc, pc, omega), + ); + + let parameters_implemented = + Arc::new(CubicParameters::new_pure(propane_implemented).unwrap()); + + let eos_implemented = Arc::new( + Cubic::peng_robinson( + parameters_implemented, + Some(Alpha::PengRobinson1976(PengRobinson1976)), + Some(MixingRule::Quadratic(Quadratic)), + ) + .unwrap(), + ); + + // Create the original PR record for comparison from feos-core. + let propane_compare = PureRecord::new( + Identifier::new(None, Some("propane"), None, None, None, None), + mw, + PengRobinsonRecord::new(tc, pc, omega), + ); + + let parameters_compare = PengRobinsonParameters::new_pure(propane_compare).unwrap(); + let eos_compare = Arc::new(PengRobinson::new(Arc::new(parameters_compare))); + + // Build the test state + // Set volume and moles to define the state + let temp = 300.0 * KELVIN; + let vol = 8.7e-5 * METER.powi::(); + let mol = arr1(&[1.0]) * MOL; + + // Build the state with implemented pr eos + let state_implemented = StateBuilder::new(&eos_implemented) + .temperature(temp) + .volume(vol) + .moles(&mol) + .build() + .unwrap(); + + // Build the state with compare eos + let state_compare = StateBuilder::new(&eos_compare) + .temperature(temp) + .volume(vol) + .moles(&mol) + .build() + .unwrap(); + + // Pressure + println!( + "Implemented Total pressure {}", + state_compare.pressure(Total) + ); + + println!("Compare Total pressure {}", state_compare.pressure(Total)); + + // // other properties: + // // feos-core::state -> residual_properties.rs work fine to get from the state + // let a_r = state_implemented.residual_helmholtz_energy(); + // // feos-core::state --> properties.rs complain that IdeaGas was not implemented correctly + // let a = state_implemented.helmholtz_energy(); + + assert_eq!( + state_implemented.pressure(Total), + state_compare.pressure(Total) + ); + } +} diff --git a/crates/feos/src/cubic/parameters.rs b/crates/feos/src/cubic/parameters.rs new file mode 100644 index 000000000..d78d00818 --- /dev/null +++ b/crates/feos/src/cubic/parameters.rs @@ -0,0 +1,249 @@ +use feos_core::parameter::{Identifier, Parameters, PureRecord}; +use feos_core::{FeosError, FeosResult}; +use ndarray::{Array1, Array2}; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; + +/// Cubic parameters for a single substance. +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct CubicRecord { + /// critical temperature in Kelvin + pub(crate) tc: f64, + /// critical pressure in Pascal + pub(crate) pc: f64, + /// acentric factor + pub(crate) acentric_factor: f64, +} + +impl CubicRecord { + /// Create a new pure substance record for the Cubic equation of state. + pub fn new(tc: f64, pc: f64, acentric_factor: f64) -> Self { + Self { + tc, + pc, + acentric_factor, + } + } +} + +impl std::fmt::Display for CubicRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "CubicRecord(tc={} K", self.tc)?; + write!(f, ", pc={} Pa", self.pc)?; + write!(f, ", acentric factor={}", self.acentric_factor) + } +} + +/// Cubic binary interaction parameters. +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct CubicBinaryRecord { + /// Binary interaction parameter for a + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub k_ij: f64, + /// Binary interaction parameter for b + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub l_ij: f64, + // /// Binary association parameters + // #[serde(flatten)] + // association: Option, +} + +impl CubicBinaryRecord { + pub fn new( + k_ij: Option, + l_ij: Option, + // rc_ab: Option, + // epsilon_k_ab: Option, + ) -> Self { + let k_ij = k_ij.unwrap_or_default(); + let l_ij = l_ij.unwrap_or_default(); + // let association = if rc_ab.is_none() && epsilon_k_ab.is_none() { + // None + // } else { + // Some(BinaryAssociationRecord::new(rc_ab, epsilon_k_ab, None)) + // }; + Self { + k_ij, + l_ij, + // association, + } + } +} + +impl From for CubicBinaryRecord { + fn from(k_ij: f64) -> Self { + Self { + k_ij, + l_ij: f64::default(), + // association: None, + } + } +} + +impl From for f64 { + fn from(binary_record: CubicBinaryRecord) -> Self { + binary_record.k_ij + } +} + +impl std::fmt::Display for CubicBinaryRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut tokens = vec![]; + if !self.k_ij.is_zero() { + tokens.push(format!("k_ij={}", self.k_ij)); + } + if !self.l_ij.is_zero() { + tokens.push(format!("l_ij={}", self.l_ij)); + } + // if let Some(association) = self.association { + // if let Some(rc_ab) = association.rc_ab { + // tokens.push(format!("rc_ab={}", rc_ab)); + // } + // if let Some(epsilon_k_ab) = association.epsilon_k_ab { + // tokens.push(format!("epsilon_k_ab={}", epsilon_k_ab)); + // } + // } + write!(f, "CubicBinaryRecord({})", tokens.join(", ")) + } +} + +/// Cubic EoS parameters for one ore more substances. +pub type CubicParameters = Parameters; + +pub struct CubicPars { + /// Critical temperature in Kelvin + pub(super) tc: Array1, + pub(super) pc: Array1, + pub(super) acentric_factor: Array1, + /// Binary interaction parameter for a + pub(super) k_ij: Array2, + /// Binary interaction parameter for b + pub(super) l_ij: Array2, + /// Molar weight in units of g/mol + pub(super) molarweight: Array1, + /// List of pure component records + pub(super) pure_records: Vec>, + /// List of binary records + pub binary_records: Option>, +} + +impl std::fmt::Display for CubicPars { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.pure_records + .iter() + .try_for_each(|pr| writeln!(f, "{}", pr))?; + writeln!(f, "\nk_ij:\n{}", self.k_ij) + } +} + +impl CubicPars { + /// Build a simple parameter set without binary interaction parameters. + pub fn new_simple( + tc: &[f64], + pc: &[f64], + acentric_factor: &[f64], + molarweight: &[f64], + ) -> Result { + if [pc.len(), acentric_factor.len(), molarweight.len()] + .iter() + .any(|&l| l != tc.len()) + { + return Err(FeosError::IncompatibleParameters(String::from( + "each component has to have parameters.", + ))); + } + let records = (0..tc.len()) + .map(|i| { + let record = CubicRecord { + tc: tc[i], + pc: pc[i], + acentric_factor: acentric_factor[i], + }; + let id = Identifier::default(); + PureRecord::new(id, molarweight[i], record) + }) + .collect(); + CubicPars::new(records, vec![]) + } +} + + +//// Origional CubicParameters struct instead of the Parameters type implementation +// pub struct CubicParameters { +// /// Critical temperature in Kelvin +// pub(super) tc: Array1, +// pub(super) pc: Array1, +// pub(super) acentric_factor: Array1, +// /// Binary interaction parameter for a +// pub(super) k_ij: Array2, +// /// Binary interaction parameter for b +// pub(super) l_ij: Array2, +// /// Molar weight in units of g/mol +// pub(super) molarweight: Array1, +// /// List of pure component records +// pub(super) pure_records: Vec>, +// /// List of binary records +// pub binary_records: Option>, +// } + +// impl std::fmt::Display for CubicParameters { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// self.pure_records +// .iter() +// .try_for_each(|pr| writeln!(f, "{}", pr))?; +// writeln!(f, "\nk_ij:\n{}", self.k_ij) +// } +// } + + +// impl Parameter for CubicParameters { +// type Pure = CubicRecord; +// type Binary = CubicBinaryRecord; +// +// /// Creates parameters from pure component records. +// fn from_records( +// pure_records: Vec>, +// binary_records: Option>, +// ) -> FeosResult { +// let n = pure_records.len(); +// +// let mut tc = Array1::zeros(n); +// let mut pc = Array1::zeros(n); +// let mut acentric_factor = Array1::zeros(n); +// let mut molarweight = Array1::zeros(n); +// +// for (i, record) in pure_records.iter().enumerate() { +// molarweight[i] = record.molarweight; +// let r = &record.model_record; +// tc[i] = r.tc; +// pc[i] = r.pc; +// acentric_factor[i] = r.acentric_factor; +// } +// +// let br = binary_records.as_ref(); +// let k_ij = br.map_or_else(|| Array2::zeros([n; 2]), |br| br.mapv(|br| br.k_ij)); +// let l_ij = br.map_or_else(|| Array2::zeros([n; 2]), |br| br.mapv(|br| br.l_ij)); +// +// Ok(Self { +// tc, +// pc, +// acentric_factor, +// k_ij, +// l_ij, +// molarweight, +// pure_records, +// binary_records, +// }) +// } +// +// fn records( +// &self, +// ) -> ( +// &[PureRecord], +// Option<&Array2>, +// ) { +// (&self.pure_records, self.binary_records.as_ref()) +// } +// } diff --git a/crates/feos/src/lib.rs b/crates/feos/src/lib.rs index d2a9ae3c1..44c4bf42d 100644 --- a/crates/feos/src/lib.rs +++ b/crates/feos/src/lib.rs @@ -42,6 +42,8 @@ pub mod association; pub mod hard_sphere; // models +#[cfg(feature = "cubic")] +pub mod cubic; #[cfg(feature = "epcsaft")] pub mod epcsaft; #[cfg(feature = "gc_pcsaft")] diff --git a/crates/feos/src/pcsaft/parameters.rs b/crates/feos/src/pcsaft/parameters.rs index d7959f4a8..1b619c09e 100644 --- a/crates/feos/src/pcsaft/parameters.rs +++ b/crates/feos/src/pcsaft/parameters.rs @@ -238,7 +238,7 @@ impl PcSaftPars { let mut e_k_ij = Array::zeros((n, n)); for i in 0..n { for j in 0..n { - e_k_ij[[i, j]] = (epsilon_k[i] * epsilon_k[j]).sqrt(); + e_k_ij[[i, j]] = (epsilon_k[i] * epsilon_k[j]).sqrt; sigma_ij[[i, j]] = 0.5 * (sigma[i] + sigma[j]); } } diff --git a/crates/feos/src/saftvrmie/eos/association.rs b/crates/feos/src/saftvrmie/eos/association.rs new file mode 100644 index 000000000..e69de29bb diff --git a/docs/theory/dft/pdgt.md b/docs/theory/dft/pdgt.md old mode 100755 new mode 100644 diff --git a/examples/pcsaft/structure_parameters/DREIDING.dat b/examples/pcsaft/structure_parameters/DREIDING.dat old mode 100755 new mode 100644 diff --git a/examples/pcsaft/structure_parameters/Dha.dat b/examples/pcsaft/structure_parameters/Dha.dat old mode 100755 new mode 100644 diff --git a/examples/pcsaft/structure_parameters/LTA.dat b/examples/pcsaft/structure_parameters/LTA.dat old mode 100755 new mode 100644 diff --git a/examples/pcsaft/structure_parameters/MFI.nml b/examples/pcsaft/structure_parameters/MFI.nml old mode 100755 new mode 100644 diff --git a/examples/pcsaft/structure_parameters/PASCUAL.dat b/examples/pcsaft/structure_parameters/PASCUAL.dat old mode 100755 new mode 100644 diff --git a/examples/pcsaft/structure_parameters/TpPA.dat b/examples/pcsaft/structure_parameters/TpPA.dat old mode 100755 new mode 100644 diff --git a/examples/pcsaft/structure_parameters/solid_database.json b/examples/pcsaft/structure_parameters/solid_database.json old mode 100755 new mode 100644 diff --git a/license-apache b/license-apache index aafc2037d..b26f09a86 100644 --- a/license-apache +++ b/license-apache @@ -1,176 +1 @@ - Copyright (c) 2021-present feos-org and Contributors. https://github.com/feos-org/feos - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. \ No newline at end of file +../../license-apache \ No newline at end of file diff --git a/license-mit b/license-mit index d37df21be..c962239cc 100644 --- a/license-mit +++ b/license-mit @@ -1,7 +1 @@ -Copyright (c) 2023 Gernot Bauer, Philipp Rehner - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +../../license-mit \ No newline at end of file