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