diff --git a/include/teqp/algorithms/critical_tracing.hpp b/include/teqp/algorithms/critical_tracing.hpp index abc81c28ef81d541138e741800367618132107ad..5ee60dd17e58ab562f01643ae040810041214707 100644 --- a/include/teqp/algorithms/critical_tracing.hpp +++ b/include/teqp/algorithms/critical_tracing.hpp @@ -20,10 +20,13 @@ struct TCABOptions { rel_err = 1.0e-6, init_dt = 10, ///< The initial step size max_dt = 10000000000, - T_tol = 1e-6; ///< The tolerance on temperature to indicate that it is converged + T_tol = 1e-6, ///< The tolerance on temperature to indicate that it is converged + init_c = 1.0; ///< The c parameter which controls the initial search direction for the first step. Choices are 1 or -1 int small_T_count = 5; ///< How many small temperature steps indicates convergence - int integration_order = 5; ///< - int max_step_count = 1000; + int integration_order = 5; ///< The order of integration, either 1 for simple Euler or 5 for adaptive RK45 + int max_step_count = 1000; ///< Maximum number of steps allowed + int skip_dircheck_count = 1; ///< Only start checking the direction dot product after this many steps + bool polish = false; ///< If true, polish the solution at every step }; template<typename Model, typename Scalar = double, typename VecType = Eigen::ArrayXd> @@ -290,25 +293,26 @@ struct CriticalTracing { return (Eigen::ArrayXd(2) << derivs.tot[2], derivs.tot[3]).finished(); } + /// Polish a critical point while keeping the overall composition constant and iterating for temperature and overall density static auto critical_polish_molefrac(const Model& model, const Scalar T, const VecType& rhovec, const Scalar z0) { auto polish_x_resid = [&model, &z0](const auto& x) { auto T = x[0]; - Eigen::ArrayXd rhovec(2); rhovec << x[1], x[2]; + Eigen::ArrayXd rhovec(2); rhovec << z0*x[1], (1-z0)*x[1]; auto z0new = rhovec[0] / rhovec.sum(); auto derivs = get_derivs(model, T, rhovec); - // First two are residuals on critical point, third is residual on composition - return (Eigen::ArrayXd(3) << derivs.tot[2], derivs.tot[3], z0new - z0).finished(); + // First two are residuals on critical point + return (Eigen::ArrayXd(2) << derivs.tot[2], derivs.tot[3]).finished(); }; - Eigen::ArrayXd x0(3); x0 << T, rhovec[0], rhovec[1]; + Eigen::ArrayXd x0(2); x0 << T, rhovec[0]+rhovec[1]; auto r0 = polish_x_resid(x0); auto x = NewtonRaphson(polish_x_resid, x0, 1e-10); auto r = polish_x_resid(x); Eigen::ArrayXd change = x0 - x; - if (!std::isfinite(T) || !std::isfinite(x[1]) || !std::isfinite(x[2])) { + if (!std::isfinite(x[0]) || !std::isfinite(x[1])) { throw std::invalid_argument("Something not finite; aborting polishing"); } - Eigen::ArrayXd rho = x.tail(x.size() - 1); - return std::make_tuple(x[0], rho); + Eigen::ArrayXd rhovecsoln(2); rhovecsoln << x[1]*z0, x[1] * (1 - z0); + return std::make_tuple(x[0], rhovecsoln); } static auto critical_polish_fixedrho(const Model& model, const Scalar T, const VecType& rhovec, const int i) { Scalar rhoval = rhovec[i]; @@ -353,18 +357,24 @@ struct CriticalTracing { VecType last_drhodt; + // Typedefs for the types for odeint for simple Euler and RK45 integrators + using state_type = std::vector<double>; + using namespace boost::numeric::odeint; + euler<state_type> eul; // Class for simple Euler integration + typedef runge_kutta_cash_karp54< state_type > error_stepper_type; + typedef controlled_runge_kutta< error_stepper_type > controlled_stepper_type; + auto dot = [](const auto& v1, const auto& v2) { return (v1 * v2).sum(); }; auto norm = [](const auto& v) { return sqrt((v * v).sum()); }; auto JSONdata = nlohmann::json::array(); std::ofstream ofs = (filename.empty()) ? std::ofstream() : std::ofstream(filename); - - using state_type = std::vector<double>; - double c = 1.0; + + double c = options.init_c; // The function for the derivative in the form of odeint // x is [T, rhovec] - auto xprime = [&model, &c, norm](const state_type& x, state_type& dxdt, const double /* t */) + auto xprime = [&](const state_type& x, state_type& dxdt, const double /* t */) { // Unpack the inputs const double T = x[0]; @@ -372,19 +382,29 @@ struct CriticalTracing { auto drhodT = get_drhovec_dT_crit(model, T, rhovec).array().eval(); auto dTdt = 1.0 / norm(drhodT); - dxdt[0] = c*dTdt; - auto drhodt = Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]) + 1, dxdt.size() - 1); - drhodt = c*(drhodT * dTdt).eval(); - }; + Eigen::ArrayXd drhodt = c * (drhodT * dTdt).eval(); - // Typedefs for the types - using namespace boost::numeric::odeint; + dxdt[0] = c*dTdt; - // Class for simple Euler integration - euler<state_type> eul; + auto drhodtview = Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]) + 1, dxdt.size() - 1); + drhodtview = drhodt; // Copy values into the view - typedef runge_kutta_cash_karp54< state_type > error_stepper_type; - typedef controlled_runge_kutta< error_stepper_type > controlled_stepper_type; + if (last_drhodt.size() > 0) { + auto rhodot = dot(drhodt, last_drhodt); + if (rhodot < 0) { + Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]), dxdt.size()) *= -1; + } + } + }; + auto get_dxdt = [&](const state_type& x) { + auto dxdt = state_type(x.size()); + xprime(x, dxdt, -1); + return dxdt; + }; + // Pull out the drhodt from dxdt, just a convenience function + auto extract_drhodt = [](const state_type& dxdt) -> Eigen::ArrayXd { + return Eigen::Map<const Eigen::ArrayXd>(&(dxdt[0]) + 1, dxdt.size() - 1); + }; // Define the tolerances double abs_err = options.abs_err, rel_err = options.rel_err, a_x = 1.0, a_dxdt = 1.0; @@ -392,102 +412,92 @@ struct CriticalTracing { double t = 0, dt = options.init_dt; - // Build the initial state arrary, T, followed by rhovec + // Build the initial state array, with T followed by rhovec std::vector<double> x0(rhovec0.size() + 1); - x0[0] = T0; Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + 1, x0.size() - 1) = rhovec0; - - int counter_T_converged = 0, retry_count = 0; - ofs << "z0 / mole frac.,rho0 / mol/m^3,rho1 / mol/m^3,T / K,p / Pa,c,dt,condition(1),condition(2)" << std::endl; - for (auto iter = 0; iter < options.max_step_count; ++iter) { - // Make T and rhovec references to the contents of x0 vector - // The views are mutable (danger!) - double& T = x0[0]; - auto rhovec = Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + 1, x0.size() - 1); - - auto store_point = [&]() { - - // Calculate some other parameters, for debugging, or scientific interest - auto rhotot = rhovec.sum(); - using id = IsochoricDerivatives<decltype(model), Scalar, VecType>; - double p = rhotot * model.R(rhovec / rhovec.sum()) * T + id::get_pr(model, T, rhovec); - auto conditions = get_criticality_conditions(model, T, rhovec); - double splus = id::get_splus(model, T, rhovec); - - // Store the data in a JSON structure - nlohmann::json point = { - {"t", t}, - {"T / K", T}, - {"rho0 / mol/m^3", static_cast<double>(rhovec[0])}, - {"rho1 / mol/m^3", static_cast<double>(rhovec[1])}, - {"c", c}, - {"s^+", splus}, - {"p / Pa", p}, - {"lambda1", conditions[0]}, - {"dirderiv(lambda1)/dalpha", conditions[1]}, - }; - JSONdata.push_back(point); - }; + // Make variables T and rhovec references to the contents of x0 vector + // The views are mutable (danger!) + double& T = x0[0]; + auto rhovec = Eigen::Map<Eigen::ArrayXd>(&(x0[0]) + 1, x0.size() - 1); - { - auto dxdt = x0; - xprime(x0, dxdt, -1.0); - const auto drhodt = Eigen::Map<const Eigen::ArrayXd>(&(dxdt[0]) + 1, dxdt.size() - 1); - - // Flip the sign if the tracing wants to go backwards, or if the first step would yield any negative concentrations - const auto this_drhodt = drhodt.eval(); - const auto step = rhovec + this_drhodt * dt; - Eigen::ArrayX<bool> negativestepvals = (step < 0).eval(); - if (iter == 0 && retry_count == 0 && negativestepvals.any()) { - c *= -1; - } - else if (iter > 1 && retry_count == 0 && dot(this_drhodt, last_drhodt) < 0) { - c *= -1; - } - } + auto store_drhodt = [&](const state_type& x0) { + last_drhodt = extract_drhodt(get_dxdt(x0)); + }; - auto write_line = [&]() { - std::stringstream out; - auto rhotot = rhovec.sum(); - double z0 = rhovec[0] / rhotot; - using id = IsochoricDerivatives<decltype(model)>; - auto conditions = get_criticality_conditions(model, T, rhovec); - out << z0 << "," << rhovec[0] << "," << rhovec[1] << "," << T << "," << rhotot * model.R(rhovec / rhovec.sum()) * T + id::get_pr(model, T, rhovec) << "," << c << "," << dt << "," << conditions(0) << "," << conditions(1) << std::endl; - std::string sout(out.str()); - std::cout << sout; - if (ofs.is_open()) { - ofs << sout; - } + auto store_point = [&]() { + + // Calculate some other parameters, for debugging, or scientific interest + auto rhotot = rhovec.sum(); + using id = IsochoricDerivatives<decltype(model), Scalar, VecType>; + double p = rhotot * model.R(rhovec / rhovec.sum()) * T + id::get_pr(model, T, rhovec); + auto conditions = get_criticality_conditions(model, T, rhovec); + double splus = id::get_splus(model, T, rhovec); + auto dxdt = x0; + xprime(x0, dxdt, -1.0); + + // Store the data in a JSON structure + nlohmann::json point = { + {"t", t}, + {"T / K", T}, + {"rho0 / mol/m^3", static_cast<double>(rhovec[0])}, + {"rho1 / mol/m^3", static_cast<double>(rhovec[1])}, + {"c", c}, + {"s^+", splus}, + {"p / Pa", p}, + {"dT/dt", dxdt[0]}, + {"drho0/dt", dxdt[1]}, + {"drho1/dt", dxdt[2]}, + {"lambda1", conditions[0]}, + {"dirderiv(lambda1)/dalpha", conditions[1]}, }; - if (iter == 0) { - if (!filename.empty()) { - write_line(); - } + JSONdata.push_back(point); + }; + + // Line writer + auto write_line = [&]() { + std::stringstream out; + auto rhotot = rhovec.sum(); + double z0 = rhovec[0] / rhotot; + using id = IsochoricDerivatives<decltype(model)>; + auto conditions = get_criticality_conditions(model, T, rhovec); + out << z0 << "," << rhovec[0] << "," << rhovec[1] << "," << T << "," << rhotot * model.R(rhovec / rhovec.sum()) * T + id::get_pr(model, T, rhovec) << "," << c << "," << dt << "," << conditions(0) << "," << conditions(1) << std::endl; + std::string sout(out.str()); + std::cout << sout; + if (ofs.is_open()) { + ofs << sout; } - if (iter == 0 && retry_count == 0) { - store_point(); + }; + + int counter_T_converged = 0, retry_count = 0; + ofs << "z0 / mole frac.,rho0 / mol/m^3,rho1 / mol/m^3,T / K,p / Pa,c,dt,condition(1),condition(2)" << std::endl; + + // Determine the initial direction of integration + { + const auto step = (rhovec + extract_drhodt(get_dxdt(x0))*dt).eval(); + Eigen::ArrayX<bool> negativestepvals = (step < 0).eval(); + // Flip the sign if the first step would yield any negative concentrations + if (negativestepvals.any()) { + c *= -1; } + } + //store_drhodt(x0); + if (!filename.empty()) { + write_line(); + } - double dtold = dt; - auto x0_previous = x0; + for (auto iter = 0; iter < options.max_step_count; ++iter) { + + // Calculate the derivatives at the beginning of the step + auto dxdt_start_step = get_dxdt(x0); + auto x_start_step = x0; - // Call the function to get drho/dt in order - // to cache it at the *beginning* of the step - auto dxdt = x0; - try { - xprime(x0, dxdt, -1.0); - } - catch (const std::exception &e) { - std::cout << e.what() << std::endl; - break; - } - auto drhodt = Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]) + 1, dxdt.size() - 1); - last_drhodt = drhodt; + if (iter == 0 && retry_count == 0) { + store_point(); } if (options.integration_order == 5) { - controlled_step_result res = controlled_step_result::fail; + auto res = controlled_step_result::fail; try { res = controlled_stepper.try_step(xprime, x0, t, dt); } @@ -504,6 +514,11 @@ struct CriticalTracing { } else { retry_count = 0; + /*auto dxdt = x0; + xprime(x0, dxdt, -1.0); + auto drhodt = Eigen::Map<Eigen::ArrayXd>(&(dxdt[0]) + 1, dxdt.size() - 1); + auto rhodotproduct = dot(drhodt, last_drhodt); + int rr = 0;*/ } // Reduce step size if greater than the specified max step size dt = std::min(dt, options.max_dt); @@ -522,23 +537,37 @@ struct CriticalTracing { throw std::invalid_argument("integration order is invalid:" + std::to_string(options.integration_order)); } - auto conditions = get_criticality_conditions(model, T, rhovec); auto z0 = rhovec[0] / rhovec.sum(); if (z0 < 0 || z0 > 1) { break; } - try { - int i = 0; - auto [Tnew, rhovecnew] = critical_polish_fixedrho(model, T, rhovec, i); - //int rrrr = 0; - T = Tnew; rhovec = rhovecnew; - } - catch (std::exception& e) { - std::cout << e.what() << std::endl; + if (options.polish) { + try { + int i = 0; + //auto [Tnew, rhovecnew] = critical_polish_fixedrho(model, T, rhovec, i); + auto [Tnew, rhovecnew] = critical_polish_molefrac(model, T, rhovec, z0); + T = Tnew; rhovec = rhovecnew; + } + catch (std::exception& e) { + //int i = 0; + //auto [Tnew, rhovecnew] = critical_polish_fixedrho(model, T, rhovec, i); + std::cout << e.what() << std::endl; + } } - auto actualstep = (Eigen::Map<Eigen::ArrayXd>(&(x0[0]), x0.size()) - Eigen::Map<Eigen::ArrayXd>(&(x0_previous[0]), x0_previous.size())).eval(); + // Store the derivative vector from the beginning of the step, before + // the actual step is taken. We don't want to use the values at the end + // because otherwise simple Euler will never consider the possible change in direction + // + // Also, we only do this after two completed steps because sometimes the infinite + // dilution derivatives seem to be not quite right. There is still a risk that the first + // step will try to turn around... + if (iter >= options.skip_dircheck_count) { + store_drhodt(x_start_step); } + + auto actualstep = (Eigen::Map<Eigen::ArrayXd>(&(x0[0]), x0.size()) - Eigen::Map<Eigen::ArrayXd>(&(x_start_step[0]), x_start_step.size())).eval(); + // Check if T has stopped changing if (std::abs(actualstep[0]) < options.T_tol) { counter_T_converged++; @@ -553,16 +582,14 @@ struct CriticalTracing { break; } - - if (!filename.empty()) { - write_line(); - } + if (!filename.empty()) { write_line(); } store_point(); if (counter_T_converged > options.small_T_count) { break; } } + auto N = JSONdata.size(); return JSONdata; } diff --git a/include/teqp/algorithms/rootfinding.hpp b/include/teqp/algorithms/rootfinding.hpp index 04c5b59a7cc055c9fac9c995ff8e3084c147d52d..4f2e7921a5bf68bdd4bcc060ac6a0a7b83dd3044 100644 --- a/include/teqp/algorithms/rootfinding.hpp +++ b/include/teqp/algorithms/rootfinding.hpp @@ -10,7 +10,7 @@ auto NewtonRaphson(Callable f, const Inputs& args, double tol) { for (int iter = 0; iter < 30; ++iter) { r0 = f(x); for (auto i = 0; i < args.size(); ++i) { - auto dri = std::max(1e-3 * x[i], 1e-8); + auto dri = std::max(1e-6 * x[i], 1e-8); auto argsplus = x; argsplus[i] += dri; J.col(i) = (f(argsplus) - r0) / dri; // Forward diff to avoid negative concentration possibility diff --git a/interface/pybind11_wrapper.cpp b/interface/pybind11_wrapper.cpp index 2d3fd3a65da2b32620a10999bab82b46ad2d0000..868fe32c2129c7891d9923fd7cf5e5c8781cea50 100644 --- a/interface/pybind11_wrapper.cpp +++ b/interface/pybind11_wrapper.cpp @@ -34,8 +34,11 @@ void init_teqp(py::module& m) { .def_readwrite("abs_err", &TCABOptions::abs_err) .def_readwrite("rel_err", &TCABOptions::rel_err) .def_readwrite("init_dt", &TCABOptions::init_dt) + .def_readwrite("init_c", &TCABOptions::init_c) .def_readwrite("max_dt", &TCABOptions::max_dt) .def_readwrite("max_step_count", &TCABOptions::max_step_count) + .def_readwrite("polish", &TCABOptions::polish) + .def_readwrite("skip_dircheck_count", &TCABOptions::skip_dircheck_count) .def_readwrite("integration_order", &TCABOptions::integration_order) ;