1use super::{Contributions, Derivative::*, PartialDerivative, State};
2use crate::equation_of_state::{EntropyScaling, Molarweight, Residual};
3use crate::errors::EosResult;
4use crate::phase_equilibria::PhaseEquilibrium;
5use crate::ReferenceSystem;
6use ndarray::{arr1, Array1, Array2};
7use quantity::*;
8use std::ops::{Add, Div};
9use std::sync::Arc;
10use typenum::P2;
11
12impl<E: Residual> State<E> {
14 pub(super) fn get_or_compute_derivative_residual(&self, derivative: PartialDerivative) -> f64 {
15 let mut cache = self.cache.lock().unwrap();
16
17 match derivative {
18 PartialDerivative::Zeroth => {
19 let new_state = self.derive0();
20 let computation =
21 || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
22 cache.get_or_insert_with_f64(computation)
23 }
24 PartialDerivative::First(v) => {
25 let new_state = self.derive1(v);
26 let computation =
27 || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
28 cache.get_or_insert_with_d64(v, computation)
29 }
30 PartialDerivative::Second(v) => {
31 let new_state = self.derive2(v);
32 let computation =
33 || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
34 cache.get_or_insert_with_d2_64(v, computation)
35 }
36 PartialDerivative::SecondMixed(v1, v2) => {
37 let new_state = self.derive2_mixed(v1, v2);
38 let computation =
39 || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
40 cache.get_or_insert_with_hd64(v1, v2, computation)
41 }
42 PartialDerivative::Third(v) => {
43 let new_state = self.derive3(v);
44 let computation =
45 || self.eos.residual_helmholtz_energy(&new_state) * new_state.temperature;
46 cache.get_or_insert_with_hd364(v, computation)
47 }
48 }
49 }
50}
51
52impl<E: Residual> State<E> {
53 fn contributions<T: Add<T, Output = T>, U>(
54 ideal_gas: Quantity<T, U>,
55 residual: Quantity<T, U>,
56 contributions: Contributions,
57 ) -> Quantity<T, U> {
58 match contributions {
59 Contributions::IdealGas => ideal_gas,
60 Contributions::Total => ideal_gas + residual,
61 Contributions::Residual => residual,
62 }
63 }
64
65 pub fn residual_helmholtz_energy(&self) -> Energy {
67 Energy::from_reduced(self.get_or_compute_derivative_residual(PartialDerivative::Zeroth))
68 }
69
70 pub fn residual_molar_helmholtz_energy(&self) -> MolarEnergy {
72 self.residual_helmholtz_energy() / self.total_moles
73 }
74
75 pub fn residual_helmholtz_energy_contributions(&self) -> Vec<(String, Energy)> {
77 let new_state = self.derive0();
78 let residual_contributions = self.eos.residual_helmholtz_energy_contributions(&new_state);
79 let mut res = Vec::with_capacity(residual_contributions.len());
80 for (s, v) in residual_contributions {
81 res.push((s, Energy::from_reduced(v * new_state.temperature)));
82 }
83 res
84 }
85
86 pub fn residual_entropy(&self) -> Entropy {
88 Entropy::from_reduced(
89 -self.get_or_compute_derivative_residual(PartialDerivative::First(DT)),
90 )
91 }
92
93 pub fn residual_molar_entropy(&self) -> MolarEntropy {
95 self.residual_entropy() / self.total_moles
96 }
97
98 pub fn pressure(&self, contributions: Contributions) -> Pressure {
100 let ideal_gas = self.density * RGAS * self.temperature;
101 let residual = Pressure::from_reduced(
102 -self.get_or_compute_derivative_residual(PartialDerivative::First(DV)),
103 );
104 Self::contributions(ideal_gas, residual, contributions)
105 }
106
107 pub fn residual_chemical_potential(&self) -> MolarEnergy<Array1<f64>> {
109 MolarEnergy::from_reduced(Array1::from_shape_fn(self.eos.components(), |i| {
110 self.get_or_compute_derivative_residual(PartialDerivative::First(DN(i)))
111 }))
112 }
113
114 pub fn residual_chemical_potential_contributions(
116 &self,
117 component: usize,
118 ) -> Vec<(String, MolarEnergy)> {
119 let new_state = self.derive1(DN(component));
120 let contributions = self.eos.residual_helmholtz_energy_contributions(&new_state);
121 let mut res = Vec::with_capacity(contributions.len());
122 for (s, v) in contributions {
123 res.push((
124 s,
125 MolarEnergy::from_reduced((v * new_state.temperature).eps),
126 ));
127 }
128 res
129 }
130
131 pub fn compressibility(&self, contributions: Contributions) -> f64 {
133 (self.pressure(contributions) / (self.density * self.temperature * RGAS)).into_value()
134 }
135
136 pub fn dp_dv(&self, contributions: Contributions) -> <Pressure as Div<Volume>>::Output {
140 let ideal_gas = -self.density * RGAS * self.temperature / self.volume;
141 let residual = Quantity::from_reduced(
142 -self.get_or_compute_derivative_residual(PartialDerivative::Second(DV)),
143 );
144 Self::contributions(ideal_gas, residual, contributions)
145 }
146
147 pub fn dp_drho(&self, contributions: Contributions) -> <Pressure as Div<Density>>::Output {
149 -self.volume / self.density * self.dp_dv(contributions)
150 }
151
152 pub fn dp_dt(&self, contributions: Contributions) -> <Pressure as Div<Temperature>>::Output {
154 let ideal_gas = self.density * RGAS;
155 let residual = Quantity::from_reduced(
156 -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DT)),
157 );
158 Self::contributions(ideal_gas, residual, contributions)
159 }
160
161 pub fn dp_dni(
163 &self,
164 contributions: Contributions,
165 ) -> <Pressure as Div<Moles<Array1<f64>>>>::Output {
166 let ideal_gas = Quantity::from_vec(vec![
167 RGAS * self.temperature / self.volume;
168 self.eos.components()
169 ]);
170 let residual = Quantity::from_reduced(Array1::from_shape_fn(self.eos.components(), |i| {
171 -self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DV, DN(i)))
172 }));
173 Self::contributions(ideal_gas, residual, contributions)
174 }
175
176 pub fn d2p_dv2(
178 &self,
179 contributions: Contributions,
180 ) -> <<Pressure as Div<Volume>>::Output as Div<Volume>>::Output {
181 let ideal_gas = 2.0 * self.density * RGAS * self.temperature / (self.volume * self.volume);
182 let residual = Quantity::from_reduced(
183 -self.get_or_compute_derivative_residual(PartialDerivative::Third(DV)),
184 );
185 Self::contributions(ideal_gas, residual, contributions)
186 }
187
188 pub fn d2p_drho2(
190 &self,
191 contributions: Contributions,
192 ) -> <<Pressure as Div<Density>>::Output as Div<Density>>::Output {
193 self.volume / (self.density * self.density)
194 * (self.volume * self.d2p_dv2(contributions) + 2.0 * self.dp_dv(contributions))
195 }
196
197 pub fn structure_factor(&self) -> f64 {
199 -(RGAS * self.temperature * self.density / (self.volume * self.dp_dv(Contributions::Total)))
200 .into_value()
201 }
202
203 pub(crate) fn p_dpdrho(&self) -> (Pressure, <Pressure as Div<Density>>::Output) {
205 let dp_dv = self.dp_dv(Contributions::Total);
206 (
207 self.pressure(Contributions::Total),
208 (-self.volume * dp_dv / self.density),
209 )
210 }
211
212 pub fn partial_molar_volume(&self) -> MolarVolume<Array1<f64>> {
214 -self.dp_dni(Contributions::Total) / self.dp_dv(Contributions::Total)
215 }
216
217 pub fn dmu_dni(
219 &self,
220 contributions: Contributions,
221 ) -> <MolarEnergy<Array2<f64>> as Div<Moles>>::Output {
222 let n = self.eos.components();
223 Quantity::from_shape_fn((n, n), |(i, j)| {
224 let ideal_gas = if i == j {
225 RGAS * self.temperature / self.moles.get(i)
226 } else {
227 Quantity::from_reduced(0.0)
228 };
229 let residual =
230 Quantity::from_reduced(self.get_or_compute_derivative_residual(
231 PartialDerivative::SecondMixed(DN(i), DN(j)),
232 ));
233 Self::contributions(ideal_gas, residual, contributions)
234 })
235 }
236
237 #[allow(clippy::type_complexity)]
239 pub(crate) fn d2pdrho2(
240 &self,
241 ) -> (
242 Pressure,
243 <Pressure as Div<Density>>::Output,
244 <<Pressure as Div<Density>>::Output as Div<Density>>::Output,
245 ) {
246 let d2p_dv2 = self.d2p_dv2(Contributions::Total);
247 let dp_dv = self.dp_dv(Contributions::Total);
248 (
249 self.pressure(Contributions::Total),
250 (-self.volume * dp_dv / self.density),
251 (self.volume / (self.density * self.density) * (2.0 * dp_dv + self.volume * d2p_dv2)),
252 )
253 }
254
255 pub fn isothermal_compressibility(&self) -> <f64 as Div<Pressure>>::Output {
257 -1.0 / (self.dp_dv(Contributions::Total) * self.volume)
258 }
259
260 pub fn pressure_contributions(&self) -> Vec<(String, Pressure)> {
262 let new_state = self.derive1(DV);
263 let contributions = self.eos.residual_helmholtz_energy_contributions(&new_state);
264 let mut res = Vec::with_capacity(contributions.len() + 1);
265 res.push(("Ideal gas".into(), self.density * RGAS * self.temperature));
266 for (s, v) in contributions {
267 res.push((s, Pressure::from_reduced(-(v * new_state.temperature).eps)));
268 }
269 res
270 }
271
272 pub fn ds_res_dt(&self) -> <Entropy as Div<Temperature>>::Output {
276 Quantity::from_reduced(
277 -self.get_or_compute_derivative_residual(PartialDerivative::Second(DT)),
278 )
279 }
280
281 pub fn d2s_res_dt2(
283 &self,
284 ) -> <<Entropy as Div<Temperature>>::Output as Div<Temperature>>::Output {
285 Quantity::from_reduced(
286 -self.get_or_compute_derivative_residual(PartialDerivative::Third(DT)),
287 )
288 }
289
290 pub fn dmu_res_dt(&self) -> <MolarEnergy<Array1<f64>> as Div<Temperature>>::Output {
292 Quantity::from_reduced(Array1::from_shape_fn(self.eos.components(), |i| {
293 self.get_or_compute_derivative_residual(PartialDerivative::SecondMixed(DT, DN(i)))
294 }))
295 }
296
297 pub fn ln_phi(&self) -> Array1<f64> {
299 (self.residual_chemical_potential() / (RGAS * self.temperature)).into_value()
300 - self.compressibility(Contributions::Total).ln()
301 }
302
303 pub fn ln_phi_pure_liquid(&self) -> EosResult<Array1<f64>> {
305 let pressure = self.pressure(Contributions::Total);
306 (0..self.eos.components())
307 .map(|i| {
308 let eos = Arc::new(self.eos.subset(&[i]));
309 let state = Self::new_npt(
310 &eos,
311 self.temperature,
312 pressure,
313 &Moles::from_reduced(arr1(&[1.0])),
314 crate::DensityInitialization::Liquid,
315 )?;
316 Ok(state.ln_phi()[0])
317 })
318 .collect()
319 }
320
321 pub fn ln_symmetric_activity_coefficient(&self) -> EosResult<Array1<f64>> {
323 match self.eos.components() {
324 1 => Ok(arr1(&[0.0])),
325 _ => Ok(self.ln_phi() - &self.ln_phi_pure_liquid()?),
326 }
327 }
328
329 pub fn henrys_law_constant(
333 eos: &Arc<E>,
334 temperature: Temperature,
335 molefracs: &Array1<f64>,
336 ) -> EosResult<Pressure<Array1<f64>>> {
337 let (solvent_comps, solvent_molefracs): (Vec<_>, Vec<_>) = molefracs
339 .iter()
340 .enumerate()
341 .filter_map(|(i, &x)| (x != 0.0).then_some((i, x)))
342 .unzip();
343 let solvent_molefracs = Array1::from_vec(solvent_molefracs);
344 let solvent = Arc::new(eos.subset(&solvent_comps));
345 let vle = if solvent_comps.len() == 1 {
346 PhaseEquilibrium::pure(&solvent, temperature, None, Default::default())
347 } else {
348 PhaseEquilibrium::bubble_point(
349 &solvent,
350 temperature,
351 &solvent_molefracs,
352 None,
353 None,
354 Default::default(),
355 )
356 }?;
357
358 let liquid = State::new_nvt(
360 eos,
361 temperature,
362 vle.liquid().volume,
363 &(molefracs * vle.liquid().total_moles),
364 )?;
365
366 let mut molefracs_vapor = molefracs.clone();
368 solvent_comps
369 .into_iter()
370 .zip(&vle.vapor().molefracs)
371 .for_each(|(i, &y)| molefracs_vapor[i] = y);
372 let vapor = State::new_nvt(
373 eos,
374 temperature,
375 vle.vapor().volume,
376 &(molefracs_vapor * vle.vapor().total_moles),
377 )?;
378
379 let p = vle.vapor().pressure(Contributions::Total);
381 let h = (liquid.ln_phi() - vapor.ln_phi()).mapv(f64::exp) * p;
382 Ok(h.into_iter()
383 .zip(molefracs)
384 .filter_map(|(h, &x)| (x == 0.0).then_some(h))
385 .collect())
386 }
387
388 pub fn henrys_law_constant_binary(
392 eos: &Arc<E>,
393 temperature: Temperature,
394 ) -> EosResult<Pressure> {
395 Ok(Self::henrys_law_constant(eos, temperature, &arr1(&[0.0, 1.0]))?.get(0))
396 }
397
398 pub fn dln_phi_dt(&self) -> <f64 as Div<Temperature<Array1<f64>>>>::Output {
400 let vi = self.partial_molar_volume();
401 (self.dmu_res_dt()
402 - self.residual_chemical_potential() / self.temperature
403 - vi * self.dp_dt(Contributions::Total))
404 / (RGAS * self.temperature)
405 + 1.0 / self.temperature
406 }
407
408 pub fn dln_phi_dp(&self) -> <f64 as Div<Pressure<Array1<f64>>>>::Output {
410 self.partial_molar_volume() / (RGAS * self.temperature)
411 - 1.0 / self.pressure(Contributions::Total)
412 }
413
414 pub fn dln_phi_dnj(&self) -> <f64 as Div<Moles<Array2<f64>>>>::Output {
416 let n = self.eos.components();
417 let dmu_dni = self.dmu_dni(Contributions::Residual);
418 let dp_dni = self.dp_dni(Contributions::Total);
419 let dp_dv = self.dp_dv(Contributions::Total);
420 let dp_dn_2 = Quantity::from_shape_fn((n, n), |(i, j)| dp_dni.get(i) * dp_dni.get(j));
421 (dmu_dni + dp_dn_2 / dp_dv) / (RGAS * self.temperature) + 1.0 / self.total_moles
422 }
423
424 pub fn thermodynamic_factor(&self) -> Array2<f64> {
426 let dln_phi_dnj = (self.dln_phi_dnj() * Moles::from_reduced(1.0)).into_value();
427 let moles = self.moles.to_reduced();
428 let n = self.eos.components() - 1;
429 Array2::from_shape_fn((n, n), |(i, j)| {
430 moles[i] * (dln_phi_dnj[[i, j]] - dln_phi_dnj[[i, n]]) + if i == j { 1.0 } else { 0.0 }
431 })
432 }
433
434 pub fn residual_molar_isochoric_heat_capacity(&self) -> MolarEntropy {
436 self.temperature * self.ds_res_dt() / self.total_moles
437 }
438
439 pub fn dc_v_res_dt(&self) -> <MolarEntropy as Div<Temperature>>::Output {
441 (self.temperature * self.d2s_res_dt2() + self.ds_res_dt()) / self.total_moles
442 }
443
444 pub fn residual_molar_isobaric_heat_capacity(&self) -> MolarEntropy {
446 self.temperature / self.total_moles
447 * (self.ds_res_dt()
448 - self.dp_dt(Contributions::Total).powi::<P2>() / self.dp_dv(Contributions::Total))
449 - RGAS
450 }
451
452 pub fn residual_enthalpy(&self) -> Energy {
454 self.temperature * self.residual_entropy()
455 + self.residual_helmholtz_energy()
456 + self.pressure(Contributions::Residual) * self.volume
457 }
458
459 pub fn residual_molar_enthalpy(&self) -> MolarEnergy {
461 self.residual_enthalpy() / self.total_moles
462 }
463
464 pub fn residual_internal_energy(&self) -> Energy {
466 self.temperature * self.residual_entropy() + self.residual_helmholtz_energy()
467 }
468
469 pub fn residual_molar_internal_energy(&self) -> MolarEnergy {
471 self.residual_internal_energy() / self.total_moles
472 }
473
474 pub fn residual_gibbs_energy(&self) -> Energy {
476 self.pressure(Contributions::Residual) * self.volume + self.residual_helmholtz_energy()
477 - self.total_moles
478 * RGAS
479 * self.temperature
480 * self.compressibility(Contributions::Total).ln()
481 }
482
483 pub fn residual_molar_gibbs_energy(&self) -> MolarEnergy {
485 self.residual_gibbs_energy() / self.total_moles
486 }
487}
488
489impl<E: Residual + Molarweight> State<E> {
490 pub fn total_molar_weight(&self) -> MolarWeight {
492 (self.eos.molar_weight() * Dimensionless::new(&self.molefracs)).sum()
493 }
494
495 pub fn mass(&self) -> Mass<Array1<f64>> {
497 self.moles.clone() * self.eos.molar_weight()
498 }
499
500 pub fn total_mass(&self) -> Mass {
502 self.total_moles * self.total_molar_weight()
503 }
504
505 pub fn mass_density(&self) -> MassDensity {
507 self.density * self.total_molar_weight()
508 }
509
510 pub fn massfracs(&self) -> Array1<f64> {
512 (self.mass() / self.total_mass()).into_value()
513 }
514}
515
516impl<E: Residual + EntropyScaling> State<E> {
521 pub fn viscosity(&self) -> EosResult<Viscosity> {
523 let s = self.residual_molar_entropy().to_reduced();
524 Ok(self
525 .eos
526 .viscosity_reference(self.temperature, self.volume, &self.moles)?
527 * self.eos.viscosity_correlation(s, &self.molefracs)?.exp())
528 }
529
530 pub fn ln_viscosity_reduced(&self) -> EosResult<f64> {
535 let s = self.residual_molar_entropy().to_reduced();
536 self.eos.viscosity_correlation(s, &self.molefracs)
537 }
538
539 pub fn viscosity_reference(&self) -> EosResult<Viscosity> {
541 self.eos
542 .viscosity_reference(self.temperature, self.volume, &self.moles)
543 }
544
545 pub fn diffusion(&self) -> EosResult<Diffusivity> {
547 let s = self.residual_molar_entropy().to_reduced();
548 Ok(self
549 .eos
550 .diffusion_reference(self.temperature, self.volume, &self.moles)?
551 * self.eos.diffusion_correlation(s, &self.molefracs)?.exp())
552 }
553
554 pub fn ln_diffusion_reduced(&self) -> EosResult<f64> {
559 let s = self.residual_molar_entropy().to_reduced();
560 self.eos.diffusion_correlation(s, &self.molefracs)
561 }
562
563 pub fn diffusion_reference(&self) -> EosResult<Diffusivity> {
565 self.eos
566 .diffusion_reference(self.temperature, self.volume, &self.moles)
567 }
568
569 pub fn thermal_conductivity(&self) -> EosResult<ThermalConductivity> {
571 let s = self.residual_molar_entropy().to_reduced();
572 Ok(self
573 .eos
574 .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)?
575 * self
576 .eos
577 .thermal_conductivity_correlation(s, &self.molefracs)?
578 .exp())
579 }
580
581 pub fn ln_thermal_conductivity_reduced(&self) -> EosResult<f64> {
586 let s = self.residual_molar_entropy().to_reduced();
587 self.eos
588 .thermal_conductivity_correlation(s, &self.molefracs)
589 }
590
591 pub fn thermal_conductivity_reference(&self) -> EosResult<ThermalConductivity> {
593 self.eos
594 .thermal_conductivity_reference(self.temperature, self.volume, &self.moles)
595 }
596}