feos_core/state/
critical_point.rs

1use super::{DensityInitialization, State};
2use crate::equation_of_state::Residual;
3use crate::errors::{FeosError, FeosResult};
4use crate::{ReferenceSystem, SolverOptions, Subset, TemperatureOrPressure, Verbosity};
5use nalgebra::allocator::Allocator;
6use nalgebra::{DVector, DefaultAllocator, OMatrix, OVector, SVector, U1, U2, U3};
7use num_dual::linalg::smallest_ev;
8use num_dual::{
9    Dual3, DualNum, DualSVec, DualSVec64, DualStruct, Gradients, first_derivative,
10    implicit_derivative_binary, implicit_derivative_vec, jacobian, partial, partial2,
11    third_derivative,
12};
13use quantity::{Density, Pressure, Temperature};
14
15const MAX_ITER_CRIT_POINT: usize = 50;
16const MAX_ITER_CRIT_POINT_BINARY: usize = 200;
17const TOL_CRIT_POINT: f64 = 1e-8;
18
19/// # Critical points
20impl<R: Residual + Subset> State<R> {
21    /// Calculate the pure component critical point of all components.
22    pub fn critical_point_pure(
23        eos: &R,
24        initial_temperature: Option<Temperature>,
25        initial_density: Option<Density>,
26        options: SolverOptions,
27    ) -> FeosResult<Vec<Self>> {
28        (0..eos.components())
29            .map(|i| {
30                let pure_eos = eos.subset(&[i]);
31                let cp = State::critical_point(
32                    &pure_eos,
33                    None,
34                    initial_temperature,
35                    initial_density,
36                    options,
37                )?;
38                let mut molefracs = DVector::zeros(eos.components());
39                molefracs[i] = 1.0;
40                State::new_intensive(eos, cp.temperature, cp.density, &molefracs)
41            })
42            .collect()
43    }
44}
45
46impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
47where
48    DefaultAllocator: Allocator<N> + Allocator<N, N> + Allocator<U1, N>,
49{
50    pub fn critical_point_binary<TP: TemperatureOrPressure<D>>(
51        eos: &E,
52        temperature_or_pressure: TP,
53        initial_temperature: Option<Temperature>,
54        initial_molefracs: Option<[f64; 2]>,
55        initial_density: Option<Density>,
56        options: SolverOptions,
57    ) -> FeosResult<Self> {
58        let eos_re = eos.re();
59        let n = N::from_usize(2);
60        let initial_molefracs = initial_molefracs.unwrap_or([0.5; 2]);
61        let initial_molefracs = OVector::from_fn_generic(n, U1, |i, _| initial_molefracs[i]);
62        if let Some(t) = temperature_or_pressure.temperature() {
63            let [rho0, rho1] = critical_point_binary_t(
64                &eos_re,
65                t.re(),
66                initial_molefracs,
67                initial_density,
68                options,
69            )?;
70            let rho = implicit_derivative_binary(
71                |rho0, rho1, &temperature| {
72                    let rho = [rho0, rho1];
73                    let density = rho0 + rho1;
74                    let molefracs = OVector::from_fn_generic(n, U1, |i, _| rho[i] / density);
75                    criticality_conditions(&eos.lift(), temperature, density, &molefracs)
76                },
77                rho0,
78                rho1,
79                &t.into_reduced(),
80            );
81            let density = rho[0] + rho[1];
82            let molefracs = OVector::from_fn_generic(n, U1, |i, _| rho[i] / density);
83            Self::new_intensive(eos, t, Density::from_reduced(density), &molefracs)
84        } else if let Some(p) = temperature_or_pressure.pressure() {
85            let x = critical_point_binary_p(
86                &eos_re,
87                p.re(),
88                initial_temperature,
89                initial_molefracs,
90                initial_density,
91                options,
92            )?;
93            let trho = implicit_derivative_vec::<_, _, _, _, U3>(
94                |x, &p| {
95                    let t = x[0];
96                    let rho = [x[1], x[2]];
97                    criticality_conditions_p(&eos.lift(), p, t, rho)
98                },
99                SVector::from(x),
100                &p.into_reduced(),
101            );
102            let density = trho[1] + trho[2];
103            let molefracs = OVector::from_fn_generic(n, U1, |i, _| trho[i + 1] / density);
104            let t = Temperature::from_reduced(trho[0]);
105            Self::new_intensive(eos, t, Density::from_reduced(density), &molefracs)
106        } else {
107            unreachable!()
108        }
109    }
110
111    /// Calculate the critical point of a system for given moles.
112    pub fn critical_point(
113        eos: &E,
114        molefracs: Option<&OVector<D, N>>,
115        initial_temperature: Option<Temperature>,
116        initial_density: Option<Density>,
117        options: SolverOptions,
118    ) -> FeosResult<Self> {
119        let eos_re = eos.re();
120        let molefracs = molefracs.map_or_else(E::pure_molefracs, |x| x.clone());
121        let x = &molefracs.map(|x| x.re());
122        let rho_init = initial_density.map(|r| r.into_reduced());
123        let trial_temperatures = [300.0, 700.0, 500.0];
124        let mut t_rho = None;
125        if let Some(t) = initial_temperature {
126            let t = t.into_reduced();
127            t_rho = Some(critical_point_hkm(&eos_re, x, t, rho_init, options)?);
128        }
129        for &t in trial_temperatures.iter() {
130            if t_rho.is_some() {
131                break;
132            }
133            t_rho = critical_point_hkm(&eos_re, x, t, rho_init, options).ok();
134        }
135        let Some(t_rho) = t_rho else {
136            return Err(FeosError::NotConverged(String::from("Critical point")));
137        };
138
139        // implicit derivative
140        let [temperature, density] = implicit_derivative_binary(
141            |t, rho, x| criticality_conditions(&eos.lift(), t, rho, x),
142            t_rho[0],
143            t_rho[1],
144            &molefracs,
145        );
146        Self::new_intensive(
147            eos,
148            Temperature::from_reduced(temperature),
149            Density::from_reduced(density),
150            &molefracs,
151        )
152    }
153}
154
155fn critical_point_hkm<E: Residual<N>, N: Gradients>(
156    eos: &E,
157    molefracs: &OVector<f64, N>,
158    initial_temperature: f64,
159    initial_density: Option<f64>,
160    options: SolverOptions,
161) -> FeosResult<[f64; 2]>
162where
163    DefaultAllocator: Allocator<N> + Allocator<N, N> + Allocator<U1, N>,
164{
165    let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT, TOL_CRIT_POINT);
166
167    let mut t = initial_temperature;
168    let max_density = eos.compute_max_density(molefracs);
169    let mut rho = initial_density.unwrap_or(0.3 * max_density);
170
171    log_iter!(
172        verbosity,
173        " iter |    residual    |   temperature   |       density        "
174    );
175    log_iter!(verbosity, "{:-<64}", "");
176    log_iter!(
177        verbosity,
178        " {:4} |                | {:13.8} | {:12.8}",
179        0,
180        Temperature::from_reduced(t),
181        Density::from_reduced(rho),
182    );
183
184    for i in 1..=max_iter {
185        // calculate residuals and derivatives w.r.t. temperature and density
186        let (res, jac) = jacobian::<_, _, _, U2, U2, _>(
187            |x: SVector<DualSVec64<2>, 2>| {
188                SVector::from(criticality_conditions(
189                    &eos.lift(),
190                    x[0],
191                    x[1],
192                    &molefracs.map(DualSVec::from_re),
193                ))
194            },
195            &SVector::from([t, rho]),
196        );
197
198        // calculate Newton step
199        let delta = jac.lu().solve::<U2, U1, _>(&res);
200        let mut delta = delta.ok_or(FeosError::IterationFailed("Critical point".into()))?;
201
202        // reduce step if necessary
203        if delta[0].abs() > 0.25 * t {
204            delta *= 0.25 * t / delta[0].abs()
205        }
206        if delta[1].abs() > 0.03 * max_density {
207            delta *= 0.03 * max_density / delta[1].abs()
208        }
209
210        // apply step
211        t -= delta[0];
212        rho -= delta[1];
213        rho = f64::max(rho, 1e-4 * max_density);
214
215        log_iter!(
216            verbosity,
217            " {:4} | {:14.8e} | {:13.8} | {:12.8}",
218            i,
219            res.norm(),
220            Temperature::from_reduced(t),
221            Density::from_reduced(rho),
222        );
223
224        // check convergence
225        if res.norm() < tol {
226            log_result!(
227                verbosity,
228                "Critical point calculation converged in {} step(s)\n",
229                i
230            );
231            return Ok([t, rho]);
232        }
233    }
234    Err(FeosError::NotConverged(String::from("Critical point")))
235}
236
237/// Calculate the critical point of a binary system for given temperature.
238fn critical_point_binary_t<E: Residual<N>, N: Gradients>(
239    eos: &E,
240    temperature: Temperature,
241    initial_molefracs: OVector<f64, N>,
242    initial_density: Option<Density>,
243    options: SolverOptions,
244) -> FeosResult<[f64; 2]>
245where
246    DefaultAllocator: Allocator<N> + Allocator<N, N> + Allocator<U1, N>,
247{
248    let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT_BINARY, TOL_CRIT_POINT);
249
250    let t = temperature.to_reduced();
251    let n = N::from_usize(2);
252    let max_density = eos.compute_max_density(&initial_molefracs);
253    let rho_init = initial_density.map_or(0.3 * max_density, |r| r.into_reduced());
254    let mut rho = SVector::from([initial_molefracs[0], initial_molefracs[1]]) * rho_init;
255
256    log_iter!(
257        verbosity,
258        " iter |    residual    |      density 1       |      density 2       "
259    );
260    log_iter!(verbosity, "{:-<69}", "");
261    log_iter!(
262        verbosity,
263        " {:4} |                | {:12.8} | {:12.8}",
264        0,
265        Density::from_reduced(rho[0]),
266        Density::from_reduced(rho[1]),
267    );
268
269    for i in 1..=max_iter {
270        // calculate residuals and derivatives w.r.t. partial densities
271        let (res, jac) = jacobian::<_, _, _, U2, U2, _>(
272            |rho: SVector<DualSVec64<2>, 2>| {
273                let density = rho.sum();
274                let x = rho / density;
275                let molefracs = OVector::from_fn_generic(n, U1, |i, _| x[i]);
276                let t = DualSVec::from_re(t);
277                SVector::from(criticality_conditions(&eos.lift(), t, density, &molefracs))
278            },
279            &rho,
280        );
281
282        // calculate Newton step
283        let delta = jac.lu().solve(&res);
284        let mut delta = delta.ok_or(FeosError::IterationFailed("Critical point".into()))?;
285
286        // reduce step if necessary
287        for i in 0..2 {
288            if delta[i].abs() > 0.03 * max_density {
289                delta *= 0.03 * max_density / delta[i].abs()
290            }
291        }
292
293        // apply step
294        rho -= delta;
295        rho[0] = f64::max(rho[0], 1e-4 * max_density);
296        rho[1] = f64::max(rho[1], 1e-4 * max_density);
297
298        log_iter!(
299            verbosity,
300            " {:4} | {:14.8e} | {:12.8} | {:12.8}",
301            i,
302            res.norm(),
303            Density::from_reduced(rho[0]),
304            Density::from_reduced(rho[1]),
305        );
306
307        // check convergence
308        if res.norm() < tol {
309            log_result!(
310                verbosity,
311                "Critical point calculation converged in {} step(s)\n",
312                i
313            );
314            return Ok(rho.data.0[0]);
315        }
316    }
317    Err(FeosError::NotConverged(String::from("Critical point")))
318}
319
320/// Calculate the critical point of a binary system for given pressure.
321fn critical_point_binary_p<E: Residual<N>, N: Gradients>(
322    eos: &E,
323    pressure: Pressure,
324    initial_temperature: Option<Temperature>,
325    initial_molefracs: OVector<f64, N>,
326    initial_density: Option<Density>,
327    options: SolverOptions,
328) -> FeosResult<[f64; 3]>
329where
330    DefaultAllocator: Allocator<N> + Allocator<N, N> + Allocator<U1, N>,
331{
332    let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT_BINARY, TOL_CRIT_POINT);
333
334    let p = pressure.to_reduced();
335    let mut t = initial_temperature.map(|t| t.to_reduced()).unwrap_or(300.0);
336    let max_density = eos.compute_max_density(&initial_molefracs);
337    let rho_init = initial_density.map_or(0.3 * max_density, |r| r.into_reduced());
338    let mut rho = SVector::from([initial_molefracs[0], initial_molefracs[1]]) * rho_init;
339
340    log_iter!(
341        verbosity,
342        " iter |    residual    |   temperature   |      density 1       |      density 2       "
343    );
344    log_iter!(verbosity, "{:-<87}", "");
345    log_iter!(
346        verbosity,
347        " {:4} |                | {:13.8} | {:12.8} | {:12.8}",
348        0,
349        Temperature::from_reduced(t),
350        Density::from_reduced(rho[0]),
351        Density::from_reduced(rho[1]),
352    );
353
354    for i in 1..=max_iter {
355        // calculate residuals and derivatives w.r.t. temperature and partial densities
356        let res = |x: SVector<DualSVec64<3>, 3>| {
357            let t = x[0];
358            let partial_density = [x[1], x[2]];
359            let p = DualSVec::from_re(p);
360            criticality_conditions_p(&eos.lift(), p, t, partial_density)
361        };
362        let (res, jac) = jacobian::<_, _, _, U3, U3, _>(res, &SVector::from([t, rho[0], rho[1]]));
363
364        // calculate Newton step
365        let delta = jac.lu().solve(&res);
366        let mut delta = delta.ok_or(FeosError::IterationFailed("Critical point".into()))?;
367
368        // reduce step if necessary
369        if delta[0].abs() > 0.25 * t {
370            delta *= 0.25 * t / delta[0].abs()
371        }
372        if delta[1].abs() > 0.03 * max_density {
373            delta *= 0.03 * max_density / delta[1].abs()
374        }
375        if delta[2].abs() > 0.03 * max_density {
376            delta *= 0.03 * max_density / delta[2].abs()
377        }
378
379        // apply step
380        t -= delta[0];
381        rho[0] -= delta[1];
382        rho[1] -= delta[2];
383        rho[0] = f64::max(rho[0], 1e-4 * max_density);
384        rho[1] = f64::max(rho[1], 1e-4 * max_density);
385
386        log_iter!(
387            verbosity,
388            " {:4} | {:14.8e} | {:13.8} | {:12.8} | {:12.8}",
389            i,
390            res.norm(),
391            Temperature::from_reduced(t),
392            Density::from_reduced(rho[0]),
393            Density::from_reduced(rho[1]),
394        );
395
396        // check convergence
397        if res.norm() < tol {
398            log_result!(
399                verbosity,
400                "Critical point calculation converged in {} step(s)\n",
401                i
402            );
403            return Ok([t, rho[0], rho[1]]);
404        }
405    }
406    Err(FeosError::NotConverged(String::from("Critical point")))
407}
408
409impl<E: Residual<N>, N: Gradients> State<E, N>
410where
411    DefaultAllocator: Allocator<N> + Allocator<N, N> + Allocator<U1, N>,
412{
413    pub fn spinodal(
414        eos: &E,
415        temperature: Temperature,
416        molefracs: Option<&OVector<f64, N>>,
417        options: SolverOptions,
418    ) -> FeosResult<[Self; 2]> {
419        let critical_point = Self::critical_point(eos, molefracs, None, None, options)?;
420        let molefracs = molefracs.map_or_else(E::pure_molefracs, |x| x.clone());
421        let spinodal_vapor = Self::calculate_spinodal(
422            eos,
423            temperature,
424            &molefracs,
425            DensityInitialization::Vapor,
426            options,
427        )?;
428        let rho = 2.0 * critical_point.density - spinodal_vapor.density;
429        let spinodal_liquid = Self::calculate_spinodal(
430            eos,
431            temperature,
432            &molefracs,
433            DensityInitialization::InitialDensity(rho),
434            options,
435        )?;
436        Ok([spinodal_vapor, spinodal_liquid])
437    }
438
439    fn calculate_spinodal(
440        eos: &E,
441        temperature: Temperature,
442        molefracs: &OVector<f64, N>,
443        density_initialization: DensityInitialization,
444        options: SolverOptions,
445    ) -> FeosResult<Self> {
446        let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_CRIT_POINT, TOL_CRIT_POINT);
447
448        let max_density = eos.compute_max_density(molefracs);
449        let t = temperature.to_reduced();
450        let mut rho = match density_initialization {
451            DensityInitialization::Vapor => 1e-5 * max_density,
452            DensityInitialization::Liquid => max_density,
453            DensityInitialization::InitialDensity(rho) => rho.to_reduced(),
454        };
455
456        log_iter!(verbosity, " iter |    residual    |       density        ");
457        log_iter!(verbosity, "{:-<46}", "");
458        log_iter!(
459            verbosity,
460            " {:4} |                | {:12.8}",
461            0,
462            Density::from_reduced(rho),
463        );
464
465        for i in 1..=max_iter {
466            // calculate residuals and derivative w.r.t. density
467            let (f, df) = first_derivative(
468                partial(
469                    |rho, x| stability_condition(&eos.lift(), t.into(), rho, x),
470                    molefracs,
471                ),
472                rho,
473            );
474
475            // calculate Newton step
476            let mut delta = f / df;
477
478            // reduce step if necessary
479            if delta.abs() > 0.03 * max_density {
480                delta *= 0.03 * max_density / delta.abs()
481            }
482
483            // apply step
484            rho -= delta;
485            rho = f64::max(rho, 1e-4 * max_density);
486
487            log_iter!(
488                verbosity,
489                " {:4} | {:14.8e} | {:12.8}",
490                i,
491                f.abs(),
492                Density::from_reduced(rho),
493            );
494
495            // check convergence
496            if f.abs() < tol {
497                log_result!(
498                    verbosity,
499                    "Spinodal calculation converged in {} step(s)\n",
500                    i
501                );
502                return Self::new_intensive(
503                    eos,
504                    temperature,
505                    Density::from_reduced(rho),
506                    molefracs,
507                );
508            }
509        }
510        Err(FeosError::SuperCritical)
511    }
512}
513
514fn criticality_conditions<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy>(
515    eos: &E,
516    temperature: D,
517    density: D,
518    molefracs: &OVector<D, N>,
519) -> [D; 2]
520where
521    DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
522{
523    // calculate M
524    let molar_volume = density.recip();
525    let sqrt_z = molefracs.map(|z| z.sqrt());
526    let z_mix = &sqrt_z * sqrt_z.transpose();
527    let m = dmu_dn(eos, temperature, molar_volume, molefracs);
528    let (r, c) = m.shape_generic();
529    let m = m.component_mul(&z_mix) + OMatrix::identity_generic(r, c);
530
531    // calculate smallest eigenvalue and corresponding eigenvector
532    let (l, u) = smallest_ev(m);
533
534    let (_, _, _, c2) = third_derivative(
535        |s| {
536            let n = molefracs.map(Dual3::from_re);
537            let n = n + sqrt_z.component_mul(&u).map(Dual3::from_re) * s;
538            let t = Dual3::from_re(temperature);
539            let v = Dual3::from_re(molar_volume);
540            let ig = n.dot(&n.map(|n| (n / v).ln() - 1.0));
541            eos.lift().residual_helmholtz_energy(t, v, &n) / t + ig
542        },
543        D::from(0.0),
544    );
545
546    [l, c2]
547}
548
549fn criticality_conditions_p<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy>(
550    eos: &E,
551    pressure: D,
552    temperature: D,
553    partial_density: [D; 2],
554) -> SVector<D, 3>
555where
556    DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
557{
558    let density = partial_density[0] + partial_density[1];
559    let n = N::from_usize(2);
560    let molefracs: OVector<D, N> =
561        OVector::from_fn_generic(n, U1, |i, _| partial_density[i] / density);
562
563    let [c1, c2] = criticality_conditions(eos, temperature, density, &molefracs);
564
565    // calculate pressure
566    let a = partial2(
567        |v, &t, x| eos.lift().residual_molar_helmholtz_energy(t, v, x),
568        &temperature,
569        &molefracs,
570    );
571    let (_, da) = first_derivative(a, D::one());
572    let p_calc = -da + density * temperature;
573    SVector::from([c1, c2, -p_calc + pressure])
574}
575
576fn stability_condition<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy>(
577    eos: &E,
578    temperature: D,
579    density: D,
580    molefracs: &OVector<D, N>,
581) -> D
582where
583    DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
584{
585    // calculate M
586    let molar_volume = density.recip();
587    let sqrt_z = molefracs.map(|z| z.sqrt());
588    let z_mix = &sqrt_z * sqrt_z.transpose();
589    let m = dmu_dn(eos, temperature, molar_volume, molefracs);
590    let (r, c) = m.shape_generic();
591    let m = m.component_mul(&z_mix) + OMatrix::identity_generic(r, c);
592
593    // calculate smallest eigenvalue and corresponding eigenvector
594    let (l, _) = smallest_ev(m);
595
596    l
597}
598
599fn dmu_dn<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy>(
600    eos: &E,
601    temperature: D,
602    molar_volume: D,
603    molefracs: &OVector<D, N>,
604) -> OMatrix<D, N, N>
605where
606    DefaultAllocator: Allocator<N> + Allocator<N, N>,
607{
608    let (_, _, h) = N::hessian(
609        |n, &(t, v)| eos.lift().residual_helmholtz_energy(t, v, &n) / t,
610        molefracs,
611        &(temperature, molar_volume),
612    );
613    h
614}