1use crate::equation_of_state::{Components, Molarweight, Residual};
8use crate::parameter::{Identifier, Parameter, ParameterError, PureRecord};
9use crate::state::StateHD;
10use ndarray::{Array1, Array2, ScalarOperand};
11use num_dual::DualNum;
12use quantity::{MolarWeight, GRAM, MOL};
13use serde::{Deserialize, Serialize};
14use std::f64::consts::SQRT_2;
15use std::fmt;
16use std::sync::Arc;
17
18const KB_A3: f64 = 13806490.0;
19
20#[derive(Serialize, Deserialize, Debug, Clone, Default)]
22pub struct PengRobinsonRecord {
23 tc: f64,
25 pc: f64,
27 acentric_factor: f64,
29}
30
31impl PengRobinsonRecord {
32 pub fn new(tc: f64, pc: f64, acentric_factor: f64) -> Self {
34 Self {
35 tc,
36 pc,
37 acentric_factor,
38 }
39 }
40}
41
42impl std::fmt::Display for PengRobinsonRecord {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "PengRobinsonRecord(tc={} K", self.tc)?;
45 write!(f, ", pc={} Pa", self.pc)?;
46 write!(f, ", acentric factor={}", self.acentric_factor)
47 }
48}
49
50pub struct PengRobinsonParameters {
52 tc: Array1<f64>,
54 a: Array1<f64>,
55 b: Array1<f64>,
56 k_ij: Array2<f64>,
58 kappa: Array1<f64>,
59 molarweight: Array1<f64>,
61 pure_records: Vec<PureRecord<PengRobinsonRecord>>,
63}
64
65impl std::fmt::Display for PengRobinsonParameters {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 self.pure_records
68 .iter()
69 .try_for_each(|pr| writeln!(f, "{}", pr))?;
70 writeln!(f, "\nk_ij:\n{}", self.k_ij)
71 }
72}
73
74impl PengRobinsonParameters {
75 pub fn new_simple(
77 tc: &[f64],
78 pc: &[f64],
79 acentric_factor: &[f64],
80 molarweight: &[f64],
81 ) -> Result<Self, crate::parameter::ParameterError> {
82 if [pc.len(), acentric_factor.len(), molarweight.len()]
83 .iter()
84 .any(|&l| l != tc.len())
85 {
86 return Err(ParameterError::IncompatibleParameters(String::from(
87 "each component has to have parameters.",
88 )));
89 }
90 let records = (0..tc.len())
91 .map(|i| {
92 let record = PengRobinsonRecord {
93 tc: tc[i],
94 pc: pc[i],
95 acentric_factor: acentric_factor[i],
96 };
97 let id = Identifier::default();
98 PureRecord::new(id, molarweight[i], record)
99 })
100 .collect();
101 PengRobinsonParameters::from_records(records, None)
102 }
103}
104
105impl Parameter for PengRobinsonParameters {
106 type Pure = PengRobinsonRecord;
107 type Binary = f64;
108
109 fn from_records(
111 pure_records: Vec<PureRecord<Self::Pure>>,
112 binary_records: Option<Array2<Self::Binary>>,
113 ) -> Result<Self, ParameterError> {
114 let n = pure_records.len();
115
116 let mut tc = Array1::zeros(n);
117 let mut a = Array1::zeros(n);
118 let mut b = Array1::zeros(n);
119 let mut molarweight = Array1::zeros(n);
120 let mut kappa = Array1::zeros(n);
121
122 for (i, record) in pure_records.iter().enumerate() {
123 molarweight[i] = record.molarweight;
124 let r = &record.model_record;
125 tc[i] = r.tc;
126 a[i] = 0.45724 * r.tc.powi(2) * KB_A3 / r.pc;
127 b[i] = 0.07780 * r.tc * KB_A3 / r.pc;
128 kappa[i] = 0.37464 + (1.54226 - 0.26992 * r.acentric_factor) * r.acentric_factor;
129 }
130
131 let k_ij = binary_records.unwrap_or_else(|| Array2::zeros([n; 2]));
132
133 Ok(Self {
134 tc,
135 a,
136 b,
137 k_ij,
138 kappa,
139 molarweight,
140 pure_records,
141 })
142 }
143
144 fn records(&self) -> (&[PureRecord<PengRobinsonRecord>], Option<&Array2<f64>>) {
145 (&self.pure_records, Some(&self.k_ij))
146 }
147}
148
149pub struct PengRobinson {
151 parameters: Arc<PengRobinsonParameters>,
153}
154
155impl PengRobinson {
156 pub fn new(parameters: Arc<PengRobinsonParameters>) -> Self {
158 Self { parameters }
159 }
160}
161
162impl fmt::Display for PengRobinson {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 write!(f, "Peng Robinson")
165 }
166}
167
168impl Components for PengRobinson {
169 fn components(&self) -> usize {
170 self.parameters.b.len()
171 }
172
173 fn subset(&self, component_list: &[usize]) -> Self {
174 Self::new(Arc::new(self.parameters.subset(component_list)))
175 }
176}
177
178impl Residual for PengRobinson {
179 fn compute_max_density(&self, moles: &Array1<f64>) -> f64 {
180 let b = (moles * &self.parameters.b).sum() / moles.sum();
181 0.9 / b
182 }
183
184 fn residual_helmholtz_energy<D: DualNum<f64> + Copy>(&self, state: &StateHD<D>) -> D {
185 let p = &self.parameters;
186 let x = &state.molefracs;
187 let ak = (&p.tc.mapv(|tc| (D::one() - (state.temperature / tc).sqrt())) * &p.kappa + 1.0)
188 .mapv(|x| x.powi(2))
189 * &p.a;
190
191 let mut ak_mix = D::zero();
193 for i in 0..ak.len() {
194 for j in 0..ak.len() {
195 ak_mix += (ak[i] * ak[j]).sqrt() * (x[i] * x[j] * (1.0 - p.k_ij[(i, j)]));
196 }
197 }
198 let b = (x * &p.b).sum();
199
200 let n = state.moles.sum();
202 let v = state.volume;
203 n * ((v / (v - b * n)).ln()
204 - ak_mix / (b * SQRT_2 * 2.0 * state.temperature)
205 * ((v + b * n * (1.0 + SQRT_2)) / (v + b * n * (1.0 - SQRT_2))).ln())
206 }
207
208 fn residual_helmholtz_energy_contributions<D: DualNum<f64> + Copy + ScalarOperand>(
209 &self,
210 state: &StateHD<D>,
211 ) -> Vec<(String, D)> {
212 vec![(
213 "Peng Robinson".to_string(),
214 self.residual_helmholtz_energy(state),
215 )]
216 }
217}
218
219impl Molarweight for PengRobinson {
220 fn molar_weight(&self) -> MolarWeight<Array1<f64>> {
221 &self.parameters.molarweight * (GRAM / MOL)
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::state::{Contributions, State};
229 use crate::{EosResult, SolverOptions, Verbosity};
230 use approx::*;
231 use quantity::{KELVIN, PASCAL};
232 use std::sync::Arc;
233
234 fn pure_record_vec() -> Vec<PureRecord<PengRobinsonRecord>> {
235 let records = r#"[
236 {
237 "identifier": {
238 "cas": "74-98-6",
239 "name": "propane",
240 "iupac_name": "propane",
241 "smiles": "CCC",
242 "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3",
243 "formula": "C3H8"
244 },
245 "model_record": {
246 "tc": 369.96,
247 "pc": 4250000.0,
248 "acentric_factor": 0.153
249 },
250 "molarweight": 44.0962
251 },
252 {
253 "identifier": {
254 "cas": "106-97-8",
255 "name": "butane",
256 "iupac_name": "butane",
257 "smiles": "CCCC",
258 "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3",
259 "formula": "C4H10"
260 },
261 "model_record": {
262 "tc": 425.2,
263 "pc": 3800000.0,
264 "acentric_factor": 0.199
265 },
266 "molarweight": 58.123
267 }
268 ]"#;
269 serde_json::from_str(records).expect("Unable to parse json.")
270 }
271
272 #[test]
273 fn peng_robinson() -> EosResult<()> {
274 let mixture = pure_record_vec();
275 let propane = mixture[0].clone();
276 let tc = propane.model_record.tc;
277 let pc = propane.model_record.pc;
278 let parameters = PengRobinsonParameters::new_pure(propane)?;
279 let pr = Arc::new(PengRobinson::new(Arc::new(parameters)));
280 let options = SolverOptions::new().verbosity(Verbosity::Iter);
281 let cp = State::critical_point(&pr, None, None, options)?;
282 println!("{} {}", cp.temperature, cp.pressure(Contributions::Total));
283 assert_relative_eq!(cp.temperature, tc * KELVIN, max_relative = 1e-4);
284 assert_relative_eq!(
285 cp.pressure(Contributions::Total),
286 pc * PASCAL,
287 max_relative = 1e-4
288 );
289 Ok(())
290 }
291}