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