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