1use super::PhaseEquilibrium;
2use crate::equation_of_state::Residual;
3use crate::errors::{EosError, EosResult};
4use crate::state::{Contributions, DensityInitialization, State, TPSpec};
5use crate::{ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity};
6use ndarray::{arr1, Array1};
7use quantity::{Moles, Pressure, Temperature, RGAS};
8use std::sync::Arc;
9
10const SCALE_T_NEW: f64 = 0.7;
11const MAX_ITER_PURE: usize = 50;
12const TOL_PURE: f64 = 1e-12;
13
14impl<E: Residual> PhaseEquilibrium<E, 2> {
16 pub fn pure<TP: TemperatureOrPressure>(
18 eos: &Arc<E>,
19 temperature_or_pressure: TP,
20 initial_state: Option<&PhaseEquilibrium<E, 2>>,
21 options: SolverOptions,
22 ) -> EosResult<Self> {
23 match temperature_or_pressure.into() {
24 TPSpec::Temperature(t) => Self::pure_t(eos, t, initial_state, options),
25 TPSpec::Pressure(p) => Self::pure_p(eos, p, initial_state, options),
26 }
27 }
28
29 fn pure_t(
32 eos: &Arc<E>,
33 temperature: Temperature,
34 initial_state: Option<&PhaseEquilibrium<E, 2>>,
35 options: SolverOptions,
36 ) -> EosResult<Self> {
37 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
38
39 let mut vle = initial_state.and_then(|init| {
41 Self::init_pure_state(init, temperature)
42 .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity))
43 .ok()
44 });
45
46 vle = vle.or_else(|| {
48 Self::init_pure_ideal_gas(eos, temperature)
49 .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity))
50 .ok()
51 });
52
53 vle.map_or_else(
55 || {
56 Self::init_pure_spinodal(eos, temperature)
57 .and_then(|vle| vle.iterate_pure_t(max_iter, tol, verbosity))
58 },
59 Ok,
60 )
61 }
62
63 fn iterate_pure_t(self, max_iter: usize, tol: f64, verbosity: Verbosity) -> EosResult<Self> {
64 let mut p_old = self.vapor().pressure(Contributions::Total);
65 let [mut vapor, mut liquid] = self.0;
66
67 log_iter!(verbosity,
68 " iter | residual | pressure | liquid density | vapor density | Newton steps"
69 );
70 log_iter!(verbosity, "{:-<106}", "");
71 log_iter!(
72 verbosity,
73 " {:4} | | {:12.8} | {:12.8} | {:12.8} |",
74 0,
75 p_old,
76 liquid.density,
77 vapor.density
78 );
79
80 for i in 1..=max_iter {
81 let (p_l, p_rho_l) = liquid.p_dpdrho();
83 let (p_v, p_rho_v) = vapor.p_dpdrho();
84 let a_l_res = liquid.residual_molar_helmholtz_energy();
86 let a_v_res = vapor.residual_molar_helmholtz_energy();
87
88 let kt = RGAS * vapor.temperature;
90 let delta_v = 1.0 / vapor.density - 1.0 / liquid.density;
91 let delta_a =
92 a_v_res - a_l_res + kt * (vapor.density / liquid.density).into_value().ln();
93 let mut p_new = -delta_a / delta_v;
94
95 if p_new.is_sign_negative() {
98 p_new = p_v
99 * ((-delta_a - p_v * vapor.volume / vapor.total_moles) / kt)
100 .into_value()
101 .exp();
102 }
103
104 let kt = RGAS * vapor.temperature;
106 let mut newton_iter = 0;
107 let newton_tol = p_old * delta_v * tol;
108 for _ in 0..20 {
109 let p_frac = (p_new / p_old).into_value();
110 let f = p_new * delta_v + delta_a + (p_frac.ln() + 1.0 - p_frac) * kt;
111 let df_dp = delta_v + (1.0 / p_new - 1.0 / p_old) * kt;
112 p_new -= f / df_dp;
113 newton_iter += 1;
114 if f.abs() < newton_tol {
115 break;
116 }
117 }
118
119 if p_new.is_nan() {
121 return Err(EosError::IterationFailed("pure_t".to_owned()));
122 }
123
124 let rho_l = liquid.density + (p_new - p_l) / p_rho_l;
126 let rho_v = vapor.density + (p_new - p_v) / p_rho_v;
127 liquid = State::new_pure(&liquid.eos, liquid.temperature, rho_l)?;
128 vapor = State::new_pure(&vapor.eos, vapor.temperature, rho_v)?;
129 if Self::is_trivial_solution(&vapor, &liquid) {
130 return Err(EosError::TrivialSolution);
131 }
132
133 let res = (p_new - p_old).abs();
135 log_iter!(
136 verbosity,
137 " {:4} | {:14.8e} | {:12.8} | {:12.8} | {:12.8} | {}",
138 i,
139 res,
140 p_new,
141 liquid.density,
142 vapor.density,
143 newton_iter
144 );
145 if res < p_old * tol {
146 log_result!(
147 verbosity,
148 "PhaseEquilibrium::pure_t: calculation converged in {} step(s)\n",
149 i
150 );
151 return Ok(Self([vapor, liquid]));
152 }
153 p_old = p_new;
154 }
155 Err(EosError::NotConverged("pure_t".to_owned()))
156 }
157
158 fn pure_p(
161 eos: &Arc<E>,
162 pressure: Pressure,
163 initial_state: Option<&Self>,
164 options: SolverOptions,
165 ) -> EosResult<Self> {
166 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PURE, TOL_PURE);
167
168 let mut vle = match initial_state {
170 Some(init) => init
171 .clone()
172 .update_pressure(init.vapor().temperature, pressure)?,
173 None => PhaseEquilibrium::init_pure_p(eos, pressure)?,
174 };
175
176 log_iter!(
177 verbosity,
178 " iter | residual | temperature | liquid density | vapor density "
179 );
180 log_iter!(verbosity, "{:-<89}", "");
181 log_iter!(
182 verbosity,
183 " {:4} | | {:13.8} | {:12.8} | {:12.8}",
184 0,
185 vle.vapor().temperature,
186 vle.liquid().density,
187 vle.vapor().density
188 );
189 for i in 1..=max_iter {
190 let (p_l, p_rho_l) = vle.liquid().p_dpdrho();
192 let (p_v, p_rho_v) = vle.vapor().p_dpdrho();
193 let p_t_l = vle.liquid().dp_dt(Contributions::Total);
194 let p_t_v = vle.vapor().dp_dt(Contributions::Total);
195
196 let s_l_res = vle.liquid().residual_molar_entropy();
198 let s_v_res = vle.vapor().residual_molar_entropy();
199
200 let a_l_res = vle.liquid().residual_molar_helmholtz_energy();
202 let a_v_res = vle.vapor().residual_molar_helmholtz_energy();
203
204 let v_l = 1.0 / vle.liquid().density;
206 let v_v = 1.0 / vle.vapor().density;
207
208 let kt = RGAS * vle.vapor().temperature;
210 let ln_rho = (v_l / v_v).into_value().ln();
211 let delta_t = (pressure * (v_v - v_l) + (a_v_res - a_l_res + kt * ln_rho))
212 / (s_v_res - s_l_res - RGAS * ln_rho);
213 let t_new = vle.vapor().temperature + delta_t;
214
215 let rho_l = vle.liquid().density + (pressure - p_l - p_t_l * delta_t) / p_rho_l;
217 let rho_v = vle.vapor().density + (pressure - p_v - p_t_v * delta_t) / p_rho_v;
218
219 if rho_l.is_sign_negative()
220 || rho_v.is_sign_negative()
221 || delta_t.abs() > Temperature::from_reduced(1.0)
222 {
223 vle = vle
225 .update_pressure(t_new, pressure)?
226 .check_trivial_solution()?;
227 } else {
228 vle = Self([
230 State::new_pure(eos, t_new, rho_v)?,
231 State::new_pure(eos, t_new, rho_l)?,
232 ]);
233 }
234
235 let res = delta_t.abs();
237 log_iter!(
238 verbosity,
239 " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}",
240 i,
241 res,
242 vle.vapor().temperature,
243 vle.liquid().density,
244 vle.vapor().density
245 );
246 if res < vle.vapor().temperature * tol {
247 log_result!(
248 verbosity,
249 "PhaseEquilibrium::pure_p: calculation converged in {} step(s)\n",
250 i
251 );
252 return Ok(vle);
253 }
254 }
255 Err(EosError::NotConverged("pure_p".to_owned()))
256 }
257
258 fn init_pure_state(initial_state: &Self, temperature: Temperature) -> EosResult<Self> {
259 let vapor = initial_state.vapor().update_temperature(temperature)?;
260 let liquid = initial_state.liquid().update_temperature(temperature)?;
261 Ok(Self([vapor, liquid]))
262 }
263
264 fn init_pure_ideal_gas(eos: &Arc<E>, temperature: Temperature) -> EosResult<Self> {
265 let m = Moles::from_reduced(arr1(&[1.0]));
266 let p = Self::starting_pressure_ideal_gas_bubble(eos, temperature, &arr1(&[1.0]))?.0;
267 PhaseEquilibrium::new_npt(eos, temperature, p, &m, &m)?.check_trivial_solution()
268 }
269
270 fn init_pure_spinodal(eos: &Arc<E>, temperature: Temperature) -> EosResult<Self> {
271 let p = Self::starting_pressure_spinodal(eos, temperature, &arr1(&[1.0]))?;
272 let m = Moles::from_reduced(arr1(&[1.0]));
273 PhaseEquilibrium::new_npt(eos, temperature, p, &m, &m)
274 }
275
276 fn init_pure_p(eos: &Arc<E>, pressure: Pressure) -> EosResult<Self> {
278 let trial_temperatures = [
279 Temperature::from_reduced(300.0),
280 Temperature::from_reduced(500.0),
281 Temperature::from_reduced(200.0),
282 ];
283 let m = Moles::from_reduced(arr1(&[1.0]));
284 let mut vle = None;
285 let mut t0 = Temperature::from_reduced(1.0);
286 for t in trial_temperatures.iter() {
287 t0 = *t;
288 let _vle = PhaseEquilibrium::new_npt(eos, *t, pressure, &m, &m)?;
289 if !Self::is_trivial_solution(_vle.vapor(), _vle.liquid()) {
290 return Ok(_vle);
291 }
292 vle = Some(_vle);
293 }
294
295 let cp = State::critical_point(eos, None, None, SolverOptions::default())?;
296 if pressure > cp.pressure(Contributions::Total) {
297 return Err(EosError::SuperCritical);
298 };
299 if let Some(mut e) = vle {
300 if e.vapor().density < cp.density {
301 for _ in 0..8 {
302 t0 *= SCALE_T_NEW;
303 e.0[1] = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Liquid)?;
304 if e.liquid().density > cp.density {
305 break;
306 }
307 }
308 } else {
309 for _ in 0..8 {
310 t0 /= SCALE_T_NEW;
311 e.0[0] = State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?;
312 if e.vapor().density < cp.density {
313 break;
314 }
315 }
316 }
317
318 for _ in 0..20 {
319 let h = |s: &State<_>| s.residual_enthalpy() + s.total_moles * RGAS * s.temperature;
320 t0 = (h(e.vapor()) - h(e.liquid()))
321 / (e.vapor().residual_entropy()
322 - e.liquid().residual_entropy()
323 - RGAS
324 * e.vapor().total_moles
325 * ((e.vapor().density / e.liquid().density).into_value().ln()));
326 let trial_state =
327 State::new_npt(eos, t0, pressure, &m, DensityInitialization::Vapor)?;
328 if trial_state.density < cp.density {
329 e.0[0] = trial_state;
330 }
331 let trial_state =
332 State::new_npt(eos, t0, pressure, &m, DensityInitialization::Liquid)?;
333 if trial_state.density > cp.density {
334 e.0[1] = trial_state;
335 }
336 if e.liquid().temperature == e.vapor().temperature {
337 return Ok(e);
338 }
339 }
340 Err(EosError::IterationFailed(
341 "new_init_p: could not find proper initial state".to_owned(),
342 ))
343 } else {
344 unreachable!()
345 }
346 }
347}
348
349impl<E: Residual> PhaseEquilibrium<E, 2> {
350 pub fn vapor_pressure(eos: &Arc<E>, temperature: Temperature) -> Vec<Option<Pressure>> {
353 (0..eos.components())
354 .map(|i| {
355 let pure_eos = Arc::new(eos.subset(&[i]));
356 PhaseEquilibrium::pure_t(&pure_eos, temperature, None, SolverOptions::default())
357 .map(|vle| vle.vapor().pressure(Contributions::Total))
358 .ok()
359 })
360 .collect()
361 }
362
363 pub fn boiling_temperature(eos: &Arc<E>, pressure: Pressure) -> Vec<Option<Temperature>> {
366 (0..eos.components())
367 .map(|i| {
368 let pure_eos = Arc::new(eos.subset(&[i]));
369 PhaseEquilibrium::pure_p(&pure_eos, pressure, None, SolverOptions::default())
370 .map(|vle| vle.vapor().temperature)
371 .ok()
372 })
373 .collect()
374 }
375
376 pub fn vle_pure_comps<TP: TemperatureOrPressure>(
379 eos: &Arc<E>,
380 temperature_or_pressure: TP,
381 ) -> Vec<Option<PhaseEquilibrium<E, 2>>> {
382 (0..eos.components())
383 .map(|i| {
384 let pure_eos = Arc::new(eos.subset(&[i]));
385 PhaseEquilibrium::pure(
386 &pure_eos,
387 temperature_or_pressure,
388 None,
389 SolverOptions::default(),
390 )
391 .ok()
392 .map(|vle_pure| {
393 let mut moles_vapor = Moles::from_reduced(Array1::zeros(eos.components()));
394 let mut moles_liquid = moles_vapor.clone();
395 moles_vapor.set(i, vle_pure.vapor().total_moles);
396 moles_liquid.set(i, vle_pure.liquid().total_moles);
397 let vapor = State::new_nvt(
398 eos,
399 vle_pure.vapor().temperature,
400 vle_pure.vapor().volume,
401 &moles_vapor,
402 )
403 .unwrap();
404 let liquid = State::new_nvt(
405 eos,
406 vle_pure.liquid().temperature,
407 vle_pure.liquid().volume,
408 &moles_liquid,
409 )
410 .unwrap();
411 PhaseEquilibrium::from_states(vapor, liquid)
412 })
413 })
414 .collect()
415 }
416}