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
19impl<R: Residual + Subset> State<R> {
21 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 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 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 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 let delta = jac.lu().solve::<U2, U1, _>(&res);
200 let mut delta = delta.ok_or(FeosError::IterationFailed("Critical point".into()))?;
201
202 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 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 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
237fn 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 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 let delta = jac.lu().solve(&res);
284 let mut delta = delta.ok_or(FeosError::IterationFailed("Critical point".into()))?;
285
286 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 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 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
320fn 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 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 let delta = jac.lu().solve(&res);
366 let mut delta = delta.ok_or(FeosError::IterationFailed("Critical point".into()))?;
367
368 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 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 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 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 let mut delta = f / df;
477
478 if delta.abs() > 0.03 * max_density {
480 delta *= 0.03 * max_density / delta.abs()
481 }
482
483 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 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 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 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 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 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 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}