1use crate::{IdealGasAD, ParametersAD};
2use num_dual::DualNum;
3use std::collections::HashMap;
4
5const RGAS: f64 = 6.022140857 * 1.38064852;
6const T0: f64 = 298.15;
7const T0_2: f64 = 298.15 * 298.15;
8const T0_3: f64 = T0 * T0_2;
9const T0_4: f64 = T0_2 * T0_2;
10const T0_5: f64 = T0 * T0_4;
11const P0: f64 = 1.0e5;
12const A3: f64 = 1e-30;
13const KB: f64 = 1.38064852e-23;
14
15const A: [f64; 22] = [
16 19.5, -0.909, -23.0, -66.2, 23.6, -8.0, -28.0, 32.37, -6.03, -20.5, -6.03, -20.5, -2.14, -8.25,
17 30.9, 6.45, 45.0, 24.591, 24.1, 24.5, 25.7, 26.9,
18];
19const B: [f64; 22] = [
20 -0.00808, 0.095, 0.204, 0.427, -0.0381, 0.105, 0.208, -0.007, 0.0854, 0.162, 0.0854, 0.162,
21 0.0574, 0.101, -0.0336, 0.067, -0.07128, 0.0318, 0.0427, 0.0402, -0.0691, -0.0412,
22];
23const C: [f64; 22] = [
24 0.000153, -5.44e-05, -0.000265, -0.000641, 0.000172, -9.63e-05, -0.000306, 0.00010267, -8e-06,
25 -0.00016, -8e-06, -0.00016, -1.64e-06, -0.000142, 0.00016, -3.57e-05, 0.000264, 5.66e-05,
26 8.04e-05, 4.02e-05, 0.000177, 0.000164,
27];
28const D: [f64; 22] = [
29 -9.67e-08, 1.19e-08, 1.2e-07, 3.01e-07, -1.03e-07, 3.56e-08, 1.46e-07, -6.641e-08, -1.8e-08,
30 6.24e-08, -1.8e-08, 6.24e-08, -1.59e-08, 6.78e-08, -9.88e-08, 2.86e-09, -1.515e-07, -4.29e-08,
31 -6.87e-08, -4.52e-08, -9.88e-08, -9.76e-08,
32];
33const GROUPS: [&str; 22] = [
34 "CH3", "CH2", ">CH", ">C<", "=CH2", "=CH", "=C<", "C≡CH", "CH2_hex", "CH_hex", "CH2_pent",
35 "CH_pent", "CH_arom", "C_arom", "CH=O", ">C=O", "OCH3", "OCH2", "HCOO", "COO", "OH", "NH2",
36];
37
38#[derive(Clone, Copy)]
40pub struct Joback(pub [f64; 5]);
41
42impl Joback {
43 pub fn from_groups<D: DualNum<f64> + Copy>(group_counts: [D; 22]) -> [D; 5] {
44 let a: D = A.into_iter().zip(group_counts).map(|(a, g)| g * a).sum();
45 let b: D = B.into_iter().zip(group_counts).map(|(b, g)| g * b).sum();
46 let c: D = C.into_iter().zip(group_counts).map(|(c, g)| g * c).sum();
47 let d: D = D.into_iter().zip(group_counts).map(|(d, g)| g * d).sum();
48
49 [a - 37.93, b + 0.21, c - 3.91e-4, d + 2.06e-7, D::zero()]
50 }
51
52 pub fn from_group_counts<D: DualNum<f64> + Copy>(group_counts: &HashMap<&str, D>) -> [D; 5] {
53 Self::from_groups(GROUPS.map(|g| *group_counts.get(g).unwrap_or(&D::zero())))
54 }
55}
56
57impl ParametersAD for Joback {
58 type Parameters<D: DualNum<f64> + Copy> = [D; 5];
59
60 fn params<D: DualNum<f64> + Copy>(&self) -> [D; 5] {
61 self.0.map(D::from)
62 }
63
64 fn params_from_inner<D: DualNum<f64> + Copy, D2: DualNum<f64, Inner = D> + Copy>(
65 parameters: &[D; 5],
66 ) -> [D2; 5] {
67 parameters.map(D2::from_inner)
68 }
69}
70
71impl IdealGasAD for Joback {
72 const IDEAL_GAS: &str = "Joback";
73
74 fn ln_lambda3_dual<D: DualNum<f64> + Copy>(
75 parameters: &Self::Parameters<D>,
76 temperature: D,
77 ) -> D {
78 let [a, b, c, d, e] = *parameters;
79 let t = temperature;
80 let t2 = t * t;
81 let t3 = t2 * t;
82 let t4 = t2 * t2;
83 let f = (temperature * KB / (P0 * A3)).ln();
84 let h = (t2 - T0_2) * 0.5 * b
85 + (t3 - T0_3) * c / 3.0
86 + (t4 - T0_4) * 0.25 * d
87 + (t4 * t - T0_5) * 0.2 * e
88 + (t - T0) * a;
89 let s = (t - T0) * b
90 + (t2 - T0_2) * 0.5 * c
91 + (t3 - T0_3) * d / 3.0
92 + (t4 - T0_4) * 0.25 * e
93 + (t / T0).ln() * a;
94 (h - t * s) / (t * RGAS) + f
95 }
96}
97
98#[cfg(test)]
99pub mod test {
100 use super::Joback as JobackAD;
101 use crate::{EquationOfStateAD, ParametersAD, ResidualHelmholtzEnergy, TotalHelmholtzEnergy};
102 use approx::assert_relative_eq;
103 use feos::ideal_gas::{Joback, JobackRecord};
104 use feos_core::parameter::Parameter;
105 use feos_core::{Contributions::IdealGas, EosResult, EquationOfState, ReferenceSystem, State};
106 use nalgebra::SVector;
107 use ndarray::arr1;
108 use num_dual::DualNum;
109 use quantity::{KELVIN, KILO, METER, MOL};
110 use std::sync::Arc;
111
112 pub fn joback() -> EosResult<(JobackAD, Arc<Joback>)> {
113 let a = 1.5;
114 let b = 3.4e-2;
115 let c = 180.0e-4;
116 let d = 2.2e-6;
117 let e = 0.03e-8;
118 let eos = Arc::new(Joback::from_model_records(vec![JobackRecord::new(
119 a, b, c, d, e,
120 )])?);
121 let params = [a, b, c, d, e];
122 let eos_ad = JobackAD(params);
123 Ok((eos_ad, eos))
124 }
125
126 struct NoResidual;
127 impl ParametersAD for NoResidual {
128 type Parameters<D: DualNum<f64> + Copy> = [D; 0];
129
130 fn params<D: DualNum<f64> + Copy>(&self) -> Self::Parameters<D> {
131 []
132 }
133
134 fn params_from_inner<D: DualNum<f64> + Copy, D2: DualNum<f64, Inner = D> + Copy>(
135 _: &Self::Parameters<D>,
136 ) -> Self::Parameters<D2> {
137 []
138 }
139 }
140 impl<const N: usize> ResidualHelmholtzEnergy<N> for NoResidual {
141 const RESIDUAL: &str = "No residual";
142
143 fn compute_max_density(&self, _: &SVector<f64, N>) -> f64 {
144 1.0
145 }
146
147 fn residual_helmholtz_energy_density<D: DualNum<f64> + Copy>(
148 _: &Self::Parameters<D>,
149 _: D,
150 _: &SVector<D, N>,
151 ) -> D {
152 D::zero()
153 }
154 }
155
156 type JobackEos = EquationOfStateAD<JobackAD, NoResidual, 1>;
157
158 #[test]
159 fn test_joback() -> EosResult<()> {
160 let (joback_ad, joback) = joback()?;
161 let eos = Arc::new(EquationOfState::ideal_gas(joback));
162 let eos_ad = EquationOfStateAD::new([joback_ad], NoResidual);
163
164 let temperature = 300.0 * KELVIN;
165 let volume = 2.3 * METER * METER * METER;
166 let moles = arr1(&[1.3]) * KILO * MOL;
167
168 let state = State::new_nvt(&eos, temperature, volume, &moles)?;
169 let a_feos = state.molar_helmholtz_energy(IdealGas);
170 let mu_feos = state.chemical_potential(IdealGas);
171 let p_feos = state.pressure(IdealGas);
172 let s_feos = state.molar_entropy(IdealGas);
173 let h_feos = state.molar_enthalpy(IdealGas);
174
175 let total_moles = moles.sum();
176 let t = temperature.to_reduced();
177 let v = (volume / total_moles).to_reduced();
178 let x = SVector::from_fn(|i, _| moles.get(i).convert_into(total_moles));
179 let a_ad = JobackEos::molar_helmholtz_energy(&eos_ad.params(), t, v, &x);
180 let mu_ad = JobackEos::chemical_potential(&eos_ad.params(), t, v, &x);
181 let p_ad = JobackEos::pressure(&eos_ad.params(), t, v, &x);
182 let s_ad = JobackEos::molar_entropy(&eos_ad.params(), t, v, &x);
183 let h_ad = JobackEos::molar_enthalpy(&eos_ad.params(), t, v, &x);
184
185 println!("\nMolar Helmholtz energy:\n{}", a_feos.to_reduced());
186 println!("{a_ad}");
187 assert_relative_eq!(a_feos.to_reduced(), a_ad, max_relative = 1e-14);
188
189 println!("\nChemical potential:\n{}", mu_feos.get(0).to_reduced());
190 println!("{}", mu_ad[0]);
191 assert_relative_eq!(mu_feos.get(0).to_reduced(), mu_ad[0], max_relative = 1e-14);
192
193 println!("\nPressure:\n{}", p_feos.to_reduced());
194 println!("{p_ad}");
195 assert_relative_eq!(p_feos.to_reduced(), p_ad, max_relative = 1e-14);
196
197 println!("\nMolar entropy:\n{}", s_feos.to_reduced());
198 println!("{s_ad}");
199 assert_relative_eq!(s_feos.to_reduced(), s_ad, max_relative = 1e-14);
200
201 println!("\nMolar enthalpy:\n{}", h_feos.to_reduced());
202 println!("{h_ad}");
203 assert_relative_eq!(h_feos.to_reduced(), h_ad, max_relative = 1e-14);
204
205 Ok(())
206 }
207}