1use super::{
2 FeOsWrapper, HelmholtzEnergyWrapper, ParametersAD, ResidualHelmholtzEnergy,
3 TotalHelmholtzEnergy,
4};
5use feos_core::{DensityInitialization, EosResult, ReferenceSystem, State};
6use nalgebra::{Const, SMatrix, SVector};
7use ndarray::arr1;
8use num_dual::{hessian, jacobian, third_derivative, Dual2Vec, Dual3, DualNum, DualVec};
9use quantity::{MolarEnergy, MolarEntropy, MolarVolume, Moles, Pressure, Temperature};
10
11pub struct StateAD<'a, E: ParametersAD, D: DualNum<f64> + Copy, const N: usize> {
13 pub eos: &'a HelmholtzEnergyWrapper<E, D, N>,
14 pub temperature: Temperature<D>,
15 pub molar_volume: MolarVolume<D>,
16 pub molefracs: SVector<D, N>,
17 pub reduced_temperature: D,
18 pub reduced_molar_volume: D,
19}
20
21impl<'a, E: ParametersAD, D: DualNum<f64> + Copy, const N: usize> StateAD<'a, E, D, N> {
22 pub fn new(
24 eos: &'a HelmholtzEnergyWrapper<E, D, N>,
25 temperature: D,
26 molar_volume: D,
27 molefracs: SVector<D, N>,
28 ) -> Self {
29 Self {
30 eos,
31 temperature: Temperature::from_reduced(temperature),
32 molar_volume: MolarVolume::from_reduced(molar_volume),
33 molefracs,
34 reduced_temperature: temperature,
35 reduced_molar_volume: molar_volume,
36 }
37 }
38
39 fn from_state<
40 F: Fn(
41 &E::Parameters<DualVec<D, f64, Const<2>>>,
42 DualVec<D, f64, Const<2>>,
43 DualVec<D, f64, Const<2>>,
44 &SVector<DualVec<D, f64, Const<2>>, N>,
45 ) -> SVector<DualVec<D, f64, Const<2>>, 2>,
46 >(
47 eos: &'a HelmholtzEnergyWrapper<E, D, N>,
48 state: State<FeOsWrapper<E, N>>,
49 molefracs: SVector<D, N>,
50 f: F,
51 rhs: SVector<D, 2>,
52 ) -> Self {
53 let mut vars = SVector::from([
54 D::from(state.temperature.to_reduced()),
55 D::from(state.density.to_reduced().recip()),
56 ]);
57 let x = molefracs.map(DualVec::from_re);
58 let params = E::params_from_inner(&eos.parameters);
59 for _ in 0..D::NDERIV {
60 let (mut f, jac) = jacobian(|vars| f(¶ms, vars[0], vars[1], &x), vars);
61 f -= rhs;
62 let det = (jac[(0, 0)] * jac[(1, 1)] - jac[(0, 1)] * jac[(1, 0)]).recip();
63 vars[0] -= det * (jac[(1, 1)] * f[0] - jac[(0, 1)] * f[1]);
64 vars[1] -= det * (jac[(0, 0)] * f[1] - jac[(1, 0)] * f[0]);
65 }
66 let [temperature, molar_volume] = vars.data.0[0];
67 Self::new(eos, temperature, molar_volume, molefracs)
68 }
69}
70
71impl<'a, E: ResidualHelmholtzEnergy<N>, D: DualNum<f64> + Copy, const N: usize>
72 StateAD<'a, E, D, N>
73{
74 pub fn new_tp(
76 eos: &'a HelmholtzEnergyWrapper<E, D, N>,
77 temperature: Temperature<D>,
78 pressure: Pressure<D>,
79 molefracs: SVector<D, N>,
80 density_initialization: DensityInitialization,
81 ) -> EosResult<Self> {
82 let t = temperature.re();
83 let p = pressure.re();
84 let moles = Moles::from_reduced(arr1(&molefracs.data.0[0].map(|x| x.re())));
85 let state = State::new_npt(&eos.eos, t, p, &moles, density_initialization)?;
86 let mut density = D::from(state.density.to_reduced());
87 let t = temperature.into_reduced();
88 for _ in 0..D::NDERIV {
89 let (_, p, dp_drho) = E::dp_drho(&eos.parameters, t, density.recip(), &molefracs);
90 density -= (p - pressure.into_reduced()) / dp_drho;
91 }
92 Ok(Self::new(eos, t, density.recip(), molefracs))
93 }
94
95 pub fn pressure(&self) -> Pressure<D> {
96 Pressure::from_reduced(E::pressure(
97 &self.eos.parameters,
98 self.reduced_temperature,
99 self.reduced_molar_volume,
100 &self.molefracs,
101 ))
102 }
103}
104
105impl<'a, E: TotalHelmholtzEnergy<N>, D: DualNum<f64> + Copy, const N: usize> StateAD<'a, E, D, N> {
106 pub fn new_ps(
108 eos: &'a HelmholtzEnergyWrapper<E, D, N>,
109 pressure: Pressure<D>,
110 molar_entropy: MolarEntropy<D>,
111 molefracs: SVector<D, N>,
112 density_initialization: DensityInitialization,
113 initial_temperature: Option<Temperature>,
114 ) -> EosResult<Self> {
115 let moles = Moles::from_reduced(arr1(&molefracs.data.0[0].map(|x| x.re())));
116 let state = State::new_nps(
117 &eos.eos,
118 pressure.re(),
119 molar_entropy.re(),
120 &moles,
121 density_initialization,
122 initial_temperature,
123 )?;
124 Ok(Self::from_state(
125 eos,
126 state,
127 molefracs,
128 E::pressure_entropy,
129 SVector::from([pressure.into_reduced(), molar_entropy.into_reduced()]),
130 ))
131 }
132
133 pub fn new_ph(
135 eos: &'a HelmholtzEnergyWrapper<E, D, N>,
136 pressure: Pressure<D>,
137 molar_enthalpy: MolarEnergy<D>,
138 molefracs: SVector<D, N>,
139 density_initialization: DensityInitialization,
140 initial_temperature: Option<Temperature>,
141 ) -> EosResult<Self> {
142 let moles = Moles::from_reduced(arr1(&molefracs.data.0[0].map(|x| x.re())));
143 let state = State::new_nph(
144 &eos.eos,
145 pressure.re(),
146 molar_enthalpy.re(),
147 &moles,
148 density_initialization,
149 initial_temperature,
150 )?;
151 Ok(Self::from_state(
152 eos,
153 state,
154 molefracs,
155 E::pressure_enthalpy,
156 SVector::from([pressure.into_reduced(), molar_enthalpy.into_reduced()]),
157 ))
158 }
159
160 pub fn molar_entropy(&self) -> MolarEntropy<D> {
161 MolarEntropy::from_reduced(E::molar_entropy(
162 &self.eos.parameters,
163 self.reduced_temperature,
164 self.reduced_molar_volume,
165 &self.molefracs,
166 ))
167 }
168
169 pub fn molar_enthalpy(&self) -> MolarEnergy<D> {
170 MolarEnergy::from_reduced(E::molar_enthalpy(
171 &self.eos.parameters,
172 self.reduced_temperature,
173 self.reduced_molar_volume,
174 &self.molefracs,
175 ))
176 }
177
178 pub fn molar_isochoric_heat_capacity(&self) -> MolarEntropy<D> {
179 MolarEntropy::from_reduced(E::molar_isochoric_heat_capacity(
180 &self.eos.parameters,
181 self.reduced_temperature,
182 self.reduced_molar_volume,
183 &self.molefracs,
184 ))
185 }
186
187 pub fn molar_isobaric_heat_capacity(&self) -> MolarEntropy<D> {
188 MolarEntropy::from_reduced(E::molar_isobaric_heat_capacity(
189 &self.eos.parameters,
190 self.reduced_temperature,
191 self.reduced_molar_volume,
192 &self.molefracs,
193 ))
194 }
195}
196
197impl<'a, E: ResidualHelmholtzEnergy<1>, D: DualNum<f64> + Copy> StateAD<'a, E, D, 1> {
198 pub fn critical_point_pure(eos: &'a HelmholtzEnergyWrapper<E, D, 1>) -> EosResult<Self> {
200 Self::critical_point(eos, SVector::from([D::one()]))
201 }
202}
203
204impl<'a, E: ResidualHelmholtzEnergy<N>, D: DualNum<f64> + Copy, const N: usize>
205 StateAD<'a, E, D, N>
206{
207 pub fn critical_point(
209 eos: &'a HelmholtzEnergyWrapper<E, D, N>,
210 molefracs: SVector<D, N>,
211 ) -> EosResult<Self>
212 where
213 Const<N>: Eigen<N>,
214 {
215 let moles = Moles::from_reduced(arr1(molefracs.map(|x| x.re()).as_slice()));
216 let state = State::critical_point(&eos.eos, Some(&moles), None, Default::default())?;
217 Ok(Self::from_state(
218 eos,
219 state,
220 molefracs,
221 Self::criticality_conditions,
222 SVector::from([D::from(0.0); 2]),
223 ))
224 }
225}
226
227impl<E: ResidualHelmholtzEnergy<N>, D: DualNum<f64> + Copy, const N: usize> StateAD<'_, E, D, N> {
228 fn criticality_conditions(
229 parameters: &E::Parameters<DualVec<D, f64, Const<2>>>,
230 temperature: DualVec<D, f64, Const<2>>,
231 molar_volume: DualVec<D, f64, Const<2>>,
232 molefracs: &SVector<DualVec<D, f64, Const<2>>, N>,
233 ) -> SVector<DualVec<D, f64, Const<2>>, 2>
234 where
235 Const<N>: Eigen<N>,
236 {
237 let sqrt_z = molefracs.map(|z| z.sqrt());
239 let z_mix = sqrt_z * sqrt_z.transpose();
240 let (_, _, m) = hessian(
241 |x| {
242 let params = E::params_from_inner(parameters);
243 let t = Dual2Vec::from_re(temperature);
244 let v = Dual2Vec::from_re(molar_volume);
245 E::residual_molar_helmholtz_energy(¶ms, t, v, &x)
246 },
247 *molefracs,
248 );
249 let m = m.component_mul(&z_mix) / temperature + SMatrix::identity();
250
251 let (l, u) = <Const<N> as Eigen<N>>::eigen(m);
253
254 let (_, _, _, c2) = third_derivative(
255 |s| {
256 let x = molefracs.map(Dual3::from_re);
257 let x = x + sqrt_z.component_mul(&u).map(Dual3::from_re) * s;
258 let params = E::params_from_inner(parameters);
259 let t = Dual3::from_re(temperature);
260 let v = Dual3::from_re(molar_volume);
261 let ig = x.component_mul(&x.map(|x| (x / v).ln() - 1.0)).sum();
262 E::residual_molar_helmholtz_energy(¶ms, t, v, &x) / t + ig
263 },
264 DualVec::from_re(D::zero()),
265 );
266
267 SVector::from([l, c2])
268 }
269}
270
271pub trait Eigen<const N: usize> {
272 fn eigen<D: DualNum<f64> + Copy>(matrix: SMatrix<D, N, N>) -> (D, SVector<D, N>);
273}
274
275impl Eigen<1> for Const<1> {
276 fn eigen<D: DualNum<f64> + Copy>(matrix: SMatrix<D, 1, 1>) -> (D, SVector<D, 1>) {
277 let [[l]] = matrix.data.0;
278 (l, SVector::from([D::one()]))
279 }
280}
281
282impl Eigen<2> for Const<2> {
283 fn eigen<D: DualNum<f64> + Copy>(matrix: SMatrix<D, 2, 2>) -> (D, SVector<D, 2>) {
284 let [[a, b], [_, c]] = matrix.data.0;
285 let l = (a + c - ((a - c).powi(2) + b * b * 4.0).sqrt()) * 0.5;
286 let u = SVector::from([D::one(), (l - a) / b]);
287 let u = u / (u[0] * u[0] + u[1] * u[1]).sqrt();
288 (l, u)
289 }
290}
291
292#[cfg(test)]
293mod test {
294 use super::*;
295 use crate::eos::ideal_gas::test::joback;
296 use crate::eos::pcsaft::test::pcsaft;
297 use crate::eos::PcSaftPure;
298 use crate::EquationOfStateAD;
299 use approx::assert_relative_eq;
300 use feos_core::{Contributions, EosResult, EquationOfState, PhaseEquilibrium};
301 use num_dual::{Dual, Dual64};
302 use quantity::{BAR, JOULE, KELVIN, MOL, PASCAL};
303 use std::sync::Arc;
304
305 #[test]
306 fn test_critical_point() -> EosResult<()> {
307 let (pcsaft, eos) = pcsaft()?;
308 let mut params = pcsaft.params();
309 let mut params_dual = pcsaft.params::<Dual64>();
310 params_dual[0] = params_dual[0].derivative();
311 let pcsaft = pcsaft.wrap().derivatives(params_dual);
312 let cp = State::critical_point(&eos, None, None, Default::default())?;
313 let state = StateAD::critical_point_pure(&pcsaft)?;
314 let t = state.temperature.re();
315 let rho = 1.0 / state.molar_volume.re();
316 println!("{:.5} {:.5}", t, rho);
317 println!("{:.5} {:.5}", cp.temperature, cp.density);
318 assert_relative_eq!(t, cp.temperature, max_relative = 1e-10);
319 assert_relative_eq!(rho, cp.density, max_relative = 1e-10);
320
321 let h = 1e-8;
322 params[0] += h;
323 let eos = PcSaftPure(params).wrap();
324 let cp_h = State::critical_point(&eos.eos, None, None, Default::default())?;
325 let dt = (cp_h.temperature - cp.temperature).to_reduced() / h;
326 let drho = (cp_h.density - cp.density).to_reduced() / h;
327
328 println!(
329 "{:.5e} {:.5e}",
330 state.reduced_temperature.eps,
331 state.reduced_molar_volume.recip().eps
332 );
333 println!("{:.5e} {:.5e}", dt, drho);
334 assert_relative_eq!(state.reduced_temperature.eps, dt, max_relative = 1e-6);
335 assert_relative_eq!(
336 state.reduced_molar_volume.recip().eps,
337 drho,
338 max_relative = 1e-6
339 );
340 Ok(())
341 }
342
343 #[test]
344 fn test_state_tp() -> EosResult<()> {
345 let (pcsaft, eos) = pcsaft()?;
346 let pcsaft = pcsaft.wrap();
347 let p = BAR;
348 let t = 300.0 * KELVIN;
349 let state_feos = State::new_npt(
350 &eos,
351 t,
352 p,
353 &(arr1(&[1.0]) * MOL),
354 DensityInitialization::Liquid,
355 )?;
356 let state = StateAD::new_tp(
357 &pcsaft,
358 t,
359 p,
360 SVector::from([1.0]),
361 DensityInitialization::Liquid,
362 )?;
363 let density = 1.0 / state.molar_volume;
364 println!("{:.5}", density);
365 println!("{:.5}", state_feos.density);
366 assert_relative_eq!(density, state_feos.density, max_relative = 1e-10);
367 Ok(())
368 }
369
370 #[test]
371 fn test_state_tp_derivative() -> EosResult<()> {
372 let (pcsaft, residual) = pcsaft()?;
373 let p = BAR;
374 let t = 300.0 * KELVIN;
375 let state_feos = State::new_npt(
376 &residual,
377 t,
378 p,
379 &(arr1(&[1.0]) * MOL),
380 DensityInitialization::Liquid,
381 )?;
382 let h = 1e2 * PASCAL;
383 let state_h = State::new_npt(
384 &residual,
385 t,
386 p + h,
387 &(arr1(&[1.0]) * MOL),
388 DensityInitialization::Liquid,
389 )?;
390 let params: [Dual64; 8] = pcsaft.params();
391 let eos_ad = pcsaft.wrap().derivatives(params);
392 let t = Temperature::from_reduced(Dual::from(t.to_reduced()));
393 let p = Pressure::from_reduced(Dual::from(p.to_reduced()).derivative());
394 let state = StateAD::new_tp(
395 &eos_ad,
396 t,
397 p,
398 SVector::from([Dual::from(1.0)]),
399 DensityInitialization::Liquid,
400 )?;
401 let density = state.molar_volume.into_reduced().recip();
402 println!("{:.5} {:.5}", density.re, density.eps);
403 let density_h = ((state_h.density - state_feos.density) / h).into_reduced();
404 println!("{:.5} {:.5}", state_feos.density.into_reduced(), density_h);
405 assert_relative_eq!(density.eps, density_h, max_relative = 1e-6);
406 Ok(())
407 }
408
409 #[test]
410 fn test_state_ps() -> EosResult<()> {
411 let (joback, ideal_gas) = joback()?;
412 let (pcsaft, residual) = pcsaft()?;
413 let eos_ad = EquationOfStateAD::new([joback], pcsaft).wrap();
414 let eos = Arc::new(EquationOfState::new(ideal_gas, residual));
415 let vle = PhaseEquilibrium::pure(&eos, 250.0 * KELVIN, None, Default::default())?;
416 let p = vle.liquid().pressure(Contributions::Total);
417 let s = vle.liquid().molar_entropy(Contributions::Total);
418 let t = vle.liquid().temperature;
419 let state = StateAD::new_ps(
420 &eos_ad,
421 p,
422 s,
423 SVector::from([1.0]),
424 DensityInitialization::Liquid,
425 Some(t),
426 )?;
427 let density = 1.0 / state.molar_volume;
428 println!("{:.5} {:.5}", state.temperature, density);
429 println!(
430 "{:.5} {:.5}",
431 vle.liquid().temperature,
432 vle.liquid().density,
433 );
434 assert_relative_eq!(
435 state.temperature,
436 vle.liquid().temperature,
437 max_relative = 1e-10
438 );
439 assert_relative_eq!(density, vle.liquid().density, max_relative = 1e-10);
440 Ok(())
441 }
442
443 #[test]
444 fn test_state_ps_derivative() -> EosResult<()> {
445 let (joback, ideal_gas) = joback()?;
446 let (pcsaft, residual) = pcsaft()?;
447 let eos = Arc::new(EquationOfState::new(ideal_gas, residual));
448 let vle = PhaseEquilibrium::pure(&eos, 250.0 * KELVIN, None, Default::default())?;
449 let h = 1e-3 * JOULE / KELVIN / MOL;
450 let state_h = State::new_nps(
451 &eos,
452 vle.liquid().pressure(Contributions::Total),
453 vle.liquid().molar_entropy(Contributions::Total) + h,
454 &vle.liquid().moles,
455 DensityInitialization::Liquid,
456 Some(vle.liquid().temperature),
457 )?;
458 let p = vle.liquid().pressure(Contributions::Total).to_reduced();
459 let s = vle
460 .liquid()
461 .molar_entropy(Contributions::Total)
462 .to_reduced();
463 let t = vle.liquid().temperature;
464 let eos_ad = EquationOfStateAD::new([joback], pcsaft)
465 .wrap()
466 .derivatives(([joback.params()], pcsaft.params()));
467 let p: Pressure<Dual64> = Pressure::from_reduced(Dual::from(p));
468 let s = MolarEntropy::from_reduced(Dual::from(s).derivative());
469 let state = StateAD::new_ps(
470 &eos_ad,
471 p,
472 s,
473 SVector::from([Dual::from(1.0)]),
474 DensityInitialization::Liquid,
475 Some(t),
476 )?;
477 println!(
478 "{:.5e} {:.5e}",
479 state.reduced_temperature.eps,
480 state.reduced_molar_volume.recip().eps
481 );
482 println!(
483 "{:.5e} {:.5e}",
484 ((state_h.temperature - vle.liquid().temperature) / h).to_reduced(),
485 ((state_h.density - vle.liquid().density) / h).to_reduced(),
486 );
487 assert_relative_eq!(
488 state.reduced_temperature.eps,
489 ((state_h.temperature - vle.liquid().temperature) / h).to_reduced(),
490 max_relative = 1e-6
491 );
492 assert_relative_eq!(
493 state.reduced_molar_volume.recip().eps,
494 ((state_h.density - vle.liquid().density) / h).to_reduced(),
495 max_relative = 1e-6
496 );
497 Ok(())
498 }
499
500 #[test]
501 fn test_state_ph() -> EosResult<()> {
502 let (joback, ideal_gas) = joback()?;
503 let (pcsaft, residual) = pcsaft()?;
504 let eos_ad = EquationOfStateAD::new([joback], pcsaft).wrap();
505 let eos = Arc::new(EquationOfState::new(ideal_gas, residual));
506 let vle = PhaseEquilibrium::pure(&eos, 250.0 * KELVIN, None, Default::default())?;
507 let p = vle.liquid().pressure(Contributions::Total);
508 let h = vle.liquid().molar_enthalpy(Contributions::Total);
509 let t = vle.liquid().temperature;
510 let state = StateAD::new_ph(
511 &eos_ad,
512 p,
513 h,
514 SVector::from([1.0]),
515 DensityInitialization::Liquid,
516 Some(t),
517 )?;
518 let density = 1.0 / state.molar_volume;
519 println!("{:.5} {:.5}", state.temperature, density);
520 println!(
521 "{:.5} {:.5}",
522 vle.liquid().temperature,
523 vle.liquid().density,
524 );
525 assert_relative_eq!(
526 state.temperature,
527 vle.liquid().temperature,
528 max_relative = 1e-10
529 );
530 assert_relative_eq!(density, vle.liquid().density, max_relative = 1e-10);
531 Ok(())
532 }
533
534 #[test]
535 fn test_state_ph_derivative() -> EosResult<()> {
536 let (joback, ideal_gas) = joback()?;
537 let (pcsaft, residual) = pcsaft()?;
538 let eos = Arc::new(EquationOfState::new(ideal_gas, residual));
539 let vle = PhaseEquilibrium::pure(&eos, 250.0 * KELVIN, None, Default::default())?;
540 let delta = 1e-1 * JOULE / MOL;
541 let state_h = State::new_nph(
542 &eos,
543 vle.liquid().pressure(Contributions::Total),
544 vle.liquid().molar_enthalpy(Contributions::Total) + delta,
545 &vle.liquid().moles,
546 DensityInitialization::Liquid,
547 Some(vle.liquid().temperature),
548 )?;
549 let p = vle.liquid().pressure(Contributions::Total).to_reduced();
550 let h = vle
551 .liquid()
552 .molar_enthalpy(Contributions::Total)
553 .to_reduced();
554 let t = vle.liquid().temperature;
555 let eos_ad = EquationOfStateAD::new([joback], pcsaft)
556 .wrap()
557 .derivatives(([joback.params()], pcsaft.params()));
558 let p: Pressure<Dual64> = Pressure::from_reduced(Dual::from(p));
559 let h = MolarEnergy::from_reduced(Dual::from(h).derivative());
560 let state = StateAD::new_ph(
561 &eos_ad,
562 p,
563 h,
564 SVector::from([Dual::from(1.0)]),
565 DensityInitialization::Liquid,
566 Some(t),
567 )?;
568 println!(
569 "{:.5e} {:.5e}",
570 state.reduced_temperature.eps,
571 state.reduced_molar_volume.recip().eps
572 );
573 println!(
574 "{:.5e} {:.5e}",
575 ((state_h.temperature - vle.liquid().temperature) / delta).to_reduced(),
576 ((state_h.density - vle.liquid().density) / delta).to_reduced(),
577 );
578 assert_relative_eq!(
579 state.reduced_temperature.eps,
580 ((state_h.temperature - vle.liquid().temperature) / delta).to_reduced(),
581 max_relative = 1e-6
582 );
583 assert_relative_eq!(
584 state.reduced_molar_volume.recip().eps,
585 ((state_h.density - vle.liquid().density) / delta).to_reduced(),
586 max_relative = 1e-6
587 );
588 Ok(())
589 }
590
591 #[test]
592 fn test_heat_capacities() -> EosResult<()> {
593 let (joback, ideal_gas) = joback()?;
594 let (pcsaft, residual) = pcsaft()?;
595 let eos = Arc::new(EquationOfState::new(ideal_gas, residual));
596 let eos_ad = EquationOfStateAD::new([joback], pcsaft)
597 .wrap()
598 .derivatives(([joback.params()], pcsaft.params()));
599
600 let temperature = 300.0 * KELVIN;
601 let pressure = 5.0 * BAR;
602
603 let state = State::new_npt(
604 &eos,
605 temperature,
606 pressure,
607 &(arr1(&[1.0]) * MOL),
608 DensityInitialization::None,
609 )?;
610 let state_ad = StateAD::new_tp(
611 &eos_ad,
612 temperature,
613 pressure,
614 SVector::from([1.0]),
615 DensityInitialization::None,
616 )?;
617
618 let c_v = state.molar_isochoric_heat_capacity(Contributions::Total);
619 let c_p = state.molar_isobaric_heat_capacity(Contributions::Total);
620 let c_v_ad = state_ad.molar_isochoric_heat_capacity();
621 let c_p_ad = state_ad.molar_isobaric_heat_capacity();
622
623 println!("{c_v} {c_p}");
624 println!("{c_v_ad} {c_p_ad}");
625
626 assert_relative_eq!(c_v, c_v_ad, max_relative = 1e-10);
627 assert_relative_eq!(c_p, c_p_ad, max_relative = 1e-10);
628
629 Ok(())
630 }
631}