1use super::{DensityInitialization, State, StateHD, TPSpec};
2use crate::equation_of_state::Residual;
3use crate::errors::{EosError, EosResult};
4use crate::{ReferenceSystem, SolverOptions, TemperatureOrPressure, Verbosity};
5use nalgebra::SVector;
6use ndarray::{arr1, Array1, Array2};
7use num_dual::linalg::smallest_ev;
8use num_dual::{
9 first_derivative, try_first_derivative, try_jacobian, Dual, Dual3, Dual64, DualNum, DualSVec64,
10 DualVec, HyperDual,
11};
12use num_traits::{One, Zero};
13use quantity::{Density, Moles, Pressure, Temperature, Volume};
14use std::sync::Arc;
15
16const MAX_ITER_CRIT_POINT: usize = 50;
17const MAX_ITER_CRIT_POINT_BINARY: usize = 200;
18const TOL_CRIT_POINT: f64 = 1e-8;
19
20impl<R: Residual> State<R> {
22 pub fn critical_point_pure(
24 eos: &Arc<R>,
25 initial_temperature: Option<Temperature>,
26 options: SolverOptions,
27 ) -> EosResult<Vec<Self>> {
28 (0..eos.components())
29 .map(|i| {
30 Self::critical_point(
31 &Arc::new(eos.subset(&[i])),
32 None,
33 initial_temperature,
34 options,
35 )
36 })
37 .collect()
38 }
39
40 pub fn critical_point_binary<TP: TemperatureOrPressure>(
41 eos: &Arc<R>,
42 temperature_or_pressure: TP,
43 initial_temperature: Option<Temperature>,
44 initial_molefracs: Option<[f64; 2]>,
45 options: SolverOptions,
46 ) -> EosResult<Self> {
47 match temperature_or_pressure.into() {
48 TPSpec::Temperature(t) => {
49 Self::critical_point_binary_t(eos, t, initial_molefracs, options)
50 }
51 TPSpec::Pressure(p) => Self::critical_point_binary_p(
52 eos,
53 p,
54 initial_temperature,
55 initial_molefracs,
56 options,
57 ),
58 }
59 }
60
61 pub fn critical_point(
63 eos: &Arc<R>,
64 moles: Option<&Moles<Array1<f64>>>,
65 initial_temperature: Option<Temperature>,
66 options: SolverOptions,
67 ) -> EosResult<Self> {
68 let moles = eos.validate_moles(moles)?;
69 let trial_temperatures = [
70 Temperature::from_reduced(300.0),
71 Temperature::from_reduced(700.0),
72 Temperature::from_reduced(500.0),
73 ];
74 if let Some(t) = initial_temperature {
75 return Self::critical_point_hkm(eos, &moles, t, options);
76 }
77 for &t in trial_temperatures.iter() {
78 let s = Self::critical_point_hkm(eos, &moles, t, options);
79 if s.is_ok() {
80 return s;
81 }
82 }
83 Err(EosError::NotConverged(String::from("Critical point")))
84 }
85
86 fn critical_point_hkm(
87 eos: &Arc<R>,
88 moles: &Moles<Array1<f64>>,
89 initial_temperature: Temperature,
90 options: SolverOptions,
91 ) -> EosResult<Self> {
92 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT, TOL_CRIT_POINT);
93
94 let mut t = initial_temperature.to_reduced();
95 let max_density = eos.max_density(Some(moles))?.to_reduced();
96 let mut rho = 0.3 * max_density;
97 let n = moles.to_reduced();
98
99 log_iter!(
100 verbosity,
101 " iter | residual | temperature | density "
102 );
103 log_iter!(verbosity, "{:-<64}", "");
104 log_iter!(
105 verbosity,
106 " {:4} | | {:13.8} | {:12.8}",
107 0,
108 Temperature::from_reduced(t),
109 Density::from_reduced(rho),
110 );
111
112 for i in 1..=max_iter {
113 let res = |x: SVector<DualSVec64<2>, 2>| critical_point_objective(eos, x[0], x[1], &n);
115 let (res, jac) = try_jacobian(res, SVector::from([t, rho]))?;
116
117 let delta = jac.lu().solve(&res);
119 let mut delta = delta.ok_or(EosError::IterationFailed("Critical point".into()))?;
120
121 if delta[0].abs() > 0.25 * t {
123 delta *= 0.25 * t / delta[0].abs()
124 }
125 if delta[1].abs() > 0.03 * max_density {
126 delta *= 0.03 * max_density / delta[1].abs()
127 }
128
129 t -= delta[0];
131 rho -= delta[1];
132 rho = f64::max(rho, 1e-4 * max_density);
133
134 log_iter!(
135 verbosity,
136 " {:4} | {:14.8e} | {:13.8} | {:12.8}",
137 i,
138 res.norm(),
139 Temperature::from_reduced(t),
140 Density::from_reduced(rho),
141 );
142
143 if res.norm() < tol {
145 log_result!(
146 verbosity,
147 "Critical point calculation converged in {} step(s)\n",
148 i
149 );
150 return State::new_nvt(
151 eos,
152 Temperature::from_reduced(t),
153 moles.sum() / Density::from_reduced(rho),
154 moles,
155 );
156 }
157 }
158 Err(EosError::NotConverged(String::from("Critical point")))
159 }
160
161 fn critical_point_binary_t(
163 eos: &Arc<R>,
164 temperature: Temperature,
165 initial_molefracs: Option<[f64; 2]>,
166 options: SolverOptions,
167 ) -> EosResult<Self> {
168 let (max_iter, tol, verbosity) =
169 options.unwrap_or(MAX_ITER_CRIT_POINT_BINARY, TOL_CRIT_POINT);
170
171 let t = temperature.to_reduced();
172 let x = SVector::from(initial_molefracs.unwrap_or([0.5, 0.5]));
173 let max_density = eos
174 .max_density(Some(&Moles::from_reduced(arr1(&x.data.0[0]))))?
175 .to_reduced();
176 let mut rho = x * 0.3 * max_density;
177
178 log_iter!(
179 verbosity,
180 " iter | residual | density 1 | density 2 "
181 );
182 log_iter!(verbosity, "{:-<69}", "");
183 log_iter!(
184 verbosity,
185 " {:4} | | {:12.8} | {:12.8}",
186 0,
187 Density::from_reduced(rho[0]),
188 Density::from_reduced(rho[1]),
189 );
190
191 for i in 1..=max_iter {
192 let res = |rho| critical_point_objective_t(eos, t, rho);
194 let (res, jac) = try_jacobian(res, rho)?;
195
196 let delta = jac.lu().solve(&res);
198 let mut delta = delta.ok_or(EosError::IterationFailed("Critical point".into()))?;
199
200 for i in 0..2 {
202 if delta[i].abs() > 0.03 * max_density {
203 delta *= 0.03 * max_density / delta[i].abs()
204 }
205 }
206
207 rho -= delta;
209 rho[0] = f64::max(rho[0], 1e-4 * max_density);
210 rho[1] = f64::max(rho[1], 1e-4 * max_density);
211
212 log_iter!(
213 verbosity,
214 " {:4} | {:14.8e} | {:12.8} | {:12.8}",
215 i,
216 res.norm(),
217 Density::from_reduced(rho[0]),
218 Density::from_reduced(rho[1]),
219 );
220
221 if res.norm() < tol {
223 log_result!(
224 verbosity,
225 "Critical point calculation converged in {} step(s)\n",
226 i
227 );
228 return State::new_nvt(
229 eos,
230 Temperature::from_reduced(t),
231 Volume::from_reduced(1.0),
232 &Moles::from_reduced(arr1(&rho.data.0[0])),
233 );
234 }
235 }
236 Err(EosError::NotConverged(String::from("Critical point")))
237 }
238
239 fn critical_point_binary_p(
241 eos: &Arc<R>,
242 pressure: Pressure,
243 initial_temperature: Option<Temperature>,
244 initial_molefracs: Option<[f64; 2]>,
245 options: SolverOptions,
246 ) -> EosResult<Self> {
247 let (max_iter, tol, verbosity) =
248 options.unwrap_or(MAX_ITER_CRIT_POINT_BINARY, TOL_CRIT_POINT);
249
250 let p = pressure.to_reduced();
251 let mut t = initial_temperature.map(|t| t.to_reduced()).unwrap_or(300.0);
252 let x = SVector::from(initial_molefracs.unwrap_or([0.5, 0.5]));
253 let max_density = eos
254 .max_density(Some(&Moles::from_reduced(arr1(&x.data.0[0]))))?
255 .to_reduced();
256 let mut rho = x * 0.3 * max_density;
257
258 log_iter!(
259 verbosity,
260 " iter | residual | temperature | density 1 | density 2 "
261 );
262 log_iter!(verbosity, "{:-<87}", "");
263 log_iter!(
264 verbosity,
265 " {:4} | | {:13.8} | {:12.8} | {:12.8}",
266 0,
267 Temperature::from_reduced(t),
268 Density::from_reduced(rho[0]),
269 Density::from_reduced(rho[1]),
270 );
271
272 for i in 1..=max_iter {
273 let res = |x: SVector<DualSVec64<3>, 3>| {
275 let r = SVector::from([x[1], x[2]]);
276 critical_point_objective_p(eos, p, x[0], r)
277 };
278 let (res, jac) = try_jacobian(res, SVector::from([t, rho[0], rho[1]]))?;
279
280 let delta = jac.lu().solve(&res);
282 let mut delta = delta.ok_or(EosError::IterationFailed("Critical point".into()))?;
283
284 if delta[0].abs() > 0.25 * t {
286 delta *= 0.25 * t / delta[0].abs()
287 }
288 if delta[1].abs() > 0.03 * max_density {
289 delta *= 0.03 * max_density / delta[1].abs()
290 }
291 if delta[2].abs() > 0.03 * max_density {
292 delta *= 0.03 * max_density / delta[2].abs()
293 }
294
295 t -= delta[0];
297 rho[0] -= delta[1];
298 rho[1] -= delta[2];
299 rho[0] = f64::max(rho[0], 1e-4 * max_density);
300 rho[1] = f64::max(rho[1], 1e-4 * max_density);
301
302 log_iter!(
303 verbosity,
304 " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}",
305 i,
306 res.norm(),
307 Temperature::from_reduced(t),
308 Density::from_reduced(rho[0]),
309 Density::from_reduced(rho[1]),
310 );
311
312 if res.norm() < tol {
314 log_result!(
315 verbosity,
316 "Critical point calculation converged in {} step(s)\n",
317 i
318 );
319 return State::new_nvt(
320 eos,
321 Temperature::from_reduced(t),
322 Volume::from_reduced(1.0),
323 &Moles::from_reduced(arr1(&rho.data.0[0])),
324 );
325 }
326 }
327 Err(EosError::NotConverged(String::from("Critical point")))
328 }
329
330 pub fn spinodal(
331 eos: &Arc<R>,
332 temperature: Temperature,
333 moles: Option<&Moles<Array1<f64>>>,
334 options: SolverOptions,
335 ) -> EosResult<[Self; 2]> {
336 let critical_point = Self::critical_point(eos, moles, None, options)?;
337 let moles = eos.validate_moles(moles)?;
338 let spinodal_vapor = Self::calculate_spinodal(
339 eos,
340 temperature,
341 &moles,
342 DensityInitialization::Vapor,
343 options,
344 )?;
345 let rho = 2.0 * critical_point.density - spinodal_vapor.density;
346 let spinodal_liquid = Self::calculate_spinodal(
347 eos,
348 temperature,
349 &moles,
350 DensityInitialization::InitialDensity(rho),
351 options,
352 )?;
353 Ok([spinodal_vapor, spinodal_liquid])
354 }
355
356 fn calculate_spinodal(
357 eos: &Arc<R>,
358 temperature: Temperature,
359 moles: &Moles<Array1<f64>>,
360 density_initialization: DensityInitialization,
361 options: SolverOptions,
362 ) -> EosResult<Self> {
363 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT, TOL_CRIT_POINT);
364
365 let max_density = eos.max_density(Some(moles))?.to_reduced();
366 let t = temperature.to_reduced();
367 let mut rho = match density_initialization {
368 DensityInitialization::Vapor => 1e-5 * max_density,
369 DensityInitialization::Liquid => max_density,
370 DensityInitialization::InitialDensity(rho) => rho.to_reduced(),
371 DensityInitialization::None => unreachable!(),
372 };
373 let n = moles.to_reduced();
374
375 log_iter!(verbosity, " iter | residual | density ");
376 log_iter!(verbosity, "{:-<46}", "");
377 log_iter!(
378 verbosity,
379 " {:4} | | {:12.8}",
380 0,
381 Density::from_reduced(rho),
382 );
383
384 for i in 1..=max_iter {
385 let (f, df) =
387 try_first_derivative(|rho| spinodal_objective(eos, t.into(), rho, &n), rho)?;
388
389 let mut delta = f / df;
391
392 if delta.abs() > 0.03 * max_density {
394 delta *= 0.03 * max_density / delta.abs()
395 }
396
397 rho -= delta;
399 rho = f64::max(rho, 1e-4 * max_density);
400
401 log_iter!(
402 verbosity,
403 " {:4} | {:14.8e} | {:12.8}",
404 i,
405 f.abs(),
406 Density::from_reduced(rho),
407 );
408
409 if f.abs() < tol {
411 log_result!(
412 verbosity,
413 "Spinodal calculation converged in {} step(s)\n",
414 i
415 );
416 return State::new_nvt(
417 eos,
418 temperature,
419 moles.sum() / Density::from_reduced(rho),
420 moles,
421 );
422 }
423 }
424 Err(EosError::SuperCritical)
425 }
426}
427
428fn critical_point_objective<R: Residual>(
429 eos: &Arc<R>,
430 temperature: DualSVec64<2>,
431 density: DualSVec64<2>,
432 moles: &Array1<f64>,
433) -> EosResult<SVector<DualSVec64<2>, 2>> {
434 let t = HyperDual::from_re(temperature);
436 let v = HyperDual::from_re(density.recip() * moles.sum());
437 let qij = Array2::from_shape_fn((eos.components(), eos.components()), |(i, j)| {
438 let mut m = moles.mapv(HyperDual::from);
439 m[i].eps1 = DualSVec64::one();
440 m[j].eps2 = DualSVec64::one();
441 let state = StateHD::new(t, v, m);
442 eos.residual_helmholtz_energy(&state).eps1eps2 * (moles[i] * moles[j]).sqrt()
443 + kronecker(i, j)
444 });
445
446 let (eval, evec) = smallest_ev(qij);
448
449 let moles_hd = Array1::from_shape_fn(eos.components(), |i| {
451 Dual3::new(
452 DualSVec64::from(moles[i]),
453 evec[i] * moles[i].sqrt(),
454 DualSVec64::zero(),
455 DualSVec64::zero(),
456 )
457 });
458 let state_s = StateHD::new(
459 Dual3::from_re(temperature),
460 Dual3::from_re(density.recip() * moles.sum()),
461 moles_hd,
462 );
463 let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum();
464 let res = eos.residual_helmholtz_energy(&state_s);
465 Ok(SVector::from([eval, (res + ig).v3]))
466}
467
468fn critical_point_objective_t<R: Residual>(
469 eos: &Arc<R>,
470 temperature: f64,
471 density: SVector<DualSVec64<2>, 2>,
472) -> EosResult<SVector<DualSVec64<2>, 2>> {
473 let t = HyperDual::from(temperature);
475 let v = HyperDual::from(1.0);
476 let qij = Array2::from_shape_fn((eos.components(), eos.components()), |(i, j)| {
477 let mut m = density.map(HyperDual::from_re);
478 m[i].eps1 = DualSVec64::one();
479 m[j].eps2 = DualSVec64::one();
480 let state = StateHD::new(t, v, arr1(&[m[0], m[1]]));
481 eos.residual_helmholtz_energy(&state).eps1eps2 * (density[i] * density[j]).sqrt()
482 + kronecker(i, j)
483 });
484
485 let (eval, evec) = smallest_ev(qij);
487
488 let moles_hd = Array1::from_shape_fn(eos.components(), |i| {
490 Dual3::new(
491 density[i],
492 evec[i] * density[i].sqrt(),
493 DualSVec64::zero(),
494 DualSVec64::zero(),
495 )
496 });
497 let state_s = StateHD::new(Dual3::from(temperature), Dual3::from(1.0), moles_hd);
498 let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum();
499 let res = eos.residual_helmholtz_energy(&state_s);
500 Ok(SVector::from([eval, (res + ig).v3]))
501}
502
503fn critical_point_objective_p<R: Residual>(
504 eos: &Arc<R>,
505 pressure: f64,
506 temperature: DualSVec64<3>,
507 density: SVector<DualSVec64<3>, 2>,
508) -> EosResult<SVector<DualSVec64<3>, 3>> {
509 let t = HyperDual::from_re(temperature);
511 let v = HyperDual::from(1.0);
512 let qij = Array2::from_shape_fn((eos.components(), eos.components()), |(i, j)| {
513 let mut m = density.map(HyperDual::from_re);
514 m[i].eps1 = DualSVec64::one();
515 m[j].eps2 = DualSVec64::one();
516 let state = StateHD::new(t, v, arr1(&[m[0], m[1]]));
517 eos.residual_helmholtz_energy(&state).eps1eps2 * (density[i] * density[j]).sqrt()
518 + kronecker(i, j)
519 });
520
521 let (eval, evec) = smallest_ev(qij);
523
524 let moles_hd = Array1::from_shape_fn(eos.components(), |i| {
526 Dual3::new(
527 density[i],
528 evec[i] * density[i].sqrt(),
529 DualSVec64::zero(),
530 DualSVec64::zero(),
531 )
532 });
533 let state_s = StateHD::new(Dual3::from_re(temperature), Dual3::from(1.0), moles_hd);
534 let ig = (&state_s.moles * (state_s.partial_density.mapv(|x| x.ln()) - 1.0)).sum();
535 let res = eos.residual_helmholtz_energy(&state_s);
536
537 let a = |v| {
539 let m = arr1(&[Dual::from_re(density[0]), Dual::from_re(density[1])]);
540 let state_p = StateHD::new(Dual::from_re(temperature), v, m);
541 eos.residual_helmholtz_energy(&state_p)
542 };
543 let (_, p) = first_derivative(a, DualVec::one());
544 let p = (p - density.sum()) * temperature;
545
546 Ok(SVector::from([eval, (res + ig).v3, p + pressure]))
547}
548
549fn spinodal_objective<R: Residual>(
550 eos: &Arc<R>,
551 temperature: Dual64,
552 density: Dual64,
553 moles: &Array1<f64>,
554) -> EosResult<Dual64> {
555 let t = HyperDual::from_re(temperature);
557 let v = HyperDual::from_re(density.recip() * moles.sum());
558 let qij = Array2::from_shape_fn((eos.components(), eos.components()), |(i, j)| {
559 let mut m = moles.mapv(HyperDual::from);
560 m[i].eps1 = Dual64::one();
561 m[j].eps2 = Dual64::one();
562 let state = StateHD::new(t, v, m);
563 eos.residual_helmholtz_energy(&state).eps1eps2 * (moles[i] * moles[j]).sqrt()
564 + kronecker(i, j)
565 });
566
567 let (eval, _) = smallest_ev(qij);
569
570 Ok(eval)
571}
572
573fn kronecker(i: usize, j: usize) -> f64 {
574 if i == j {
575 1.0
576 } else {
577 0.0
578 }
579}