Skip to main content

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