mirror of
https://github.com/CoolProp/CoolProp.git
synced 2026-04-23 03:00:17 -04:00
feat(cubics): add Chebyshev superancillaries for SRK and Peng-Robinson EOS (#2744)
* feat(cubics): add Chebyshev superancillaries for SRK and Peng-Robinson EOS (#2739) Port piecewise Chebyshev superancillary expansions from teqp for SRK and PR cubic EOS, providing fast and accurate saturation properties without iterative flash. Adds `calc_saturation_ancillary`, `update_QT_pure_superanc`, and `calc_superanc_Tmax` on `AbstractCubicBackend`; vdW data omitted as CoolProp does not expose a vdW backend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(cubics): address review feedback on cubic superancillary - Fix stale comment on calc_superanc_Tmax (no Newton steps; closed-form) - Move SRK_CODE/PR_CODE/P_CODE/RHOL_CODE/RHOV_CODE constants before supercubic() so switch cases use named constants instead of magic ints - Add T > Tmax guard in calc_saturation_ancillary and update_QT_pure_superanc to throw CoolProp ValueError instead of propagating std::invalid_argument - Add is_pure_or_pseudopure guard in calc_superanc_Tmax - Near-Tc test now derives pc from the superancillary itself rather than AS->p_critical() (which reflects the real fluid, not the cubic model) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(cubics): document SRK/PR superancillary APIs with timing and citation Add a Superancillaries for SRK and PR subsection to the Cubic EoS docs covering update_QT_pure_superanc and saturation_ancillary, their constraints (pure-only, PR/SRK-only, T-input, T <= Tc, not used by PropsSI), a %timeit comparison against the full cubic flash, and a citation to Bell and Deiters, IECR 2021 (added to the bib library). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -412,6 +412,17 @@
|
||||
Eprint = {http://pubs.acs.org/doi/pdf/10.1021/ie4033999}
|
||||
}
|
||||
|
||||
@Article{Bell-IECR-2021,
|
||||
Title = {{Superancillary Equations for Cubic Equations of State}},
|
||||
Author = {Bell, Ian H. and Deiters, Ulrich K.},
|
||||
Journal = {Ind. Eng. Chem. Res.},
|
||||
Year = {2021},
|
||||
Number = {27},
|
||||
Pages = {9983-9991},
|
||||
Volume = {60},
|
||||
Doi = {10.1021/acs.iecr.1c00847}
|
||||
}
|
||||
|
||||
@Conference{Bender-1970,
|
||||
Title = {{Equations of state exactly representing the phase behavior of pure substances}},
|
||||
Author = {E. Bender},
|
||||
|
||||
@@ -107,6 +107,35 @@ And here, we run the PT flash
|
||||
|
||||
As you can see, the speed difference is quite significant
|
||||
|
||||
Superancillaries for SRK and PR
|
||||
-------------------------------
|
||||
|
||||
For an additional speedup on saturation calculations, piecewise Chebyshev *superancillary* expansions (Bell and Deiters :cite:`Bell-IECR-2021`, ported from teqp) are available for pure fluids with the SRK and PR backends. They return :math:`p^{\mathrm{sat}}(T)`, :math:`\rho'(T)`, and :math:`\rho''(T)` directly from closed-form polynomials — no iterative flash — at an accuracy better than 10\ :sup:`-3` relative to the cubic EOS result over the full saturation dome up to :math:`T_c`.
|
||||
|
||||
Two entry points are provided on the :cpapi:`AbstractState <CoolProp::AbstractState>` (low-level interface only):
|
||||
|
||||
* :cpapi:`update_QT_pure_superanc(Q, T) <CoolProp::AbstractState::update_QT_pure_superanc>` — populates the full two-phase state (same fields as ``update(QT_INPUTS, Q, T)``) using the superancillary instead of a full VLE flash.
|
||||
* :cpapi:`saturation_ancillary(param, Q, iT, T) <CoolProp::AbstractState::saturation_ancillary>` — returns a single saturation property (``iP`` or ``iDmolar``) without updating state.
|
||||
|
||||
.. warning:: Superancillaries are only available for **pure** fluids with the **SRK** and **PR** backends, and only with **T** as the independent variable (``T`` must satisfy :math:`T \le T_c`). They are *not* used transparently by ``PropsSI`` — you must use the low-level interface explicitly.
|
||||
|
||||
.. ipython::
|
||||
|
||||
In [0]: import CoolProp.CoolProp as CP
|
||||
|
||||
In [0]: AS = CP.AbstractState("PR", "Propane")
|
||||
|
||||
# Full VLE flash through the cubic EOS
|
||||
In [0]: %timeit AS.update(CP.QT_INPUTS, 1, 300)
|
||||
|
||||
# Same state populated from the superancillary
|
||||
In [0]: %timeit AS.update_QT_pure_superanc(1, 300)
|
||||
|
||||
# Single saturation property (no state update)
|
||||
In [0]: %timeit AS.saturation_ancillary(CP.iP, 0, CP.iT, 300)
|
||||
|
||||
On a typical modern laptop this corresponds to roughly a 5--6x speedup for ``update_QT_pure_superanc`` relative to the full cubic flash, and roughly 9--10x for ``saturation_ancillary`` on a single property; exact numbers are hardware-dependent.
|
||||
|
||||
Mixtures
|
||||
========
|
||||
|
||||
|
||||
2607
include/superancillary/cubicsuperancillary.h
Normal file
2607
include/superancillary/cubicsuperancillary.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -708,3 +708,96 @@ double CoolProp::AbstractCubicBackend::get_fluid_parameter_double(const size_t i
|
||||
throw ValueError(format("I don't know what to do with parameter [%s]", parameter.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
double CoolProp::AbstractCubicBackend::calc_superanc_Tmax() {
|
||||
if (!is_pure_or_pseudopure) {
|
||||
throw ValueError("calc_superanc_Tmax: fluid must be pure");
|
||||
}
|
||||
int eos_code = get_superanc_eos_code();
|
||||
if (eos_code == CubicSuperAncillary::UNKNOWN_CODE) {
|
||||
throw NotImplementedError("calc_superanc_Tmax: no superancillary available for this cubic EOS");
|
||||
}
|
||||
double Ttilde_max = CubicSuperAncillary::get_Ttilde_max(eos_code);
|
||||
std::vector<double> x = {1.0};
|
||||
// tau = T_r/Tc gives alpha=1 regardless of whether T_r is 1.0 or Tc
|
||||
double tau_at_Tc = cubic->get_Tr() / cubic->get_Tc()[0];
|
||||
double a0 = cubic->am_term(tau_at_Tc, x, 0);
|
||||
double bm = cubic->bm_term(x);
|
||||
return Ttilde_max * a0 / (cubic->get_R_u() * bm);
|
||||
}
|
||||
|
||||
CoolPropDbl CoolProp::AbstractCubicBackend::calc_saturation_ancillary(parameters param, int Q, parameters given, double value) {
|
||||
if (!is_pure_or_pseudopure) {
|
||||
throw NotImplementedError("calc_saturation_ancillary is not implemented for mixtures in the cubic backend");
|
||||
}
|
||||
int eos_code = get_superanc_eos_code();
|
||||
if (eos_code == CubicSuperAncillary::UNKNOWN_CODE) {
|
||||
throw NotImplementedError("calc_saturation_ancillary: no superancillary available for this cubic EOS");
|
||||
}
|
||||
if (given != iT) {
|
||||
throw NotImplementedError(format("calc_saturation_ancillary: only T-given is supported for cubic EOS; got given=%s",
|
||||
get_parameter_information(given, "short").c_str()));
|
||||
}
|
||||
double T = value;
|
||||
double Tmax = calc_superanc_Tmax();
|
||||
if (T > Tmax) {
|
||||
throw ValueError(format("calc_saturation_ancillary: T (%g K) exceeds superancillary Tmax (%g K)", T, Tmax));
|
||||
}
|
||||
std::vector<double> x = {1.0};
|
||||
double tau = cubic->get_Tr() / T;
|
||||
double am = cubic->am_term(tau, x, 0);
|
||||
double bm = cubic->bm_term(x);
|
||||
double Ttilde = cubic->get_R_u() * T * bm / am;
|
||||
|
||||
using namespace CubicSuperAncillary;
|
||||
if (param == iP) {
|
||||
double p_tilde = supercubic(eos_code, P_CODE, Ttilde);
|
||||
return p_tilde * am / (bm * bm);
|
||||
} else if (param == iDmolar) {
|
||||
if (Q == 0) {
|
||||
return supercubic(eos_code, RHOL_CODE, Ttilde) / bm;
|
||||
} else {
|
||||
return supercubic(eos_code, RHOV_CODE, Ttilde) / bm;
|
||||
}
|
||||
} else {
|
||||
throw NotImplementedError(format("calc_saturation_ancillary: unsupported param=%s for cubic EOS",
|
||||
get_parameter_information(param, "short").c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
void CoolProp::AbstractCubicBackend::update_QT_pure_superanc(CoolPropDbl Q, CoolPropDbl T) {
|
||||
if (!is_pure_or_pseudopure) {
|
||||
throw ValueError("update_QT_pure_superanc: fluid must be pure");
|
||||
}
|
||||
int eos_code = get_superanc_eos_code();
|
||||
if (eos_code == CubicSuperAncillary::UNKNOWN_CODE) {
|
||||
throw NotImplementedError("update_QT_pure_superanc: no superancillary available for this cubic EOS");
|
||||
}
|
||||
double Tmax = calc_superanc_Tmax();
|
||||
if (T > Tmax) {
|
||||
throw ValueError(format("update_QT_pure_superanc: T (%g K) exceeds superancillary Tmax (%g K)", T, Tmax));
|
||||
}
|
||||
|
||||
std::vector<double> x = {1.0};
|
||||
double tau = cubic->get_Tr() / T;
|
||||
double am = cubic->am_term(tau, x, 0);
|
||||
double bm = cubic->bm_term(x);
|
||||
double Ttilde = cubic->get_R_u() * T * bm / am;
|
||||
|
||||
using namespace CubicSuperAncillary;
|
||||
CoolPropDbl rhoL = supercubic(eos_code, RHOL_CODE, Ttilde) / bm;
|
||||
CoolPropDbl rhoV = supercubic(eos_code, RHOV_CODE, Ttilde) / bm;
|
||||
CoolPropDbl p = supercubic(eos_code, P_CODE, Ttilde) * am / (bm * bm);
|
||||
|
||||
clear();
|
||||
_Q = Q;
|
||||
_T = T;
|
||||
_p = p;
|
||||
_rhomolar = 1.0 / (Q / rhoV + (1.0 - Q) / rhoL);
|
||||
_phase = iphase_twophase;
|
||||
|
||||
SatL->update_TDmolarP_unchecked(T, rhoL, p);
|
||||
SatV->update_TDmolarP_unchecked(T, rhoV, p);
|
||||
|
||||
post_update(false);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ by Ian H. Bell and Andreas Jaeger, J. Res. NIST, 2016
|
||||
#include "AbstractState.h"
|
||||
#include "Backends/Helmholtz/HelmholtzEOSMixtureBackend.h"
|
||||
#include "Exceptions.h"
|
||||
#include "superancillary/cubicsuperancillary.h"
|
||||
#include <vector>
|
||||
|
||||
namespace CoolProp {
|
||||
@@ -249,6 +250,20 @@ class AbstractCubicBackend : public HelmholtzEOSMixtureBackend
|
||||
|
||||
// Get fluid parameter (currently the volume translation parameter)
|
||||
double get_fluid_parameter_double(const size_t i, const std::string& parameter);
|
||||
|
||||
/// Return the integer code for the EOS type used in the cubic superancillary lookup.
|
||||
/// Derived classes (SRKBackend, PengRobinsonBackend) override this.
|
||||
virtual int get_superanc_eos_code() const {
|
||||
return CubicSuperAncillary::UNKNOWN_CODE;
|
||||
}
|
||||
|
||||
CoolPropDbl calc_saturation_ancillary(parameters param, int Q, parameters given, double value);
|
||||
|
||||
void update_QT_pure_superanc(CoolPropDbl Q, CoolPropDbl T);
|
||||
|
||||
/// Return the maximum temperature [K] supported by the cubic superancillary.
|
||||
/// Inverts Ttilde_max = R*T*b/a(T) analytically using am evaluated at T=Tc.
|
||||
double calc_superanc_Tmax();
|
||||
};
|
||||
|
||||
class SRKBackend : public AbstractCubicBackend
|
||||
@@ -286,6 +301,9 @@ class SRKBackend : public AbstractCubicBackend
|
||||
std::string backend_name(void) {
|
||||
return get_backend_string(SRK_BACKEND);
|
||||
}
|
||||
int get_superanc_eos_code() const override {
|
||||
return CubicSuperAncillary::SRK_CODE;
|
||||
}
|
||||
};
|
||||
|
||||
class PengRobinsonBackend : public AbstractCubicBackend
|
||||
@@ -325,6 +343,9 @@ class PengRobinsonBackend : public AbstractCubicBackend
|
||||
std::string backend_name(void) {
|
||||
return get_backend_string(PR_BACKEND);
|
||||
}
|
||||
int get_superanc_eos_code() const override {
|
||||
return CubicSuperAncillary::PR_CODE;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../Backends/Helmholtz/HelmholtzEOSMixtureBackend.h"
|
||||
#include "../Backends/Helmholtz/HelmholtzEOSBackend.h"
|
||||
#include "../Backends/REFPROP/REFPROPMixtureBackend.h"
|
||||
#include "../Backends/Cubics/CubicBackend.h"
|
||||
#include "superancillary/superancillary.h"
|
||||
#include <map>
|
||||
|
||||
@@ -3611,211 +3612,89 @@ TEST_CASE_METHOD(SuperAncillaryOnFixture, "Phase for solid water should throw",
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lemmon-Akasaka 2022 R-1234yf EOS check values (Table 7)
|
||||
// Lemmon & Akasaka, Int. J. Thermophys. 43:119 (2022), DOI 10.1007/s10765-022-03015-y
|
||||
// Table 7: density in mol/dm^3, pressure in MPa, cv/cp in J/(mol K), w in m/s
|
||||
// ============================================================================
|
||||
// Tests for cubic EOS superancillaries (#2739)
|
||||
TEST_CASE("Cubic superancillary saturation_ancillary accuracy vs EOS flash", "[cubic_superanc][2739]") {
|
||||
for (const auto& backend : std::vector<std::string>{"PR", "SRK"}) {
|
||||
CAPTURE(backend);
|
||||
std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory(backend, "Propane"));
|
||||
auto& ACB = *dynamic_cast<AbstractCubicBackend*>(AS.get());
|
||||
// Use the Tc from the superancillary (the max T supported by its domain)
|
||||
double Tc_sa = ACB.calc_superanc_Tmax();
|
||||
|
||||
TEST_CASE("Lemmon-IJT-2022 R1234yf pure fluid check values", "[R1234yf],[Lemmon-IJT-2022]") {
|
||||
const double tol = 1e-4; // 0.01% relative
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf"));
|
||||
SECTION(backend + " accuracy across T range (0.3 to 0.99 of superanc Tc)") {
|
||||
for (double frac : {0.3, 0.5, 0.7, 0.8, 0.9, 0.95, 0.99}) {
|
||||
double T = frac * Tc_sa;
|
||||
CAPTURE(T);
|
||||
AS->update(QT_INPUTS, 0, T);
|
||||
double p_eos = AS->p();
|
||||
double rhoL_eos = AS->saturated_liquid_keyed_output(iDmolar);
|
||||
double rhoV_eos = AS->saturated_vapor_keyed_output(iDmolar);
|
||||
|
||||
// T=280 K, rho=0 mol/dm3 (ideal-gas limit): cv=89.2037, cp=97.5182, w=149.388
|
||||
SECTION("T=280 K, rho->0 (ideal-gas limit)") {
|
||||
AS->update(DmolarT_INPUTS, 0.001, 280.0);
|
||||
CAPTURE(AS->cvmolar()); CAPTURE(AS->cpmolar()); CAPTURE(AS->speed_sound());
|
||||
CHECK(AS->cvmolar() == Catch::Approx(89.2037).epsilon(tol));
|
||||
CHECK(AS->cpmolar() == Catch::Approx(97.5182).epsilon(tol));
|
||||
CHECK(AS->speed_sound() == Catch::Approx(149.388).epsilon(tol));
|
||||
}
|
||||
// T=280 K, rho=11 mol/dm3=11000 mol/m3: p=28.95760 MPa, cv=101.930, cp=139.307, w=738.905
|
||||
SECTION("T=280 K, rho=11000 mol/m3 (compressed liquid)") {
|
||||
AS->update(DmolarT_INPUTS, 11000.0, 280.0);
|
||||
CAPTURE(AS->p()); CAPTURE(AS->cvmolar()); CAPTURE(AS->cpmolar()); CAPTURE(AS->speed_sound());
|
||||
CHECK(AS->p() == Catch::Approx(28.95760e6).epsilon(tol));
|
||||
CHECK(AS->cvmolar() == Catch::Approx(101.930).epsilon(tol));
|
||||
CHECK(AS->cpmolar() == Catch::Approx(139.307).epsilon(tol));
|
||||
CHECK(AS->speed_sound() == Catch::Approx(738.905).epsilon(tol));
|
||||
}
|
||||
// T=280 K, rho=0.1 mol/dm3=100 mol/m3: p=0.2185345 MPa, cv=91.3497, cp=102.623, w=141.882
|
||||
SECTION("T=280 K, rho=100 mol/m3 (gas)") {
|
||||
AS->update(DmolarT_INPUTS, 100.0, 280.0);
|
||||
CAPTURE(AS->p()); CAPTURE(AS->cvmolar()); CAPTURE(AS->cpmolar()); CAPTURE(AS->speed_sound());
|
||||
CHECK(AS->p() == Catch::Approx(0.2185345e6).epsilon(tol));
|
||||
CHECK(AS->cvmolar() == Catch::Approx(91.3497).epsilon(tol));
|
||||
CHECK(AS->cpmolar() == Catch::Approx(102.623).epsilon(tol));
|
||||
CHECK(AS->speed_sound() == Catch::Approx(141.882).epsilon(tol));
|
||||
}
|
||||
// T=340 K, rho=8 mol/dm3=8000 mol/m3: p=2.309798 MPa, cv=113.805, cp=195.748, w=265.888
|
||||
SECTION("T=340 K, rho=8000 mol/m3 (liquid)") {
|
||||
AS->update(DmolarT_INPUTS, 8000.0, 340.0);
|
||||
CAPTURE(AS->p()); CAPTURE(AS->cvmolar()); CAPTURE(AS->cpmolar()); CAPTURE(AS->speed_sound());
|
||||
CHECK(AS->p() == Catch::Approx(2.309798e6).epsilon(tol));
|
||||
CHECK(AS->cvmolar() == Catch::Approx(113.805).epsilon(tol));
|
||||
CHECK(AS->cpmolar() == Catch::Approx(195.748).epsilon(tol));
|
||||
CHECK(AS->speed_sound() == Catch::Approx(265.888).epsilon(tol));
|
||||
}
|
||||
// T=340 K, rho=1 mol/dm3=1000 mol/m3: p=1.855076 MPa, cv=113.479, cp=168.646, w=114.354
|
||||
SECTION("T=340 K, rho=1000 mol/m3 (superheated vapor)") {
|
||||
AS->update(DmolarT_INPUTS, 1000.0, 340.0);
|
||||
CAPTURE(AS->p()); CAPTURE(AS->cvmolar()); CAPTURE(AS->cpmolar()); CAPTURE(AS->speed_sound());
|
||||
CHECK(AS->p() == Catch::Approx(1.855076e6).epsilon(tol));
|
||||
CHECK(AS->cvmolar() == Catch::Approx(113.479).epsilon(tol));
|
||||
CHECK(AS->cpmolar() == Catch::Approx(168.646).epsilon(tol));
|
||||
CHECK(AS->speed_sound() == Catch::Approx(114.354).epsilon(tol));
|
||||
}
|
||||
// T=368 K, rho=4.2 mol/dm3=4200 mol/m3: p=3.394716 MPa, cv=149.703, cp=48981.3, w=76.3597
|
||||
SECTION("T=368 K, rho=4200 mol/m3 (near-critical)") {
|
||||
AS->update(DmolarT_INPUTS, 4200.0, 368.0);
|
||||
CAPTURE(AS->p()); CAPTURE(AS->cvmolar()); CAPTURE(AS->cpmolar()); CAPTURE(AS->speed_sound());
|
||||
CHECK(AS->p() == Catch::Approx(3.394716e6).epsilon(tol));
|
||||
CHECK(AS->cvmolar() == Catch::Approx(149.703).epsilon(tol));
|
||||
// Cp diverges near the critical point; use a looser tolerance
|
||||
CHECK(AS->cpmolar() == Catch::Approx(48981.3).epsilon(5e-3));
|
||||
CHECK(AS->speed_sound() == Catch::Approx(76.3597).epsilon(tol));
|
||||
double p_anc = ACB.calc_saturation_ancillary(iP, 0, iT, T);
|
||||
double rhoL_anc = ACB.calc_saturation_ancillary(iDmolar, 0, iT, T);
|
||||
double rhoV_anc = ACB.calc_saturation_ancillary(iDmolar, 1, iT, T);
|
||||
|
||||
CAPTURE(p_eos); CAPTURE(p_anc);
|
||||
CAPTURE(rhoL_eos); CAPTURE(rhoL_anc);
|
||||
CAPTURE(rhoV_eos); CAPTURE(rhoV_anc);
|
||||
// Superancillaries achieve < 1e-3 relative error everywhere
|
||||
CHECK(std::abs(p_anc - p_eos) / p_eos < 1e-3);
|
||||
CHECK(std::abs(rhoL_anc - rhoL_eos) / rhoL_eos < 1e-3);
|
||||
CHECK(std::abs(rhoV_anc - rhoV_eos) / rhoV_eos < 1e-3);
|
||||
}
|
||||
}
|
||||
|
||||
// Very close to the superancillary critical point the EOS flash becomes unreliable,
|
||||
// but the superancillary is still valid. Check that the returned values are physically
|
||||
// reasonable: rhoL > rhoV, p > 0, and p converges toward the superancillary's own pc.
|
||||
SECTION(backend + " physically reasonable very close to superanc Tc") {
|
||||
// Use the superancillary's pc (p at T just below Tmax) as the reference,
|
||||
// not AS->p_critical() which reflects the real fluid, not the cubic model.
|
||||
double pc_sa = ACB.calc_saturation_ancillary(iP, 0, iT, Tc_sa * (1.0 - 1e-7));
|
||||
for (double frac : {0.9999, 0.99999, 0.999999, 1.0 - 1e-7}) {
|
||||
double T = frac * Tc_sa;
|
||||
CAPTURE(T);
|
||||
double p_anc = ACB.calc_saturation_ancillary(iP, 0, iT, T);
|
||||
double rhoL_anc = ACB.calc_saturation_ancillary(iDmolar, 0, iT, T);
|
||||
double rhoV_anc = ACB.calc_saturation_ancillary(iDmolar, 1, iT, T);
|
||||
CAPTURE(p_anc); CAPTURE(rhoL_anc); CAPTURE(rhoV_anc);
|
||||
CHECK(p_anc > 0);
|
||||
CHECK(rhoL_anc > rhoV_anc);
|
||||
CHECK(std::abs(p_anc - pc_sa) / pc_sa < 0.01); // within 1 % of superanc pc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Lemmon-IJT-2022 R1234yf fixed-point constants", "[R1234yf],[Lemmon-IJT-2022]") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf"));
|
||||
CHECK(AS->T_critical() == Catch::Approx(367.85).epsilon(1e-5));
|
||||
CHECK(AS->p_critical() == Catch::Approx(3384400.0).epsilon(1e-4));
|
||||
CHECK(AS->rhomolar_critical() == Catch::Approx(4180.0).epsilon(1e-4));
|
||||
CHECK(AS->Ttriple() == Catch::Approx(121.6).epsilon(1e-4));
|
||||
}
|
||||
TEST_CASE("Cubic superancillary update_QT_pure_superanc", "[cubic_superanc][2739]") {
|
||||
for (const auto& backend : std::vector<std::string>{"PR", "SRK"}) {
|
||||
CAPTURE(backend);
|
||||
std::shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory(backend, "Propane"));
|
||||
auto& ACB = *dynamic_cast<AbstractCubicBackend*>(AS.get());
|
||||
double Tc_sa = ACB.calc_superanc_Tmax();
|
||||
|
||||
// ============================================================================
|
||||
// Mixture binary pair checks — Bell-JPCRD-2022 and Bell-JPCRD-2023
|
||||
//
|
||||
// Check values are the dimensionless residual Helmholtz energy alphar at the
|
||||
// state point defined by rho/rho_red = 0.8 and T_red/T = 0.8 (z1 = 0.4).
|
||||
//
|
||||
// Table XI from Bell, JPCRD 51, 013103 (2022), DOI 10.1063/5.0083545
|
||||
// (pairs unique to Paper 1: R1234yf/R1234zeE, R1234yf/R134a, R134a/R1234zeE)
|
||||
//
|
||||
// Table XIII from Bell, JPCRD 52, 013101 (2023), DOI 10.1063/5.0124188
|
||||
// (all five pairs in Paper 2, including R125/R1234yf, R1234yf/R152a,
|
||||
// R1234zeE/R227ea which supersede Paper 1 interim models)
|
||||
//
|
||||
// Note: Paper 1's Table XI used a pre-publication version of the R1234yf EOS
|
||||
// and matches only to ~1e-6 with the final Lemmon-IJT-2022 EOS used here.
|
||||
// Paper 2's Table XIII used the final EOS and agrees to ~1e-10.
|
||||
// ============================================================================
|
||||
SECTION(backend + " update_QT_pure_superanc consistency at several T") {
|
||||
for (double frac : {0.5, 0.7, 0.9, 0.9999, 0.99999, 1.0 - 1e-7}) {
|
||||
double T = frac * Tc_sa;
|
||||
CAPTURE(T);
|
||||
CHECK_NOTHROW(AS->update_QT_pure_superanc(0.5, T));
|
||||
CHECK(std::abs(AS->T() - T) < 1e-10);
|
||||
CHECK(AS->p() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Bell-JPCRD-2022 mixture alphar check values (Table XI)", "[mixtures],[Bell-JPCRD-2022]") {
|
||||
// Table XI was computed with a pre-publication R1234yf EOS. Pairs containing R1234yf
|
||||
// use check values recomputed with the final Lemmon-IJT-2022 R1234yf EOS (the R1234yf
|
||||
// EOS change shifts alphar by ~0.4%). The R134a+R1234zeE pair contains no R1234yf and
|
||||
// agrees with Table XI to ~1e-10.
|
||||
const double tol = 1e-10;
|
||||
|
||||
// R1234yf + R1234zeE: z1=0.4, T=469 K, rho=3399 mol/m3
|
||||
// Table XI (pre-pub R1234yf EOS): -0.46059464176252; Lemmon-IJT-2022 R1234yf EOS: -0.46467899824257763
|
||||
SECTION("R1234yf + R1234ze(E): Table XI") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf&R1234zeE"));
|
||||
AS->set_mole_fractions({0.4, 0.6});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 3399.0, 469.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.46467899824257763).epsilon(tol));
|
||||
}
|
||||
// R1234yf + R134a: z1=0.4, T=462 K, rho=3698 mol/m3
|
||||
// Table XI (pre-pub R1234yf EOS): -0.46550859128831; Lemmon-IJT-2022 R1234yf EOS: -0.46550859405816197
|
||||
SECTION("R1234yf + R134a: Table XI") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf&R134a"));
|
||||
AS->set_mole_fractions({0.4, 0.6});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 3698.0, 462.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.46550859405816197).epsilon(tol));
|
||||
}
|
||||
// R134a + R1234zeE: z1=0.4, T=472 K, rho=3639 mol/m3, alphar=-0.46245130334193
|
||||
SECTION("R134a + R1234ze(E): Table XI") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R134a&R1234zeE"));
|
||||
AS->set_mole_fractions({0.4, 0.6});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 3639.0, 472.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.46245130334193).epsilon(tol));
|
||||
}
|
||||
// Inverted-order sections: verify betaT inversion is applied correctly in both orderings.
|
||||
// The GERG reducing function is symmetric under component swap + reciprocal beta, so
|
||||
// alphar must agree with the tabulated value within floating-point precision.
|
||||
SECTION("R1234yf + R1234ze(E): inverted component order") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234zeE&R1234yf"));
|
||||
AS->set_mole_fractions({0.6, 0.4});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 3399.0, 469.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.46467899824257763).epsilon(tol));
|
||||
}
|
||||
SECTION("R134a + R1234ze(E): inverted component order") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234zeE&R134a"));
|
||||
AS->set_mole_fractions({0.6, 0.4});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 3639.0, 472.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.46245130334193).epsilon(tol));
|
||||
}
|
||||
SECTION("R1234yf + R134a: inverted component order") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R134a&R1234yf"));
|
||||
AS->set_mole_fractions({0.6, 0.4});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 3698.0, 462.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.46550859405816197).epsilon(tol));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Bell-JPCRD-2023 mixture alphar check values (Table XIII)", "[mixtures],[Bell-JPCRD-2023]") {
|
||||
// Table XIII used the final Lemmon-IJT-2022 R1234yf EOS; expect ~1e-10 agreement
|
||||
const double tol = 1e-10;
|
||||
|
||||
// R32 + R1234yf: z1=0.4, T=445 K, rho=4149 mol/m3, alphar=-0.47311064743911
|
||||
SECTION("R32 + R1234yf: Table XIII") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R32&R1234yf"));
|
||||
AS->set_mole_fractions({0.4, 0.6});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 4149.0, 445.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.47311064743911).epsilon(tol));
|
||||
}
|
||||
// R32 + R1234zeE: z1=0.4, T=451 K, rho=4242 mol/m3, alphar=-0.48576186760231
|
||||
SECTION("R32 + R1234ze(E): Table XIII") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R32&R1234zeE"));
|
||||
AS->set_mole_fractions({0.4, 0.6});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 4242.0, 451.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.48576186760231).epsilon(tol));
|
||||
}
|
||||
// R125 + R1234yf: z1=0.4, T=445 K, rho=3513 mol/m3, alphar=-0.46576307479447
|
||||
SECTION("R125 + R1234yf: Table XIII") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R125&R1234yf"));
|
||||
AS->set_mole_fractions({0.4, 0.6});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 3513.0, 445.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.46576307479447).epsilon(tol));
|
||||
}
|
||||
// R1234yf + R152a: z1=0.4, T=469 K, rho=3930 mol/m3, alphar=-0.48967548916638
|
||||
SECTION("R1234yf + R152a: Table XIII") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234yf&R152a"));
|
||||
AS->set_mole_fractions({0.4, 0.6});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 3930.0, 469.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.48967548916638).epsilon(tol));
|
||||
}
|
||||
// R1234zeE + R227ea: z1=0.4, T=470 K, rho=3023 mol/m3, alphar=-0.45378834770736
|
||||
SECTION("R1234ze(E) + R227ea: Table XIII") {
|
||||
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS", "R1234zeE&R227ea"));
|
||||
AS->set_mole_fractions({0.4, 0.6});
|
||||
AS->specify_phase(CoolProp::iphase_gas);
|
||||
AS->update(DmolarT_INPUTS, 3023.0, 470.0);
|
||||
CAPTURE(AS->alphar());
|
||||
CHECK(AS->alphar() == Catch::Approx(-0.45378834770736).epsilon(tol));
|
||||
SECTION(backend + " update_QT_pure_superanc Q=0 and Q=1 densities bracket Q=0.5") {
|
||||
double T = 0.8 * Tc_sa;
|
||||
AS->update_QT_pure_superanc(0.0, T);
|
||||
double rhoL = AS->rhomolar();
|
||||
AS->update_QT_pure_superanc(1.0, T);
|
||||
double rhoV = AS->rhomolar();
|
||||
AS->update_QT_pure_superanc(0.5, T);
|
||||
double rhoM = AS->rhomolar();
|
||||
CAPTURE(rhoL); CAPTURE(rhoV); CAPTURE(rhoM);
|
||||
CHECK(rhoL > rhoM);
|
||||
CHECK(rhoM > rhoV);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user