1#![warn(clippy::all)]
2#![allow(clippy::reversed_empty_ranges)]
3#![warn(clippy::allow_attributes)]
4use quantity::{Quantity, SIUnit};
5use std::ops::{Div, Mul};
6
7#[macro_export]
9macro_rules! log_iter {
10 ($verbosity:expr, $($arg:tt)*) => {
11 if $verbosity >= Verbosity::Iter {
12 println!($($arg)*);
13 }
14 }
15}
16
17#[macro_export]
19macro_rules! log_result {
20 ($verbosity:expr, $($arg:tt)*) => {
21 if $verbosity >= Verbosity::Result {
22 println!($($arg)*);
23 }
24 }
25}
26
27pub mod ad;
28pub mod cubic;
29mod density_iteration;
30mod equation_of_state;
31mod errors;
32pub mod parameter;
33mod phase_equilibria;
34mod state;
35pub use equation_of_state::{
36 EntropyScaling, EquationOfState, IdealGas, IdealGasAD, Molarweight, NoResidual, Residual,
37 ResidualDyn, Subset, Total,
38};
39pub use errors::{FeosError, FeosResult};
40#[cfg(feature = "ndarray")]
41pub use phase_equilibria::{PhaseDiagram, PhaseDiagramHetero};
42pub use phase_equilibria::{PhaseEquilibrium, TemperatureOrPressure};
43pub use state::{Composition, Contributions, DensityInitialization, State, StateHD, StateVec};
44
45#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Default)]
47pub enum Verbosity {
48 #[default]
50 None,
51 Result,
53 Iter,
55}
56
57#[derive(Copy, Clone, Default)]
62pub struct SolverOptions {
63 pub max_iter: Option<usize>,
65 pub tol: Option<f64>,
67 pub verbosity: Verbosity,
69}
70
71impl From<(Option<usize>, Option<f64>, Option<Verbosity>)> for SolverOptions {
72 fn from(options: (Option<usize>, Option<f64>, Option<Verbosity>)) -> Self {
73 Self {
74 max_iter: options.0,
75 tol: options.1,
76 verbosity: options.2.unwrap_or(Verbosity::None),
77 }
78 }
79}
80
81impl SolverOptions {
82 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn max_iter(mut self, max_iter: usize) -> Self {
87 self.max_iter = Some(max_iter);
88 self
89 }
90
91 pub fn tol(mut self, tol: f64) -> Self {
92 self.tol = Some(tol);
93 self
94 }
95
96 pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
97 self.verbosity = verbosity;
98 self
99 }
100
101 pub fn unwrap_or(self, max_iter: usize, tol: f64) -> (usize, f64, Verbosity) {
102 (
103 self.max_iter.unwrap_or(max_iter),
104 self.tol.unwrap_or(tol),
105 self.verbosity,
106 )
107 }
108}
109
110const REFERENCE_VALUES: [f64; 7] = [
112 1e-12, 1e-10, 1.380649e-27, 1.0, 1.0, 1.0 / 6.02214076e23, 1.0, ];
120
121const fn powi(x: f64, n: i32) -> f64 {
122 match n {
123 ..=-1 => powi(1.0 / x, -n),
124 0 => 1.0,
125 n if n % 2 == 0 => powi(x * x, n / 2),
126 n => x * powi(x * x, (n - 1) / 2),
127 }
128}
129
130pub trait ReferenceSystem {
132 type Inner;
133 const T: i8;
134 const L: i8;
135 const M: i8;
136 const I: i8;
137 const THETA: i8;
138 const N: i8;
139 const J: i8;
140 const FACTOR: f64 = powi(REFERENCE_VALUES[0], Self::T as i32)
141 * powi(REFERENCE_VALUES[1], Self::L as i32)
142 * powi(REFERENCE_VALUES[2], Self::M as i32)
143 * powi(REFERENCE_VALUES[3], Self::I as i32)
144 * powi(REFERENCE_VALUES[4], Self::THETA as i32)
145 * powi(REFERENCE_VALUES[5], Self::N as i32)
146 * powi(REFERENCE_VALUES[6], Self::J as i32);
147
148 fn from_reduced(value: Self::Inner) -> Self
149 where
150 Self::Inner: Mul<f64, Output = Self::Inner>;
151
152 fn to_reduced(&self) -> Self::Inner
153 where
154 for<'a> &'a Self::Inner: Div<f64, Output = Self::Inner>;
155
156 fn into_reduced(self) -> Self::Inner
157 where
158 Self::Inner: Div<f64, Output = Self::Inner>;
159}
160
161impl<
163 Inner,
164 const T: i8,
165 const L: i8,
166 const M: i8,
167 const I: i8,
168 const THETA: i8,
169 const N: i8,
170 const J: i8,
171> ReferenceSystem for Quantity<Inner, SIUnit<T, L, M, I, THETA, N, J>>
172{
173 type Inner = Inner;
174 const T: i8 = T;
175 const L: i8 = L;
176 const M: i8 = M;
177 const I: i8 = I;
178 const THETA: i8 = THETA;
179 const N: i8 = N;
180 const J: i8 = J;
181 fn from_reduced(value: Inner) -> Self
182 where
183 Inner: Mul<f64, Output = Inner>,
184 {
185 Self::new(value * Self::FACTOR)
186 }
187
188 fn to_reduced(&self) -> Inner
189 where
190 for<'a> &'a Inner: Div<f64, Output = Inner>,
191 {
192 self.convert_to(Quantity::new(Self::FACTOR))
193 }
194
195 fn into_reduced(self) -> Inner
196 where
197 Inner: Div<f64, Output = Inner>,
198 {
199 self.convert_into(Quantity::new(Self::FACTOR))
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use crate::Contributions;
206 use crate::FeosResult;
207 use crate::State;
208 use crate::cubic::*;
209 use crate::equation_of_state::{EquationOfState, IdealGas};
210 use crate::parameter::*;
211 use approx::*;
212 use num_dual::DualNum;
213 use quantity::{BAR, KELVIN, MOL, RGAS};
214
215 #[derive(Clone, Copy)]
217 struct NoIdealGas;
218
219 impl IdealGas for NoIdealGas {
220 fn ideal_gas_model(&self) -> &'static str {
221 "NoIdealGas"
222 }
223
224 fn ln_lambda3<D: DualNum<f64> + Copy>(&self, _: D) -> D {
225 unreachable!()
226 }
227 }
228
229 fn pure_record_vec() -> Vec<PureRecord<PengRobinsonRecord, ()>> {
230 let records = r#"[
231 {
232 "identifier": {
233 "cas": "74-98-6",
234 "name": "propane",
235 "iupac_name": "propane",
236 "smiles": "CCC",
237 "inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3",
238 "formula": "C3H8"
239 },
240 "tc": 369.96,
241 "pc": 4250000.0,
242 "acentric_factor": 0.153,
243 "molarweight": 44.0962
244 },
245 {
246 "identifier": {
247 "cas": "106-97-8",
248 "name": "butane",
249 "iupac_name": "butane",
250 "smiles": "CCCC",
251 "inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3",
252 "formula": "C4H10"
253 },
254 "tc": 425.2,
255 "pc": 3800000.0,
256 "acentric_factor": 0.199,
257 "molarweight": 58.123
258 }
259 ]"#;
260 serde_json::from_str(records).expect("Unable to parse json.")
261 }
262
263 #[test]
264 fn validate_residual_properties() -> FeosResult<()> {
265 let mixture = pure_record_vec();
266 let propane = &mixture[0];
267 let parameters = PengRobinsonParameters::new_pure(propane.clone())?;
268 let residual = PengRobinson::new(parameters);
269
270 let sr = State::new_npt(&&residual, 300.0 * KELVIN, 1.0 * BAR, 2.0 * MOL, None)?;
271
272 let parameters = PengRobinsonParameters::new_pure(propane.clone())?;
273 let residual = PengRobinson::new(parameters);
274 let eos = EquationOfState::new(vec![NoIdealGas], residual);
275 let s = State::new_npt(&&eos, 300.0 * KELVIN, 1.0 * BAR, 2.0 * MOL, None)?;
276
277 assert_relative_eq!(
279 s.pressure(Contributions::Total),
280 sr.pressure(Contributions::Total),
281 max_relative = 1e-15
282 );
283 assert_relative_eq!(
284 s.pressure(Contributions::Residual),
285 sr.pressure(Contributions::Residual),
286 max_relative = 1e-15
287 );
288 assert_relative_eq!(
289 s.compressibility(Contributions::Total),
290 sr.compressibility(Contributions::Total),
291 max_relative = 1e-15
292 );
293 assert_relative_eq!(
294 s.compressibility(Contributions::Residual),
295 sr.compressibility(Contributions::Residual),
296 max_relative = 1e-15
297 );
298
299 assert_relative_eq!(
301 s.helmholtz_energy(Contributions::Residual)?,
302 sr.residual_helmholtz_energy()?,
303 max_relative = 1e-15
304 );
305 assert_relative_eq!(
306 s.molar_helmholtz_energy(Contributions::Residual),
307 sr.residual_molar_helmholtz_energy(),
308 max_relative = 1e-15
309 );
310 assert_relative_eq!(
311 s.entropy(Contributions::Residual)?,
312 sr.residual_entropy()?,
313 max_relative = 1e-15
314 );
315 assert_relative_eq!(
316 s.molar_entropy(Contributions::Residual),
317 sr.residual_molar_entropy(),
318 max_relative = 1e-15
319 );
320 assert_relative_eq!(
321 s.enthalpy(Contributions::Residual)?,
322 sr.residual_enthalpy()?,
323 max_relative = 1e-15
324 );
325 assert_relative_eq!(
326 s.molar_enthalpy(Contributions::Residual),
327 sr.residual_molar_enthalpy(),
328 max_relative = 1e-15
329 );
330 assert_relative_eq!(
331 s.internal_energy(Contributions::Residual)?,
332 sr.residual_internal_energy()?,
333 max_relative = 1e-15
334 );
335 assert_relative_eq!(
336 s.molar_internal_energy(Contributions::Residual),
337 sr.residual_molar_internal_energy(),
338 max_relative = 1e-15
339 );
340 assert_relative_eq!(
341 s.gibbs_energy(Contributions::Residual)?
342 - s.total_moles()?
343 * RGAS
344 * s.temperature
345 * s.compressibility(Contributions::Total).ln(),
346 sr.residual_gibbs_energy()?,
347 max_relative = 1e-15
348 );
349 assert_relative_eq!(
350 s.molar_gibbs_energy(Contributions::Residual)
351 - RGAS * s.temperature * s.compressibility(Contributions::Total).ln(),
352 sr.residual_molar_gibbs_energy(),
353 max_relative = 1e-15
354 );
355 assert_relative_eq!(
356 s.chemical_potential(Contributions::Residual),
357 sr.residual_chemical_potential(),
358 max_relative = 1e-15
359 );
360
361 assert_relative_eq!(
363 s.structure_factor(),
364 sr.structure_factor(),
365 max_relative = 1e-15
366 );
367 assert_relative_eq!(
368 s.dp_dt(Contributions::Total),
369 sr.dp_dt(Contributions::Total),
370 max_relative = 1e-15
371 );
372 assert_relative_eq!(
373 s.dp_dt(Contributions::Residual),
374 sr.dp_dt(Contributions::Residual),
375 max_relative = 1e-15
376 );
377 assert_relative_eq!(
378 s.dp_dv(Contributions::Total),
379 sr.dp_dv(Contributions::Total),
380 max_relative = 1e-15
381 );
382 assert_relative_eq!(
383 s.dp_dv(Contributions::Residual),
384 sr.dp_dv(Contributions::Residual),
385 max_relative = 1e-15
386 );
387 assert_relative_eq!(
388 s.dp_drho(Contributions::Total),
389 sr.dp_drho(Contributions::Total),
390 max_relative = 1e-15
391 );
392 assert_relative_eq!(
393 s.dp_drho(Contributions::Residual),
394 sr.dp_drho(Contributions::Residual),
395 max_relative = 1e-15
396 );
397 assert_relative_eq!(
398 s.d2p_dv2(Contributions::Total),
399 sr.d2p_dv2(Contributions::Total),
400 max_relative = 1e-15
401 );
402 assert_relative_eq!(
403 s.d2p_dv2(Contributions::Residual),
404 sr.d2p_dv2(Contributions::Residual),
405 max_relative = 1e-15
406 );
407 assert_relative_eq!(
408 s.d2p_drho2(Contributions::Total),
409 sr.d2p_drho2(Contributions::Total),
410 max_relative = 1e-15
411 );
412 assert_relative_eq!(
413 s.d2p_drho2(Contributions::Residual),
414 sr.d2p_drho2(Contributions::Residual),
415 max_relative = 1e-15
416 );
417 assert_relative_eq!(
418 s.n_dp_dni(Contributions::Total),
419 sr.n_dp_dni(Contributions::Total),
420 max_relative = 1e-15
421 );
422 assert_relative_eq!(
423 s.n_dp_dni(Contributions::Residual),
424 sr.n_dp_dni(Contributions::Residual),
425 max_relative = 1e-15
426 );
427
428 assert_relative_eq!(
430 s.ds_dt(Contributions::Residual),
431 sr.ds_res_dt(),
432 max_relative = 1e-15
433 );
434
435 assert_relative_eq!(
437 s.dmu_dt(Contributions::Residual),
438 sr.dmu_res_dt(),
439 max_relative = 1e-15
440 );
441 assert_relative_eq!(
442 s.n_dmu_dni(Contributions::Residual),
443 sr.n_dmu_dni(Contributions::Residual),
444 max_relative = 1e-15
445 );
446 assert_relative_eq!(
447 s.dmu_dt(Contributions::Residual),
448 sr.dmu_res_dt(),
449 max_relative = 1e-15
450 );
451
452 assert_relative_eq!(s.ln_phi(), sr.ln_phi(), max_relative = 1e-15);
454 assert_relative_eq!(s.dln_phi_dt(), sr.dln_phi_dt(), max_relative = 1e-15);
455 assert_relative_eq!(s.dln_phi_dp(), sr.dln_phi_dp(), max_relative = 1e-15);
456 assert_relative_eq!(s.n_dln_phi_dnj(), sr.n_dln_phi_dnj(), max_relative = 1e-15);
457 assert_relative_eq!(
458 s.thermodynamic_factor(),
459 sr.thermodynamic_factor(),
460 max_relative = 1e-15
461 );
462
463 assert_relative_eq!(
465 s.molar_isochoric_heat_capacity(Contributions::Residual),
466 sr.residual_molar_isochoric_heat_capacity(),
467 max_relative = 1e-15
468 );
469 assert_relative_eq!(
470 s.dc_v_dt(Contributions::Residual),
471 sr.dc_v_res_dt(),
472 max_relative = 1e-15
473 );
474 assert_relative_eq!(
475 s.molar_isobaric_heat_capacity(Contributions::Residual),
476 sr.residual_molar_isobaric_heat_capacity(),
477 max_relative = 1e-15
478 );
479 Ok(())
480 }
481}