1use super::{PhaseEquilibrium, TRIVIAL_REL_DEVIATION};
2use crate::density_iteration::{_density_iteration, _pressure_spinodal};
3use crate::equation_of_state::{Residual, Subset};
4use crate::errors::{FeosError, FeosResult};
5use crate::state::{Contributions, DensityInitialization, State};
6use crate::{ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity};
7use nalgebra::allocator::Allocator;
8use nalgebra::{DVector, DefaultAllocator, Dim, SVector, U1, U2};
9use num_dual::{DualNum, DualStruct, Gradients, gradient, partial};
10use quantity::{Density, Pressure, Temperature};
11
12const SCALE_T_NEW: f64 = 0.7;
13const MAX_ITER_PURE: usize = 50;
14const TOL_PURE: f64 = 1e-12;
15
16impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> PhaseEquilibrium<E, 2, N, D>
18where
19 DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
20{
21 pub fn pure<TP: TemperatureOrPressure<D>>(
23 eos: &E,
24 temperature_or_pressure: TP,
25 initial_state: Option<&Self>,
26 options: SolverOptions,
27 ) -> FeosResult<Self> {
28 let (t, [rho_v, rho_l]) = if let Some(t) = temperature_or_pressure.temperature() {
29 let (_, rho) = Self::pure_t(eos, t, initial_state, options)?;
30 (t, rho)
31 } else if let Some(p) = temperature_or_pressure.pressure() {
32 Self::pure_p(eos, p, initial_state, options)?
33 } else {
34 unreachable!()
35 };
36 let x = E::pure_molefracs();
37 Ok(Self::two_phase(
38 State::new(eos, t, rho_v, &x)?,
39 State::new(eos, t, rho_l, x)?,
40 ))
41 }
42
43 pub fn pure_t(
46 eos: &E,
47 temperature: Temperature<D>,
48 initial_state: Option<&Self>,
49 options: SolverOptions,
50 ) -> FeosResult<(Pressure<D>, [Density<D>; 2])> {
51 let eos_f64 = eos.re();
52 let t = temperature.into_reduced();
53
54 let mut vle = initial_state.and_then(|init| {
56 let vle = (
57 init.vapor()
58 .pressure(Contributions::Total)
59 .into_reduced()
60 .re(),
61 [
62 init.vapor().density.into_reduced().re(),
63 init.liquid().density.into_reduced().re(),
64 ],
65 );
66 iterate_pure_t(&eos_f64, t.re(), vle, options).ok()
67 });
68
69 vle = vle.or_else(|| {
71 _init_pure_ideal_gas(&eos_f64, temperature.re())
72 .and_then(|vle| iterate_pure_t(&eos_f64, t.re(), vle, options))
73 .ok()
74 });
75
76 let (p, [rho_v, rho_l]) = vle.map_or_else(
78 || {
79 _init_pure_spinodal(&eos_f64, temperature.re())
80 .and_then(|vle| iterate_pure_t(&eos_f64, t.re(), vle, options))
81 },
82 Ok,
83 )?;
84
85 let mut pressure = D::from(p);
87 let mut vapor_density = D::from(rho_v);
88 let mut liquid_density = D::from(rho_l);
89 let x = E::pure_molefracs();
90 for _ in 0..D::NDERIV {
91 let v_l = liquid_density.recip();
92 let v_v = vapor_density.recip();
93 let (a_l, p_l, dp_l) = eos.p_dpdrho(t, liquid_density, &x);
94 let (a_v, p_v, dp_v) = eos.p_dpdrho(t, vapor_density, &x);
95 pressure = -(a_l - a_v + t * (v_v / v_l).ln()) / (v_l - v_v);
96 liquid_density += (pressure - p_l) / dp_l;
97 vapor_density += (pressure - p_v) / dp_v;
98 }
99 Ok((
100 Pressure::from_reduced(pressure),
101 [
102 Density::from_reduced(vapor_density),
103 Density::from_reduced(liquid_density),
104 ],
105 ))
106 }
107}
108
109fn iterate_pure_t<E: Residual<N>, N: Dim>(
110 eos: &E,
111 temperature: f64,
112 (mut pressure, [mut vapor_density, mut liquid_density]): (f64, [f64; 2]),
113 options: SolverOptions,
114) -> FeosResult<(f64, [f64; 2])>
115where
116 DefaultAllocator: Allocator<N>,
117{
118 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
119 let x = E::pure_molefracs();
120
121 log_iter!(
122 verbosity,
123 " iter | residual | pressure | liquid density | vapor density | Newton steps"
124 );
125 log_iter!(verbosity, "{:-<103}", "");
126 log_iter!(
127 verbosity,
128 " {:4} | | {:12.8} | {:12.8} | {:12.8} |",
129 0,
130 Pressure::from_reduced(pressure),
131 Density::from_reduced(liquid_density),
132 Density::from_reduced(vapor_density)
133 );
134
135 for i in 1..=max_iter {
136 let (a_l_res, p_l, p_rho_l) = eos.p_dpdrho(temperature, liquid_density, &x);
138 let (a_v_res, p_v, p_rho_v) = eos.p_dpdrho(temperature, vapor_density, &x);
139
140 let v_v = vapor_density.recip();
142 let v_l = liquid_density.recip();
143 let delta_v = v_v - v_l;
144 let delta_a = a_v_res - a_l_res + temperature * (vapor_density / liquid_density).ln();
145 let mut p_new = -delta_a / delta_v;
146
147 if p_new.is_sign_negative() {
150 p_new = p_v * ((-delta_a - p_v / vapor_density) / temperature).exp();
151 }
152
153 let mut newton_iter = 0;
155 let newton_tol = pressure * delta_v * tol;
156 for _ in 0..20 {
157 let p_frac = p_new / pressure;
158 let f = p_new * delta_v + delta_a + (p_frac.ln() + 1.0 - p_frac) * temperature;
159 let df_dp = delta_v + (1.0 / p_new - 1.0 / pressure) * temperature;
160 p_new -= f / df_dp;
161 newton_iter += 1;
162 if f.abs() < newton_tol {
163 break;
164 }
165 }
166
167 if p_new.is_nan() {
169 return Err(FeosError::IterationFailed("pure_t".to_owned()));
170 }
171
172 liquid_density += (p_new - p_l) / p_rho_l;
174 vapor_density += (p_new - p_v) / p_rho_v;
175 if (vapor_density / liquid_density - 1.0).abs() < TRIVIAL_REL_DEVIATION {
176 return Err(FeosError::TrivialSolution);
177 }
178
179 let res = (p_new - pressure).abs();
181 log_iter!(
182 verbosity,
183 " {:4} | {:14.8e} | {:12.8} | {:12.8} | {:12.8} | {}",
184 i,
185 res,
186 Pressure::from_reduced(p_new),
187 Density::from_reduced(liquid_density),
188 Density::from_reduced(vapor_density),
189 newton_iter
190 );
191 if res < pressure * tol {
192 log_result!(
193 verbosity,
194 "PhaseEquilibrium::pure_t: calculation converged in {} step(s)\n",
195 i
196 );
197 return Ok((pressure, [vapor_density, liquid_density]));
198 }
199 pressure = p_new;
200 }
201 Err(FeosError::NotConverged("pure_t".to_owned()))
202}
203
204fn _init_pure_ideal_gas<E: Residual<N>, N: Dim>(
205 eos: &E,
206 temperature: Temperature,
207) -> FeosResult<(f64, [f64; 2])>
208where
209 DefaultAllocator: Allocator<N>,
210{
211 let x = E::pure_molefracs();
212 let v = (0.75 * eos.compute_max_density(&x)).recip();
213 let t = temperature.into_reduced();
214 let a_res = eos.residual_helmholtz_energy(t, v, &x);
215 let p = t / v * (a_res / t - 1.0).exp();
216 let rho_v = p / t;
217 let rho_l = v.recip();
218 let rho_v = _density_iteration(eos, t, p, &x, DensityInitialization::InitialDensity(rho_v))?;
219 let rho_l = _density_iteration(eos, t, p, &x, DensityInitialization::InitialDensity(rho_l))?;
220 Ok((p, [rho_v, rho_l]))
221}
222
223fn _init_pure_spinodal<E: Residual<N>, N: Dim>(
224 eos: &E,
225 temperature: Temperature,
226) -> FeosResult<(f64, [f64; 2])>
227where
228 DefaultAllocator: Allocator<N>,
229{
230 let x = E::pure_molefracs();
231 let maxdensity = eos.compute_max_density(&x);
232 let t = temperature.into_reduced();
233 let (p_l, _) = _pressure_spinodal(eos, t, 0.8 * maxdensity, &x)?;
234 let (p_v, _) = _pressure_spinodal(eos, t, 0.001 * maxdensity, &x)?;
235 let p = 0.5 * (0.0_f64.max(p_l) + p_v);
236 let rho_l = _density_iteration(eos, t, p, &x, DensityInitialization::Liquid)?;
237 let rho_v = _density_iteration(eos, t, p, &x, DensityInitialization::Vapor)?;
238 Ok((p, [rho_v, rho_l]))
239}
240
241impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> PhaseEquilibrium<E, 2, N, D>
242where
243 DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
244{
245 pub fn pure_p(
248 eos: &E,
249 pressure: Pressure<D>,
250 initial_state: Option<&Self>,
251 options: SolverOptions,
252 ) -> FeosResult<(Temperature<D>, [Density<D>; 2])> {
253 let eos_f64 = eos.re();
254 let p = pressure.into_reduced();
255
256 let vle = match initial_state {
258 Some(init) => (
259 init.vapor().temperature.into_reduced().re(),
260 [
261 init.vapor().density.into_reduced().re(),
262 init.liquid().density.into_reduced().re(),
263 ],
264 ),
265 None => init_pure_p(&eos_f64, pressure.re())?,
266 };
267 let (t, [rho_v, rho_l]) = iterate_pure_p(&eos_f64, p.re(), vle, options)?;
268
269 let mut temperature = D::from(t);
271 let mut vapor_density = D::from(rho_v);
272 let mut liquid_density = D::from(rho_l);
273 let x = E::pure_molefracs();
274 for _ in 0..D::NDERIV {
275 let v_l = liquid_density.recip();
276 let v_v = vapor_density.recip();
277 let (a_l, p_l, s_l, p_rho_l, p_t_l) =
278 eos.p_dpdrho_dpdt(temperature, liquid_density, &x);
279 let (a_v, p_v, s_v, p_rho_v, p_t_v) = eos.p_dpdrho_dpdt(temperature, vapor_density, &x);
280 let ln_rho = (v_l / v_v).ln();
281 let delta_t =
282 (p * (v_v - v_l) + (a_v - a_l + temperature * ln_rho)) / (s_v - s_l - ln_rho);
283 temperature += delta_t;
284 liquid_density += (p - p_l - p_t_l * delta_t) / p_rho_l;
285 vapor_density += (p - p_v - p_t_v * delta_t) / p_rho_v;
286 }
287 Ok((
288 Temperature::from_reduced(temperature),
289 [
290 Density::from_reduced(vapor_density),
291 Density::from_reduced(liquid_density),
292 ],
293 ))
294 }
295}
296
297fn iterate_pure_p<E: Residual<N>, N: Dim>(
300 eos: &E,
301 pressure: f64,
302 (mut temperature, [mut vapor_density, mut liquid_density]): (f64, [f64; 2]),
303 options: SolverOptions,
304) -> FeosResult<(f64, [f64; 2])>
305where
306 DefaultAllocator: Allocator<N>,
307{
308 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
309 let x = E::pure_molefracs();
310
311 log_iter!(
312 verbosity,
313 " iter | residual | temperature | liquid density | vapor density "
314 );
315 log_iter!(verbosity, "{:-<89}", "");
316 log_iter!(
317 verbosity,
318 " {:4} | | {:13.8} | {:12.8} | {:12.8}",
319 0,
320 Temperature::from_reduced(temperature),
321 Density::from_reduced(liquid_density),
322 Density::from_reduced(vapor_density)
323 );
324 for i in 1..=max_iter {
325 let (a_l_res, p_l, s_l_res, p_rho_l, p_t_l) =
327 eos.p_dpdrho_dpdt(temperature, liquid_density, &x);
328 let (a_v_res, p_v, s_v_res, p_rho_v, p_t_v) =
329 eos.p_dpdrho_dpdt(temperature, vapor_density, &x);
330
331 let v_l = liquid_density.recip();
333 let v_v = vapor_density.recip();
334
335 let ln_rho = (v_l / v_v).ln();
337 let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + temperature * ln_rho))
338 / (s_v_res - s_l_res - ln_rho);
339 temperature += delta_t;
340
341 let rho_l = liquid_density + (pressure - p_l - p_t_l * delta_t) / p_rho_l;
343 let rho_v = vapor_density + (pressure - p_v - p_t_v * delta_t) / p_rho_v;
344
345 if rho_l.is_sign_negative() || rho_v.is_sign_negative() || delta_t.abs() > 1.0 {
346 liquid_density = _density_iteration(
348 eos,
349 temperature,
350 pressure,
351 &x,
352 DensityInitialization::InitialDensity(liquid_density),
353 )?;
354 vapor_density = _density_iteration(
355 eos,
356 temperature,
357 pressure,
358 &x,
359 DensityInitialization::InitialDensity(vapor_density),
360 )?;
361 } else {
362 liquid_density = rho_l;
363 vapor_density = rho_v;
364 }
365
366 if (vapor_density / liquid_density - 1.0).abs() < TRIVIAL_REL_DEVIATION {
368 return Err(FeosError::TrivialSolution);
369 }
370
371 let res = delta_t.abs();
373 log_iter!(
374 verbosity,
375 " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}",
376 i,
377 res,
378 Temperature::from_reduced(temperature),
379 Density::from_reduced(liquid_density),
380 Density::from_reduced(vapor_density)
381 );
382 if res < temperature * tol {
383 log_result!(
384 verbosity,
385 "PhaseEquilibrium::pure_p: calculation converged in {} step(s)\n",
386 i
387 );
388 return Ok((temperature, [vapor_density, liquid_density]));
389 }
390 }
391 Err(FeosError::NotConverged("pure_p".to_owned()))
392}
393
394fn init_pure_p<E: Residual<N>, N: Gradients>(
396 eos: &E,
397 pressure: Pressure,
398) -> FeosResult<(f64, [f64; 2])>
399where
400 DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
401{
402 let trial_temperatures = [300.0, 500.0, 200.0];
403 let p = pressure.into_reduced();
404 let x = E::pure_molefracs();
405 let mut vle = None;
406 for t in trial_temperatures {
407 let liquid_density = _density_iteration(eos, t, p, &x, DensityInitialization::Liquid)?;
408 let vapor_density = _density_iteration(eos, t, p, &x, DensityInitialization::Vapor)?;
409 let _vle = (t, [vapor_density, liquid_density]);
410 if (vapor_density / liquid_density - 1.0).abs() >= TRIVIAL_REL_DEVIATION {
411 return Ok(_vle);
412 }
413 vle = Some(_vle);
414 }
415 let Some((t0, [mut rho_v, mut rho_l])) = vle else {
416 unreachable!()
417 };
418 let [mut t_v, mut t_l] = [t0, t0];
419
420 let cp = State::critical_point(eos, &x, None, None, SolverOptions::default())?;
421 let cp_density = cp.density.into_reduced();
422 if pressure > cp.pressure(Contributions::Total) {
423 return Err(FeosError::SuperCritical);
424 };
425
426 if rho_v < cp_density {
427 for _ in 0..8 {
429 t_l *= SCALE_T_NEW;
430 rho_l = _density_iteration(eos, t_l, p, &x, DensityInitialization::Liquid)?;
431 if rho_l > cp_density {
432 break;
433 }
434 }
435 } else {
436 for _ in 0..8 {
438 t_v /= SCALE_T_NEW;
439 rho_v = _density_iteration(eos, t_v, p, &x, DensityInitialization::Vapor)?;
440 if rho_v < cp_density {
441 break;
442 }
443 }
444 }
445
446 for _ in 0..20 {
449 let h_s = |t, v| {
450 let (a_res, da_res) = gradient::<_, _, _, U2, _>(
451 partial(
452 |t_v: SVector<_, _>, x| {
453 let [[t, v]] = t_v.data.0;
454 eos.lift().residual_helmholtz_energy(t, v, x)
455 },
456 &x,
457 ),
458 &SVector::from([t, v]),
459 );
460 let [[da_res_dt, da_res_dv]] = da_res.data.0;
461 (a_res - t * da_res_dt - v * da_res_dv + t, -da_res_dt)
462 };
463 let (h_l, s_l_res) = h_s(t_l, rho_l.recip());
464 let (h_v, s_v_res) = h_s(t_v, rho_v.recip());
465 let t = (h_v - h_l) / (s_v_res - s_l_res - (rho_v / rho_l).ln());
466 let trial_density = _density_iteration(eos, t, p, &x, DensityInitialization::Vapor)?;
467 if trial_density < cp_density {
468 rho_v = trial_density;
469 t_v = t;
470 }
471 let trial_density = _density_iteration(eos, t, p, &x, DensityInitialization::Liquid)?;
472 if trial_density > cp_density {
473 rho_l = trial_density;
474 t_l = t;
475 }
476 if t_l == t_v {
477 return Ok((t_l, [rho_v, rho_l]));
478 }
479 }
480 Err(FeosError::IterationFailed(
481 "new_init_p: could not find proper initial state".to_owned(),
482 ))
483}
484
485impl<E: Residual + Subset> PhaseEquilibrium<E, 2> {
486 pub fn vapor_pressure(eos: &E, temperature: Temperature) -> Vec<Option<Pressure>> {
489 (0..eos.components())
490 .map(|i| {
491 let pure_eos = eos.subset(&[i]);
492 PhaseEquilibrium::pure_t(&pure_eos, temperature, None, SolverOptions::default())
493 .map(|(p, _)| p)
494 .ok()
495 })
496 .collect()
497 }
498
499 pub fn boiling_temperature(eos: &E, pressure: Pressure) -> Vec<Option<Temperature>> {
502 (0..eos.components())
503 .map(|i| {
504 let pure_eos = eos.subset(&[i]);
505 PhaseEquilibrium::pure_p(&pure_eos, pressure, None, SolverOptions::default())
506 .map(|(t, _)| t)
507 .ok()
508 })
509 .collect()
510 }
511
512 pub fn vle_pure_comps<TP: TemperatureOrPressure>(
515 eos: &E,
516 temperature_or_pressure: TP,
517 ) -> Vec<Option<PhaseEquilibrium<E, 2>>> {
518 (0..eos.components())
519 .map(|i| {
520 let pure_eos = eos.subset(&[i]);
521 PhaseEquilibrium::pure(
522 &pure_eos,
523 temperature_or_pressure,
524 None,
525 SolverOptions::default(),
526 )
527 .and_then(|vle_pure| {
528 let mut molefracs_vapor = DVector::zeros(eos.components());
529 let mut molefracs_liquid = molefracs_vapor.clone();
530 molefracs_vapor[i] = 1.0;
531 molefracs_liquid[i] = 1.0;
532 let vapor = State::new(
533 eos,
534 vle_pure.vapor().temperature,
535 vle_pure.vapor().density,
536 molefracs_vapor,
537 )?;
538 let liquid = State::new(
539 eos,
540 vle_pure.liquid().temperature,
541 vle_pure.liquid().density,
542 molefracs_liquid,
543 )?;
544 Ok(PhaseEquilibrium::two_phase(vapor, liquid))
545 })
546 .ok()
547 })
548 .collect()
549 }
550}