diff --git a/libecole/include/ecole/reward/bound-integral.hpp b/libecole/include/ecole/reward/bound-integral.hpp index f7feffcf..4dd1ec2b 100644 --- a/libecole/include/ecole/reward/bound-integral.hpp +++ b/libecole/include/ecole/reward/bound-integral.hpp @@ -14,7 +14,7 @@ template class ECOLE_EXPORT BoundIntegral : public RewardFunction public: using BoundFunction = std::function(scip::Model& model)>; - ECOLE_EXPORT BoundIntegral(bool wall_ = false, const BoundFunction& bound_function_ = {}); + ECOLE_EXPORT BoundIntegral(bool wall_ = false, bool use_nnodes_ = false, const BoundFunction& bound_function_ = {}); ECOLE_EXPORT void before_reset(scip::Model& model) override; ECOLE_EXPORT Reward extract(scip::Model& model, bool done = false) override; @@ -26,6 +26,7 @@ template class ECOLE_EXPORT BoundIntegral : public RewardFunction Reward initial_dual_bound = 0.0; Reward offset = 0.0; bool wall = false; + bool use_nnodes; }; using PrimalIntegral = BoundIntegral; diff --git a/libecole/src/reward/bound-integral.cpp b/libecole/src/reward/bound-integral.cpp index b0d82454..b0d8a327 100644 --- a/libecole/src/reward/bound-integral.cpp +++ b/libecole/src/reward/bound-integral.cpp @@ -25,15 +25,23 @@ class IntegralEventHandler : public ::scip::ObjEventhdlr { inline static auto constexpr base_name = "ecole::reward::IntegralEventHandler"; inline static auto integral_reward_function_counter = 0; - IntegralEventHandler(SCIP* scip, bool wall_, bool extract_primal_, bool extract_dual_, const char* name_) : + IntegralEventHandler( + SCIP* scip, + bool wall_, + bool use_nnodes_, + bool extract_primal_, + bool extract_dual_, + const char* name_) : ObjEventhdlr(scip, name_, "Event handler for primal and dual integrals"), wall{wall_}, + use_nnodes{use_nnodes_}, extract_primal{extract_primal_}, extract_dual{extract_dual_} {} ~IntegralEventHandler() override = default; [[nodiscard]] std::vector const& get_times() const noexcept { return times; } + [[nodiscard]] std::vector const& get_nodes() const noexcept { return nodes; } [[nodiscard]] std::vector const& get_primal_bounds() const noexcept { return primal_bounds; } [[nodiscard]] std::vector const& get_dual_bounds() const noexcept { return dual_bounds; } @@ -50,9 +58,11 @@ class IntegralEventHandler : public ::scip::ObjEventhdlr { private: bool wall; + bool use_nnodes; bool extract_primal; bool extract_dual; std::vector times; + std::vector nodes; std::vector primal_bounds; std::vector dual_bounds; }; @@ -127,6 +137,23 @@ auto get_dual_bound(SCIP* scip) { } } +/* Get the number of nodes in the branch and bound tree */ +auto get_nnodes(SCIP* scip) { + switch (SCIPgetStage(scip)) { + case SCIP_STAGE_TRANSFORMED: + case SCIP_STAGE_INITPRESOLVE: + case SCIP_STAGE_PRESOLVING: + case SCIP_STAGE_EXITPRESOLVE: + case SCIP_STAGE_PRESOLVED: + case SCIP_STAGE_INITSOLVE: + case SCIP_STAGE_SOLVING: + case SCIP_STAGE_SOLVED: + return SCIPgetNNodes(scip); + default: + return static_cast(0); + } +} + auto time_now(bool wall) -> std::chrono::nanoseconds { if (wall) { return std::chrono::steady_clock::now().time_since_epoch(); @@ -157,7 +184,11 @@ void IntegralEventHandler::extract_metrics(SCIP* scip, SCIP_EVENTTYPE event_type dual_bounds.push_back(dual_bounds.back()); } } - times.push_back(time_now(wall)); + if (use_nnodes) { + nodes.push_back(get_nnodes(scip)); + } else { + times.push_back(time_now(wall)); + } } void IntegralEventHandler::clear_bounds() { @@ -171,9 +202,16 @@ void IntegralEventHandler::clear_bounds() { primal_bounds.clear(); primal_bounds.push_back(last_primal); } - auto last_time = times.back(); - times.clear(); - times.push_back(last_time); + + if (use_nnodes) { + auto last_node = nodes.back(); + nodes.clear(); + nodes.push_back(last_node); + } else { + auto last_time = times.back(); + times.clear(); + times.push_back(last_time); + } } /************************************* @@ -183,17 +221,24 @@ void IntegralEventHandler::clear_bounds() { auto compute_dual_integral( std::vector const& dual_bounds, std::vector const& times, + std::vector const& nodes, + bool use_nnodes, SCIP_Real const offset, SCIP_Real const initial_dual_bound, SCIP_Objsense obj_sense) { SCIP_Real dual_integral = 0.0; for (std::size_t i = 0; i < dual_bounds.size() - 1; ++i) { - auto const time_diff = std::chrono::duration(times[i + 1] - times[i]).count(); + double metric_diff; + if (use_nnodes) { + metric_diff = static_cast(nodes[i + 1] - nodes[i]); + } else { + metric_diff = std::chrono::duration(times[i + 1] - times[i]).count(); + } auto const dual_bound = dual_bounds[i]; if (obj_sense == SCIP_OBJSENSE_MINIMIZE) { - dual_integral += (offset - std::max(dual_bound, initial_dual_bound)) * time_diff; + dual_integral += (offset - std::max(dual_bound, initial_dual_bound)) * metric_diff; } else { - dual_integral += -(offset - std::min(dual_bound, initial_dual_bound)) * time_diff; + dual_integral += -(offset - std::min(dual_bound, initial_dual_bound)) * metric_diff; } } return dual_integral; @@ -202,17 +247,24 @@ auto compute_dual_integral( auto compute_primal_integral( std::vector const& primal_bounds, std::vector const& times, + std::vector const& nodes, + bool use_nnodes, SCIP_Real const offset, SCIP_Real const initial_primal_bound, SCIP_Objsense obj_sense) { SCIP_Real primal_integral = 0.0; for (std::size_t i = 0; i < primal_bounds.size() - 1; ++i) { - auto const time_diff = std::chrono::duration(times[i + 1] - times[i]).count(); + double metric_diff; + if (use_nnodes) { + metric_diff = static_cast(nodes[i + 1] - nodes[i]); + } else { + metric_diff = std::chrono::duration(times[i + 1] - times[i]).count(); + } auto const primal_bound = primal_bounds[i]; if (obj_sense == SCIP_OBJSENSE_MINIMIZE) { - primal_integral += -(offset - std::min(primal_bound, initial_primal_bound)) * time_diff; + primal_integral += -(offset - std::min(primal_bound, initial_primal_bound)) * metric_diff; } else { - primal_integral += (offset - std::max(primal_bound, initial_primal_bound)) * time_diff; + primal_integral += (offset - std::max(primal_bound, initial_primal_bound)) * metric_diff; } } return primal_integral; @@ -222,21 +274,28 @@ auto compute_primal_dual_integral( std::vector const& primal_bounds, std::vector const& dual_bounds, std::vector const& times, + std::vector const& nodes, + bool use_nnodes, SCIP_Real const initial_primal_bound, SCIP_Real const initial_dual_bound, SCIP_Objsense obj_sense) { SCIP_Real primal_dual_integral = 0.0; for (std::size_t i = 0; i < primal_bounds.size() - 1; ++i) { - auto const time_diff = std::chrono::duration(times[i + 1] - times[i]).count(); + double metric_diff; + if (use_nnodes) { + metric_diff = static_cast(nodes[i + 1] - nodes[i]); + } else { + metric_diff = std::chrono::duration(times[i + 1] - times[i]).count(); + } auto const dual_bound = dual_bounds[i]; auto const primal_bound = primal_bounds[i]; if (obj_sense == SCIP_OBJSENSE_MINIMIZE) { primal_dual_integral += - -(std::max(dual_bound, initial_dual_bound) - std::min(primal_bound, initial_primal_bound)) * time_diff; + -(std::max(dual_bound, initial_dual_bound) - std::min(primal_bound, initial_primal_bound)) * metric_diff; } else { primal_dual_integral += - (std::min(dual_bound, initial_dual_bound) - std::max(primal_bound, initial_primal_bound)) * time_diff; + (std::min(dual_bound, initial_dual_bound) - std::max(primal_bound, initial_primal_bound)) * metric_diff; } } return primal_dual_integral; @@ -252,8 +311,15 @@ auto get_eventhdlr(scip::Model& model, const char* name) -> auto& { } /** Add the integral event handler to the model. */ -void add_eventhdlr(scip::Model& model, bool wall, bool extract_primal, bool extract_dual, const char* name) { - auto handler = std::make_unique(model.get_scip_ptr(), wall, extract_primal, extract_dual, name); +void add_eventhdlr( + scip::Model& model, + bool wall, + bool use_nnodes, + bool extract_primal, + bool extract_dual, + const char* name) { + auto handler = + std::make_unique(model.get_scip_ptr(), wall, use_nnodes, extract_primal, extract_dual, name); scip::call(SCIPincludeObjEventhdlr, model.get_scip_ptr(), handler.get(), true); // NOLINTNEXTLINE memory ownership is passed to SCIP handler.release(); @@ -287,7 +353,8 @@ auto default_primal_dual_bound_function(scip::Model& model) -> std::tuple -ecole::reward::BoundIntegral::BoundIntegral(bool wall_, const BoundFunction& bound_function_) : wall{wall_} { +ecole::reward::BoundIntegral::BoundIntegral(bool wall_, bool use_nnodes_, const BoundFunction& bound_function_) : + wall{wall_}, use_nnodes{use_nnodes_} { if constexpr (bound == Bound::dual) { bound_function = bound_function_ ? bound_function_ : default_dual_bound_function; } else if constexpr (bound == Bound::primal) { @@ -306,13 +373,13 @@ template void BoundIntegral::before_reset(scip::Model& mode // Initalize bounds and event handler if constexpr (bound == Bound::dual) { std::tie(offset, initial_dual_bound) = bound_function(model); - add_eventhdlr(model, wall, false, true, name.c_str()); + add_eventhdlr(model, wall, use_nnodes, false, true, name.c_str()); } else if constexpr (bound == Bound::primal) { std::tie(offset, initial_primal_bound) = bound_function(model); - add_eventhdlr(model, wall, true, false, name.c_str()); + add_eventhdlr(model, wall, use_nnodes, true, false, name.c_str()); } else if constexpr (bound == Bound::primal_dual) { std::tie(initial_primal_bound, initial_dual_bound) = bound_function(model); - add_eventhdlr(model, wall, true, true, name.c_str()); + add_eventhdlr(model, wall, use_nnodes, true, true, name.c_str()); } // Extract metrics before resetting to get initial reference point @@ -327,17 +394,19 @@ template Reward BoundIntegral::extract(scip::Model& model, auto const& dual_bounds = handler.get_dual_bounds(); auto const& primal_bounds = handler.get_primal_bounds(); auto const& times = handler.get_times(); + auto const& nodes = handler.get_nodes(); auto const obj_sense = SCIPgetObjsense(model.get_scip_ptr()); // Compute primal integral and difference SCIP_Real integral = 0.; if constexpr (bound == Bound::dual) { - integral = compute_dual_integral(dual_bounds, times, offset, initial_dual_bound, obj_sense); + integral = compute_dual_integral(dual_bounds, times, nodes, use_nnodes, offset, initial_dual_bound, obj_sense); } else if constexpr (bound == Bound::primal) { - integral = compute_primal_integral(primal_bounds, times, offset, initial_primal_bound, obj_sense); + integral = + compute_primal_integral(primal_bounds, times, nodes, use_nnodes, offset, initial_primal_bound, obj_sense); } else if constexpr (bound == Bound::primal_dual) { integral = compute_primal_dual_integral( - primal_bounds, dual_bounds, times, initial_primal_bound, initial_dual_bound, obj_sense); + primal_bounds, dual_bounds, times, nodes, use_nnodes, initial_primal_bound, initial_dual_bound, obj_sense); } // Reset arrays for storing bounds diff --git a/python/src/ecole/core/reward.cpp b/python/src/ecole/core/reward.cpp index 984c7b34..b7d636e3 100644 --- a/python/src/ecole/core/reward.cpp +++ b/python/src/ecole/core/reward.cpp @@ -179,8 +179,9 @@ void bind_submodule(py::module_ const& m) { it includes time spent in :py:meth:`~ecole.environment.Environment.reset` and time spent waiting on the agent. )"); dualintegral.def( - py::init(), + py::init(), py::arg("wall") = false, + py::arg("use_nnodes") = false, py::arg("bound_function") = DualIntegral::BoundFunction{}, R"( @@ -190,6 +191,9 @@ void bind_submodule(py::module_ const& m) { ---------- wall : If true, the wall time will be used. If False (default), the process time will be used. + use_nnodes : + If true, the integral will be computed with respect to the number of nodes. Otherwise + the integral is computed with respect to time. bound_function : A function which takes an ecole model and returns a tuple of an initial dual bound and the offset to compute the dual bound with respect to. Values should be ordered as (offset, initial_dual_bound). @@ -212,8 +216,9 @@ void bind_submodule(py::module_ const& m) { it includes time spent in :py:meth:`~ecole.environment.Environment.reset` and time spent waiting on the agent. )"); primalintegral.def( - py::init(), + py::init(), py::arg("wall") = false, + py::arg("use_nnodes") = false, py::arg("bound_function") = PrimalIntegral::BoundFunction{}, R"( Create a PrimalIntegral reward function. @@ -222,6 +227,9 @@ void bind_submodule(py::module_ const& m) { ---------- wall : If true, the wall time will be used. If False (default), the process time will be used. + use_nnodes : + If true, the integral will be computed with respect to the number of nodes. Otherwise + the integral is computed with respect to time. bound_function : A function which takes an ecole model and returns a tuple of an initial primal bound and the offset to compute the primal bound with respect to. Values should be ordered as (offset, initial_primal_bound). @@ -243,8 +251,9 @@ void bind_submodule(py::module_ const& m) { it includes time spent in :py:meth:`~ecole.environment.Environment.reset` and time spent waiting on the agent. )"); primaldualintegral.def( - py::init(), + py::init(), py::arg("wall") = false, + py::arg("use_nnodes") = false, py::arg("bound_function") = PrimalDualIntegral::BoundFunction{}, R"( Create a PrimalDualIntegral reward function. @@ -253,6 +262,9 @@ void bind_submodule(py::module_ const& m) { ---------- wall : If true, the wall time will be used. If False (default), the process time will be used. + use_nnodes : + If true, the integral will be computed with respect to the number of nodes. Otherwise + the integral is computed with respect to time. bound_function : A function which takes an ecole model and returns a tuple of an initial primal bound and dual bound. Values should be ordered as (initial_primal_bound, initial_dual_bound). The default function returns diff --git a/python/tests/test_reward.py b/python/tests/test_reward.py index d2079fd6..8b42b3cb 100644 --- a/python/tests/test_reward.py +++ b/python/tests/test_reward.py @@ -29,6 +29,9 @@ def pytest_generate_tests(metafunc): ecole.reward.PrimalIntegral(bound_function=lambda x: (0.0, 0.0)), ecole.reward.DualIntegral(bound_function=lambda x: (0.0, 0.0)), ecole.reward.PrimalDualIntegral(bound_function=lambda x: (0.0, 0.0)), + ecole.reward.PrimalIntegral(use_nnodes=True, bound_function=lambda x: (0.0, 0.0)), + ecole.reward.DualIntegral(use_nnodes=True, bound_function=lambda x: (0.0, 0.0)), + ecole.reward.PrimalDualIntegral(use_nnodes=True, bound_function=lambda x: (0.0, 0.0)), ) metafunc.parametrize("reward_function", all_reward_functions)