mirror of
https://github.com/CoolProp/CoolProp.git
synced 2026-02-11 14:25:09 -05:00
Closes https://github.com/CoolProp/CoolProp/issues/172 Closes https://github.com/CoolProp/CoolProp/issues/121 Signed-off-by: Ian Bell <ian.h.bell@gmail.com>
420 lines
14 KiB
C++
420 lines
14 KiB
C++
#include "Ancillaries.h"
|
|
#include "DataStructures.h"
|
|
#include "AbstractState.h"
|
|
|
|
#if defined(ENABLE_CATCH)
|
|
|
|
#include "crossplatform_shared_ptr.h"
|
|
#include "catch.hpp"
|
|
|
|
#endif
|
|
|
|
namespace CoolProp{
|
|
|
|
SaturationAncillaryFunction::SaturationAncillaryFunction(rapidjson::Value &json_code)
|
|
{
|
|
std::string type = cpjson::get_string(json_code,"type");
|
|
if (!type.compare("rational_polynomial"))
|
|
{
|
|
num_coeffs = vec_to_eigen(cpjson::get_double_array(json_code["A"]));
|
|
den_coeffs = vec_to_eigen(cpjson::get_double_array(json_code["B"]));
|
|
max_abs_error = cpjson::get_double(json_code,"max_abs_error");
|
|
try{
|
|
Tmin = cpjson::get_double(json_code,"Tmin");
|
|
Tmax = cpjson::get_double(json_code,"Tmax");
|
|
}
|
|
catch (std::exception &e){
|
|
std::cout << "Missing Tmin, Tmax" << std::endl;
|
|
Tmin = _HUGE;
|
|
Tmax = _HUGE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
n = cpjson::get_double_array(json_code["n"]);
|
|
t = cpjson::get_double_array(json_code["t"]);
|
|
Tmin = cpjson::get_double(json_code,"Tmin");
|
|
Tmax = cpjson::get_double(json_code,"Tmax");
|
|
reducing_value = cpjson::get_double(json_code,"reducing_value");
|
|
using_tau_r = cpjson::get_bool(json_code,"using_tau_r");
|
|
T_r = cpjson::get_double(json_code,"T_r");
|
|
}
|
|
|
|
if (!type.compare("rational_polynomial"))
|
|
this->type = TYPE_RATIONAL_POLYNOMIAL;
|
|
else if (!type.compare("rhoLnoexp"))
|
|
this->type = TYPE_NOT_EXPONENTIAL;
|
|
else
|
|
this->type = TYPE_EXPONENTIAL;
|
|
this->N = n.size();
|
|
s = n;
|
|
};
|
|
|
|
double SaturationAncillaryFunction::evaluate(double T)
|
|
{
|
|
if (type == TYPE_NOT_SET)
|
|
{
|
|
throw ValueError(format("type not set"));
|
|
}
|
|
else if (type == TYPE_RATIONAL_POLYNOMIAL)
|
|
{
|
|
Polynomial2D poly;
|
|
return poly.evaluate(num_coeffs, T)/poly.evaluate(den_coeffs, T);
|
|
}
|
|
else
|
|
{
|
|
double THETA = 1-T/T_r;
|
|
|
|
for (std::size_t i = 0; i < N; ++i)
|
|
{
|
|
s[i] = n[i]*pow(THETA, t[i]);
|
|
}
|
|
double summer = std::accumulate(s.begin(), s.end(), 0.0);
|
|
|
|
if (type == TYPE_NOT_EXPONENTIAL)
|
|
{
|
|
return reducing_value*(1+summer);
|
|
}
|
|
else
|
|
{
|
|
double tau_r_value;
|
|
if (using_tau_r)
|
|
tau_r_value = T_r/T;
|
|
else
|
|
tau_r_value = 1.0;
|
|
return reducing_value*exp(tau_r_value*summer);
|
|
}
|
|
}
|
|
}
|
|
double SaturationAncillaryFunction::invert(double value, double min_bound, double max_bound)
|
|
{
|
|
// Invert the ancillary curve to get the temperature as a function of the output variable
|
|
// Define the residual to be driven to zero
|
|
class solver_resid : public FuncWrapper1D
|
|
{
|
|
public:
|
|
int other;
|
|
SaturationAncillaryFunction *anc;
|
|
long double T, value, r, current_value;
|
|
|
|
solver_resid(SaturationAncillaryFunction *anc, long double value) : anc(anc), value(value){};
|
|
|
|
double call(double T){
|
|
this->T = T;
|
|
current_value = anc->evaluate(T);
|
|
r = current_value - value;
|
|
return r;
|
|
};
|
|
};
|
|
solver_resid resid(this, value);
|
|
std::string errstring;
|
|
if (min_bound < 0){ min_bound = Tmin-0.01;}
|
|
if (max_bound < 0){ max_bound = Tmax;}
|
|
|
|
try{
|
|
// Safe to expand the domain a little bit to lower temperature, absolutely cannot exceed Tmax
|
|
// because then you get (negative number)^(double) which is undefined.
|
|
return Brent(resid,min_bound,max_bound,DBL_EPSILON,1e-10,100,errstring);
|
|
}
|
|
catch(std::exception &e){
|
|
return Secant(resid,max_bound, -0.01, 1e-12, 100, errstring);
|
|
}
|
|
}
|
|
|
|
void MeltingLineVariables::set_limits(void)
|
|
{
|
|
if (type == MELTING_LINE_SIMON_TYPE){
|
|
|
|
// Fill in the min and max pressures for each part
|
|
for (std::size_t i = 0; i < simon.parts.size(); ++i){
|
|
MeltingLinePiecewiseSimonSegment &part = simon.parts[i];
|
|
part.p_min = part.p_0 + part.a*(pow(part.T_min/part.T_0,part.c)-1);
|
|
part.p_max = part.p_0 + part.a*(pow(part.T_max/part.T_0,part.c)-1);
|
|
}
|
|
pmin = simon.parts.front().p_min;
|
|
pmax = simon.parts.back().p_max;
|
|
Tmin = simon.parts.front().T_min;
|
|
Tmax = simon.parts.back().T_max;
|
|
}
|
|
else if (type == MELTING_LINE_POLYNOMIAL_IN_TR_TYPE){
|
|
// Fill in the min and max pressures for each part
|
|
for (std::size_t i = 0; i < polynomial_in_Tr.parts.size(); ++i){
|
|
MeltingLinePiecewisePolynomialInTrSegment &part = polynomial_in_Tr.parts[i];
|
|
part.p_min = part.evaluate(part.T_min);
|
|
part.p_max = part.evaluate(part.T_max);
|
|
}
|
|
Tmin = polynomial_in_Tr.parts.front().T_min;
|
|
pmin = polynomial_in_Tr.parts.front().p_min;
|
|
Tmax = polynomial_in_Tr.parts.back().T_max;
|
|
pmax = polynomial_in_Tr.parts.back().p_max;
|
|
}
|
|
else if (type == MELTING_LINE_POLYNOMIAL_IN_THETA_TYPE){
|
|
// Fill in the min and max pressures for each part
|
|
for (std::size_t i = 0; i < polynomial_in_Theta.parts.size(); ++i){
|
|
MeltingLinePiecewisePolynomialInThetaSegment &part = polynomial_in_Theta.parts[i];
|
|
part.p_min = part.evaluate(part.T_min);
|
|
part.p_max = part.evaluate(part.T_max);
|
|
}
|
|
Tmin = polynomial_in_Theta.parts.front().T_min;
|
|
pmin = polynomial_in_Theta.parts.front().p_min;
|
|
Tmax = polynomial_in_Theta.parts.back().T_max;
|
|
pmax = polynomial_in_Theta.parts.back().p_max;
|
|
}
|
|
else{
|
|
throw ValueError("only Simon supported now");
|
|
}
|
|
}
|
|
|
|
long double MeltingLineVariables::evaluate(int OF, int GIVEN, long double value)
|
|
{
|
|
if (type == MELTING_LINE_NOT_SET){throw ValueError("Melting line curve not set");}
|
|
if (OF == iP_max){ return pmax;}
|
|
else if (OF == iP_min){ return pmin;}
|
|
else if (OF == iT_max){ return Tmax;}
|
|
else if (OF == iT_min){ return Tmin;}
|
|
else if (OF == iP && GIVEN == iT){
|
|
long double T = value;
|
|
if (type == MELTING_LINE_SIMON_TYPE){
|
|
// Need to find the right segment
|
|
for (std::size_t i = 0; i < simon.parts.size(); ++i){
|
|
MeltingLinePiecewiseSimonSegment &part = simon.parts[i];
|
|
if (is_in_closed_range(part.T_min, part.T_max, T)){
|
|
return part.p_0 + part.a*(pow(T/part.T_0,part.c)-1);
|
|
}
|
|
}
|
|
throw ValueError("unable to calculate melting line (p,T) for Simon curve");
|
|
}
|
|
else if (type == MELTING_LINE_POLYNOMIAL_IN_TR_TYPE){
|
|
// Need to find the right segment
|
|
for (std::size_t i = 0; i < polynomial_in_Tr.parts.size(); ++i){
|
|
MeltingLinePiecewisePolynomialInTrSegment &part = polynomial_in_Tr.parts[i];
|
|
if (is_in_closed_range(part.T_min, part.T_max, T)){
|
|
return part.evaluate(T);
|
|
}
|
|
}
|
|
throw ValueError("unable to calculate melting line (p,T) for polynomial_in_Tr curve");
|
|
}
|
|
else if (type == MELTING_LINE_POLYNOMIAL_IN_THETA_TYPE){
|
|
// Need to find the right segment
|
|
for (std::size_t i = 0; i < polynomial_in_Theta.parts.size(); ++i){
|
|
MeltingLinePiecewisePolynomialInThetaSegment &part = polynomial_in_Theta.parts[i];
|
|
if (is_in_closed_range(part.T_min, part.T_max, T)){
|
|
return part.evaluate(T);
|
|
}
|
|
}
|
|
throw ValueError("unable to calculate melting line (p,T) for polynomial_in_Theta curve");
|
|
}
|
|
else{
|
|
throw ValueError(format("Invalid melting line type [%d]",type));
|
|
}
|
|
}
|
|
else{
|
|
if (type == MELTING_LINE_SIMON_TYPE){
|
|
// Need to find the right segment
|
|
for (std::size_t i = 0; i < simon.parts.size(); ++i){
|
|
MeltingLinePiecewiseSimonSegment &part = simon.parts[i];
|
|
// p = part.p_0 + part.a*(pow(T/part.T_0,part.c)-1);
|
|
long double T = pow((value-part.p_0)/part.a+1,1/part.c)*part.T_0;
|
|
if (T >= part.T_0 && T <= part.T_max){
|
|
return T;
|
|
}
|
|
}
|
|
throw ValueError(format("unable to calculate melting line T(p) for Simon curve for p=%Lg; bounds are %Lg,%Lg Pa", value, pmin, pmax));
|
|
}
|
|
else if (type == MELTING_LINE_POLYNOMIAL_IN_TR_TYPE)
|
|
{
|
|
class solver_resid : public FuncWrapper1D
|
|
{
|
|
public:
|
|
MeltingLinePiecewisePolynomialInTrSegment *part;
|
|
long double r, given_p, calc_p, T;
|
|
solver_resid(MeltingLinePiecewisePolynomialInTrSegment *part, long double p) : part(part), given_p(p){};
|
|
double call(double T){
|
|
|
|
this->T = T;
|
|
|
|
calc_p = part->evaluate(T);
|
|
|
|
// Difference between the two is to be driven to zero
|
|
r = given_p - calc_p;
|
|
|
|
return r;
|
|
};
|
|
};
|
|
|
|
// Need to find the right segment
|
|
for (std::size_t i = 0; i < polynomial_in_Tr.parts.size(); ++i){
|
|
MeltingLinePiecewisePolynomialInTrSegment &part = polynomial_in_Tr.parts[i];
|
|
if (is_in_closed_range(part.p_min, part.p_max, value)){
|
|
std::string errstr;
|
|
solver_resid resid(&part, value);
|
|
double T = Brent(resid, part.T_min, part.T_max, DBL_EPSILON, 1e-12, 100, errstr);
|
|
return T;
|
|
}
|
|
}
|
|
throw ValueError(format("unable to calculate melting line T(p) for polynomial_in_Theta curve for p=%Lg; bounds are %Lg,%Lg Pa", value, pmin, pmax));
|
|
}
|
|
else if (type == MELTING_LINE_POLYNOMIAL_IN_THETA_TYPE)
|
|
{
|
|
|
|
class solver_resid : public FuncWrapper1D
|
|
{
|
|
public:
|
|
MeltingLinePiecewisePolynomialInThetaSegment *part;
|
|
long double r, given_p, calc_p, T;
|
|
solver_resid(MeltingLinePiecewisePolynomialInThetaSegment *part, long double p) : part(part), given_p(p){};
|
|
double call(double T){
|
|
|
|
this->T = T;
|
|
|
|
calc_p = part->evaluate(T);
|
|
|
|
// Difference between the two is to be driven to zero
|
|
r = given_p - calc_p;
|
|
|
|
return r;
|
|
};
|
|
};
|
|
|
|
// Need to find the right segment
|
|
for (std::size_t i = 0; i < polynomial_in_Theta.parts.size(); ++i){
|
|
MeltingLinePiecewisePolynomialInThetaSegment &part = polynomial_in_Theta.parts[i];
|
|
if (is_in_closed_range(part.p_min, part.p_max, value)){
|
|
std::string errstr;
|
|
solver_resid resid(&part, value);
|
|
double T = Brent(resid, part.T_min, part.T_max, DBL_EPSILON, 1e-12, 100, errstr);
|
|
return T;
|
|
}
|
|
}
|
|
|
|
throw ValueError(format("unable to calculate melting line T(p) for polynomial_in_Theta curve for p=%Lg; bounds are %Lg,%Lg Pa", value, pmin, pmax));
|
|
}
|
|
else{
|
|
throw ValueError(format("Invalid melting line type T(p) [%d]",type));
|
|
}
|
|
}
|
|
}
|
|
|
|
}; /* namespace CoolProp */
|
|
|
|
#if defined(ENABLE_CATCH)
|
|
TEST_CASE("Water melting line", "[melting]")
|
|
{
|
|
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS","water"));
|
|
int iT = CoolProp::iT, iP = CoolProp::iP;
|
|
SECTION("Ice Ih-liquid")
|
|
{
|
|
double actual = AS->melting_line(iT, iP, 138.268e6);
|
|
double expected = 260.0;
|
|
CAPTURE(actual);
|
|
CAPTURE(expected);
|
|
CHECK(std::abs(actual-expected) < 0.01);
|
|
}
|
|
SECTION("Ice III-liquid")
|
|
{
|
|
double actual = AS->melting_line(iT, iP, 268.685e6);
|
|
double expected = 254;
|
|
CAPTURE(actual);
|
|
CAPTURE(expected);
|
|
CHECK(std::abs(actual-expected) < 0.01);
|
|
}
|
|
SECTION("Ice V-liquid")
|
|
{
|
|
double actual = AS->melting_line(iT, iP, 479.640e6);
|
|
double expected = 265;
|
|
CAPTURE(actual);
|
|
CAPTURE(expected);
|
|
CHECK(std::abs(actual-expected) < 0.01);
|
|
}
|
|
SECTION("Ice VI-liquid")
|
|
{
|
|
double actual = AS->melting_line(iT, iP, 1356.76e6);
|
|
double expected = 320;
|
|
CAPTURE(actual);
|
|
CAPTURE(expected);
|
|
CHECK(std::abs(actual-expected) < 1);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Tests for values from melting lines", "[melting]")
|
|
{
|
|
int iT = CoolProp::iT, iP = CoolProp::iP;
|
|
std::vector<std::string> fluids = strsplit(CoolProp::get_global_param_string("fluids_list"),',');
|
|
for (std::size_t i = 0; i < fluids.size(); ++i) {
|
|
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS",fluids[i]));
|
|
|
|
// Water has its own better tests; skip fluids without melting line
|
|
if (!AS->has_melting_line() || !fluids[i].compare("Water")){continue;}
|
|
|
|
double pmax = AS->melting_line(CoolProp::iP_max, iT, 0);
|
|
double pmin = AS->melting_line(CoolProp::iP_min, iT, 0);
|
|
double Tmax = AS->melting_line(CoolProp::iT_max, iT, 0);
|
|
double Tmin = AS->melting_line(CoolProp::iT_min, iT, 0);
|
|
|
|
// See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
|
|
std::ostringstream ss0;
|
|
ss0 << "Check melting line limits for fluid " << fluids[i];
|
|
SECTION(ss0.str(),"")
|
|
{
|
|
CAPTURE(Tmin);
|
|
CAPTURE(Tmax);
|
|
CAPTURE(pmin);
|
|
CAPTURE(pmax);
|
|
CHECK(Tmax > Tmin);
|
|
CHECK(pmax > pmin);
|
|
CHECK(pmin > 0);
|
|
}
|
|
for (double p = 0.1*(pmax-pmin) + pmin; p < pmax; p += 0.2*(pmax-pmin)){
|
|
// See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
|
|
std::ostringstream ss1;
|
|
ss1 << "Melting line for " << fluids[i] << " at p=" << p;
|
|
SECTION(ss1.str(),"")
|
|
{
|
|
double actual_T = AS->melting_line(iT, iP, p);
|
|
CAPTURE(Tmin);
|
|
CAPTURE(Tmax);
|
|
CAPTURE(actual_T);
|
|
CHECK(actual_T > Tmin);
|
|
CHECK(actual_T < Tmax);
|
|
}
|
|
}
|
|
// See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
|
|
std::ostringstream ss2;
|
|
ss2 << "Ensure melting line valid for " << fluids[i] << " @ EOS pmax";
|
|
SECTION(ss2.str(),"")
|
|
{
|
|
double actual_T;
|
|
double EOS_pmax = AS->pmax();
|
|
CAPTURE(EOS_pmax);
|
|
CHECK_NOTHROW(actual_T = AS->melting_line(iT, iP, EOS_pmax));
|
|
CAPTURE(actual_T);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Test that hs_anchor enthalpy/entropy agrees with EOS", "[ancillaries]")
|
|
{
|
|
std::vector<std::string> fluids = strsplit(CoolProp::get_global_param_string("fluids_list"),',');
|
|
for (std::size_t i = 0; i < fluids.size(); ++i)
|
|
{
|
|
shared_ptr<CoolProp::AbstractState> AS(CoolProp::AbstractState::factory("HEOS",fluids[i]));
|
|
|
|
CoolProp::SimpleState hs_anchor = AS->get_state("hs_anchor");
|
|
|
|
// See https://groups.google.com/forum/?fromgroups#!topic/catch-forum/mRBKqtTrITU
|
|
std::ostringstream ss1;
|
|
ss1 << "Check hs_anchor for " << fluids[i];
|
|
SECTION(ss1.str(),"")
|
|
{
|
|
std::string note = "The enthalpy and entropy are hardcoded in the fluid JSON files. They MUST agree with the values calculated by the EOS";
|
|
AS->update(CoolProp::DmolarT_INPUTS, hs_anchor.rhomolar, hs_anchor.T);
|
|
CAPTURE(hs_anchor.hmolar);
|
|
CAPTURE(hs_anchor.smolar);
|
|
double EOS_hmolar = AS->hmolar();
|
|
double EOS_smolar = AS->smolar();
|
|
CHECK( std::abs(EOS_hmolar - hs_anchor.hmolar) < 1e-3);
|
|
CHECK( std::abs(EOS_smolar - hs_anchor.smolar) < 1e-3);
|
|
}
|
|
}
|
|
}
|
|
#endif |