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 DpDn<T> = Quantity<T, <_Pressure as Sub<_Moles>>::Output>;
11type DeDn<T> = Quantity<T, <_MolarEnergy as Sub<_Moles>>::Output>;
12type InvT<T> = Quantity<T, <_Temperature as Neg>::Output>;
13type InvP<T> = Quantity<T, <_Pressure as Neg>::Output>;
14type InvM<T> = Quantity<T, <_Moles as Neg>::Output>;
15type POverT<T> = Quantity<T, <_Pressure as Sub<_Temperature>>::Output>;
16
17impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
19where
20 DefaultAllocator: Allocator<N>,
21{
22 pub(super) fn contributions<
23 T: Add<T, Output = T>,
24 U,
25 I: FnOnce() -> Quantity<T, U>,
26 R: FnOnce() -> Quantity<T, U>,
27 >(
28 ideal_gas: I,
29 residual: R,
30 contributions: Contributions,
31 ) -> Quantity<T, U> {
32 match contributions {
33 Contributions::IdealGas => ideal_gas(),
34 Contributions::Total => ideal_gas() + residual(),
35 Contributions::Residual => residual(),
36 }
37 }
38
39 pub fn residual_helmholtz_energy(&self) -> Energy<D> {
41 *self.cache.a.get_or_init(|| {
42 self.eos
43 .residual_helmholtz_energy_unit(self.temperature, self.volume, &self.moles)
44 })
45 }
46
47 pub fn residual_molar_helmholtz_energy(&self) -> MolarEnergy<D> {
49 self.residual_helmholtz_energy() / self.total_moles
50 }
51
52 pub fn residual_entropy(&self) -> Entropy<D> {
54 -*self.cache.da_dt.get_or_init(|| {
55 let (a, da_dt) = quantity::ad::first_derivative(
56 partial2(
57 |t, &v, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n),
58 &self.volume,
59 &self.moles,
60 ),
61 self.temperature,
62 );
63 let _ = self.cache.a.set(a);
64 da_dt
65 })
66 }
67
68 pub fn residual_molar_entropy(&self) -> MolarEntropy<D> {
70 self.residual_entropy() / self.total_moles
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_helmholtz_energy_unit(t, v, n),
81 &self.temperature,
82 &self.moles,
83 ),
84 self.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, &t, &v| self.eos.lift().residual_helmholtz_energy_unit(t, v, &n),
101 &self.temperature,
102 &self.volume,
103 ),
104 &self.moles,
105 );
106 let _ = self.cache.a.set(a);
107 mu
108 })
109 .clone()
110 }
111
112 pub fn compressibility(&self, contributions: Contributions) -> D {
114 (self.pressure(contributions) / (self.density * self.temperature * RGAS)).into_value()
115 }
116
117 pub fn dp_dv(&self, contributions: Contributions) -> <Pressure<D> as Div<Volume<D>>>::Output {
121 let ideal_gas = || -self.density * RGAS * self.temperature / self.volume;
122 let residual = || {
123 -*self.cache.d2a_dv2.get_or_init(|| {
124 let (a, da_dv, d2a_dv2) = quantity::ad::second_derivative(
125 partial2(
126 |v, &t, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n),
127 &self.temperature,
128 &self.moles,
129 ),
130 self.volume,
131 );
132 let _ = self.cache.a.set(a);
133 let _ = self.cache.da_dv.set(da_dv);
134 d2a_dv2
135 })
136 };
137 Self::contributions(ideal_gas, residual, contributions)
138 }
139
140 pub fn dp_drho(
142 &self,
143 contributions: Contributions,
144 ) -> <Pressure<D> as Div<Density<D>>>::Output {
145 -self.volume / self.density * self.dp_dv(contributions)
146 }
147
148 pub fn dp_dt(&self, contributions: Contributions) -> POverT<D> {
150 let ideal_gas = || self.density * RGAS;
151 let residual = || {
152 -*self.cache.d2a_dtdv.get_or_init(|| {
153 let (a, da_dt, da_dv, d2a_dtdv) = quantity::ad::second_partial_derivative(
154 partial(
155 |(t, v), n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n),
156 &self.moles,
157 ),
158 (self.temperature, self.volume),
159 );
160 let _ = self.cache.a.set(a);
161 let _ = self.cache.da_dt.set(da_dt);
162 let _ = self.cache.da_dv.set(da_dv);
163 d2a_dtdv
164 })
165 };
166 Self::contributions(ideal_gas, residual, contributions)
167 }
168
169 pub fn dp_dni(&self, contributions: Contributions) -> DpDn<OVector<D, N>> {
171 let residual = -self
172 .cache
173 .d2a_dndv
174 .get_or_init(|| {
175 let (a, da_dn, da_dv, dmu_dv) = quantity::ad::partial_hessian_copy(
176 partial(
177 |(n, v), &t| self.eos.lift().residual_helmholtz_energy_unit(t, v, &n),
178 &self.temperature,
179 ),
180 (&self.moles, self.volume),
181 );
182 let _ = self.cache.a.set(a);
183 let _ = self.cache.da_dn.set(da_dn);
184 let _ = self.cache.da_dv.set(da_dv);
185 dmu_dv
186 })
187 .clone();
188 let (r, c) = residual.shape_generic();
189 let ideal_gas = || self.temperature / self.volume * RGAS;
190 Quantity::from_fn_generic(r, c, |i, _| {
191 Self::contributions(ideal_gas, || residual.get(i), contributions)
192 })
193 }
194
195 pub fn d2p_dv2(
197 &self,
198 contributions: Contributions,
199 ) -> <<Pressure<D> as Div<Volume<D>>>::Output as Div<Volume<D>>>::Output {
200 let ideal_gas =
201 || self.density * RGAS * self.temperature / (self.volume * self.volume) * 2.0;
202 let residual = || {
203 -*self.cache.d3a_dv3.get_or_init(|| {
204 let (a, da_dv, d2a_dv2, d3a_dv3) = quantity::ad::third_derivative(
205 partial2(
206 |v, &t, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n),
207 &self.temperature,
208 &self.moles,
209 ),
210 self.volume,
211 );
212 let _ = self.cache.a.set(a);
213 let _ = self.cache.da_dv.set(da_dv);
214 let _ = self.cache.d2a_dv2.set(d2a_dv2);
215 d3a_dv3
216 })
217 };
218 Self::contributions(ideal_gas, residual, contributions)
219 }
220
221 pub fn d2p_drho2(
223 &self,
224 contributions: Contributions,
225 ) -> <<Pressure<D> as Div<Density<D>>>::Output as Div<Density<D>>>::Output {
226 self.volume / (self.density * self.density)
227 * (self.volume * self.d2p_dv2(contributions) + self.dp_dv(contributions) * 2.0)
228 }
229
230 pub fn structure_factor(&self) -> D {
232 -(self.temperature * self.density * RGAS / (self.volume * self.dp_dv(Contributions::Total)))
233 .into_value()
234 }
235
236 pub(crate) fn p_dpdrho(&self) -> (Pressure<D>, <Pressure<D> as Div<Density<D>>>::Output) {
238 let dp_dv = self.dp_dv(Contributions::Total);
239 (
240 self.pressure(Contributions::Total),
241 (-self.volume * dp_dv / self.density),
242 )
243 }
244
245 pub fn partial_molar_volume(&self) -> MolarVolume<OVector<D, N>> {
247 -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total)
248 }
249
250 pub fn dmu_dni(&self, contributions: Contributions) -> DeDn<OMatrix<D, N, N>>
252 where
253 DefaultAllocator: Allocator<N, N>,
254 {
255 let (a, da_dn, d2a_dn2) = quantity::ad::hessian_copy(
256 partial2(
257 |n, &t, &v| self.eos.lift().residual_helmholtz_energy_unit(t, v, &n),
258 &self.temperature,
259 &self.volume,
260 ),
261 &self.moles,
262 );
263 let _ = self.cache.a.set(a);
264 let _ = self.cache.da_dn.set(da_dn);
265 let residual = || d2a_dn2;
266 let ideal_gas = || {
267 Dimensionless::new(OMatrix::from_diagonal(&self.molefracs.map(|x| x.recip())))
268 * (self.temperature * RGAS / self.total_moles)
269 };
270 Self::contributions(ideal_gas, residual, contributions)
271 }
272
273 pub fn isothermal_compressibility(&self) -> InvP<D> {
275 (self.dp_dv(Contributions::Total) * self.volume).inv()
276 }
277
278 pub fn ds_res_dt(&self) -> <Entropy<D> as Div<Temperature<D>>>::Output {
282 -*self.cache.d2a_dt2.get_or_init(|| {
283 let (a, da_dt, d2a_dt2) = quantity::ad::second_derivative(
284 partial2(
285 |t, &v, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n),
286 &self.volume,
287 &self.moles,
288 ),
289 self.temperature,
290 );
291 let _ = self.cache.a.set(a);
292 let _ = self.cache.da_dt.set(da_dt);
293 d2a_dt2
294 })
295 }
296
297 pub fn d2s_res_dt2(
299 &self,
300 ) -> <<Entropy<D> as Div<Temperature<D>>>::Output as Div<Temperature<D>>>::Output {
301 -*self.cache.d3a_dt3.get_or_init(|| {
302 let (a, da_dt, d2a_dt2, d3a_dt3) = quantity::ad::third_derivative(
303 partial2(
304 |t, &v, n| self.eos.lift().residual_helmholtz_energy_unit(t, v, n),
305 &self.volume,
306 &self.moles,
307 ),
308 self.temperature,
309 );
310 let _ = self.cache.a.set(a);
311 let _ = self.cache.da_dt.set(da_dt);
312 let _ = self.cache.d2a_dt2.set(d2a_dt2);
313 d3a_dt3
314 })
315 }
316
317 pub fn dmu_res_dt(&self) -> MolarEntropy<OVector<D, N>> {
319 self.cache
320 .d2a_dndt
321 .get_or_init(|| {
322 let (a, da_dn, da_dt, d2a_dndt) = quantity::ad::partial_hessian_copy(
323 partial(
324 |(n, t), &v| self.eos.lift().residual_helmholtz_energy_unit(t, v, &n),
325 &self.volume,
326 ),
327 (&self.moles, self.temperature),
328 );
329 let _ = self.cache.a.set(a);
330 let _ = self.cache.da_dn.set(da_dn);
331 let _ = self.cache.da_dt.set(da_dt);
332 d2a_dndt
333 })
334 .clone()
335 }
336
337 pub fn ln_phi(&self) -> OVector<D, N> {
339 let mu_res = self.residual_chemical_potential();
340 let ln_z = self.compressibility(Contributions::Total).ln();
341 (mu_res / (self.temperature * RGAS))
342 .into_value()
343 .map(|mu| mu - ln_z)
344 }
345
346 pub fn dln_phi_dt(&self) -> InvT<OVector<D, N>> {
348 let vi = self.partial_molar_volume();
349 ((self.dmu_res_dt()
350 - self.residual_chemical_potential() / self.temperature
351 - vi * self.dp_dt(Contributions::Total))
352 / (self.temperature * RGAS))
353 .add_scalar(self.temperature.inv())
354 }
355
356 pub fn dln_phi_dp(&self) -> InvP<OVector<D, N>> {
358 (self.partial_molar_volume() / (self.temperature * RGAS))
359 .add_scalar(-self.pressure(Contributions::Total).inv())
360 }
361
362 pub fn dln_phi_dnj(&self) -> InvM<OMatrix<D, N, N>>
364 where
365 DefaultAllocator: Allocator<N, N>,
366 {
367 let dmu_dni = self.dmu_dni(Contributions::Residual);
368 let dp_dni = self.dp_dni(Contributions::Total);
369 let dp_dv = self.dp_dv(Contributions::Total);
370 let (r, c) = dmu_dni.shape_generic();
371 let dp_dn_2 = Quantity::from_fn_generic(r, c, |i, j| dp_dni.get(i) * dp_dni.get(j));
372 ((dmu_dni + dp_dn_2 / dp_dv) / (self.temperature * RGAS)).add_scalar(self.total_moles.inv())
373 }
374}
375
376impl<E: Residual + Subset> State<E> {
377 pub fn ln_phi_pure_liquid(&self) -> FeosResult<DVector<f64>> {
379 let pressure = self.pressure(Contributions::Total);
380 (0..self.eos.components())
381 .map(|i| {
382 let eos = self.eos.subset(&[i]);
383 let state = State::new_xpt(
384 &eos,
385 self.temperature,
386 pressure,
387 &dvector![1.0],
388 Some(crate::DensityInitialization::Liquid),
389 )?;
390 Ok(state.ln_phi()[0])
391 })
392 .collect::<FeosResult<Vec<_>>>()
393 .map(DVector::from)
394 }
395
396 pub fn ln_symmetric_activity_coefficient(&self) -> FeosResult<DVector<f64>> {
398 Ok(match self.eos.components() {
399 1 => dvector![0.0],
400 _ => self.ln_phi() - &self.ln_phi_pure_liquid()?,
401 })
402 }
403
404 pub fn henrys_law_constant(
410 eos: &E,
411 temperature: Temperature,
412 molefracs: &DVector<f64>,
413 ) -> FeosResult<Vec<Pressure>> {
414 let (solvent_comps, solvent_molefracs): (Vec<_>, Vec<_>) = molefracs
416 .iter()
417 .enumerate()
418 .filter_map(|(i, &x)| (x != 0.0).then_some((i, x)))
419 .unzip();
420 let solvent_molefracs = DVector::from_vec(solvent_molefracs);
421 let solvent = eos.subset(&solvent_comps);
422 let vle = if solvent_comps.len() == 1 {
423 PhaseEquilibrium::pure(&solvent, temperature, None, Default::default())
424 } else {
425 PhaseEquilibrium::bubble_point(
426 &solvent,
427 temperature,
428 &solvent_molefracs,
429 None,
430 None,
431 Default::default(),
432 )
433 }?;
434
435 let liquid = State::new_nvt(
437 eos,
438 temperature,
439 vle.liquid().volume,
440 &(molefracs * vle.liquid().total_moles),
441 )?;
442
443 let mut molefracs_vapor = molefracs.clone();
445 solvent_comps
446 .into_iter()
447 .zip(&vle.vapor().molefracs)
448 .for_each(|(i, &y)| molefracs_vapor[i] = y);
449 let vapor = State::new_nvt(
450 eos,
451 temperature,
452 vle.vapor().volume,
453 &(molefracs_vapor * vle.vapor().total_moles),
454 )?;
455
456 let p = vle.vapor().pressure(Contributions::Total).into_reduced();
458 let h = (liquid.ln_phi() - vapor.ln_phi()).map(f64::exp) * p;
459 Ok(h.into_iter()
460 .zip(molefracs)
461 .filter_map(|(h, &x)| (x == 0.0).then_some(h))
462 .map(|&h| Pressure::from_reduced(h))
463 .collect())
464 }
465
466 pub fn henrys_law_constant_binary(eos: &E, temperature: Temperature) -> FeosResult<Pressure> {
470 Ok(Self::henrys_law_constant(eos, temperature, &dvector![0.0, 1.0])?[0])
471 }
472}
473
474impl<E: Residual> State<E> {
475 pub fn thermodynamic_factor(&self) -> DMatrix<f64> {
477 let dln_phi_dnj = (self.dln_phi_dnj() * Moles::from_reduced(1.0)).into_value();
478 let moles = &self.molefracs * self.total_moles.into_reduced();
479 let n = self.eos.components() - 1;
480 DMatrix::from_fn(n, n, |i, j| {
481 moles[i] * (dln_phi_dnj[(i, j)] - dln_phi_dnj[(i, n)]) + if i == j { 1.0 } else { 0.0 }
482 })
483 }
484}
485
486impl<E: Residual<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
487where
488 DefaultAllocator: Allocator<N>,
489{
490 pub fn residual_molar_isochoric_heat_capacity(&self) -> MolarEntropy<D> {
492 self.ds_res_dt() * self.temperature / self.total_moles
493 }
494
495 pub fn dc_v_res_dt(&self) -> <MolarEntropy<D> as Div<Temperature<D>>>::Output {
497 (self.temperature * self.d2s_res_dt2() + self.ds_res_dt()) / self.total_moles
498 }
499
500 pub fn residual_molar_isobaric_heat_capacity(&self) -> MolarEntropy<D> {
502 let dp_dt = self.dp_dt(Contributions::Total);
503 self.temperature / self.total_moles
504 * (self.ds_res_dt() - dp_dt * dp_dt / self.dp_dv(Contributions::Total))
505 - RGAS
506 }
507
508 pub fn residual_enthalpy(&self) -> Energy<D> {
510 self.temperature * self.residual_entropy()
511 + self.residual_helmholtz_energy()
512 + self.pressure(Contributions::Residual) * self.volume
513 }
514
515 pub fn residual_molar_enthalpy(&self) -> MolarEnergy<D> {
517 self.residual_enthalpy() / self.total_moles
518 }
519
520 pub fn residual_internal_energy(&self) -> Energy<D> {
522 self.temperature * self.residual_entropy() + self.residual_helmholtz_energy()
523 }
524
525 pub fn residual_molar_internal_energy(&self) -> MolarEnergy<D> {
527 self.residual_internal_energy() / self.total_moles
528 }
529
530 pub fn residual_gibbs_energy(&self) -> Energy<D> {
532 self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy()
533 - self.total_moles
534 * RGAS
535 * self.temperature
536 * Dimensionless::new(self.compressibility(Contributions::Total).ln())
537 }
538
539 pub fn residual_molar_gibbs_energy(&self) -> MolarEnergy<D> {
541 self.residual_gibbs_energy() / self.total_moles
542 }
543
544 pub fn residual_molar_helmholtz_energy_contributions(
546 &self,
547 ) -> Vec<(&'static str, MolarEnergy<D>)> {
548 let residual_contributions = self.eos.molar_helmholtz_energy_contributions(
549 self.temperature.into_reduced(),
550 self.density.into_reduced().recip(),
551 &self.molefracs,
552 );
553 let mut res = Vec::with_capacity(residual_contributions.len());
554 for (s, v) in residual_contributions {
555 res.push((s, MolarEnergy::from_reduced(v)));
556 }
557 res
558 }
559
560 pub fn residual_chemical_potential_contributions(
562 &self,
563 component: usize,
564 ) -> Vec<(&'static str, MolarEnergy<D>)> {
565 let t = Dual::from_re(self.temperature.into_reduced());
566 let v = Dual::from_re(self.temperature.into_reduced());
567 let mut x = self.molefracs.map(Dual::from_re);
568 x[component].eps = D::one();
569 let contributions = self
570 .eos
571 .lift()
572 .molar_helmholtz_energy_contributions(t, v, &x);
573 let mut res = Vec::with_capacity(contributions.len());
574 for (s, v) in contributions {
575 res.push((s, MolarEnergy::from_reduced(v.eps)));
576 }
577 res
578 }
579
580 pub fn pressure_contributions(&self) -> Vec<(&'static str, Pressure<D>)> {
582 let t = Dual::from_re(self.temperature.into_reduced());
583 let v = Dual::from_re(self.density.into_reduced().recip()).derivative();
584 let x = self.molefracs.map(Dual::from_re);
585 let contributions = self
586 .eos
587 .lift()
588 .molar_helmholtz_energy_contributions(t, v, &x);
589 let mut res = Vec::with_capacity(contributions.len() + 1);
590 res.push(("Ideal gas", self.density * RGAS * self.temperature));
591 for (s, v) in contributions {
592 res.push((s, Pressure::from_reduced(-v.eps)));
593 }
594 res
595 }
596}
597
598impl<E: Residual<N, D> + Molarweight<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
599where
600 DefaultAllocator: Allocator<N>,
601{
602 pub fn total_molar_weight(&self) -> MolarWeight<D> {
604 self.eos
605 .molar_weight()
606 .dot(&Dimensionless::new(self.molefracs.clone()))
607 }
608
609 pub fn mass(&self) -> Mass<OVector<D, N>> {
611 self.eos.molar_weight().component_mul(&self.moles)
612 }
613
614 pub fn total_mass(&self) -> Mass<D> {
616 self.total_moles * self.total_molar_weight()
617 }
618
619 pub fn mass_density(&self) -> MassDensity<D> {
621 self.density * self.total_molar_weight()
622 }
623
624 pub fn massfracs(&self) -> OVector<D, N> {
626 (self.mass() / self.total_mass()).into_value()
627 }
628}
629
630impl<E: Residual<N, D> + EntropyScaling<N, D>, N: Gradients, D: DualNum<f64> + Copy> State<E, N, D>
635where
636 DefaultAllocator: Allocator<N>,
637{
638 pub fn viscosity(&self) -> Viscosity<D> {
640 let s = self.residual_molar_entropy().into_reduced();
641 self.eos
642 .viscosity_reference(self.temperature, self.volume, &self.moles)
643 * Dimensionless::new(self.eos.viscosity_correlation(s, &self.molefracs).exp())
644 }
645
646 pub fn ln_viscosity_reduced(&self) -> D {
651 let s = self.residual_molar_entropy().into_reduced();
652 self.eos.viscosity_correlation(s, &self.molefracs)
653 }
654
655 pub fn viscosity_reference(&self) -> Viscosity<D> {
657 self.eos
658 .viscosity_reference(self.temperature, self.volume, &self.moles)
659 }
660
661 pub fn diffusion(&self) -> Diffusivity<D> {
663 let s = self.residual_molar_entropy().into_reduced();
664 self.eos
665 .diffusion_reference(self.temperature, self.volume, &self.moles)
666 * Dimensionless::new(self.eos.diffusion_correlation(s, &self.molefracs).exp())
667 }
668
669 pub fn ln_diffusion_reduced(&self) -> D {
674 let s = self.residual_molar_entropy().into_reduced();
675 self.eos.diffusion_correlation(s, &self.molefracs)
676 }
677
678 pub fn diffusion_reference(&self) -> Diffusivity<D> {
680 self.eos
681 .diffusion_reference(self.temperature, self.volume, &self.moles)
682 }
683
684 pub fn thermal_conductivity(&self) -> ThermalConductivity<D> {
686 let s = self.residual_molar_entropy().into_reduced();
687 self.eos
688 .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)
689 * Dimensionless::new(
690 self.eos
691 .thermal_conductivity_correlation(s, &self.molefracs)
692 .exp(),
693 )
694 }
695
696 pub fn ln_thermal_conductivity_reduced(&self) -> D {
701 let s = self.residual_molar_entropy().into_reduced();
702 self.eos
703 .thermal_conductivity_correlation(s, &self.molefracs)
704 }
705
706 pub fn thermal_conductivity_reference(&self) -> ThermalConductivity<D> {
708 self.eos
709 .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)
710 }
711}