feos_core/state/
critical_point.rs

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
20/// # Critical points
21impl<R: Residual> State<R> {
22    /// Calculate the pure component critical point of all components.
23    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    /// Calculate the critical point of a system for given moles.
62    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            // calculate residuals and derivatives w.r.t. temperature and density
114            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            // calculate Newton step
118            let delta = jac.lu().solve(&res);
119            let mut delta = delta.ok_or(EosError::IterationFailed("Critical point".into()))?;
120
121            // reduce step if necessary
122            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            // apply step
130            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            // check convergence
144            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    /// Calculate the critical point of a binary system for given temperature.
162    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            // calculate residuals and derivatives w.r.t. partial densities
193            let res = |rho| critical_point_objective_t(eos, t, rho);
194            let (res, jac) = try_jacobian(res, rho)?;
195
196            // calculate Newton step
197            let delta = jac.lu().solve(&res);
198            let mut delta = delta.ok_or(EosError::IterationFailed("Critical point".into()))?;
199
200            // reduce step if necessary
201            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            // apply step
208            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            // check convergence
222            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    /// Calculate the critical point of a binary system for given pressure.
240    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            // calculate residuals and derivatives w.r.t. temperature and partial densities
274            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            // calculate Newton step
281            let delta = jac.lu().solve(&res);
282            let mut delta = delta.ok_or(EosError::IterationFailed("Critical point".into()))?;
283
284            // reduce step if necessary
285            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            // apply step
296            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            // check convergence
313            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            // calculate residuals and derivative w.r.t. density
386            let (f, df) =
387                try_first_derivative(|rho| spinodal_objective(eos, t.into(), rho, &n), rho)?;
388
389            // calculate Newton step
390            let mut delta = f / df;
391
392            // reduce step if necessary
393            if delta.abs() > 0.03 * max_density {
394                delta *= 0.03 * max_density / delta.abs()
395            }
396
397            // apply step
398            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            // check convergence
410            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    // calculate second partial derivatives w.r.t. moles
435    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    // calculate smallest eigenvalue and corresponding eigenvector of q
447    let (eval, evec) = smallest_ev(qij);
448
449    // evaluate third partial derivative w.r.t. s
450    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    // calculate second partial derivatives w.r.t. moles
474    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    // calculate smallest eigenvalue and corresponding eigenvector of q
486    let (eval, evec) = smallest_ev(qij);
487
488    // evaluate third partial derivative w.r.t. s
489    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    // calculate second partial derivatives w.r.t. moles
510    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    // calculate smallest eigenvalue and corresponding eigenvector of q
522    let (eval, evec) = smallest_ev(qij);
523
524    // evaluate third partial derivative w.r.t. s
525    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    // calculate pressure
538    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    // calculate second partial derivatives w.r.t. moles
556    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    // calculate smallest eigenvalue of q
568    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}