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
20impl<R: Residual + Subset> State<R> {
22 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 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 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 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 let delta = jac.lu().solve::<U2, U1, _>(&res);
201 let mut delta = delta.ok_or(FeosError::IterationFailed("Critical point".into()))?;
202
203 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 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 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
238fn 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 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 let delta = jac.lu().solve(&res);
285 let mut delta = delta.ok_or(FeosError::IterationFailed("Critical point".into()))?;
286
287 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 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 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
321fn 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 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 let delta = jac.lu().solve(&res);
367 let mut delta = delta.ok_or(FeosError::IterationFailed("Critical point".into()))?;
368
369 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 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 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 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 let mut delta = f / df;
478
479 if delta.abs() > 0.03 * max_density {
481 delta *= 0.03 * max_density / delta.abs()
482 }
483
484 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 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 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 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 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 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 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}