feos_core/phase_equilibria/
stability_analysis.rs1use super::PhaseEquilibrium;
2use crate::equation_of_state::Residual;
3use crate::errors::{FeosError, FeosResult};
4use crate::state::{Contributions, DensityInitialization, State};
5use crate::{ReferenceSystem, SolverOptions, Verbosity};
6use nalgebra::{DMatrix, DVector};
7use num_dual::linalg::LU;
8use num_dual::linalg::smallest_ev;
9use quantity::Moles;
10
11const X_DOMINANT: f64 = 0.99;
12const MINIMIZE_TOL: f64 = 1E-06;
13const MIN_EIGENVAL: f64 = 1E-03;
14const ETA_STEP: f64 = 0.25;
15const MINIMIZE_KMAX: usize = 100;
16const ZERO_TPD: f64 = -1E-08;
17
18impl<E: Residual> State<E> {
20 pub fn is_stable(&self, options: SolverOptions) -> FeosResult<bool> {
23 Ok(self.stability_analysis(options)?.is_empty())
24 }
25
26 pub fn stability_analysis(&self, options: SolverOptions) -> FeosResult<Vec<State<E>>> {
30 let mut result = Vec::new();
31 for i_trial in 0..self.eos.components() + 1 {
32 let phase = if i_trial == self.eos.components() {
33 "Vapor phase".to_string()
34 } else {
35 format!("Liquid phase {}", i_trial + 1)
36 };
37 if let Ok(mut trial_state) = self.define_trial_state(i_trial) {
38 let (tpd, i) = self.minimize_tpd(&mut trial_state, options)?;
39 let msg = if let Some(tpd) = tpd {
40 if tpd < ZERO_TPD {
41 if result
42 .iter()
43 .any(|s| PhaseEquilibrium::is_trivial_solution(s, &trial_state))
44 {
45 "Found already identified minimum"
46 } else {
47 result.push(trial_state);
48 "Found candidate"
49 }
50 } else {
51 "Found minimum > 0"
52 }
53 } else {
54 "Found trivial solution"
55 };
56 log_result!(options.verbosity, "{}: {} in {} step(s)\n", phase, msg, i);
57 }
58 }
59 Ok(result)
60 }
61
62 fn define_trial_state(&self, dominant_component: usize) -> FeosResult<State<E>> {
63 let x_feed = &self.molefracs;
64
65 let (x_trial, phase) = if dominant_component == self.eos.components() {
66 let x_trial = self.ln_phi().map(f64::exp).component_mul(x_feed);
68 (&x_trial / x_trial.sum(), DensityInitialization::Vapor)
69 } else {
70 let factor = (1.0 - X_DOMINANT) / (x_feed.sum() - x_feed[dominant_component]);
72 (
73 DVector::from_fn(self.eos.components(), |i, _| {
74 if i == dominant_component {
75 X_DOMINANT
76 } else {
77 x_feed[i] * factor
78 }
79 }),
80 DensityInitialization::Liquid,
81 )
82 };
83
84 State::new_npt(
85 &self.eos,
86 self.temperature,
87 self.pressure(Contributions::Total),
88 &Moles::from_reduced(x_trial),
89 Some(phase),
90 )
91 }
92
93 fn minimize_tpd(
94 &self,
95 trial: &mut State<E>,
96 options: SolverOptions,
97 ) -> FeosResult<(Option<f64>, usize)> {
98 let (max_iter, tol, verbosity) = options.unwrap_or(MINIMIZE_KMAX, MINIMIZE_TOL);
99 let mut newton = false;
100 let mut scaled_tol = tol;
101 let mut tpd = 1E10;
102 let di = self.molefracs.map(f64::ln) + self.ln_phi();
103
104 log_iter!(verbosity, " iter | residual | tpd | Newton");
105 log_iter!(verbosity, "{:-<46}", "");
106
107 for i in 1..=max_iter {
108 let error = if !newton {
109 let y = (&di - &trial.ln_phi()).map(f64::exp);
111 let tpd_old = tpd;
112 tpd = 1.0 - y.sum();
113 let error = (&y / y.sum() - &trial.molefracs).map(f64::abs).sum();
114
115 *trial = State::new_npt(
116 &trial.eos,
117 trial.temperature,
118 trial.pressure(Contributions::Total),
119 &Moles::from_reduced(y),
120 Some(DensityInitialization::InitialDensity(trial.density)),
121 )?;
122 if (i > 4 && error > scaled_tol) || (tpd > tpd_old + 1E-05 && i > 2) {
123 newton = true; }
125 error
126 } else {
127 trial.stability_newton_step(&di, &mut tpd)?
129 };
130 log_iter!(
131 verbosity,
132 " {:4} | {:14.8e} | {:11.8} | {}",
133 i,
134 error,
135 tpd,
136 newton
137 );
138 if PhaseEquilibrium::is_trivial_solution(self, &*trial) {
139 return Ok((None, i));
140 }
141 if tpd < -1E-02 {
142 scaled_tol = tol * 1E01
143 }
144 if tpd < -1E-01 {
145 scaled_tol = tol * 1E02
146 }
147 if tpd < -1E-01 && i > 5 {
148 scaled_tol = tol * 1E03
149 }
150 if error < scaled_tol {
151 return Ok((Some(tpd), i));
152 }
153 }
154 Err(FeosError::NotConverged(String::from("stability analysis")))
155 }
156
157 fn stability_newton_step(&mut self, di: &DVector<f64>, tpd: &mut f64) -> FeosResult<f64> {
158 let tpd_old = *tpd;
160
161 let mut hesse = (self.dln_phi_dnj() * Moles::from_reduced(1.0)).into_value();
163 let lnphi = self.ln_phi();
164 let y = self.moles.to_reduced();
165 let ln_y = y.map(|y| if y > f64::EPSILON { y.ln() } else { 0.0 });
166 let sq_y = y.map(f64::sqrt);
167 let gradient = (&ln_y + &lnphi - di).component_mul(&sq_y);
168
169 let hesse_ig = DMatrix::identity(self.eos.components(), self.eos.components());
170 for i in 0..self.eos.components() {
171 hesse.column_mut(i).component_mul_assign(&(sq_y[i] * &sq_y));
172 if y[i] > f64::EPSILON {
173 hesse[(i, i)] += ln_y[i] + lnphi[i] - di[i];
174 }
175 }
176
177 let mut adjust_hessian = true;
184 let mut hessian: DMatrix<f64>;
185 let mut eta_h = 1.0;
186
187 while adjust_hessian {
188 adjust_hessian = false;
189 hessian = &hesse + &(eta_h * &hesse_ig);
190
191 let (min_eigenval, _) = smallest_ev(hessian.clone());
192 if min_eigenval < MIN_EIGENVAL && eta_h < 20.0 {
193 eta_h += 2.0 * ETA_STEP;
194 adjust_hessian = true;
195 continue; }
197
198 let delta_y = LU::new(hessian)?.solve(&gradient);
200 if delta_y
201 .iter()
202 .zip(y.iter())
203 .any(|(dy, y)| ((0.5 * dy).powi(2) / y).abs() > 5.0)
204 {
205 adjust_hessian = true;
206 eta_h += 2.0 * ETA_STEP;
207 continue; }
209
210 let y = (&sq_y - &(delta_y / 2.0)).map(|v| v.powi(2));
211 let ln_y = y.map(|y| if y > f64::EPSILON { y.ln() } else { 0.0 });
212 *tpd = 1.0 + y.dot(&(&ln_y + &lnphi - di.add_scalar(1.0)));
213 if *tpd > tpd_old + 0.0 * 1E-03 && eta_h < 30.0 {
214 eta_h += ETA_STEP;
215 adjust_hessian = true;
216 continue; }
218
219 *self = State::new_npt(
221 &self.eos,
222 self.temperature,
223 self.pressure(Contributions::Total),
224 &Moles::from_reduced(y),
225 Some(DensityInitialization::InitialDensity(self.density)),
226 )?;
227 }
228 Ok(gradient.map(f64::abs).sum())
229 }
230}