1use super::{Contributions, State};
2use crate::equation_of_state::{EntropyScaling, Molarweight, Residual, Subset};
3use crate::{FeosResult, PhaseEquilibrium, ReferenceSystem};
4use nalgebra::allocator::Allocator;
5use nalgebra::{DMatrix, DVector, DefaultAllocator, OMatrix, OVector, dvector};
6use num_dual::{Dual, DualNum, Gradients, partial, partial2};
7use quantity::*;
8use std::ops::{Add, Div, Neg, Sub};
9
10type InvT<T> = Quantity<T, <_Temperature as Neg>::Output>;
11type InvP<T> = Quantity<T, <_Pressure as Neg>::Output>;
12type POverT<T> = Quantity<T, <_Pressure as Sub<_Temperature>>::Output>;
13
14impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
16where
17 DefaultAllocator: Allocator<N>,
18{
19 pub(super) fn contributions<
20 T: Add<T, Output = T>,
21 U,
22 I: FnOnce() -> Quantity<T, U>,
23 R: FnOnce() -> Quantity<T, U>,
24 >(
25 ideal_gas: I,
26 residual: R,
27 contributions: Contributions,
28 ) -> Quantity<T, U> {
29 match contributions {
30 Contributions::IdealGas => ideal_gas(),
31 Contributions::Total => ideal_gas() + residual(),
32 Contributions::Residual => residual(),
33 }
34 }
35
36 pub fn residual_helmholtz_energy(&self) -> FeosResult<Energy<D>> {
38 Ok(self.residual_molar_helmholtz_energy() * self.total_moles()?)
39 }
40
41 pub fn residual_molar_helmholtz_energy(&self) -> MolarEnergy<D> {
43 *self.cache.a.get_or_init(|| {
44 self.eos.residual_molar_helmholtz_energy(
45 self.temperature,
46 self.molar_volume,
47 &self.molefracs,
48 )
49 })
50 }
51
52 pub fn residual_entropy(&self) -> FeosResult<Entropy<D>> {
54 Ok(self.residual_molar_entropy() * self.total_moles()?)
55 }
56
57 pub fn residual_molar_entropy(&self) -> MolarEntropy<D> {
59 -*self.cache.da_dt.get_or_init(|| {
60 let (a, da_dt) = quantity::ad::first_derivative(
61 partial2(
62 |t, &v, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
63 &self.molar_volume,
64 &self.molefracs,
65 ),
66 self.temperature,
67 );
68 let _ = self.cache.a.set(a);
69 da_dt
70 })
71 }
72
73 pub fn pressure(&self, contributions: Contributions) -> Pressure<D> {
75 let ideal_gas = || self.density * RGAS * self.temperature;
76 let residual = || {
77 -*self.cache.da_dv.get_or_init(|| {
78 let (a, da_dv) = quantity::ad::first_derivative(
79 partial2(
80 |v, &t, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
81 &self.temperature,
82 &self.molefracs,
83 ),
84 self.molar_volume,
85 );
86 let _ = self.cache.a.set(a);
87 da_dv
88 })
89 };
90 Self::contributions(ideal_gas, residual, contributions)
91 }
92
93 pub fn residual_chemical_potential(&self) -> MolarEnergy<OVector<D, N>> {
95 self.cache
96 .da_dn
97 .get_or_init(|| {
98 let (a, mu) = quantity::ad::gradient_copy(
99 partial2(
100 |n: Dimensionless<_>, &t, &v| {
101 self.eos.lift().residual_molar_helmholtz_energy(t, v, &n)
102 },
103 &self.temperature,
104 &self.molar_volume,
105 ),
106 &Dimensionless::new(self.molefracs.clone()),
107 );
108 let _ = self.cache.a.set(a);
109 mu
110 })
111 .clone()
112 }
113
114 pub fn compressibility(&self, contributions: Contributions) -> D {
116 (self.pressure(contributions) / (self.density * self.temperature * RGAS)).into_value()
117 }
118
119 pub fn dp_dv(
123 &self,
124 contributions: Contributions,
125 ) -> <Pressure<D> as Div<MolarVolume<D>>>::Output {
126 let ideal_gas = || -self.density * RGAS * self.temperature / self.molar_volume;
127 let residual = || {
128 -*self.cache.d2a_dv2.get_or_init(|| {
129 let (a, da_dv, d2a_dv2) = quantity::ad::second_derivative(
130 partial2(
131 |v, &t, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
132 &self.temperature,
133 &self.molefracs,
134 ),
135 self.molar_volume,
136 );
137 let _ = self.cache.a.set(a);
138 let _ = self.cache.da_dv.set(da_dv);
139 d2a_dv2
140 })
141 };
142 Self::contributions(ideal_gas, residual, contributions)
143 }
144
145 pub fn dp_drho(
147 &self,
148 contributions: Contributions,
149 ) -> <Pressure<D> as Div<Density<D>>>::Output {
150 -self.molar_volume / self.density * self.dp_dv(contributions)
151 }
152
153 pub fn dp_dt(&self, contributions: Contributions) -> POverT<D> {
155 let ideal_gas = || self.density * RGAS;
156 let residual = || {
157 -*self.cache.d2a_dtdv.get_or_init(|| {
158 let (a, da_dt, da_dv, d2a_dtdv) = quantity::ad::second_partial_derivative(
159 partial(
160 |(t, v), n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
161 &self.molefracs,
162 ),
163 (self.temperature, self.molar_volume),
164 );
165 let _ = self.cache.a.set(a);
166 let _ = self.cache.da_dt.set(da_dt);
167 let _ = self.cache.da_dv.set(da_dv);
168 d2a_dtdv
169 })
170 };
171 Self::contributions(ideal_gas, residual, contributions)
172 }
173
174 pub fn n_dp_dni(&self, contributions: Contributions) -> Pressure<OVector<D, N>> {
176 let residual = -self
177 .cache
178 .d2a_dndv
179 .get_or_init(|| {
180 let (a, da_dn, da_dv, dmu_dv) = quantity::ad::partial_hessian_copy(
181 partial(
182 |(n, v): (Dimensionless<_>, _), &t| {
183 self.eos.lift().residual_molar_helmholtz_energy(t, v, &n)
184 },
185 &self.temperature,
186 ),
187 (
188 &Dimensionless::new(self.molefracs.clone()),
189 self.molar_volume,
190 ),
191 );
192 let _ = self.cache.a.set(a);
193 let _ = self.cache.da_dn.set(da_dn);
194 let _ = self.cache.da_dv.set(da_dv);
195 dmu_dv
196 })
197 .clone();
198 let (r, c) = residual.shape_generic();
199 let ideal_gas = || self.temperature * self.density * RGAS;
200 Quantity::from_fn_generic(r, c, |i, _| {
201 Self::contributions(ideal_gas, || residual.get(i), contributions)
202 })
203 }
204
205 pub fn d2p_dv2(
207 &self,
208 contributions: Contributions,
209 ) -> <<Pressure<D> as Div<MolarVolume<D>>>::Output as Div<MolarVolume<D>>>::Output {
210 let ideal_gas = || {
211 self.density * RGAS * self.temperature / (self.molar_volume * self.molar_volume) * 2.0
212 };
213 let residual = || {
214 -*self.cache.d3a_dv3.get_or_init(|| {
215 let (a, da_dv, d2a_dv2, d3a_dv3) = quantity::ad::third_derivative(
216 partial2(
217 |v, &t, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
218 &self.temperature,
219 &self.molefracs,
220 ),
221 self.molar_volume,
222 );
223 let _ = self.cache.a.set(a);
224 let _ = self.cache.da_dv.set(da_dv);
225 let _ = self.cache.d2a_dv2.set(d2a_dv2);
226 d3a_dv3
227 })
228 };
229 Self::contributions(ideal_gas, residual, contributions)
230 }
231
232 pub fn d2p_drho2(
234 &self,
235 contributions: Contributions,
236 ) -> <<Pressure<D> as Div<Density<D>>>::Output as Div<Density<D>>>::Output {
237 self.molar_volume.powi::<3>()
238 * (self.molar_volume * self.d2p_dv2(contributions) + self.dp_dv(contributions) * 2.0)
239 }
240
241 pub fn structure_factor(&self) -> D {
243 -(self.temperature * self.density * RGAS
244 / (self.molar_volume * self.dp_dv(Contributions::Total)))
245 .into_value()
246 }
247
248 pub fn partial_molar_volume(&self) -> MolarVolume<OVector<D, N>> {
250 -self.n_dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total)
251 }
252
253 pub fn n_dmu_dni(&self, contributions: Contributions) -> MolarEnergy<OMatrix<D, N, N>>
255 where
256 DefaultAllocator: Allocator<N, N>,
257 {
258 let (a, da_dn, d2a_dn2) = quantity::ad::hessian_copy(
259 partial2(
260 |n: Dimensionless<_>, &t, &v| {
261 self.eos.lift().residual_molar_helmholtz_energy(t, v, &n)
262 },
263 &self.temperature,
264 &self.molar_volume,
265 ),
266 &Dimensionless::new(self.molefracs.clone()),
267 );
268 let _ = self.cache.a.set(a);
269 let _ = self.cache.da_dn.set(da_dn);
270 let residual = || d2a_dn2;
271 let ideal_gas = || {
272 Dimensionless::new(OMatrix::from_diagonal(&self.molefracs.map(|x| x.recip())))
273 * (self.temperature * RGAS)
274 };
275 Self::contributions(ideal_gas, residual, contributions)
276 }
277
278 pub fn isothermal_compressibility(&self) -> InvP<D> {
280 -(self.dp_dv(Contributions::Total) * self.molar_volume).inv()
281 }
282
283 pub fn ds_res_dt(&self) -> <MolarEntropy<D> as Div<Temperature<D>>>::Output {
287 -*self.cache.d2a_dt2.get_or_init(|| {
288 let (a, da_dt, d2a_dt2) = quantity::ad::second_derivative(
289 partial2(
290 |t, &v, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
291 &self.molar_volume,
292 &self.molefracs,
293 ),
294 self.temperature,
295 );
296 let _ = self.cache.a.set(a);
297 let _ = self.cache.da_dt.set(da_dt);
298 d2a_dt2
299 })
300 }
301
302 pub fn d2s_res_dt2(
304 &self,
305 ) -> <<MolarEntropy<D> as Div<Temperature<D>>>::Output as Div<Temperature<D>>>::Output {
306 -*self.cache.d3a_dt3.get_or_init(|| {
307 let (a, da_dt, d2a_dt2, d3a_dt3) = quantity::ad::third_derivative(
308 partial2(
309 |t, &v, n| self.eos.lift().residual_molar_helmholtz_energy(t, v, n),
310 &self.molar_volume,
311 &self.molefracs,
312 ),
313 self.temperature,
314 );
315 let _ = self.cache.a.set(a);
316 let _ = self.cache.da_dt.set(da_dt);
317 let _ = self.cache.d2a_dt2.set(d2a_dt2);
318 d3a_dt3
319 })
320 }
321
322 pub fn dmu_res_dt(&self) -> MolarEntropy<OVector<D, N>> {
324 self.cache
325 .d2a_dndt
326 .get_or_init(|| {
327 let (a, da_dn, da_dt, d2a_dndt) = quantity::ad::partial_hessian_copy(
328 partial(
329 |(n, t): (Dimensionless<_>, _), &v| {
330 self.eos.lift().residual_molar_helmholtz_energy(t, v, &n)
331 },
332 &self.molar_volume,
333 ),
334 (
335 &Dimensionless::new(self.molefracs.clone()),
336 self.temperature,
337 ),
338 );
339 let _ = self.cache.a.set(a);
340 let _ = self.cache.da_dn.set(da_dn);
341 let _ = self.cache.da_dt.set(da_dt);
342 d2a_dndt
343 })
344 .clone()
345 }
346
347 pub fn ln_phi(&self) -> OVector<D, N> {
349 let mu_res = self.residual_chemical_potential();
350 let ln_z = self.compressibility(Contributions::Total).ln();
351 (mu_res / (self.temperature * RGAS))
352 .into_value()
353 .map(|mu| mu - ln_z)
354 }
355
356 pub fn dln_phi_dt(&self) -> InvT<OVector<D, N>> {
358 let vi = self.partial_molar_volume();
359 ((self.dmu_res_dt()
360 - self.residual_chemical_potential() / self.temperature
361 - vi * self.dp_dt(Contributions::Total))
362 / (self.temperature * RGAS))
363 .add_scalar(self.temperature.inv())
364 }
365
366 pub fn dln_phi_dp(&self) -> InvP<OVector<D, N>> {
368 (self.partial_molar_volume() / (self.temperature * RGAS))
369 .add_scalar(-self.pressure(Contributions::Total).inv())
370 }
371
372 pub fn n_dln_phi_dnj(&self) -> OMatrix<D, N, N>
374 where
375 DefaultAllocator: Allocator<N, N>,
376 {
377 let dmu_dni = self.n_dmu_dni(Contributions::Residual);
378 let dp_dni = self.n_dp_dni(Contributions::Total);
379 let dp_dv = self.dp_dv(Contributions::Total);
380 let (r, c) = dmu_dni.shape_generic();
381 let dp_dn_2 = Quantity::from_fn_generic(r, c, |i, j| dp_dni.get(i) * dp_dni.get(j));
382 ((dmu_dni + dp_dn_2 / dp_dv) / (self.temperature * RGAS))
383 .into_value()
384 .add_scalar(D::from(1.0))
385 }
386}
387
388impl<E: Residual + Subset> State<E> {
389 pub fn ln_phi_pure_liquid(&self) -> FeosResult<DVector<f64>> {
391 let pressure = self.pressure(Contributions::Total);
392 (0..self.eos.components())
393 .map(|i| {
394 let eos = self.eos.subset(&[i]);
395 let state = State::new_npt(
396 &eos,
397 self.temperature,
398 pressure,
399 dvector![1.0],
400 Some(crate::DensityInitialization::Liquid),
401 )?;
402 Ok(state.ln_phi()[0])
403 })
404 .collect::<FeosResult<Vec<_>>>()
405 .map(DVector::from)
406 }
407
408 pub fn ln_symmetric_activity_coefficient(&self) -> FeosResult<DVector<f64>> {
410 Ok(match self.eos.components() {
411 1 => dvector![0.0],
412 _ => self.ln_phi() - &self.ln_phi_pure_liquid()?,
413 })
414 }
415
416 pub fn henrys_law_constant(
422 eos: &E,
423 temperature: Temperature,
424 molefracs: &DVector<f64>,
425 ) -> FeosResult<Vec<Pressure>> {
426 let (solvent_comps, solvent_molefracs): (Vec<_>, Vec<_>) = molefracs
428 .iter()
429 .enumerate()
430 .filter_map(|(i, &x)| (x != 0.0).then_some((i, x)))
431 .unzip();
432 let solvent_molefracs = DVector::from_vec(solvent_molefracs);
433 let solvent = eos.subset(&solvent_comps);
434 let vle = PhaseEquilibrium::bubble_point(
435 &solvent,
436 temperature,
437 &solvent_molefracs,
438 None,
439 None,
440 Default::default(),
441 )?;
442
443 let liquid = State::new(eos, temperature, vle.liquid().density, molefracs.clone())?;
445
446 let mut molefracs_vapor = molefracs.clone();
448 solvent_comps
449 .into_iter()
450 .zip(&vle.vapor().molefracs)
451 .for_each(|(i, &y)| molefracs_vapor[i] = y);
452 let vapor = State::new(eos, temperature, vle.vapor().density, molefracs.clone())?;
453
454 let p = vle.vapor().pressure(Contributions::Total).into_reduced();
456 let h = (liquid.ln_phi() - vapor.ln_phi()).map(f64::exp) * p;
457 Ok(h.into_iter()
458 .zip(molefracs)
459 .filter_map(|(h, &x)| (x == 0.0).then_some(h))
460 .map(|&h| Pressure::from_reduced(h))
461 .collect())
462 }
463
464 pub fn henrys_law_constant_binary(eos: &E, temperature: Temperature) -> FeosResult<Pressure> {
468 Ok(Self::henrys_law_constant(eos, temperature, &dvector![0.0, 1.0])?[0])
469 }
470}
471
472impl<E: Residual> State<E> {
473 pub fn thermodynamic_factor(&self) -> DMatrix<f64> {
475 let dln_phi_dnj = self.n_dln_phi_dnj();
476 let n = self.eos.components() - 1;
477 DMatrix::from_fn(n, n, |i, j| {
478 dln_phi_dnj[(i, j)] - dln_phi_dnj[(i, n)] + if i == j { 1.0 } else { 0.0 }
479 })
480 }
481}
482
483impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
484where
485 DefaultAllocator: Allocator<N>,
486{
487 pub fn residual_molar_isochoric_heat_capacity(&self) -> MolarEntropy<D> {
489 self.ds_res_dt() * self.temperature
490 }
491
492 pub fn dc_v_res_dt(&self) -> <MolarEntropy<D> as Div<Temperature<D>>>::Output {
494 self.temperature * self.d2s_res_dt2() + self.ds_res_dt()
495 }
496
497 pub fn residual_molar_isobaric_heat_capacity(&self) -> MolarEntropy<D> {
499 let dp_dt = self.dp_dt(Contributions::Total);
500 self.temperature * (self.ds_res_dt() - dp_dt * dp_dt / self.dp_dv(Contributions::Total))
501 - RGAS
502 }
503
504 pub fn residual_enthalpy(&self) -> FeosResult<Energy<D>> {
506 Ok(self.residual_molar_enthalpy() * self.total_moles()?)
507 }
508
509 pub fn residual_molar_enthalpy(&self) -> MolarEnergy<D> {
511 self.temperature * self.residual_molar_entropy()
512 + self.residual_molar_helmholtz_energy()
513 + self.pressure(Contributions::Residual) * self.molar_volume
514 }
515
516 pub fn residual_internal_energy(&self) -> FeosResult<Energy<D>> {
518 Ok(self.residual_molar_internal_energy() * self.total_moles()?)
519 }
520
521 pub fn residual_molar_internal_energy(&self) -> MolarEnergy<D> {
523 self.temperature * self.residual_molar_entropy() + self.residual_molar_helmholtz_energy()
524 }
525
526 pub fn residual_gibbs_energy(&self) -> FeosResult<Energy<D>> {
528 Ok(self.residual_molar_gibbs_energy() * self.total_moles()?)
529 }
530
531 pub fn residual_molar_gibbs_energy(&self) -> MolarEnergy<D> {
533 self.pressure(Contributions::Residual) * self.molar_volume
534 + self.residual_molar_helmholtz_energy()
535 - self.temperature
536 * RGAS
537 * Dimensionless::new(self.compressibility(Contributions::Total).ln())
538 }
539
540 pub fn residual_molar_helmholtz_energy_contributions(
542 &self,
543 ) -> Vec<(&'static str, MolarEnergy<D>)> {
544 let residual_contributions = self.eos.helmholtz_energy_contributions(
545 self.temperature.into_reduced(),
546 self.density.into_reduced().recip(),
547 &self.molefracs,
548 );
549 let mut res = Vec::with_capacity(residual_contributions.len());
550 for (s, v) in residual_contributions {
551 res.push((s, MolarEnergy::from_reduced(v)));
552 }
553 res
554 }
555
556 pub fn residual_chemical_potential_contributions(
558 &self,
559 component: usize,
560 ) -> Vec<(&'static str, MolarEnergy<D>)> {
561 let t = Dual::from_re(self.temperature.into_reduced());
562 let v = Dual::from_re(self.temperature.into_reduced());
563 let mut x = self.molefracs.map(Dual::from_re);
564 x[component].eps = D::one();
565 let contributions = self.eos.lift().helmholtz_energy_contributions(t, v, &x);
566 let mut res = Vec::with_capacity(contributions.len());
567 for (s, v) in contributions {
568 res.push((s, MolarEnergy::from_reduced(v.eps)));
569 }
570 res
571 }
572
573 pub fn pressure_contributions(&self) -> Vec<(&'static str, Pressure<D>)> {
575 let t = Dual::from_re(self.temperature.into_reduced());
576 let v = Dual::from_re(self.density.into_reduced().recip()).derivative();
577 let x = self.molefracs.map(Dual::from_re);
578 let contributions = self.eos.lift().helmholtz_energy_contributions(t, v, &x);
579 let mut res = Vec::with_capacity(contributions.len() + 1);
580 res.push(("Ideal gas", self.density * RGAS * self.temperature));
581 for (s, v) in contributions {
582 res.push((s, Pressure::from_reduced(-v.eps)));
583 }
584 res
585 }
586}
587
588impl<E: Residual<N, D> + Molarweight<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
589where
590 DefaultAllocator: Allocator<N>,
591{
592 pub fn total_molar_weight(&self) -> MolarWeight<D> {
594 self.eos
595 .molar_weight()
596 .dot(&Dimensionless::new(self.molefracs.clone()))
597 }
598
599 pub fn mass(&self) -> FeosResult<Mass<OVector<D, N>>> {
601 Ok(self
602 .eos
603 .molar_weight()
604 .component_mul(&Dimensionless::new(self.molefracs.clone()))
605 * self.total_moles()?)
606 }
607
608 pub fn total_mass(&self) -> FeosResult<Mass<D>> {
610 Ok(self.total_molar_weight() * self.total_moles()?)
611 }
612
613 pub fn mass_density(&self) -> MassDensity<D> {
615 self.density * self.total_molar_weight()
616 }
617
618 pub fn massfracs(&self) -> OVector<D, N> {
620 self.eos
621 .molar_weight()
622 .convert_into(self.total_molar_weight())
623 .component_mul(&self.molefracs)
624 }
625}
626
627impl<E: Residual<N, D> + EntropyScaling<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
632where
633 DefaultAllocator: Allocator<N>,
634{
635 pub fn viscosity(&self) -> Viscosity<D> {
637 let s = self.residual_molar_entropy().into_reduced();
638 self.eos
639 .viscosity_reference(self.temperature, self.molar_volume, &self.molefracs)
640 * Dimensionless::new(self.eos.viscosity_correlation(s, &self.molefracs).exp())
641 }
642
643 pub fn ln_viscosity_reduced(&self) -> D {
648 let s = self.residual_molar_entropy().into_reduced();
649 self.eos.viscosity_correlation(s, &self.molefracs)
650 }
651
652 pub fn viscosity_reference(&self) -> Viscosity<D> {
654 self.eos
655 .viscosity_reference(self.temperature, self.molar_volume, &self.molefracs)
656 }
657
658 pub fn diffusion(&self) -> Diffusivity<D> {
660 let s = self.residual_molar_entropy().into_reduced();
661 self.eos
662 .diffusion_reference(self.temperature, self.molar_volume, &self.molefracs)
663 * Dimensionless::new(self.eos.diffusion_correlation(s, &self.molefracs).exp())
664 }
665
666 pub fn ln_diffusion_reduced(&self) -> D {
671 let s = self.residual_molar_entropy().into_reduced();
672 self.eos.diffusion_correlation(s, &self.molefracs)
673 }
674
675 pub fn diffusion_reference(&self) -> Diffusivity<D> {
677 self.eos
678 .diffusion_reference(self.temperature, self.molar_volume, &self.molefracs)
679 }
680
681 pub fn thermal_conductivity(&self) -> ThermalConductivity<D> {
683 let s = self.residual_molar_entropy().into_reduced();
684 self.eos.thermal_conductivity_reference(
685 self.temperature,
686 self.molar_volume,
687 &self.molefracs,
688 ) * Dimensionless::new(
689 self.eos
690 .thermal_conductivity_correlation(s, &self.molefracs)
691 .exp(),
692 )
693 }
694
695 pub fn ln_thermal_conductivity_reduced(&self) -> D {
700 let s = self.residual_molar_entropy().into_reduced();
701 self.eos
702 .thermal_conductivity_correlation(s, &self.molefracs)
703 }
704
705 pub fn thermal_conductivity_reference(&self) -> ThermalConductivity<D> {
707 self.eos.thermal_conductivity_reference(
708 self.temperature,
709 self.molar_volume,
710 &self.molefracs,
711 )
712 }
713}