diff --git a/include/teqp/algorithms/VLE.hpp b/include/teqp/algorithms/VLE.hpp index de62203304207df2c941006b6f21dc3eb458ec06..3a17d69c6e90fc2ba4f71db5ddabd146c651442f 100644 --- a/include/teqp/algorithms/VLE.hpp +++ b/include/teqp/algorithms/VLE.hpp @@ -168,6 +168,36 @@ auto pure_VLE_T(const Model& model, Scalar T, Scalar rhoL, Scalar rhoV, int maxi return do_pure_VLE_T(res, rhoL, rhoV, maxiter); } +/*** + * \brief Calculate the derivative of vapor pressure with respect to temperature + * \param model The model to operate on + * \param T Temperature + * \param rhoL Liquid density + * \param rhoV Vapor density + * + * Based upon + * \f[ + * \frac{dp_{\sigma}}{dT} = \frac{h''-h'}{T(v''-v')} = \frac{s''-s'}{v''-v'} + * \f] + * where the \f$h''-h'\f$ is given by the difference in residual enthalpy \f$h''-h' = h^r''-h^r'\f$ because the ideal-gas parts cancel + */ +template<typename Model, typename Scalar, ADBackends backend = ADBackends::autodiff> +auto dpsatdT_pure(const Model& model, Scalar T, Scalar rhoL, Scalar rhoV) { + + auto molefrac = (Eigen::ArrayXd(1) << 1.0).finished(); + + using tdx = TDXDerivatives<decltype(model), double, decltype(molefrac)>; + using iso = IsochoricDerivatives<decltype(model), double, decltype(molefrac)>; + + auto R = model.R(molefrac); + + auto hrVLERTV = tdx::get_Ar01(model, T, rhoV, molefrac) + tdx::get_Ar10(model, T, rhoV, molefrac); + auto hrVLERTL = tdx::get_Ar01(model, T, rhoL, molefrac) + tdx::get_Ar10(model, T, rhoL, molefrac); + auto deltahr_over_T = R*(hrVLERTV-hrVLERTL); + auto dpsatdT = deltahr_over_T/(1/rhoV-1/rhoL); // From Clausius-Clapeyron; dp/dT = Deltas/Deltav = Deltah/(T*Deltav); Delta=V-L + return dpsatdT; +} + /*** * \brief Do a vapor-liquid phase equilibrium problem for a mixture (binary only for now) with mole fractions specified in the liquid phase * \param model The model to operate on diff --git a/include/teqp/cpp/teqpcpp.hpp b/include/teqp/cpp/teqpcpp.hpp index ca0cac427e7da0ba4e6ba6c395697dac3bf4388d..b1981235274304f7613177a8c260c7a99061499e 100644 --- a/include/teqp/cpp/teqpcpp.hpp +++ b/include/teqp/cpp/teqpcpp.hpp @@ -125,6 +125,7 @@ namespace teqp { virtual std::tuple<double, double> extrapolate_from_critical(const double Tc, const double rhoc, const double Tgiven) const = 0; virtual std::tuple<EArrayd, EMatrixd> get_pure_critical_conditions_Jacobian(const double T, const double rho, int alternative_pure_index=-1, int alternative_length=2) const = 0; virtual EArray2 pure_VLE_T(const double T, const double rhoL, const double rhoV, int maxiter) const = 0; + virtual double dpsatdT_pure(const double T, const double rhoL, const double rhoV) const = 0; virtual std::tuple<EArrayd, EArrayd> get_drhovecdp_Tsat(const double T, const REArrayd& rhovecL, const REArrayd& rhovecV) const = 0; virtual std::tuple<EArrayd, EArrayd> get_drhovecdT_psat(const double T, const REArrayd& rhovecL, const REArrayd& rhovecV) const = 0; diff --git a/interface/CPP/teqp_impl_VLE.cpp b/interface/CPP/teqp_impl_VLE.cpp index 017d76127ddab417e610097bb497f70e98ec03be..91be45db837653c24104a13b6c0af5adf376606e 100644 --- a/interface/CPP/teqp_impl_VLE.cpp +++ b/interface/CPP/teqp_impl_VLE.cpp @@ -50,3 +50,9 @@ EArray2 MI::pure_VLE_T(const double T, const double rhoL, const double rhoV, int return teqp::pure_VLE_T(model, T, rhoL, rhoV, maxiter); }, m_model); } + +double MI::dpsatdT_pure(const double T, const double rhoL, const double rhoV) const { + return std::visit([&](const auto& model) { + return teqp::dpsatdT_pure(model, T, rhoL, rhoV); + }, m_model); +} diff --git a/interface/CPP/teqpcpp.cpp b/interface/CPP/teqpcpp.cpp index d23eaa9ef9a7d5c9d21fc53111c78d489c8db34a..11e268b7a293a6fa7b5f7846f9518beab035baaa 100644 --- a/interface/CPP/teqpcpp.cpp +++ b/interface/CPP/teqpcpp.cpp @@ -84,6 +84,7 @@ namespace teqp { double get_neff(const double T, const double rho, const EArrayd& molefracs) const override; EArray2 pure_VLE_T(const double T, const double rhoL, const double rhoV, int maxiter) const override; + double dpsatdT_pure(const double T, const double rhoL, const double rhoV) const override; std::tuple<EArrayd, EArrayd> get_drhovecdp_Tsat(const double T, const REArrayd& rhovecL, const REArrayd& rhovecV) const override; std::tuple<EArrayd, EArrayd> get_drhovecdT_psat(const double T, const REArrayd& rhovecL, const REArrayd& rhovecV) const override; double get_dpsat_dTsat_isopleth(const double T, const REArrayd& rhovecL, const REArrayd& rhovecV) const override; diff --git a/interface/pybind11_wrapper.cpp b/interface/pybind11_wrapper.cpp index 6054024223273dfd9aaf907a1a61486a5a4649fc..88ac1f5fc8332dfb1e0695f0bbf8637631522e62 100644 --- a/interface/pybind11_wrapper.cpp +++ b/interface/pybind11_wrapper.cpp @@ -347,6 +347,7 @@ void init_teqp(py::module& m) { .def("get_dp_dT_crit", &am::get_dp_dT_crit, "T"_a, "rhovec"_a.noconvert()) .def("pure_VLE_T", &am::pure_VLE_T, "T"_a, "rhoL"_a, "rhoV"_a, "max_iter"_a) + .def("dpsatdT_pure", &am::dpsatdT_pure, "T"_a, "rhoL"_a, "rhoV"_a) .def("get_drhovecdp_Tsat", &am::get_drhovecdp_Tsat, "T"_a, "rhovecL"_a.noconvert(), "rhovecV"_a.noconvert()) .def("get_drhovecdT_psat", &am::get_drhovecdT_psat, "T"_a, "rhovecL"_a.noconvert(), "rhovecV"_a.noconvert()) diff --git a/src/tests/catch_test_cubics.cxx b/src/tests/catch_test_cubics.cxx index 6141d2745b1b309d6f5d53e663b5bda0b136d9a4..531cca9dd3c9e1299db15f72fd595fb6c78a4b4a 100644 --- a/src/tests/catch_test_cubics.cxx +++ b/src/tests/catch_test_cubics.cxx @@ -80,6 +80,46 @@ TEST_CASE("Check calling superancillary curves", "[cubic][superanc]") } } +TEST_CASE("Check orthobaric density derivatives for pure fluid", "[cubic][superanc]") +{ + std::valarray<double> Tc_K = { 150.687 }; + std::valarray<double> pc_Pa = { 4863000.0}; + std::valarray<double> acentric = { 0.0}; + + double T = 130.0, dT = 0.001; + auto molefrac = (Eigen::ArrayXd(1) << 1.0).finished(); + + auto model = canonical_PR(Tc_K, pc_Pa, acentric); + using tdx = TDXDerivatives<decltype(model)>; + using iso = IsochoricDerivatives<decltype(model)>; + + auto R = model.R(molefrac); + auto [rhoL, rhoV] = model.superanc_rhoLV(T); + CHECK(rhoL > rhoV); + + // Finite difference test + auto [rhoLp, rhoVp] = model.superanc_rhoLV(T+dT); + auto [rhoLm, rhoVm] = model.superanc_rhoLV(T-dT); + auto pLp = rhoLp*R*(T+dT) + iso::get_pr(model, T+dT, rhoLp*molefrac); + auto pLm = rhoLm*R*(T-dT) + iso::get_pr(model, T-dT, rhoLm*molefrac); + + // Exact solution for density derivative + // Change in enthalpy (Deltah) is equal to change in residual enthalpy (Deltahr) because ideal parts cancel + auto hrVLERTV = tdx::get_Ar01(model, T, rhoV, molefrac) + tdx::get_Ar10(model, T, rhoV, molefrac); + auto hrVLERTL = tdx::get_Ar01(model, T, rhoL, molefrac) + tdx::get_Ar10(model, T, rhoL, molefrac); + auto deltahr_over_T = R*(hrVLERTV-hrVLERTL); + auto dpsatdT = deltahr_over_T/(1/rhoV-1/rhoL); // From Clausius-Clapeyron; dp/dT = Deltas/Deltav = Deltah/(T*Deltav); Delta=V-L + + auto dpsatdT_routine = dpsatdT_pure(model, T, rhoL, rhoV); + + CHECK(dpsatdT == Approx((pLp - pLm)/(2*dT))); + CHECK(dpsatdT_routine == Approx((pLp - pLm)/(2*dT))); + +// CHECK(drhovecdTL(0) == Approx((rhoLp-rhoLm)/(2*dT))); +// auto drhoLdT = + +} + TEST_CASE("Check manual integration of subcritical VLE isotherm for binary mixture", "[cubic][isochoric][traceisotherm]") { using namespace boost::numeric::odeint; @@ -389,4 +429,4 @@ TEST_CASE("Check manual integration of subcritical VLE isobar for binary mixture std::ofstream file("isoP.json"); file << J; } -} \ No newline at end of file +}