#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 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 fluids = strsplit(CoolProp::get_global_param_string("fluids_list"),','); for (std::size_t i = 0; i < fluids.size(); ++i) { shared_ptr 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 fluids = strsplit(CoolProp::get_global_param_string("fluids_list"),','); for (std::size_t i = 0; i < fluids.size(); ++i) { shared_ptr 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