diff --git a/include/teqp/algorithms/VLE.hpp b/include/teqp/algorithms/VLE.hpp index 226d21b667834b4acdeb28fd5b326e98f5142dd5..477d7f946ec7ec72b1933e8a42158ea9876125cb 100644 --- a/include/teqp/algorithms/VLE.hpp +++ b/include/teqp/algorithms/VLE.hpp @@ -265,6 +265,137 @@ auto mix_VLE_Tx(const Model& model, Scalar T, const Vector& rhovecL0, const Vect return std::make_tuple(return_code, rhovecLfinal, rhovecVfinal); } +struct MixVLETPFlags { + double atol = 1e-10, + reltol = 1e-10, + axtol = 1e-10, + relxtol = 1e-10; + int maxiter = 10; +}; + +/*** +* \brief Do a vapor-liquid phase equilibrium problem for a mixture (binary only for now) with temperature and pressure specified +* +* The mole concentrations are solved for to give the right pressure +* +* \param model The model to operate on +* \param T Temperature +* \param pgiven Given pressure +* \param rhovecL0 Initial values for liquid mole concentrations +* \param rhovecV0 Initial values for vapor mole concentrations +* \param flags Flags controlling the iteration and stopping conditions +*/ +template<typename Model, typename Scalar, typename Vector> +auto mix_VLE_TP(const Model& model, Scalar T, Scalar pgiven, const Vector& rhovecL0, const Vector& rhovecV0, const MixVLETPFlags& flags = {}) { + + const Eigen::Index N = rhovecL0.size(); + auto lengths = (Eigen::ArrayXi(2) << rhovecL0.size(), rhovecV0.size()).finished(); + if (lengths.minCoeff() != lengths.maxCoeff()) { + throw InvalidArgument("lengths of rhovecs must be the same in mix_VLE_Tx"); + } + Eigen::MatrixXd J(2 * N, 2 * N), r(2 * N, 1), x(2 * N, 1); + x.col(0).array().head(N) = rhovecL0; + x.col(0).array().tail(N) = rhovecV0; + using isochoric = IsochoricDerivatives<Model, Scalar, Vector>; + + Eigen::Map<Eigen::ArrayXd> rhovecL(&(x(0)), N); + Eigen::Map<Eigen::ArrayXd> rhovecV(&(x(0 + N)), N); + + VLE_return_code return_code = VLE_return_code::unset; + std::string message = ""; + + for (int iter = 0; iter < flags.maxiter; ++iter) { + + auto RT = model.R((rhovecL / rhovecL.sum()).eval())*T; + + auto [PsirL, PsirgradL, hessianL] = isochoric::build_Psir_fgradHessian_autodiff(model, T, rhovecL); + auto [PsirV, PsirgradV, hessianV] = isochoric::build_Psir_fgradHessian_autodiff(model, T, rhovecV); + auto rhoL = rhovecL.sum(); + auto rhoV = rhovecV.sum(); + Scalar pL = rhoL * RT - PsirL + (rhovecL.array() * PsirgradL.array()).sum(); // The (array*array).sum is a dot product + Scalar pV = rhoV * RT - PsirV + (rhovecV.array() * PsirgradV.array()).sum(); + auto dpdrhovecL = RT + (hessianL * rhovecL.matrix()).array(); + auto dpdrhovecV = RT + (hessianV * rhovecV.matrix()).array(); + + bool index0nonzero = rhovecL(0) > 0 && rhovecV(0) > 0; + bool index1nonzero = rhovecL(1) > 0 && rhovecV(1) > 0; + + if (index0nonzero) { + r(0) = PsirgradL(0) + RT * log(rhovecL(0)) - (PsirgradV(0) + RT * log(rhovecV(0))); + } + else { + r(0) = PsirgradL(0) - PsirgradV(0); + } + if (index1nonzero) { + r(1) = PsirgradL(1) + RT * log(rhovecL(1)) - (PsirgradV(1) + RT * log(rhovecV(1))); + } + else { + r(1) = PsirgradL(1) - PsirgradV(1); + } + r(2) = pL - pV; + r(3) = (pL - pgiven)/pgiven; + + // Chemical potential contributions in Jacobian + J(0, 0) = hessianL(0, 0) + (index0nonzero ? RT / rhovecL(0) : 0); + J(0, 1) = hessianL(0, 1); + J(1, 0) = hessianL(1, 0); // symmetric, so same as above + J(1, 1) = hessianL(1, 1) + (index1nonzero ? RT / rhovecL(1) : 0); + J(0, 2) = -(hessianV(0, 0) + (index0nonzero ? RT / rhovecV(0) : 0)); + J(0, 3) = -(hessianV(0, 1)); + J(1, 2) = -(hessianV(1, 0)); // symmetric, so same as above + J(1, 3) = -(hessianV(1, 1) + (index1nonzero ? RT / rhovecV(1) : 0)); + // Pressure contributions in Jacobian + J(2, 0) = dpdrhovecL(0); + J(2, 1) = dpdrhovecL(1); + J(2, 2) = -dpdrhovecV(0); + J(2, 3) = -dpdrhovecV(1); + // Mole fraction composition specification in Jacobian + J.row(3).array() = 0.0; + J(3, 0) = dpdrhovecL(0)/pgiven; + J(3, 1) = dpdrhovecL(1)/pgiven; + + // Solve for the step + Eigen::ArrayXd dx = J.colPivHouseholderQr().solve(-r); + + x.array() += dx; + + if ((!dx.isFinite()).all()) { + return_code = VLE_return_code::notfinite_step; + message = "Not finite step"; + break; + } + + auto xtol_threshold = (flags.axtol + flags.relxtol * x.array().cwiseAbs()).eval(); + if ((dx.array().cwiseAbs() < xtol_threshold).all()) { + return_code = VLE_return_code::xtol_satisfied; + message = "X tolerance satisfied"; + break; + } + + auto error_threshold = (flags.atol + flags.reltol * r.array().cwiseAbs()).eval(); + if ((r.array().cwiseAbs() < error_threshold).all()) { + return_code = VLE_return_code::functol_satisfied; + message = "func tolerance satisfied"; + break; + } + + // If the solution has stopped improving, stop. The change in x is equal to dx in infinite precision, but + // not when finite precision is involved, use the minimum non-denormal float as the determination of whether + // the values are done changing + if (((x.array() - dx.array()).cwiseAbs() < std::numeric_limits<Scalar>::min()).all()) { + return_code = VLE_return_code::xtol_satisfied; + message = "solution is not longer improving"; + break; + } + if (iter == flags.maxiter - 1) { + return_code = VLE_return_code::maxiter_met; + message = "max iterations met"; + } + } + Eigen::ArrayXd rhovecLfinal = rhovecL, rhovecVfinal = rhovecV; + return std::make_tuple(return_code, message, rhovecLfinal, rhovecVfinal); +} + struct MixVLEPxFlags { double atol = 1e-10, reltol = 1e-10, diff --git a/interface/pybind11_wrapper.hpp b/interface/pybind11_wrapper.hpp index b8abd82496906f5c4c40c5f7a484e17e5194be2e..d0ba4f9a639701fdcb74414695611a63265df924 100644 --- a/interface/pybind11_wrapper.hpp +++ b/interface/pybind11_wrapper.hpp @@ -79,6 +79,7 @@ void add_derivatives(py::module &m, Wrapper &cls) { cls.def("get_pure_critical_conditions_Jacobian", &get_pure_critical_conditions_Jacobian<Model, double, ADBackends::autodiff>, py::arg("T"), py::arg("rho"), py::arg_v("alternative_pure_index", -1), py::arg_v("alternative_length", 2)); cls.def("solve_pure_critical", &solve_pure_critical<Model, double, ADBackends::autodiff>, py::arg("T"), py::arg("rho"), py::arg_v("flags", std::nullopt, "None")); cls.def("mix_VLE_Tx", &mix_VLE_Tx<Model, double, Eigen::ArrayXd>); + cls.def("mix_VLE_TP", &mix_VLE_TP<Model, double, Eigen::ArrayXd>); cls.def("mixture_VLE_px", &mixture_VLE_px<Model, double, Eigen::ArrayXd>, py::arg("p_spec"), py::arg("xmolar_spec").noconvert(), py::arg("T0"), py::arg("rhovecL0").noconvert(), py::arg("rhovecV0").noconvert(), py::arg_v("flags", std::nullopt, "None")); cls.def("get_drhovecdp_Tsat", &get_drhovecdp_Tsat<Model, double, RAX>, py::arg("T"), py::arg("rhovecL").noconvert(), py::arg("rhovecV").noconvert()); diff --git a/src/tests/catch_test_cubics.cxx b/src/tests/catch_test_cubics.cxx index fba99ed5969e146bd60bd466c78ac619e2c2503d..d4e616e99a4a0e201275e23e8b41190cf7dad3c7 100644 --- a/src/tests/catch_test_cubics.cxx +++ b/src/tests/catch_test_cubics.cxx @@ -155,6 +155,11 @@ TEST_CASE("Check manual integration of subcritical VLE isotherm for binary mixtu auto rhovecV = Eigen::Map<const Eigen::ArrayXd>(&(X0[0 + N]), N).eval(); auto x = (Eigen::ArrayXd(2) << rhovecL(0) / rhovecL.sum(), rhovecL(1) / rhovecL.sum()).finished(); auto [return_code, rhoL, rhoV] = mix_VLE_Tx(model, T, rhovecL, rhovecV, x, 1e-10, 1e-8, 1e-10, 1e-8, 10); + + // And the other way around just to test the routine for TP solving + auto [return_code2, msg, rhoL_, rhoV_] = mix_VLE_TP(model, T, p, rhovecL, rhovecV); + int rr = 0; + } }