1#![expect(clippy::toplevel_ref_arg)]
2use super::PhaseEquilibrium;
3use crate::errors::FeosResult;
4use crate::state::State;
5use crate::{Composition, FeosError, ReferenceSystem, SolverOptions, Total, Verbosity};
6use nalgebra::allocator::Allocator;
7use nalgebra::{DefaultAllocator, Dim, DimAdd, OVector, U1, U2, U3, stack, vector};
8use num_dual::linalg::LU;
9use num_dual::{
10 Dual, Dual64, DualNum, DualStruct, Gradients, first_derivative, implicit_derivative_sp, partial,
11};
12use quantity::{Density, MolarEnergy, MolarEntropy, Pressure, Quantity, SIUnit, Temperature};
13
14const MAX_ITER_PX: usize = 20;
15const TOL_PX: f64 = 1e-11;
16
17type PXVars<N> = <N as DimAdd<U3>>::Output;
18type TPVars<N> = <N as DimAdd<U2>>::Output;
19
20impl<E: Total<N, D>, N: Gradients + DimAdd<U2> + DimAdd<U3>, D: DualNum<f64> + Copy>
21 PhaseEquilibrium<E, 2, N, D>
22where
23 DefaultAllocator: Allocator<N>
24 + Allocator<N, N>
25 + Allocator<PXVars<N>>
26 + Allocator<U1, PXVars<N>>
27 + Allocator<PXVars<N>, PXVars<N>>
28 + Allocator<TPVars<N>>
29 + Allocator<U1, TPVars<N>>
30 + Allocator<TPVars<N>, TPVars<N>>,
31 PXVars<N>: Gradients,
32 TPVars<N>: Gradients,
33{
34 pub fn ph_flash<X: Composition<D, N>>(
40 eos: &E,
41 pressure: Pressure<D>,
42 molar_enthalpy: MolarEnergy<D>,
43 feed: X,
44 initial_temperature: Temperature,
45 options: SolverOptions,
46 ) -> FeosResult<Self> {
47 PhaseEquilibrium::px_flash(
48 eos,
49 pressure,
50 molar_enthalpy,
51 feed,
52 initial_temperature,
53 options,
54 )
55 }
56
57 pub fn ps_flash<X: Composition<D, N>>(
63 eos: &E,
64 pressure: Pressure<D>,
65 molar_entropy: MolarEntropy<D>,
66 feed: X,
67 initial_temperature: Temperature,
68 options: SolverOptions,
69 ) -> FeosResult<Self> {
70 PhaseEquilibrium::px_flash(
71 eos,
72 pressure,
73 molar_entropy,
74 feed,
75 initial_temperature,
76 options,
77 )
78 }
79
80 fn px_flash<X: Composition<D, N>, U: PXFlash>(
82 eos: &E,
83 pressure: Pressure<D>,
84 specification: Quantity<D, U>,
85 feed: X,
86 initial_temperature: Temperature,
87 options: SolverOptions,
88 ) -> FeosResult<Self>
89 where
90 Quantity<D, U>: ReferenceSystem<Inner = D>,
91 Quantity<Dual64, U>: ReferenceSystem<Inner = Dual64>,
92 {
93 let (max_iter, tol, verbosity) = options.unwrap_or(MAX_ITER_PX, TOL_PX);
94 let (molefracs, total_moles) = feed.into_molefracs(eos)?;
95
96 let eos_f64 = eos.re_total();
98 let vle = PhaseEquilibrium::tp_flash(
99 &eos_f64,
100 initial_temperature,
101 pressure.re(),
102 molefracs.map(|x| x.re()),
103 None,
104 Default::default(),
105 None,
106 )?;
107
108 let p = pressure.into_reduced().re();
110 let hs = specification.into_reduced().re();
111 let z = molefracs.map(|x| x.re());
112 let specs = (p, hs, z.clone());
113
114 let t = initial_temperature.into_reduced();
116 let beta = vle.vapor_phase_fraction();
117 let rho_v = vle.vapor().density.into_reduced();
118 let rho_l = vle.liquid().partial_density().into_reduced();
119 let mut vars = stack![rho_l; vector![t, beta, rho_v]];
120 let mut old_res = None;
121
122 log_iter!(
123 verbosity,
124 " iter | method | temperature | residual | phase I mole fractions | phase II mole fractions "
125 );
126 log_iter!(verbosity, "{:-<102}", "");
127 log_iter!(
128 verbosity,
129 " {:4} | | {:9.5} | | {:10.8?} | {:10.8?}",
130 0,
131 Temperature::from_reduced(t),
132 (&rho_l / rho_l.sum() + (&z - &rho_l / rho_l.sum()) / beta).as_slice(),
133 (&rho_l / rho_l.sum()).as_slice(),
134 );
135
136 for k in 0..max_iter {
138 let (grad, new_vars) = U::newton_step(&eos_f64, &vars, &specs)?;
140 let new_res = grad.norm();
141 let (method, res) = if let Some(r) = old_res
142 && r < new_res
143 {
144 vars = U::tp_step(&eos_f64, &vars, &specs)?;
146 ("Tp-flash", None)
147 } else {
148 vars = new_vars;
149 ("Newton", Some(new_res))
150 };
151
152 if let Verbosity::Iter = verbosity {
153 let (t, _, _, _, x, y) = unpack_variables(&z, &vars);
154 log_iter!(
155 verbosity,
156 " {:4} | {:^8} | {:9.5} | {} | {:10.8?} | {:10.8?}",
157 k + 1,
158 method,
159 Temperature::from_reduced(t),
160 res.map_or(String::from(" "), |r| format!("{r:14.8e}")),
161 y.as_slice(),
162 x.as_slice(),
163 );
164 }
165
166 if let Some(res) = res
167 && res < tol
168 {
169 log_result!(
170 verbosity,
171 "px flash: calculation converged in {} step(s)\n",
172 k + 1
173 );
174
175 let specs = (
177 pressure.into_reduced(),
178 specification.into_reduced(),
179 molefracs.clone(),
180 );
181 let vars = implicit_derivative_sp(
182 |variables, specifications| {
183 U::state_function(&eos.lift_total(), variables, specifications)
184 },
185 vars,
186 &specs,
187 );
188 let (t, beta, rho_l, rho_v, x, y) = unpack_variables(&molefracs, &vars);
189
190 let liquid = State::new(
192 eos,
193 Temperature::from_reduced(t),
194 Density::from_reduced(rho_l),
195 x,
196 )?;
197 let vapor = State::new(
198 eos,
199 Temperature::from_reduced(t),
200 Density::from_reduced(rho_v),
201 y,
202 )?;
203 return Ok(PhaseEquilibrium::with_vapor_phase_fraction(
204 vapor,
205 liquid,
206 beta,
207 total_moles,
208 ));
209 }
210 old_res = res;
211 }
212 Err(FeosError::NotConverged("px flash".to_owned()))
213 }
214}
215
216fn unpack_variables<D: DualNum<f64> + Copy, N: Dim + DimAdd<U3>>(
217 molefracs: &OVector<D, N>,
218 variables: &OVector<D, PXVars<N>>,
219) -> (D, D, D, D, OVector<D, N>, OVector<D, N>)
220where
221 DefaultAllocator: Allocator<N> + Allocator<PXVars<N>>,
222{
223 let n = molefracs.len();
224 let rho_i_l = variables.rows_generic(0, N::from_usize(n)).clone_owned();
225 let [[t, beta, rho_v]] = variables.rows_generic(n, U3).clone_owned().data.0;
226 let rho_l = rho_i_l.sum();
227 let x = rho_i_l / rho_l;
228 let y = &x + (molefracs - &x) / beta;
229 (t, beta, rho_l, rho_v, x, y)
230}
231
232fn unpack_tp_variables<D: DualNum<f64> + Copy, N: Dim + DimAdd<U2>>(
233 molefracs: &OVector<D, N>,
234 variables: &OVector<D, TPVars<N>>,
235) -> (D, D, D, OVector<D, N>, OVector<D, N>)
236where
237 DefaultAllocator: Allocator<N> + Allocator<TPVars<N>>,
238{
239 let n = molefracs.len();
240 let rho_i_l = variables.rows_generic(0, N::from_usize(n)).clone_owned();
241 let [[beta, rho_v]] = variables.rows_generic(n, U2).clone_owned().data.0;
242 let rho_l = rho_i_l.sum();
243 let x = rho_i_l / rho_l;
244 let y = &x + (molefracs - &x) / beta;
245 (beta, rho_l, rho_v, x, y)
246}
247
248trait PXFlash: Sized + Copy {
249 fn state_function<E: Total<N, D>, N: Dim + DimAdd<U3>, D: DualNum<f64> + Copy>(
251 eos: &E,
252 variables: OVector<D, PXVars<N>>,
253 args: &(D, D, OVector<D, N>),
254 ) -> D
255 where
256 DefaultAllocator: Allocator<N> + Allocator<PXVars<N>>;
257
258 fn evaluate_property<E: Total<N, D>, N: Gradients, D: DualNum<f64> + Copy>(
259 vle: &PhaseEquilibrium<E, 2, N, D>,
260 ) -> Quantity<D, Self>
261 where
262 DefaultAllocator: Allocator<N>;
263
264 fn tp_state_function<E: Total<N, D>, N: Dim + DimAdd<U2>, D: DualNum<f64> + Copy>(
266 eos: &E,
267 variables: OVector<D, TPVars<N>>,
268 &(t, p, ref z): &(D, D, OVector<D, N>),
269 ) -> D
270 where
271 DefaultAllocator: Allocator<N> + Allocator<TPVars<N>>,
272 {
273 let (beta, rho_l, rho_v, x, y) = unpack_tp_variables(z, &variables);
274 let potential = |molefracs, rho: D, t| {
275 let v = rho.recip();
276 let a_res = eos.residual_helmholtz_energy(t, v, &molefracs);
277 let a_ig = eos.ideal_gas_molar_helmholtz_energy(t, v, &molefracs);
278 a_res + a_ig + v * p
279 };
280 potential(y, rho_v, t) * beta + potential(x, rho_l, t) * (-beta + 1.0)
281 }
282
283 #[expect(clippy::type_complexity)]
287 fn newton_step<E: Total<N, D>, N: Dim + DimAdd<U3>, D: DualNum<f64> + Copy>(
288 eos: &E,
289 variables: &OVector<D, PXVars<N>>,
290 specifications: &(D, D, OVector<D, N>),
291 ) -> FeosResult<(OVector<D, PXVars<N>>, OVector<D, PXVars<N>>)>
292 where
293 DefaultAllocator: Allocator<N> + Allocator<PXVars<N>> + Allocator<PXVars<N>, PXVars<N>>,
294 PXVars<N>: Gradients,
295 {
296 let (_, grad, hess) = PXVars::<N>::hessian(
297 |variables, specifications| {
298 Self::state_function(&eos.lift_total(), variables, specifications)
299 },
300 variables,
301 specifications,
302 );
303 let dx = LU::new(hess)?.solve(&grad);
304 Ok((grad, variables - &dx))
305 }
306
307 fn tp_step<E: Total<N, f64>, N: Gradients + DimAdd<U2> + DimAdd<U3>>(
312 eos: &E,
313 variables: &OVector<f64, PXVars<N>>,
314 &(p, hs_spec, ref z): &(f64, f64, OVector<f64, N>),
315 ) -> FeosResult<OVector<f64, PXVars<N>>>
316 where
317 Quantity<Dual64, Self>: ReferenceSystem<Inner = Dual64>,
318 DefaultAllocator: Allocator<N>
319 + Allocator<N, N>
320 + Allocator<PXVars<N>>
321 + Allocator<U1, TPVars<N>>
322 + Allocator<TPVars<N>>
323 + Allocator<TPVars<N>, TPVars<N>>,
324 TPVars<N>: Gradients,
325 {
326 let (mut t, beta, rho_l, rho_v, x, y) = unpack_variables(z, variables);
327 let rho_i_l = rho_l * x;
328 let (hs, dhs) = first_derivative(
329 partial(
330 |t: Dual<_, _>, args: &(_, OVector<_, _>)| {
331 let &(p, ref z) = args;
332 let args = (t, p, z.clone_owned());
333
334 let tp_vars = implicit_derivative_sp(
337 |variables, args| {
338 Self::tp_state_function(&eos.lift_total().lift_total(), variables, args)
339 },
340 stack![rho_i_l; vector![beta, rho_v]],
341 &args,
342 );
343 let (beta, rho_l, rho_v, x, y) = unpack_tp_variables(z, &tp_vars);
344
345 let liquid = State::new(
347 &eos.lift_total(),
348 Temperature::from_reduced(t),
349 Density::from_reduced(rho_l),
350 x,
351 )?;
352 let vapor = State::new(
353 &eos.lift_total(),
354 Temperature::from_reduced(t),
355 Density::from_reduced(rho_v),
356 y,
357 )?;
358 Ok::<_, FeosError>(
359 Self::evaluate_property(&PhaseEquilibrium::with_vapor_phase_fraction(
360 vapor, liquid, beta, None,
361 ))
362 .into_reduced(),
363 )
364 },
365 &(p, z.clone_owned()),
366 ),
367 t,
368 )?;
369
370 t -= (hs - hs_spec) / dhs;
372
373 let liquid = State::new_density(
375 eos,
376 Temperature::from_reduced(t),
377 Density::from_reduced(rho_i_l),
378 )?;
379 let vapor = State::new(
380 eos,
381 Temperature::from_reduced(t),
382 Density::from_reduced(rho_v),
383 y,
384 )?;
385 let vle = PhaseEquilibrium::with_vapor_phase_fraction(vapor, liquid, beta, None);
386
387 let vle = PhaseEquilibrium::tp_flash(
389 eos,
390 Temperature::from_reduced(t),
391 Pressure::from_reduced(p),
392 z,
393 Some(&vle),
394 Default::default(),
395 None,
396 )?;
397 let beta = vle.vapor_phase_fraction();
398 let rho_v = vle.vapor().density.into_reduced();
399 let rho_l = vle.liquid().partial_density().into_reduced();
400 Ok(stack![rho_l; vector![t, beta, rho_v]])
401 }
402}
403
404impl PXFlash for SIUnit<-2, 2, 1, 0, 0, -1, 0> {
405 fn state_function<E: Total<N, D>, N: Dim + DimAdd<U3>, D: DualNum<f64> + Copy>(
407 eos: &E,
408 variables: OVector<D, PXVars<N>>,
409 &(p, h, ref z): &(D, D, OVector<D, N>),
410 ) -> D
411 where
412 DefaultAllocator: Allocator<N> + Allocator<PXVars<N>>,
413 {
414 let (t, beta, rho_l, rho_v, x, y) = unpack_variables(z, &variables);
415 let potential = |molefracs, rho: D, t| {
416 let v = rho.recip();
417 let a_res = eos.residual_helmholtz_energy(t, v, &molefracs);
418 let a_ig = eos.ideal_gas_molar_helmholtz_energy(t, v, &molefracs);
419 (a_res + a_ig + v * p - h) / t
420 };
421 potential(y, rho_v, t) * beta + potential(x, rho_l, t) * (-beta + 1.0)
422 }
423
424 fn evaluate_property<E: Total<N, D>, N: Gradients, D: DualNum<f64> + Copy>(
425 vle: &PhaseEquilibrium<E, 2, N, D>,
426 ) -> Quantity<D, Self>
427 where
428 DefaultAllocator: Allocator<N>,
429 {
430 vle.molar_enthalpy()
431 }
432}
433
434impl PXFlash for SIUnit<-2, 2, 1, 0, -1, -1, 0> {
435 fn state_function<E: Total<N, D>, N: Dim + DimAdd<U3>, D: DualNum<f64> + Copy>(
437 eos: &E,
438 variables: OVector<D, PXVars<N>>,
439 &(p, s, ref z): &(D, D, OVector<D, N>),
440 ) -> D
441 where
442 DefaultAllocator: Allocator<N> + Allocator<PXVars<N>>,
443 {
444 let (t, beta, rho_l, rho_v, x, y) = unpack_variables(z, &variables);
445 let potential = |molefracs, rho: D, t| {
446 let v = rho.recip();
447 let a_res = eos.residual_helmholtz_energy(t, v, &molefracs);
448 let a_ig = eos.ideal_gas_molar_helmholtz_energy(t, v, &molefracs);
449 (a_res + a_ig + t * s + v * p) / t.re()
453 };
454 potential(y, rho_v, t) * beta + potential(x, rho_l, t) * (-beta + 1.0)
455 }
456
457 fn evaluate_property<E: Total<N, D>, N: Gradients, D: DualNum<f64> + Copy>(
458 vle: &PhaseEquilibrium<E, 2, N, D>,
459 ) -> Quantity<D, Self>
460 where
461 DefaultAllocator: Allocator<N>,
462 {
463 vle.molar_entropy()
464 }
465}