From 4779d9c35390046832025b0697852349896f0123 Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Wed, 4 Feb 2026 12:07:02 +0100 Subject: [PATCH 01/15] add gurobi interface --- src/search/CMakeLists.txt | 9 + src/search/cmake/FindGurobi.cmake | 43 +++ src/search/lp/gurobi_solver_interface.cc | 368 +++++++++++++++++++++++ src/search/lp/gurobi_solver_interface.h | 55 ++++ src/search/lp/lp_solver.cc | 13 +- src/search/lp/lp_solver.h | 3 +- 6 files changed, 489 insertions(+), 2 deletions(-) create mode 100644 src/search/cmake/FindGurobi.cmake create mode 100644 src/search/lp/gurobi_solver_interface.cc create mode 100644 src/search/lp/gurobi_solver_interface.h diff --git a/src/search/CMakeLists.txt b/src/search/CMakeLists.txt index e6d7cfede3..7de311bdae 100644 --- a/src/search/CMakeLists.txt +++ b/src/search/CMakeLists.txt @@ -594,6 +594,7 @@ if(USE_LP) target_sources(lp_solver INTERFACE lp/cplex_solver_interface.h lp/cplex_solver_interface.cc) endif() + find_package(soplex 7.1.0 QUIET) if (SOPLEX_FOUND) message(STATUS "Found SoPlex: ${SOPLEX_INCLUDE_DIRS}") @@ -601,6 +602,14 @@ if(USE_LP) target_compile_definitions(lp_solver INTERFACE HAS_SOPLEX) target_sources(lp_solver INTERFACE lp/soplex_solver_interface.h lp/soplex_solver_interface.cc) endif() + + find_package(Gurobi QUIET) + if(Gurobi_FOUND) + message(STATUS "Found Gurobi: ${GUROBI_LIBRARY} ${GUROBI_INCLUDE_DIR}") + target_compile_definitions(lp_solver INTERFACE HAS_GUROBI) + target_link_libraries(lp_solver INTERFACE gurobi::gurobi) + target_sources(lp_solver INTERFACE lp/gurobi_solver_interface.h lp/gurobi_solver_interface.cc) + endif() endif() create_fast_downward_library( diff --git a/src/search/cmake/FindGurobi.cmake b/src/search/cmake/FindGurobi.cmake new file mode 100644 index 0000000000..b1cad55de5 --- /dev/null +++ b/src/search/cmake/FindGurobi.cmake @@ -0,0 +1,43 @@ +# Find Gurobi and export the target gurobi::gurobi +# +# Usage: +# find_package(Gurobi) +# target_link_libraries( PRIVATE gurobi::gurobi) +# +# Hints: +# -DGurobi_ROOT=... +# -Dgurobi_DIR=... +# env GUROBI_HOME=... (preferred) + +set(HINT_PATHS ${Gurobi_ROOT} ${gurobi_DIR} $ENV{GUROBI_HOME}) + +find_path(GUROBI_INCLUDE_DIR + NAMES gurobi_c.h gurobi_c++.h + HINTS ${HINT_PATHS} + PATH_SUFFIXES include +) + +# For linux. +find_library(GUROBI_LIBRARY + NAMES + gurobi130 # Gurobi 13.0 + HINTS ${HINT_PATHS} + PATH_SUFFIXES lib +) + +# Check if everything was found and set Gurobi_FOUND. +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + Gurobi + REQUIRED_VARS GUROBI_INCLUDE_DIR GUROBI_LIBRARY +) + +if(Gurobi_FOUND AND NOT TARGET gurobi::gurobi) + add_library(gurobi::gurobi UNKNOWN IMPORTED) + set_target_properties(gurobi::gurobi PROPERTIES + IMPORTED_LOCATION "${GUROBI_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${GUROBI_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(GUROBI_INCLUDE_DIR GUROBI_LIBRARY) diff --git a/src/search/lp/gurobi_solver_interface.cc b/src/search/lp/gurobi_solver_interface.cc new file mode 100644 index 0000000000..6738b7f2ff --- /dev/null +++ b/src/search/lp/gurobi_solver_interface.cc @@ -0,0 +1,368 @@ +#include "gurobi_solver_interface.h" + +#include "lp_solver.h" + +#include "../utils/system.h" + +#include +#include +#include + +/* + Core Gurobi functionality is provided in C API (gurobi_c.h). + We implement the interface using the C API to avoid linking issues. + + The implementation is based on examples from https://www.gurobi.com/documentation/9.5/refman/c_api_overview.html + + We use the following functions from the Gurobi C API: + - GRBsetdblattrarray: sets the values of an array of double attributes. + - model: model object (GRBmodel *) + - attr: attribute to set (const char *) + - first: index of first element to set (int, zero-based) + - len: number of elements to set (int) + - values: a pointer to an array of double values (double *) + - GRBsetdblattrelement: sets the value of a single element of a double attribute array. + - model: model object (GRBmodel *) + - attr: attribute to set (const char *) + - element: index of element to set (int, zero-based) + - newvalue: new value for the element (double) + + The following attributes are used: + - GRB_DBL_ATTR_OBJ: is a double array attribute that contains the objective coefficients for all variables in the model. + - GRB_INT_ATTR_MODELSENSE: is an integer attribute that defines the optimization sense of the model. + - 1 for minimization + - -1 for maximization +*/ + +using namespace std; + +namespace lp { + +namespace { +NO_RETURN void handle_gurobi_error(GRBenv *env, int error_code) { + if (error_code == GRB_ERROR_OUT_OF_MEMORY) { + utils::exit_with(utils::ExitCode::SEARCH_OUT_OF_MEMORY); + } + const char *msg = env ? GRBgeterrormsg(env) : nullptr; + if (msg) { + cerr << msg << endl; + } else { + cerr << "Gurobi error: code " << error_code << endl; + } + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); +} + +template +void GRB_CALL(GRBenv *env, Func fn, Args &&...args) { + int status = fn(std::forward(args)...); + if (status) { + handle_gurobi_error(env, status); + } +} + +int objective_sense_to_gurobi(LPObjectiveSense sense) { + if (sense == LPObjectiveSense::MINIMIZE) { + return 1; + } else { + return -1; + } +} + +bool is_pos_infinity(double x) { + return x >= GRB_INFINITY; +} + +bool is_neg_infinity(double x) { + return x <= -GRB_INFINITY; +} + +void add_constraint(GRBenv *env, GRBmodel *model, const LPConstraint &constraint) { + const vector &indices = constraint.get_variables(); + const vector &coefficients = constraint.get_coefficients(); + int numnz = static_cast(indices.size()); + int *cind = numnz ? const_cast(indices.data()) : nullptr; + double *cval = numnz ? const_cast(coefficients.data()) : nullptr; + + double lb = constraint.get_lower_bound(); + double ub = constraint.get_upper_bound(); + if (lb > ub) { + // Add a trivially unsatisfiable row. + // TODO: should we handle this differently? + GRB_CALL(env, GRBaddconstr, model, 0, nullptr, nullptr, GRB_LESS_EQUAL, -1.0, nullptr); + return; + } + if (is_neg_infinity(lb) && is_pos_infinity(ub)) { + // Constraint without bounds: do nothing. + // TODO: is this the right behavior? + return; + } + if (is_neg_infinity(lb)) { + GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, ub, nullptr); + } else if (is_pos_infinity(ub)) { + GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_GREATER_EQUAL, lb, nullptr); + } else if (lb == ub) { + GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_EQUAL, lb, nullptr); + } else { + cerr << "Range constraints are not supported by Gurobi." << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + } +} + +} // Have to add this otherwise the compiler complains. + +GurobiSolverInterface::GurobiSolverInterface(): env(nullptr), model(nullptr), num_permanent_constraints(0), num_temporary_constraints(0), model_dirty(false) { + GRB_CALL(env, GRBloadenvinternal, &env, "", GRB_VERSION_MAJOR, GRB_VERSION_MINOR, GRB_VERSION_TECHNICAL); + GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_OUTPUTFLAG, 0); + GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_LOGTOCONSOLE, 0); + GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_THREADS, 1); + GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_METHOD, GRB_METHOD_DUAL); +} + +GurobiSolverInterface::~GurobiSolverInterface() { + if (model) { + GRBfreemodel(model); + } + if (env) { + GRBfreeenv(env); + } +} + +void GurobiSolverInterface::load_problem(const LinearProgram &lp) { + if (model) { + GRBfreemodel(model); + model = nullptr; + } + const auto &variables = lp.get_variables(); + int num_vars = variables.size(); + vector obj; + vector lb; + vector ub; + vector vtype; + obj.reserve(num_vars); + lb.reserve(num_vars); + ub.reserve(num_vars); + vtype.reserve(num_vars); + for (const LPVariable &var : variables) { + obj.push_back(var.objective_coefficient); + lb.push_back(var.lower_bound); + ub.push_back(var.upper_bound); + vtype.push_back(var.is_integer ? GRB_INTEGER : GRB_CONTINUOUS); + } + double *obj_ptr = num_vars ? obj.data() : nullptr; + double *lb_ptr = num_vars ? lb.data() : nullptr; + double *ub_ptr = num_vars ? ub.data() : nullptr; + char *vtype_ptr = num_vars ? vtype.data() : nullptr; + GRB_CALL(env, GRBnewmodel, env, &model, "downward", num_vars, obj_ptr, lb_ptr, ub_ptr, vtype_ptr, nullptr); + GRB_CALL(env, GRBsetintattr, model, GRB_INT_ATTR_MODELSENSE, objective_sense_to_gurobi(lp.get_sense())); + const auto &constraints = lp.get_constraints(); + for (const LPConstraint &constraint : constraints) { + add_constraint(env, model, constraint); + } + //GRB_CALL(env, GRBupdatemodel, model); + num_permanent_constraints = constraints.size(); + num_temporary_constraints = 0; + model_dirty = true; +} + +void GurobiSolverInterface::add_temporary_constraints(const named_vector::NamedVector &constraints) { + for (const LPConstraint &constraint : constraints) { + add_constraint(env, model, constraint); + } + //GRB_CALL(env, GRBupdatemodel, model); + model_dirty = true; + num_temporary_constraints += constraints.size(); +} + +void GurobiSolverInterface::clear_temporary_constraints() { + if (!has_temporary_constraints()) { + return; + } + vector indices(num_temporary_constraints); + iota(indices.begin(), indices.end(), num_permanent_constraints); + GRB_CALL(env, GRBdelconstrs, model, num_temporary_constraints, indices.data()); + //GRB_CALL(env, GRBupdatemodel, model); + model_dirty = true; + num_temporary_constraints = 0; +} + +double GurobiSolverInterface::get_infinity() const { + return GRB_INFINITY; +} + +void GurobiSolverInterface::set_objective_coefficients(const vector &coefficients) { + int num_coefficients = coefficients.size(); + if (!num_coefficients) { + return; + } // TODO: is there a more elegant way to handle this? + GRB_CALL(env, GRBsetdblattrarray, model, GRB_DBL_ATTR_OBJ, 0, num_coefficients, const_cast(coefficients.data())); + //GRB_CALL(env, GRBupdatemodel, model); + model_dirty = true; +} + +void GurobiSolverInterface::set_objective_coefficient(int index, double coefficient) { + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_OBJ, index, coefficient); + //GRB_CALL(env, GRBupdatemodel, model); + model_dirty = true; +} + +void GurobiSolverInterface::set_constraint_lower_bound(int index, double bound) { + char sense; + GRB_CALL(env, GRBgetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, &sense); + if(sense == GRB_LESS_EQUAL) { + if (!is_neg_infinity(bound)) { + cerr << "Error: cannot set lower bound on <= constraint to a finite value." << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + } + return; + } + else if(sense == GRB_GREATER_EQUAL) { + if (is_neg_infinity(bound)) { + cerr << "Error: cannot set lower bound on >= constraint to -infinity." << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + } else { + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); + return; + } + } + else if(sense == GRB_EQUAL) { + double current_rhs; + GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); + if (current_rhs != bound) { + cerr << "Error: cannot change lower bound on = constraint." << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + } + return; + } + //GRB_CALL(env, GRBupdatemodel, model); + model_dirty = true; +} + +void GurobiSolverInterface::set_constraint_upper_bound(int index, double bound) { + char sense; + GRB_CALL(env, GRBgetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, &sense); + if(sense == GRB_LESS_EQUAL) { + if (is_pos_infinity(bound)) { + cerr << "Error: cannot set upper bound on <= constraint to +infinity." << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + } else { + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); + return; + } + } + else if(sense == GRB_GREATER_EQUAL) { + if (!is_pos_infinity(bound)) { + cerr << "Error: cannot set upper bound on >= constraint to a finite value." << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + } + return; + } + else if(sense == GRB_EQUAL) { + double current_rhs; + GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); + if (current_rhs != bound) { + cerr << "Error: cannot change upper bound on = constraint." << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + } + return; + } + //GRB_CALL(env, GRBupdatemodel, model); + model_dirty = true; +} + +void GurobiSolverInterface::set_variable_lower_bound(int index, double bound) { + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_LB, index, bound); + //GRB_CALL(env, GRBupdatemodel, model); + model_dirty = true; +} + +void GurobiSolverInterface::set_variable_upper_bound(int index, double bound) { + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_UB, index, bound); + //GRB_CALL(env, GRBupdatemodel, model); + model_dirty = true; +} + +void GurobiSolverInterface::set_mip_gap(double gap) { + GRB_CALL(env, GRBsetdblparam, env, GRB_DBL_PAR_MIPGAP, gap); +} + +void GurobiSolverInterface::solve() { + if (model_dirty) { + GRB_CALL(env, GRBupdatemodel, model); + model_dirty = false; + } + GRB_CALL(env, GRBoptimize, model); +} + +void GurobiSolverInterface::write_lp(const string &filename) const { + //if (model_dirty) { + // GRB_CALL(env, GRBupdatemodel, model); + // model_dirty = false; + //} + GRB_CALL(env, GRBwrite, model, filename.c_str()); +} + +// TODO: implement more detailed failure analysis +void GurobiSolverInterface::print_failure_analysis() const { + int status = 0; + GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); + cout << "Gurobi status code: " << status << endl; +} + +// TODO: check if a solution is available +bool GurobiSolverInterface::is_infeasible() const { + int status = 0; + GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); + return status == GRB_INFEASIBLE; +} + +// TODO: check if a solution is available +bool GurobiSolverInterface::is_unbounded() const { + int status = 0; + GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); + return status == GRB_UNBOUNDED; +} + +// TODO: check if a solution is available +bool GurobiSolverInterface::has_optimal_solution() const { + int status = 0; + GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); + return status == GRB_OPTIMAL; +} + +// TODO: check if a solution is available +double GurobiSolverInterface::get_objective_value() const { + double value = 0.0; + GRB_CALL(env, GRBgetdblattr, model, GRB_DBL_ATTR_OBJVAL, &value); + return value; +} + +// TODO: check if an optimal solution is available +vector GurobiSolverInterface::extract_solution() const { + int num_variables = get_num_variables(); + vector solution(num_variables); + if (num_variables > 0) { + GRB_CALL(env, GRBgetdblattrarray, model, GRB_DBL_ATTR_X, 0, num_variables, solution.data()); + } + return solution; +} + +int GurobiSolverInterface::get_num_variables() const { + int num_variables = 0; + GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_NUMVARS, &num_variables); + return num_variables; +} + +int GurobiSolverInterface::get_num_constraints() const { + return num_permanent_constraints + num_temporary_constraints; +} + +bool GurobiSolverInterface::has_temporary_constraints() const { + return num_temporary_constraints > 0; +} + +void GurobiSolverInterface::print_statistics() const { + double runtime = 0.0; + GRB_CALL(env, GRBgetdblattr, model, GRB_DBL_ATTR_RUNTIME, &runtime); + cout << "Gurobi runtime: " << runtime << "s" << endl; +} +} diff --git a/src/search/lp/gurobi_solver_interface.h b/src/search/lp/gurobi_solver_interface.h new file mode 100644 index 0000000000..b93456d53a --- /dev/null +++ b/src/search/lp/gurobi_solver_interface.h @@ -0,0 +1,55 @@ +#ifndef LP_GUROBI_SOLVER_INTERFACE_H +#define LP_GUROBI_SOLVER_INTERFACE_H + +#include "solver_interface.h" + +#include + +namespace lp { +class GurobiSolverInterface : public SolverInterface { + GRBenv *env; + GRBmodel *model; + int num_permanent_constraints; + int num_temporary_constraints; + bool model_dirty; + +public: + GurobiSolverInterface(); + virtual ~GurobiSolverInterface() override; + + virtual void load_problem(const LinearProgram &lp) override; + virtual void add_temporary_constraints( + const named_vector::NamedVector &constraints) override; + virtual void clear_temporary_constraints() override; + virtual double get_infinity() const override; + + virtual void set_objective_coefficients( + const std::vector &coefficients) override; + virtual void set_objective_coefficient(int index, double coefficient) override; + virtual void set_constraint_lower_bound(int index, double bound) override; + virtual void set_constraint_upper_bound(int index, double bound) override; + virtual void set_variable_lower_bound(int index, double bound) override; + virtual void set_variable_upper_bound(int index, double bound) override; + + virtual void set_mip_gap(double gap) override; + + virtual void solve() override; + virtual void write_lp(const std::string &filename) const override; + virtual void print_failure_analysis() const override; + virtual bool is_infeasible() const override; + virtual bool is_unbounded() const override; + + virtual bool has_optimal_solution() const override; + + virtual double get_objective_value() const override; + + virtual std::vector extract_solution() const override; + + virtual int get_num_variables() const override; + virtual int get_num_constraints() const override; + virtual bool has_temporary_constraints() const override; + virtual void print_statistics() const override; +}; +} + +#endif diff --git a/src/search/lp/lp_solver.cc b/src/search/lp/lp_solver.cc index 37b35952d3..a82d5eb78f 100644 --- a/src/search/lp/lp_solver.cc +++ b/src/search/lp/lp_solver.cc @@ -6,6 +6,9 @@ #ifdef HAS_SOPLEX #include "soplex_solver_interface.h" #endif +#ifdef HAS_GUROBI +#include "gurobi_solver_interface.h" +#endif #include "../plugins/plugin.h" @@ -135,6 +138,13 @@ LPSolver::LPSolver(LPSolverType solver_type) { pimpl = make_unique(); #else missing_solver = "SoPlex"; +#endif + break; + case LPSolverType::GUROBI: +#ifdef HAS_GUROBI + pimpl = make_unique(); +#else + missing_solver = "Gurobi"; #endif break; default: @@ -245,5 +255,6 @@ void LPSolver::print_statistics() const { static plugins::TypedEnumPlugin _enum_plugin( {{"cplex", "commercial solver by IBM"}, - {"soplex", "open source solver by ZIB"}}); + {"soplex", "open source solver by ZIB"}, + {"gurobi", "commercial solver by Gurobi"}}); } diff --git a/src/search/lp/lp_solver.h b/src/search/lp/lp_solver.h index 88baaedb86..9be78079b3 100644 --- a/src/search/lp/lp_solver.h +++ b/src/search/lp/lp_solver.h @@ -17,7 +17,8 @@ class Options; namespace lp { enum class LPSolverType { CPLEX, - SOPLEX + SOPLEX, + GUROBI }; enum class LPObjectiveSense { From a14fd5bdc17d071a04e78b094d813a9bb3c11d4e Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Wed, 4 Feb 2026 14:24:21 +0100 Subject: [PATCH 02/15] update FindGurobi; update gurobi interface --- src/search/cmake/FindGurobi.cmake | 1 + src/search/lp/gurobi_solver_interface.cc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/search/cmake/FindGurobi.cmake b/src/search/cmake/FindGurobi.cmake index b1cad55de5..ba9471fff2 100644 --- a/src/search/cmake/FindGurobi.cmake +++ b/src/search/cmake/FindGurobi.cmake @@ -21,6 +21,7 @@ find_path(GUROBI_INCLUDE_DIR find_library(GUROBI_LIBRARY NAMES gurobi130 # Gurobi 13.0 + gurobi110 HINTS ${HINT_PATHS} PATH_SUFFIXES lib ) diff --git a/src/search/lp/gurobi_solver_interface.cc b/src/search/lp/gurobi_solver_interface.cc index 6738b7f2ff..36a5b174e2 100644 --- a/src/search/lp/gurobi_solver_interface.cc +++ b/src/search/lp/gurobi_solver_interface.cc @@ -111,7 +111,7 @@ void add_constraint(GRBenv *env, GRBmodel *model, const LPConstraint &constraint } // Have to add this otherwise the compiler complains. GurobiSolverInterface::GurobiSolverInterface(): env(nullptr), model(nullptr), num_permanent_constraints(0), num_temporary_constraints(0), model_dirty(false) { - GRB_CALL(env, GRBloadenvinternal, &env, "", GRB_VERSION_MAJOR, GRB_VERSION_MINOR, GRB_VERSION_TECHNICAL); + GRB_CALL(env, GRBloadenv, &env, ""); GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_OUTPUTFLAG, 0); GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_LOGTOCONSOLE, 0); GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_THREADS, 1); From 1ec87d078061dbaa890c850dbcea4f3449afe1bb Mon Sep 17 00:00:00 2001 From: Travis Rivera Petit Date: Wed, 4 Feb 2026 14:29:53 +0100 Subject: [PATCH 03/15] Add HiGHS interface --- src/search/CMakeLists.txt | 10 ++ src/search/lp/highs_solver_interface.cc | 206 ++++++++++++++++++++++++ src/search/lp/highs_solver_interface.h | 56 +++++++ src/search/lp/lp_solver.cc | 12 ++ src/search/lp/lp_solver.h | 1 + 5 files changed, 285 insertions(+) create mode 100644 src/search/lp/highs_solver_interface.cc create mode 100644 src/search/lp/highs_solver_interface.h diff --git a/src/search/CMakeLists.txt b/src/search/CMakeLists.txt index 7de311bdae..037ca666f7 100644 --- a/src/search/CMakeLists.txt +++ b/src/search/CMakeLists.txt @@ -610,6 +610,16 @@ if(USE_LP) target_link_libraries(lp_solver INTERFACE gurobi::gurobi) target_sources(lp_solver INTERFACE lp/gurobi_solver_interface.h lp/gurobi_solver_interface.cc) endif() + + find_package(HiGHS CONFIG QUIET) + if(HiGHS_FOUND) + target_compile_definitions(lp_solver INTERFACE HAS_HIGHS) + target_link_libraries(lp_solver INTERFACE highs::highs) + target_sources(lp_solver INTERFACE + lp/highs_solver_interface.h + lp/highs_solver_interface.cc + ) + endif() endif() create_fast_downward_library( diff --git a/src/search/lp/highs_solver_interface.cc b/src/search/lp/highs_solver_interface.cc new file mode 100644 index 0000000000..cb16620556 --- /dev/null +++ b/src/search/lp/highs_solver_interface.cc @@ -0,0 +1,206 @@ +#include "highs_solver_interface.h" + +#include +#include +#include + +using namespace std; + +namespace lp { + +static void highs_ok(HighsStatus s, const char* where) { + if (s == HighsStatus::kOk) return; + throw std::runtime_error(std::string("HiGHS error in ") + where); +} + +HiGHSSolverInterface::HiGHSSolverInterface() { + // Nothing to do here :) +} + +void HiGHSSolverInterface::load_problem(const LinearProgram &lp) { + highs_ok(highs_.clear(), "clear"); + if (lp.get_sense() == LPObjectiveSense::MAXIMIZE){ + highs_ok(highs_.changeObjectiveSense(ObjSense::kMaximize), "changeObjectiveSense"); + } + else{ + highs_ok(highs_.changeObjectiveSense(ObjSense::kMinimize), "changeObjectiveSense"); + } + + // variables + named_vector::NamedVector vars = lp.get_variables(); + const int n = static_cast(vars.size()); + for (int i = 0; i < n; ++i) { + const auto &v = vars[i]; + highs_ok(highs_.addCol(v.objective_coefficient, + v.lower_bound, v.upper_bound, + 0, nullptr, nullptr), + "addCol"); + + if (v.is_integer) { + highs_ok(highs_.changeColIntegrality(i, HighsVarType::kInteger), + "changeColIntegrality"); + } + } + + // constraints + const auto &cons = lp.get_constraints(); + const int m = static_cast(cons.size()); + for (int r = 0; r < m; ++r) { + const auto &c = cons[r]; + const auto &idx = c.get_variables(); + const auto &val = c.get_coefficients(); + assert(idx.size() == val.size()); + + highs_ok(highs_.addRow(c.get_lower_bound(), + c.get_upper_bound(), + (int)idx.size(), + idx.data(), + val.data()), + "addRow"); + } + num_permanent_constraints = lp.get_constraints().size(); + num_temporary_constraints = 0; + + cout << "so far so good" << endl; +} + +void HiGHSSolverInterface::add_temporary_constraints( + const named_vector::NamedVector &constraints) { + num_temporary_constraints += constraints.size(); + + for (int i = 0; i < (int)constraints.size(); ++i) { + const auto &c = constraints[i]; + const auto &idx = c.get_variables(); + const auto &val = c.get_coefficients(); + + highs_ok(highs_.addRow(c.get_lower_bound(), + c.get_upper_bound(), + (int)idx.size(), + idx.data(), + val.data()), + "addRow(temporary_constraints)"); + } +} + +void HiGHSSolverInterface::clear_temporary_constraints() { + if (num_temporary_constraints == 0) + return; + + const int fst_temp_row = num_permanent_constraints; + const int lst_temp_row = highs_.getNumRow() - 1; + highs_ok(highs_.deleteRows(fst_temp_row, lst_temp_row), "deleteRows"); + + num_temporary_constraints = 0; +} + +double HiGHSSolverInterface::get_infinity() const { + return highs_.getInfinity(); +} + +void HiGHSSolverInterface::set_objective_coefficients( + const std::vector& coefficients) +{ + const int n = highs_.getNumCol(); + + if (coefficients.size() != static_cast(n)) { + throw std::runtime_error( + "set_objective_coefficients size mismatch.\nExpected " + + std::to_string(n) + + " but instead got " + + std::to_string(coefficients.size()) + ); + } + + for (int i = 0; i < n; ++i) { + highs_ok(highs_.changeColCost(i, coefficients[i]), + "changeColCost(vec)"); + } +} + +void HiGHSSolverInterface::set_objective_coefficient(int index, double coefficient) { + highs_ok(highs_.changeColCost(index, coefficient), "changeColCost"); +} + +void HiGHSSolverInterface::set_constraint_lower_bound(int index, double bound) { + const double ub = highs_.getLp().row_upper_[index]; + highs_ok(highs_.changeRowBounds(index, bound, ub), "changeRowBounds(lb)"); +} + +void HiGHSSolverInterface::set_constraint_upper_bound(int index, double bound) { + const double lb = highs_.getLp().row_lower_[index]; + highs_ok(highs_.changeRowBounds(index, lb, bound), "changeRowBounds(ub)"); +} + +void HiGHSSolverInterface::set_variable_lower_bound(int index, double bound) { + const double ub = highs_.getLp().col_upper_[index]; + highs_ok(highs_.changeColBounds(index, bound, ub), "changeColBounds(lb)"); +} + +void HiGHSSolverInterface::set_variable_upper_bound(int index, double bound) { + const double lb = highs_.getLp().col_lower_[index]; + highs_ok(highs_.changeColBounds(index, lb, bound), "changeColBounds(ub)"); +} + +void HiGHSSolverInterface::set_mip_gap(double gap) { + highs_ok(highs_.setOptionValue("mip_rel_gap", gap), "setOptionValue(mip_rel_gap)"); +} + +void HiGHSSolverInterface::solve() { + highs_ok(highs_.run(), "run"); +} + + +void HiGHSSolverInterface::write_lp(const std::string &filename) const { + highs_ok(highs_.writeModel(filename), "writeModel"); +} + +void HiGHSSolverInterface::print_failure_analysis() const { + cout << highs_.modelStatusToString(highs_.getModelStatus()) << endl; +} + +bool HiGHSSolverInterface::is_infeasible() const { + return highs_.getModelStatus() == HighsModelStatus::kInfeasible; +} + +bool HiGHSSolverInterface::is_unbounded() const { + return highs_.getModelStatus() == HighsModelStatus::kUnbounded; +} + +bool HiGHSSolverInterface::has_optimal_solution() const { + return highs_.getModelStatus() == HighsModelStatus::kOptimal; +} + +double HiGHSSolverInterface::get_objective_value() const { + assert(has_optimal_solution()); + const HighsInfo& info = highs_.getInfo(); + return info.objective_function_value; +} + +std::vector HiGHSSolverInterface::extract_solution() const { + assert (has_optimal_solution()); + const HighsSolution sol = highs_.getSolution(); + const int n = highs_.getNumCol(); + std::vector x(n, 0.0); + for (int i = 0; i < n; i++){ + x[i] = sol.col_value[i]; + } + return x; +} + +int HiGHSSolverInterface::get_num_variables() const { + return highs_.getNumCol(); +} + +int HiGHSSolverInterface::get_num_constraints() const { + return highs_.getNumRow(); +} + +bool HiGHSSolverInterface::has_temporary_constraints() const { + return num_temporary_constraints != 0; +} + +void HiGHSSolverInterface::print_statistics() const { + cout << highs_.modelStatusToString(highs_.getModelStatus()) << endl; +} // namespace lp + +} diff --git a/src/search/lp/highs_solver_interface.h b/src/search/lp/highs_solver_interface.h new file mode 100644 index 0000000000..1f6004a25d --- /dev/null +++ b/src/search/lp/highs_solver_interface.h @@ -0,0 +1,56 @@ + +#ifndef LP_HIGHS_SOLVER_INTERFACE_H +#define LP_HIGHS_SOLVER_INTERFACE_H + +#include "Highs.h" +#include "lp_solver.h" +#include "solver_interface.h" + + + +namespace lp { +class HiGHSSolverInterface : public SolverInterface { + mutable Highs highs_; + HighsModelStatus model_status_ = HighsModelStatus::kNotset; + int num_permanent_constraints; + int num_temporary_constraints; +public: + HiGHSSolverInterface(); + + virtual void load_problem(const LinearProgram &lp) override; + virtual void add_temporary_constraints( + const named_vector::NamedVector &constraints) override; + virtual void clear_temporary_constraints() override; + virtual double get_infinity() const override; + + virtual void set_objective_coefficients( + const std::vector &coefficients) override; + virtual void set_objective_coefficient( + int index, double coefficient) override; + virtual void set_constraint_lower_bound(int index, double bound) override; + virtual void set_constraint_upper_bound(int index, double bound) override; + virtual void set_variable_lower_bound(int index, double bound) override; + virtual void set_variable_upper_bound(int index, double bound) override; + + virtual void set_mip_gap(double gap) override; + + virtual void solve() override; + virtual void write_lp(const std::string &filename) const override; + virtual void print_failure_analysis() const override; + virtual bool is_infeasible() const override; + virtual bool is_unbounded() const override; + + virtual bool has_optimal_solution() const override; + + virtual double get_objective_value() const override; + + virtual std::vector extract_solution() const override; + + virtual int get_num_variables() const override; + virtual int get_num_constraints() const override; + virtual bool has_temporary_constraints() const override; + virtual void print_statistics() const override; +}; +} + +#endif diff --git a/src/search/lp/lp_solver.cc b/src/search/lp/lp_solver.cc index a82d5eb78f..ff7134aa18 100644 --- a/src/search/lp/lp_solver.cc +++ b/src/search/lp/lp_solver.cc @@ -9,6 +9,9 @@ #ifdef HAS_GUROBI #include "gurobi_solver_interface.h" #endif +#ifdef HAS_HIGHS +#include "highs_solver_interface.h" +#endif #include "../plugins/plugin.h" @@ -147,6 +150,14 @@ LPSolver::LPSolver(LPSolverType solver_type) { missing_solver = "Gurobi"; #endif break; + case LPSolverType::HIGHS: +#ifdef HAS_HIGHS + pimpl = make_unique(); +#else + missing_solver = "HiGHS"; +#endif + break; + default: ABORT("Unknown LP solver type."); } @@ -256,5 +267,6 @@ void LPSolver::print_statistics() const { static plugins::TypedEnumPlugin _enum_plugin( {{"cplex", "commercial solver by IBM"}, {"soplex", "open source solver by ZIB"}, + {"highs", "open source solver by the HiGHS team"}, {"gurobi", "commercial solver by Gurobi"}}); } diff --git a/src/search/lp/lp_solver.h b/src/search/lp/lp_solver.h index 9be78079b3..f3aaf8011e 100644 --- a/src/search/lp/lp_solver.h +++ b/src/search/lp/lp_solver.h @@ -18,6 +18,7 @@ namespace lp { enum class LPSolverType { CPLEX, SOPLEX, + HIGHS, GUROBI }; From 45ebdafc933a86ad83f28c43379f125a49745723 Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Wed, 4 Feb 2026 23:07:39 +0100 Subject: [PATCH 04/15] update gurobi interface --- src/search/lp/gurobi_solver_interface.cc | 99 ++++++++++-------------- 1 file changed, 41 insertions(+), 58 deletions(-) diff --git a/src/search/lp/gurobi_solver_interface.cc b/src/search/lp/gurobi_solver_interface.cc index 6738b7f2ff..844eaa90ae 100644 --- a/src/search/lp/gurobi_solver_interface.cc +++ b/src/search/lp/gurobi_solver_interface.cc @@ -46,6 +46,7 @@ NO_RETURN void handle_gurobi_error(GRBenv *env, int error_code) { const char *msg = env ? GRBgeterrormsg(env) : nullptr; if (msg) { cerr << msg << endl; + cerr << "Gurobi error code: " << error_code << endl; } else { cerr << "Gurobi error: code " << error_code << endl; } @@ -85,25 +86,21 @@ void add_constraint(GRBenv *env, GRBmodel *model, const LPConstraint &constraint double lb = constraint.get_lower_bound(); double ub = constraint.get_upper_bound(); - if (lb > ub) { - // Add a trivially unsatisfiable row. - // TODO: should we handle this differently? - GRB_CALL(env, GRBaddconstr, model, 0, nullptr, nullptr, GRB_LESS_EQUAL, -1.0, nullptr); - return; - } - if (is_neg_infinity(lb) && is_pos_infinity(ub)) { - // Constraint without bounds: do nothing. - // TODO: is this the right behavior? - return; - } - if (is_neg_infinity(lb)) { + + if (!is_neg_infinity(lb) && !is_pos_infinity(ub)) { + cerr << "Error: Two-sided constraints are not supported by Gurobi." << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + } else if (is_neg_infinity(lb) && is_pos_infinity(ub)) { + cerr << " Adding free constraint." << endl; + GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, ub, nullptr); // We add a <= +infinity constraint whenever we have a free constraint. Whether the sense is '<=' or '>=' will be handled by the bounds. + }else if (is_neg_infinity(lb)) { GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, ub, nullptr); } else if (is_pos_infinity(ub)) { GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_GREATER_EQUAL, lb, nullptr); } else if (lb == ub) { GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_EQUAL, lb, nullptr); } else { - cerr << "Range constraints are not supported by Gurobi." << endl; + cerr << "Two-sided constraints are not supported by Gurobi." << endl; utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); } } @@ -122,7 +119,7 @@ GurobiSolverInterface::~GurobiSolverInterface() { if (model) { GRBfreemodel(model); } - if (env) { + if (env) { GRBfreeenv(env); } } @@ -155,20 +152,20 @@ void GurobiSolverInterface::load_problem(const LinearProgram &lp) { GRB_CALL(env, GRBnewmodel, env, &model, "downward", num_vars, obj_ptr, lb_ptr, ub_ptr, vtype_ptr, nullptr); GRB_CALL(env, GRBsetintattr, model, GRB_INT_ATTR_MODELSENSE, objective_sense_to_gurobi(lp.get_sense())); const auto &constraints = lp.get_constraints(); + num_permanent_constraints = 0; + num_temporary_constraints = 0; for (const LPConstraint &constraint : constraints) { add_constraint(env, model, constraint); + num_permanent_constraints++; } - //GRB_CALL(env, GRBupdatemodel, model); - num_permanent_constraints = constraints.size(); - num_temporary_constraints = 0; - model_dirty = true; + GRB_CALL(env, GRBupdatemodel, model); + model_dirty = false; } void GurobiSolverInterface::add_temporary_constraints(const named_vector::NamedVector &constraints) { for (const LPConstraint &constraint : constraints) { add_constraint(env, model, constraint); } - //GRB_CALL(env, GRBupdatemodel, model); model_dirty = true; num_temporary_constraints += constraints.size(); } @@ -180,7 +177,6 @@ void GurobiSolverInterface::clear_temporary_constraints() { vector indices(num_temporary_constraints); iota(indices.begin(), indices.end(), num_permanent_constraints); GRB_CALL(env, GRBdelconstrs, model, num_temporary_constraints, indices.data()); - //GRB_CALL(env, GRBupdatemodel, model); model_dirty = true; num_temporary_constraints = 0; } @@ -190,40 +186,39 @@ double GurobiSolverInterface::get_infinity() const { } void GurobiSolverInterface::set_objective_coefficients(const vector &coefficients) { + assert(coefficients.size() == get_num_variables()); int num_coefficients = coefficients.size(); if (!num_coefficients) { return; } // TODO: is there a more elegant way to handle this? GRB_CALL(env, GRBsetdblattrarray, model, GRB_DBL_ATTR_OBJ, 0, num_coefficients, const_cast(coefficients.data())); - //GRB_CALL(env, GRBupdatemodel, model); model_dirty = true; } void GurobiSolverInterface::set_objective_coefficient(int index, double coefficient) { + assert(index >= 0 && index < get_num_variables()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_OBJ, index, coefficient); - //GRB_CALL(env, GRBupdatemodel, model); model_dirty = true; } void GurobiSolverInterface::set_constraint_lower_bound(int index, double bound) { + assert(index >= 0 && index < get_num_constraints()); char sense; + double current_rhs; GRB_CALL(env, GRBgetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, &sense); - if(sense == GRB_LESS_EQUAL) { - if (!is_neg_infinity(bound)) { + GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); + if(sense == GRB_LESS_EQUAL) { // Constraint is of the form ax <= b + if (!is_pos_infinity(current_rhs)) { // Constraint is of the form ax <= finite value cerr << "Error: cannot set lower bound on <= constraint to a finite value." << endl; utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); - } - return; - } - else if(sense == GRB_GREATER_EQUAL) { - if (is_neg_infinity(bound)) { - cerr << "Error: cannot set lower bound on >= constraint to -infinity." << endl; - utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); - } else { + } else { // Constraint is of the form ax <= infty, which is essentially a free constraint + GRB_CALL(env, GRBsetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_GREATER_EQUAL); // change sense to >= GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); - return; } } + else if(sense == GRB_GREATER_EQUAL) { // constraint is of the form ax >= b + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); + } else if(sense == GRB_EQUAL) { double current_rhs; GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); @@ -231,53 +226,46 @@ void GurobiSolverInterface::set_constraint_lower_bound(int index, double bound) cerr << "Error: cannot change lower bound on = constraint." << endl; utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); } - return; } - //GRB_CALL(env, GRBupdatemodel, model); model_dirty = true; } void GurobiSolverInterface::set_constraint_upper_bound(int index, double bound) { + assert(index >= 0 && index < get_num_constraints()); char sense; + double current_rhs; GRB_CALL(env, GRBgetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, &sense); - if(sense == GRB_LESS_EQUAL) { - if (is_pos_infinity(bound)) { - cerr << "Error: cannot set upper bound on <= constraint to +infinity." << endl; - utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); - } else { - GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); - return; - } - } - else if(sense == GRB_GREATER_EQUAL) { - if (!is_pos_infinity(bound)) { + GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); + if(sense == GRB_LESS_EQUAL) { // constraint is of the form ax <= b + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); + } else if(sense == GRB_GREATER_EQUAL) { // constraint is of the form ax >= b + if (!is_neg_infinity(current_rhs)) { // constraint is of the form ax >= finite_value cerr << "Error: cannot set upper bound on >= constraint to a finite value." << endl; utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + }else { // constraint is of the form ax >= -infinity, we is essentially a free constraint + GRB_CALL(env, GRBsetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_LESS_EQUAL); // change sense to <= + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); } - return; - } - else if(sense == GRB_EQUAL) { + } else if(sense == GRB_EQUAL) { // constraint is of the form ax = b double current_rhs; GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); if (current_rhs != bound) { cerr << "Error: cannot change upper bound on = constraint." << endl; utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); } - return; } - //GRB_CALL(env, GRBupdatemodel, model); model_dirty = true; } void GurobiSolverInterface::set_variable_lower_bound(int index, double bound) { + assert(index >= 0 && index < get_num_variables()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_LB, index, bound); - //GRB_CALL(env, GRBupdatemodel, model); model_dirty = true; } void GurobiSolverInterface::set_variable_upper_bound(int index, double bound) { + assert(index >= 0 && index < get_num_variables()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_UB, index, bound); - //GRB_CALL(env, GRBupdatemodel, model); model_dirty = true; } @@ -294,10 +282,6 @@ void GurobiSolverInterface::solve() { } void GurobiSolverInterface::write_lp(const string &filename) const { - //if (model_dirty) { - // GRB_CALL(env, GRBupdatemodel, model); - // model_dirty = false; - //} GRB_CALL(env, GRBwrite, model, filename.c_str()); } @@ -305,7 +289,6 @@ void GurobiSolverInterface::write_lp(const string &filename) const { void GurobiSolverInterface::print_failure_analysis() const { int status = 0; GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); - cout << "Gurobi status code: " << status << endl; } // TODO: check if a solution is available From 11a5754cf48cea52a7ee17704e3fd7ceaa4dfce7 Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Thu, 5 Feb 2026 09:41:11 +0100 Subject: [PATCH 05/15] comment out log message in gurobi interface --- src/search/lp/gurobi_solver_interface.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search/lp/gurobi_solver_interface.cc b/src/search/lp/gurobi_solver_interface.cc index 7faee0cf6d..83f6513b8e 100644 --- a/src/search/lp/gurobi_solver_interface.cc +++ b/src/search/lp/gurobi_solver_interface.cc @@ -91,7 +91,7 @@ void add_constraint(GRBenv *env, GRBmodel *model, const LPConstraint &constraint cerr << "Error: Two-sided constraints are not supported by Gurobi." << endl; utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); } else if (is_neg_infinity(lb) && is_pos_infinity(ub)) { - cerr << " Adding free constraint." << endl; + // cerr << " Adding free constraint." << endl; GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, ub, nullptr); // We add a <= +infinity constraint whenever we have a free constraint. Whether the sense is '<=' or '>=' will be handled by the bounds. }else if (is_neg_infinity(lb)) { GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, ub, nullptr); From 6880d520683e867cdf27b0344d530014fe689033 Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Thu, 5 Feb 2026 15:36:07 +0100 Subject: [PATCH 06/15] implement dirty hack to make things work --- .../landmark_cost_partitioning_algorithms.cc | 4 ++-- src/search/lp/gurobi_solver_interface.cc | 18 +++++++++++++++--- src/search/lp/highs_solver_interface.cc | 5 ++++- .../delete_relaxation_rr_constraints.cc | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/search/landmarks/landmark_cost_partitioning_algorithms.cc b/src/search/landmarks/landmark_cost_partitioning_algorithms.cc index 18a1c0b4bc..d149737b91 100644 --- a/src/search/landmarks/landmark_cost_partitioning_algorithms.cc +++ b/src/search/landmarks/landmark_cost_partitioning_algorithms.cc @@ -190,9 +190,9 @@ lp::LinearProgram OptimalCostPartitioningAlgorithm::build_initial_lp() { say that the operator's total cost must fall between 0 and the real operator cost. */ - lp_constraints.resize(num_rows, lp::LPConstraint(0.0, 0.0)); + lp_constraints.resize(num_rows, lp::LPConstraint(-lp_solver.get_infinity(), 0.0)); // TODO: is this right? for (size_t op_id = 0; op_id < operator_costs.size(); ++op_id) { - lp_constraints[op_id].set_lower_bound(0); + //lp_constraints[op_id].set_lower_bound(0); TODO:: is this right? lp_constraints[op_id].set_upper_bound(operator_costs[op_id]); } diff --git a/src/search/lp/gurobi_solver_interface.cc b/src/search/lp/gurobi_solver_interface.cc index 7faee0cf6d..4d62e2b176 100644 --- a/src/search/lp/gurobi_solver_interface.cc +++ b/src/search/lp/gurobi_solver_interface.cc @@ -87,11 +87,22 @@ void add_constraint(GRBenv *env, GRBmodel *model, const LPConstraint &constraint double lb = constraint.get_lower_bound(); double ub = constraint.get_upper_bound(); - if (!is_neg_infinity(lb) && !is_pos_infinity(ub)) { + if (!is_neg_infinity(lb) && !is_pos_infinity(ub) && lb != ub) { cerr << "Error: Two-sided constraints are not supported by Gurobi." << endl; + cerr << "Constraint: " << lb << " <= "; + for (size_t i = 0; i < indices.size(); ++i) { + if (i > 0) { + cerr << " + "; + } + cerr << "x" << indices[i] << " * " << coefficients[i]; + } + cerr << " <= " << ub << endl; + cerr << "Infinity value: " << GRB_INFINITY << endl; + cerr << "Lower bound is finite: " << !is_neg_infinity(lb) << endl; + cerr << "Upper bound is finite: " << !is_pos_infinity(ub) << endl; utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); } else if (is_neg_infinity(lb) && is_pos_infinity(ub)) { - cerr << " Adding free constraint." << endl; + //cerr << " Adding free constraint." << endl; GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, ub, nullptr); // We add a <= +infinity constraint whenever we have a free constraint. Whether the sense is '<=' or '>=' will be handled by the bounds. }else if (is_neg_infinity(lb)) { GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, ub, nullptr); @@ -108,7 +119,8 @@ void add_constraint(GRBenv *env, GRBmodel *model, const LPConstraint &constraint } // Have to add this otherwise the compiler complains. GurobiSolverInterface::GurobiSolverInterface(): env(nullptr), model(nullptr), num_permanent_constraints(0), num_temporary_constraints(0), model_dirty(false) { - GRB_CALL(env, GRBloadenv, &env, ""); + //GRB_CALL(env, GRBloadenv, &env, ""); + GRBloadenv(&env, ""); GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_OUTPUTFLAG, 0); GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_LOGTOCONSOLE, 0); GRB_CALL(env, GRBsetintparam, env, GRB_INT_PAR_THREADS, 1); diff --git a/src/search/lp/highs_solver_interface.cc b/src/search/lp/highs_solver_interface.cc index cb16620556..c766cbb5f3 100644 --- a/src/search/lp/highs_solver_interface.cc +++ b/src/search/lp/highs_solver_interface.cc @@ -26,6 +26,9 @@ void HiGHSSolverInterface::load_problem(const LinearProgram &lp) { highs_ok(highs_.changeObjectiveSense(ObjSense::kMinimize), "changeObjectiveSense"); } + highs_ok(highs_.setOptionValue("output_flag", false), "setOptionValue(output_flag)"); + highs_ok(highs_.setOptionValue("log_to_console", false), "setOptionValue(log_to_console)"); + // variables named_vector::NamedVector vars = lp.get_variables(); const int n = static_cast(vars.size()); @@ -61,7 +64,7 @@ void HiGHSSolverInterface::load_problem(const LinearProgram &lp) { num_permanent_constraints = lp.get_constraints().size(); num_temporary_constraints = 0; - cout << "so far so good" << endl; + //cout << "so far so good" << endl; } void HiGHSSolverInterface::add_temporary_constraints( diff --git a/src/search/operator_counting/delete_relaxation_rr_constraints.cc b/src/search/operator_counting/delete_relaxation_rr_constraints.cc index 65fe249ebd..b047fbb083 100644 --- a/src/search/operator_counting/delete_relaxation_rr_constraints.cc +++ b/src/search/operator_counting/delete_relaxation_rr_constraints.cc @@ -348,7 +348,7 @@ void DeleteRelaxationRRConstraints::create_constraints( pair key = make_pair(pre, eff); if (!constraint3_ids.contains(key)) { constraint3_ids[key] = constraints.size(); - lp::LPConstraint constraint(0, 1); + lp::LPConstraint constraint(0, lp.get_infinity()); // TODO: is this right? constraint.insert(lp_var_ids.id_of_fp(pre), 1); constraints.push_back(move(constraint)); } From ea7dd69aab3ecc948a4be4676fc5fb97e83c25c6 Mon Sep 17 00:00:00 2001 From: Travis Rivera Petit Date: Mon, 9 Feb 2026 17:42:40 +0100 Subject: [PATCH 07/15] Update interface to RHS + sense --- src/search/lp/lp_solver.cc | 52 ++++++++++++++++---------------------- src/search/lp/lp_solver.h | 40 ++++++++++++++--------------- 2 files changed, 42 insertions(+), 50 deletions(-) diff --git a/src/search/lp/lp_solver.cc b/src/search/lp/lp_solver.cc index ff7134aa18..81d355e181 100644 --- a/src/search/lp/lp_solver.cc +++ b/src/search/lp/lp_solver.cc @@ -18,6 +18,17 @@ using namespace std; namespace lp { + +std::ostream& operator<<(std::ostream& os, Sense s) { + switch (s) { + case Sense::GE: return os << ">="; + case Sense::LE: return os << "<="; + case Sense::EQ: return os << "=="; + } + return os; +} + + void add_lp_solver_option_to_feature(plugins::Feature &feature) { feature.add_option( "lpsolver", @@ -35,8 +46,8 @@ tuple get_lp_solver_arguments_from_options( return make_tuple(opts.get("lpsolver")); } -LPConstraint::LPConstraint(double lower_bound, double upper_bound) - : lower_bound(lower_bound), upper_bound(upper_bound) { +LPConstraint::LPConstraint(Sense sense, double right_hand_side) + : sense(sense), right_hand_side(right_hand_side) { } void LPConstraint::clear() { @@ -55,13 +66,6 @@ void LPConstraint::insert(int index, double coefficient) { ostream &LPConstraint::dump( ostream &stream, const LinearProgram *program) const { - double infinity = numeric_limits::infinity(); - if (program) { - infinity = program->get_infinity(); - } - if (lower_bound != -infinity) { - stream << lower_bound << " <= "; - } for (size_t i = 0; i < variables.size(); ++i) { if (i != 0) stream << " + "; @@ -75,19 +79,14 @@ ostream &LPConstraint::dump( } stream << coefficients[i] << " * " << variable_name; } - if (upper_bound != infinity) { - stream << " <= " << upper_bound; - } else if (lower_bound == -infinity) { - stream << " <= infinity"; - } + stream << get_sense() << get_right_hand_side(); return stream; } LPVariable::LPVariable( - double lower_bound, double upper_bound, double objective_coefficient, - bool is_integer) - : lower_bound(lower_bound), - upper_bound(upper_bound), + Sense sense, double right_hand_side, double objective_coefficient, bool is_integer = false) + : sense(sense), + right_hand_side(right_hand_side), objective_coefficient(objective_coefficient), is_integer(is_integer) { } @@ -196,23 +195,16 @@ void LPSolver::set_objective_coefficient(int index, double coefficient) { pimpl->set_objective_coefficient(index, coefficient); } -void LPSolver::set_constraint_lower_bound(int index, double bound) { - pimpl->set_constraint_lower_bound(index, bound); -} - -void LPSolver::set_constraint_upper_bound(int index, double bound) { - pimpl->set_constraint_upper_bound(index, bound); -} - -void LPSolver::set_variable_lower_bound(int index, double bound) { - pimpl->set_variable_lower_bound(index, bound); +void LPSolver::set_constraint_bound(int index, Sense sense, double right_hand_side) { + pimpl->set_constraint_bound(index, sense, right_hand_side); } -void LPSolver::set_variable_upper_bound(int index, double bound) { - pimpl->set_variable_upper_bound(index, bound); +void LPSolver::set_variable_bound(int index, Sense sense, double right_hand_side) { + pimpl->set_variable_bound(index, sense, right_hand_side); } void LPSolver::set_mip_gap(double gap) { + // relative mip gap pimpl->set_mip_gap(gap); } diff --git a/src/search/lp/lp_solver.h b/src/search/lp/lp_solver.h index f3aaf8011e..ff9118501d 100644 --- a/src/search/lp/lp_solver.h +++ b/src/search/lp/lp_solver.h @@ -33,13 +33,20 @@ std::tuple get_lp_solver_arguments_from_options( class LinearProgram; +enum class Sense { + GE, LE, EQ +}; + +std::ostream& operator<<(std::ostream& os, Sense s); + + class LPConstraint { std::vector variables; std::vector coefficients; - double lower_bound; - double upper_bound; + Sense sense; + double right_hand_side; public: - LPConstraint(double lower_bound, double upper_bound); + LPConstraint(Sense sense, double right_hand_side); const std::vector &get_variables() const { return variables; @@ -48,17 +55,12 @@ class LPConstraint { return coefficients; } - double get_lower_bound() const { - return lower_bound; + double get_right_hand_side() const { + return right_hand_side; } - void set_lower_bound(double lb) { - lower_bound = lb; - } - double get_upper_bound() const { - return upper_bound; - } - void set_upper_bound(double ub) { - upper_bound = ub; + + Sense get_sense() const { + return sense; } void clear(); @@ -71,13 +73,13 @@ class LPConstraint { }; struct LPVariable { - double lower_bound; - double upper_bound; + Sense sense; + double right_hand_side; double objective_coefficient; bool is_integer; LPVariable( - double lower_bound, double upper_bound, double objective_coefficient, + Sense sense, double right_hand_side, double objective_coefficient, bool is_integer = false); }; @@ -130,10 +132,8 @@ class LPSolver { void set_objective_coefficients(const std::vector &coefficients); void set_objective_coefficient(int index, double coefficient); - void set_constraint_lower_bound(int index, double bound); - void set_constraint_upper_bound(int index, double bound); - void set_variable_lower_bound(int index, double bound); - void set_variable_upper_bound(int index, double bound); + void set_constraint_bound(int index, Sense sense, double right_hand_side); + void set_variable_bound(int index, Sense sense, double right_hand_side); void set_mip_gap(double gap); From 4af54063f3225e1bcc4f54a96c29053cf5521f1c Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Wed, 11 Feb 2026 09:08:06 +0100 Subject: [PATCH 08/15] add new lp interface; update algorithms to comply with the new interface --- .../landmark_cost_partitioning_algorithms.cc | 26 +++- src/search/lp/lp_solver.cc | 25 ++-- src/search/lp/lp_solver.h | 24 +++- src/search/lp/solver_interface.h | 7 +- .../delete_relaxation_if_constraints.cc | 94 ++++++++++++-- .../delete_relaxation_rr_constraints.cc | 118 +++++++++++++++--- .../operator_counting/lm_cut_constraints.cc | 12 +- .../operator_counting/pho_constraints.cc | 22 +++- .../state_equation_constraints.cc | 23 +++- src/search/potentials/potential_optimizer.cc | 23 +++- 10 files changed, 320 insertions(+), 54 deletions(-) diff --git a/src/search/landmarks/landmark_cost_partitioning_algorithms.cc b/src/search/landmarks/landmark_cost_partitioning_algorithms.cc index d149737b91..81dc2878a2 100644 --- a/src/search/landmarks/landmark_cost_partitioning_algorithms.cc +++ b/src/search/landmarks/landmark_cost_partitioning_algorithms.cc @@ -190,12 +190,30 @@ lp::LinearProgram OptimalCostPartitioningAlgorithm::build_initial_lp() { say that the operator's total cost must fall between 0 and the real operator cost. */ - lp_constraints.resize(num_rows, lp::LPConstraint(-lp_solver.get_infinity(), 0.0)); // TODO: is this right? + + /* + OLD INTERFACE CODE + lp_constraints.resize(num_rows, lp::LPConstraint(0.0, 0.0)); + for (size_t op_id = 0; op_id < operator_costs.size(); ++op_id) { + lp_constraints[op_id].set_lower_bound(0); + lp_constraints[op_id].set_upper_bound(operator_costs[op_id]); + } + NEW INTERFACE CODE + lp_constraints.resize(num_rows, lp::LPConstraint(lp::Sense::LE, 0.0)); + for (size_t op_id = 0; op_id < operator_costs.size(); ++op_id) { + lp_constraints[op_id].set_right_hand_side(operator_costs[op_id]); + } + + All variables are by default non-negative, so we only need to set the upper bounds of the constraints to the operator costs. + + TODO: double check that the lower bound is indeed redundant. + */ + lp_constraints.resize(num_rows, lp::LPConstraint(lp::Sense::LE, 0.0)); for (size_t op_id = 0; op_id < operator_costs.size(); ++op_id) { - //lp_constraints[op_id].set_lower_bound(0); TODO:: is this right? - lp_constraints[op_id].set_upper_bound(operator_costs[op_id]); + lp_constraints[op_id].set_right_hand_side(operator_costs[op_id]); } + /* Coefficients of constraints will be updated and recreated in each state. We ignore them for the initial LP. */ return lp::LinearProgram( @@ -295,4 +313,4 @@ double OptimalCostPartitioningAlgorithm::get_cost_partitioned_heuristic_value( assert(lp_solver.has_optimal_solution()); return lp_solver.get_objective_value(); } -} +} \ No newline at end of file diff --git a/src/search/lp/lp_solver.cc b/src/search/lp/lp_solver.cc index 81d355e181..e98ad9b26a 100644 --- a/src/search/lp/lp_solver.cc +++ b/src/search/lp/lp_solver.cc @@ -64,6 +64,7 @@ void LPConstraint::insert(int index, double coefficient) { coefficients.push_back(coefficient); } +// TODO: double check that this preserves the semantics of the old implementation. ostream &LPConstraint::dump( ostream &stream, const LinearProgram *program) const { for (size_t i = 0; i < variables.size(); ++i) { @@ -84,9 +85,10 @@ ostream &LPConstraint::dump( } LPVariable::LPVariable( - Sense sense, double right_hand_side, double objective_coefficient, bool is_integer = false) - : sense(sense), - right_hand_side(right_hand_side), + double lower_bound, double upper_bound, double objective_coefficient, + bool is_integer) + : lower_bound(lower_bound), + upper_bound(upper_bound), objective_coefficient(objective_coefficient), is_integer(is_integer) { } @@ -195,16 +197,23 @@ void LPSolver::set_objective_coefficient(int index, double coefficient) { pimpl->set_objective_coefficient(index, coefficient); } -void LPSolver::set_constraint_bound(int index, Sense sense, double right_hand_side) { - pimpl->set_constraint_bound(index, sense, right_hand_side); +void LPSolver::set_constraint_rhs(int index, double right_hand_side) { + pimpl->set_constraint_rhs(index, right_hand_side); } -void LPSolver::set_variable_bound(int index, Sense sense, double right_hand_side) { - pimpl->set_variable_bound(index, sense, right_hand_side); +void LPSolver::set_constraint_sense(int index, Sense sense) { + pimpl->set_constraint_sense(index, sense); +} + +void LPSolver::set_variable_lower_bound(int index, double bound) { + pimpl->set_variable_lower_bound(index, bound); +} + +void LPSolver::set_variable_upper_bound(int index, double bound) { + pimpl->set_variable_upper_bound(index, bound); } void LPSolver::set_mip_gap(double gap) { - // relative mip gap pimpl->set_mip_gap(gap); } diff --git a/src/search/lp/lp_solver.h b/src/search/lp/lp_solver.h index ff9118501d..f2a302123c 100644 --- a/src/search/lp/lp_solver.h +++ b/src/search/lp/lp_solver.h @@ -34,7 +34,9 @@ std::tuple get_lp_solver_arguments_from_options( class LinearProgram; enum class Sense { - GE, LE, EQ + GE, // ax >= b + LE, // ax <= b + EQ // ax = b }; std::ostream& operator<<(std::ostream& os, Sense s); @@ -63,6 +65,14 @@ class LPConstraint { return sense; } + void set_right_hand_side(double rhs) { + right_hand_side = rhs; + } + + void set_sense(Sense s) { + sense = s; + } + void clear(); bool empty() const; // Coefficients must be added without duplicate indices. @@ -73,13 +83,13 @@ class LPConstraint { }; struct LPVariable { - Sense sense; - double right_hand_side; + double lower_bound; + double upper_bound; double objective_coefficient; bool is_integer; LPVariable( - Sense sense, double right_hand_side, double objective_coefficient, + double lower_bound, double upper_bound, double objective_coefficient, bool is_integer = false); }; @@ -132,8 +142,10 @@ class LPSolver { void set_objective_coefficients(const std::vector &coefficients); void set_objective_coefficient(int index, double coefficient); - void set_constraint_bound(int index, Sense sense, double right_hand_side); - void set_variable_bound(int index, Sense sense, double right_hand_side); + void set_constraint_rhs(int index, double right_hand_side); + void set_constraint_sense(int index, Sense sense); + void set_variable_lower_bound(int index, double bound); + void set_variable_upper_bound(int index, double bound); void set_mip_gap(double gap); diff --git a/src/search/lp/solver_interface.h b/src/search/lp/solver_interface.h index 8699a6930c..e636812eea 100644 --- a/src/search/lp/solver_interface.h +++ b/src/search/lp/solver_interface.h @@ -12,6 +12,7 @@ class NamedVector; namespace lp { class LinearProgram; class LPConstraint; +enum class Sense; class SolverInterface { public: @@ -26,8 +27,8 @@ class SolverInterface { virtual void set_objective_coefficients( const std::vector &coefficients) = 0; virtual void set_objective_coefficient(int index, double coefficient) = 0; - virtual void set_constraint_lower_bound(int index, double bound) = 0; - virtual void set_constraint_upper_bound(int index, double bound) = 0; + virtual void set_constraint_rhs(int index, double right_hand_side) = 0; + virtual void set_constraint_sense(int index, lp::Sense sense) = 0; virtual void set_variable_lower_bound(int index, double bound) = 0; virtual void set_variable_upper_bound(int index, double bound) = 0; @@ -70,4 +71,4 @@ class SolverInterface { }; } -#endif +#endif \ No newline at end of file diff --git a/src/search/operator_counting/delete_relaxation_if_constraints.cc b/src/search/operator_counting/delete_relaxation_if_constraints.cc index e52818edb4..1ed1b9a3ce 100644 --- a/src/search/operator_counting/delete_relaxation_if_constraints.cc +++ b/src/search/operator_counting/delete_relaxation_if_constraints.cc @@ -102,7 +102,6 @@ void DeleteRelaxationIFConstraints::create_constraints( const TaskProxy &task_proxy, lp::LinearProgram &lp) { LPVariables &variables = lp.get_variables(); LPConstraints &constraints = lp.get_constraints(); - double infinity = lp.get_infinity(); OperatorsProxy ops = task_proxy.get_operators(); VariablesProxy vars = task_proxy.get_variables(); @@ -126,7 +125,19 @@ void DeleteRelaxationIFConstraints::create_constraints( constraint_ids[var_id].resize(var.get_domain_size()); for (int value = 0; value < var.get_domain_size(); ++value) { constraint_ids[var_id][value] = constraints.size(); - constraints.emplace_back(0, infinity); + + /* + OLD INTERFACE CODE + constraints.emplace_back(0, infinity); + NEW INTERFACE CODE + constraints.emplace_back(lp::Sense::GE, 0); + + This creates a constraint of the form 0 <= ax <= infinity, which is equivalent to ax >= 0.\ + TODO: double check. + */ + constraints.emplace_back(lp::Sense::GE, 0); + + /* We add "- R_f" here, collect the achiever below and adapt the lower bound in each iteration, i.e., in update_constraints. */ @@ -149,7 +160,17 @@ void DeleteRelaxationIFConstraints::create_constraints( for (OperatorProxy op : ops) { for (EffectProxy eff : op.get_effects()) { FactPair f = eff.get_fact().get_pair(); - lp::LPConstraint constraint(0, infinity); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(0, infinity); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::GE, 0); + + This creates a constraint of the form 0 <= ax <= infinity, which is equivalent to ax >= 0. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::GE, 0); + constraint.insert(get_var_op_used(op), 1); constraint.insert(get_var_first_achiever(op, f), -1); constraints.push_back(constraint); @@ -162,7 +183,16 @@ void DeleteRelaxationIFConstraints::create_constraints( */ for (OperatorProxy op : ops) { for (FactProxy f : op.get_preconditions()) { - lp::LPConstraint constraint(0, infinity); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(0, infinity); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::GE, 0); + + This creates a constraint of the form 0 <= ax <= infinity, which is equivalent to ax >= 0. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::GE, 0); constraint.insert(get_var_fact_reached(f.get_pair()), 1); constraint.insert(get_var_op_used(op), -1); constraints.push_back(constraint); @@ -176,7 +206,16 @@ void DeleteRelaxationIFConstraints::create_constraints( */ for (OperatorProxy op : ops) { for (FactProxy f : op.get_preconditions()) { - lp::LPConstraint constraint(0, infinity); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(0, infinity); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::GE, 0); + + This creates a constraint of the form 0 <= ax <= infinity, which is equivalent to ax >= 0. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::GE, 0); constraint.insert(get_var_op_time(op), 1); constraint.insert(get_var_fact_time(f.get_pair()), -1); constraints.push_back(constraint); @@ -195,7 +234,16 @@ void DeleteRelaxationIFConstraints::create_constraints( for (OperatorProxy op : ops) { for (EffectProxy eff : op.get_effects()) { FactPair f = eff.get_fact().get_pair(); - lp::LPConstraint constraint(1 - M, infinity); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(1 - M, infinity); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::GE, 1 - M); + + This creates a constraint of the form 1 - M <= ax <= infinity, which is equivalent to ax >= 1 - M. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::GE, 1 - M); constraint.insert(get_var_fact_time(f), 1); constraint.insert(get_var_op_time(op), -1); constraint.insert(get_var_first_achiever(op, f), -M); @@ -209,7 +257,16 @@ void DeleteRelaxationIFConstraints::create_constraints( U_o <= C_o for each operator o. */ for (OperatorProxy op : ops) { - lp::LPConstraint constraint(0, infinity); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(0, infinity); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::GE, 0); + + This creates a constraint of the form 0 <= ax <= infinity, which is equivalent to ax >= 0. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::GE, 0); constraint.insert(op.get_id(), 1); constraint.insert(get_var_op_used(op), -1); constraints.push_back(constraint); @@ -227,13 +284,30 @@ bool DeleteRelaxationIFConstraints::update_constraints( const State &state, lp::LPSolver &lp_solver) { // Unset old bounds. for (FactPair f : last_state) { - lp_solver.set_constraint_lower_bound(get_constraint_id(f), 0); + /* + OLD INTERFACE CODE + lp_solver.set_constraint_lower_bound(get_constraint_id(f), 0); + NEW INTERFACE CODE + lp_solver.set_constraint_rhs(get_constraint_id(f), 0); + + All created constraints have sense >= (lp::Sense::GE), so setting the rhs to 0 is equivalent to setting the lower bound to 0. + TODO: double check. + */ + lp_solver.set_constraint_rhs(get_constraint_id(f), 0); } last_state.clear(); // Set new bounds. for (FactProxy f : state) { - lp_solver.set_constraint_lower_bound( - get_constraint_id(f.get_pair()), -1); + /* + OLD INTERFACE CODE + lp_solver.set_constraint_lower_bound(get_constraint_id(f.get_pair()), -1); + NEW INTERFACE CODE + lp_solver.set_constraint_rhs(get_constraint_id(f.get_pair()), -1); + + All created constraints have sense >= (lp::Sense::GE), so setting the rhs to -1 is equivalent to setting the lower bound to -1. + TODO: double check. + */ + lp_solver.set_constraint_rhs(get_constraint_id(f.get_pair()), -1); last_state.push_back(f.get_pair()); } return false; diff --git a/src/search/operator_counting/delete_relaxation_rr_constraints.cc b/src/search/operator_counting/delete_relaxation_rr_constraints.cc index b047fbb083..14765ce45e 100644 --- a/src/search/operator_counting/delete_relaxation_rr_constraints.cc +++ b/src/search/operator_counting/delete_relaxation_rr_constraints.cc @@ -286,7 +286,6 @@ void DeleteRelaxationRRConstraints::create_constraints( lp::LinearProgram &lp) { LPVariables &variables = lp.get_variables(); LPConstraints &constraints = lp.get_constraints(); - double infinity = lp.get_infinity(); OperatorsProxy ops = task_proxy.get_operators(); VariablesProxy vars = task_proxy.get_variables(); @@ -309,7 +308,16 @@ void DeleteRelaxationRRConstraints::create_constraints( constraint_offsets.push_back(constraints.size()); for (int value_p = 0; value_p < var_p.get_domain_size(); ++value_p) { FactPair fact_p(var_id_p, value_p); - lp::LPConstraint constraint(0, 0); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(0, 0); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::EQ, 0); + + This creates a constraint of the form 0 <= ax <= 0, which is equivalent to ax = 0. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::EQ, 0); constraint.insert(lp_var_ids.id_of_fp(fact_p), 1); constraints.push_back(move(constraint)); } @@ -348,7 +356,20 @@ void DeleteRelaxationRRConstraints::create_constraints( pair key = make_pair(pre, eff); if (!constraint3_ids.contains(key)) { constraint3_ids[key] = constraints.size(); - lp::LPConstraint constraint(0, lp.get_infinity()); // TODO: is this right? + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(0, 1); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::GE, 0); + + This creates a constraint sum_{a in A where q in pre(a) and p in add(a)} f_{p,a} <= f_q, which is equivalent to 0 <= f_q - sum_{a in A where q in pre(a) and p in add(a)} f_{p,a}. + Variables f_q have a lower bound of 0 and an upper bound of 1. + Variables f_{p,a} have a lower bound of 0 and an upper bound of 1. + The upper bound of the constraint is necessarily 1. We only need to set the lower bound to 0 to get the correct semantics. + + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::GE, 0); constraint.insert(lp_var_ids.id_of_fp(pre), 1); constraints.push_back(move(constraint)); } @@ -388,7 +409,16 @@ void DeleteRelaxationRRConstraints::create_constraints( for (OperatorProxy op : ops) { for (EffectProxy eff_proxy : op.get_effects()) { FactPair eff = eff_proxy.get_fact().get_pair(); - lp::LPConstraint constraint(0, infinity); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(0, infinity); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::GE, 0); + + This creates a constraint of the form 0 <= ax <= infinity, which is equivalent to ax >= 0. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::GE, 0); constraint.insert(lp_var_ids.id_of_fpa(eff, op), -1); constraint.insert(op.get_id(), 1); constraints.push_back(move(constraint)); @@ -401,7 +431,6 @@ void DeleteRelaxationRRConstraints::create_constraints_ve( const DeleteRelaxationRRConstraints::LPVariableIDs &lp_var_ids, lp::LinearProgram &lp) { LPConstraints &constraints = lp.get_constraints(); - double infinity = lp.get_infinity(); OperatorsProxy ops = task_proxy.get_operators(); /* @@ -417,7 +446,16 @@ void DeleteRelaxationRRConstraints::create_constraints_ve( FactPair pre = pre_proxy.get_pair(); for (EffectProxy eff_proxy : op.get_effects()) { FactPair eff = eff_proxy.get_fact().get_pair(); - lp::LPConstraint constraint(0, infinity); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(0, infinity); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::GE, 0); + + This creates a constraint of the form 0 <= ax <= infinity, which is equivalent to ax >= 0. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::GE, 0); constraint.insert(lp_var_ids.id_of_e(make_pair(pre, eff)), 1); constraint.insert(lp_var_ids.id_of_fpa(eff, op), -1); constraints.push_back(move(constraint)); @@ -439,7 +477,16 @@ void DeleteRelaxationRRConstraints::create_constraints_ve( pair reverse_edge = make_pair(edge.second, edge.first); if (lp_var_ids.has_e(reverse_edge)) { - lp::LPConstraint constraint(-infinity, 1); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(-infinity, 1); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::LE, 1); + + This creates a constraint of the form -infinity <= ax <= 1, which is equivalent to ax <= 1. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::LE, 1); constraint.insert(lp_var_ids.id_of_e(edge), 1); constraint.insert(lp_var_ids.id_of_e(reverse_edge), 1); constraints.push_back(move(constraint)); @@ -457,7 +504,16 @@ void DeleteRelaxationRRConstraints::create_constraints_ve( not have both p_i ordered before p_j, and p_j ordered before p_k. */ for (auto [pi, pj, pk] : ve_graph.get_delta()) { - lp::LPConstraint constraint(-infinity, 1); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(-infinity, 1); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::LE, 1); + + This creates a constraint of the form -infinity <= ax <= 1, which is equivalent to ax <= 1. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::LE, 1); constraint.insert(lp_var_ids.id_of_e(make_pair(pi, pj)), 1); constraint.insert(lp_var_ids.id_of_e(make_pair(pj, pk)), 1); constraint.insert(lp_var_ids.id_of_e(make_pair(pi, pk)), -1); @@ -481,7 +537,6 @@ void DeleteRelaxationRRConstraints::create_constraints_tl( preconditions, we have to achieve p_i before p_j. */ LPConstraints &constraints = lp.get_constraints(); - double infinity = lp.get_infinity(); int num_facts = 0; for (VariableProxy var : task_proxy.get_variables()) { num_facts += var.get_domain_size(); @@ -496,7 +551,16 @@ void DeleteRelaxationRRConstraints::create_constraints_tl( // Prevail conditions are compiled away in the paper. continue; } - lp::LPConstraint constraint(-infinity, num_facts - 1); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(-infinity, num_facts - 1); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::LE, num_facts - 1); + + This creates a constraint of the form -infinity <= ax <= num_facts - 1, which is equivalent to ax <= num_facts - 1. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::LE, num_facts - 1); constraint.insert(lp_var_ids.id_of_t(pre), 1); constraint.insert(lp_var_ids.id_of_t(eff), -1); constraint.insert(lp_var_ids.id_of_fpa(eff, op), num_facts); @@ -541,15 +605,41 @@ bool DeleteRelaxationRRConstraints::update_constraints( int con_id; for (FactPair f : last_state) { con_id = get_constraint_id(f); - lp_solver.set_constraint_lower_bound(con_id, 0); - lp_solver.set_constraint_upper_bound(con_id, 0); + /* + OLD INTERFACE CODE + lp_solver.set_constraint_lower_bound(con_id, 0); + lp_solver.set_constraint_upper_bound(con_id, 0); + NEW INTERFACE CODE + lp_solver.set_constraint_sense(con_id, lp::Sense::EQ); + lp_solver.set_constraint_rhs(con_id, 0); + + This creates a constraint of the form 0 <= ax <= 0, which is equivalent to ax = 0. + + TODO: double check, + TODO: double check if the updated constraints already are equality constraints, in which case we don't need to update the sense. + */ + lp_solver.set_constraint_sense(con_id, lp::Sense::EQ); + lp_solver.set_constraint_rhs(con_id, 0); } last_state.clear(); // Set new bounds. for (FactProxy f : state) { con_id = get_constraint_id(f.get_pair()); - lp_solver.set_constraint_lower_bound(con_id, 1); - lp_solver.set_constraint_upper_bound(con_id, 1); + /* + OLD INTERFACE CODE + lp_solver.set_constraint_lower_bound(con_id, 1); + lp_solver.set_constraint_upper_bound(con_id, 1); + NEW INTERFACE CODE + lp_solver.set_constraint_sense(con_id, lp::Sense::EQ); + lp_solver.set_constraint_rhs(con_id, 1); + + This creates a constraint of the form 1 <= ax <= 1, which is equivalent to ax = 1. + + TODO: double check, + TODO: double check if the updated constraints already are equality constraints, in which case we don't need to update the sense. + */ + lp_solver.set_constraint_sense(con_id, lp::Sense::EQ); + lp_solver.set_constraint_rhs(con_id, 1); last_state.push_back(f.get_pair()); } return false; diff --git a/src/search/operator_counting/lm_cut_constraints.cc b/src/search/operator_counting/lm_cut_constraints.cc index 0f0a9ad9cb..794f9cbf38 100644 --- a/src/search/operator_counting/lm_cut_constraints.cc +++ b/src/search/operator_counting/lm_cut_constraints.cc @@ -22,11 +22,19 @@ bool LMCutConstraints::update_constraints( const State &state, lp::LPSolver &lp_solver) { assert(landmark_generator); named_vector::NamedVector constraints; - double infinity = lp_solver.get_infinity(); bool dead_end = landmark_generator->compute_landmarks( state, nullptr, [&](const vector &op_ids, int /*cost*/) { - constraints.emplace_back(1.0, infinity); + /* + OLD INTERFACE CODE + constraints.emplace_back(1.0, infinity); + NEW INTERFACE CODE + constraints.emplace_back(lp::Sense::GE, 1.0); + + This creates a constraint of the form 1.0 <= ax <= infinity, which is equivalent to ax >= 1.0. + TODO: double check. + */ + constraints.emplace_back(lp::Sense::GE, 1.0); lp::LPConstraint &landmark_constraint = constraints.back(); for (int op_id : op_ids) { landmark_constraint.insert(op_id, 1.0); diff --git a/src/search/operator_counting/pho_constraints.cc b/src/search/operator_counting/pho_constraints.cc index 3e874b4b76..3e8c30c628 100644 --- a/src/search/operator_counting/pho_constraints.cc +++ b/src/search/operator_counting/pho_constraints.cc @@ -38,7 +38,16 @@ void PhOConstraints::initialize_constraints( lp.get_constraints(); constraint_offset = constraints.size(); for (const shared_ptr &pdb : *pdbs) { - constraints.emplace_back(0, lp.get_infinity()); + /* + OLD INTERFACE CODE + constraints.emplace_back(0, lp.get_infinity()); + NEW INTERFACE CODE + constraints.emplace_back(lp::Sense::GE, 0); + + This creates a constraint of the form 0 <= ax <= infinity, which is equivalent to ax >= 0. + TODO: double check. + */ + constraints.emplace_back(lp::Sense::GE, 0); lp::LPConstraint &constraint = constraints.back(); for (OperatorProxy op : task_proxy.get_operators()) { if (pdbs::is_operator_relevant(pdb->get_pattern(), op)) { @@ -58,7 +67,16 @@ bool PhOConstraints::update_constraints( if (h == numeric_limits::max()) { return true; } - lp_solver.set_constraint_lower_bound(constraint_id, h); + /* + OLD INTERFACE CODE + lp_solver.set_constraint_lower_bound(constraint_id, h); + NEW INTERFACE CODE + lp_solver.set_constraint_rhs(constraint_id, h); + + All created constraints have sense >= (lp::Sense::GE), so setting the rhs to h is equivalent to setting the lower bound to h. + TODO: double check. + */ + lp_solver.set_constraint_rhs(constraint_id, h); } return false; } diff --git a/src/search/operator_counting/state_equation_constraints.cc b/src/search/operator_counting/state_equation_constraints.cc index 81112fcfbf..29cb657c6f 100644 --- a/src/search/operator_counting/state_equation_constraints.cc +++ b/src/search/operator_counting/state_equation_constraints.cc @@ -58,7 +58,16 @@ void StateEquationConstraints::add_constraints( named_vector::NamedVector &constraints, double infinity) { for (vector &var_propositions : propositions) { for (Proposition &prop : var_propositions) { - lp::LPConstraint constraint(-infinity, infinity); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(-infinity, infinity); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::GE, -infinity); + + StateEquationConstraints::update_constraints updates the lower bound of the constraint, making it effectively a constraint of the form ax >= b. + TODO: double check + */ + lp::LPConstraint constraint(lp::Sense::GE, -infinity); add_indices_to_constraint(constraint, prop.always_produced_by, 1.0); add_indices_to_constraint( constraint, prop.sometimes_produced_by, 1.0); @@ -111,8 +120,16 @@ bool StateEquationConstraints::update_constraints( if (goal_state[var] == value) { ++lower_bound; } - lp_solver.set_constraint_lower_bound( - prop.constraint_index, lower_bound); + /* + OLD INTERFACE CODE + lp_solver.set_constraint_lower_bound(prop.constraint_index, lower_bound); + NEW INTERFACE CODE + lp_solver.set_constraint_rhs(prop.constraint_index, lower_bound); + + All created constraints have sense >= (lp::Sense::GE), so setting the rhs to lower_bound is equivalent to setting the lower bound to lower_bound. + TODO: double check. + */ + lp_solver.set_constraint_rhs(prop.constraint_index, lower_bound); } } } diff --git a/src/search/potentials/potential_optimizer.cc b/src/search/potentials/potential_optimizer.cc index 6a1c5955cd..3a0012d9cf 100644 --- a/src/search/potentials/potential_optimizer.cc +++ b/src/search/potentials/potential_optimizer.cc @@ -118,7 +118,16 @@ void PotentialOptimizer::construct_lp() { for (FactProxy pre : op.get_preconditions()) { var_to_precondition[pre.get_variable().get_id()] = pre.get_value(); } - lp::LPConstraint constraint(-infinity, op.get_cost()); + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(-infinity, op.get_cost()); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::LE, op.get_cost()); + + This creates a constraint of the form -infinity <= ax <= cost(o), which is equivalent to ax <= cost(o). + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::LE, op.get_cost()); vector> coefficients; for (EffectProxy effect : op.get_effects()) { VariableProxy var = effect.get_fact().get_variable(); @@ -185,7 +194,17 @@ void PotentialOptimizer::construct_lp() { // Create constraint: P_{V=v} <= P_{V=u} // Note that we could eliminate variables P_{V=u} if V is // undefined in the goal. - lp::LPConstraint constraint(-infinity, 0); + + /* + OLD INTERFACE CODE + lp::LPConstraint constraint(-infinity, 0); + NEW INTERFACE CODE + lp::LPConstraint constraint(lp::Sense::LE, 0); + + This creates a constraint of the form -infinity <= ax <= 0, which is equivalent to ax <= 0. + TODO: double check. + */ + lp::LPConstraint constraint(lp::Sense::LE, 0); constraint.insert(val_lp, 1); constraint.insert(undef_val_lp, -1); lp_constraints.push_back(constraint); From 5d7e81f2729a21e2f71e7cdf1561eed7e0d15b70 Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Wed, 11 Feb 2026 10:38:00 +0100 Subject: [PATCH 09/15] implement new interface for gurobi and highs --- src/search/CMakeLists.txt | 30 ++-- src/search/lp/cplex_solver_interface.h | 4 +- src/search/lp/gurobi_solver_interface.cc | 173 +++++++++++------------ src/search/lp/gurobi_solver_interface.h | 4 +- src/search/lp/highs_solver_interface.cc | 150 ++++++++++++++++++-- src/search/lp/highs_solver_interface.h | 4 +- src/search/lp/soplex_solver_interface.h | 4 +- 7 files changed, 247 insertions(+), 122 deletions(-) diff --git a/src/search/CMakeLists.txt b/src/search/CMakeLists.txt index 037ca666f7..647867ee77 100644 --- a/src/search/CMakeLists.txt +++ b/src/search/CMakeLists.txt @@ -587,21 +587,21 @@ create_fast_downward_library( DEPENDENCY_ONLY ) if(USE_LP) - find_package(Cplex 12) - if(CPLEX_FOUND) - target_compile_definitions(lp_solver INTERFACE HAS_CPLEX) - target_link_libraries(lp_solver INTERFACE cplex::cplex) - target_sources(lp_solver INTERFACE lp/cplex_solver_interface.h lp/cplex_solver_interface.cc) - endif() - - - find_package(soplex 7.1.0 QUIET) - if (SOPLEX_FOUND) - message(STATUS "Found SoPlex: ${SOPLEX_INCLUDE_DIRS}") - target_link_libraries(lp_solver INTERFACE libsoplex) - target_compile_definitions(lp_solver INTERFACE HAS_SOPLEX) - target_sources(lp_solver INTERFACE lp/soplex_solver_interface.h lp/soplex_solver_interface.cc) - endif() + #find_package(Cplex 12) + #if(CPLEX_FOUND) + # target_compile_definitions(lp_solver INTERFACE HAS_CPLEX) + # target_link_libraries(lp_solver INTERFACE cplex::cplex) + # target_sources(lp_solver INTERFACE lp/cplex_solver_interface.h lp/cplex_solver_interface.cc) + #endif() + + + #find_package(soplex 7.1.0 QUIET) + #if (SOPLEX_FOUND) + # message(STATUS "Found SoPlex: ${SOPLEX_INCLUDE_DIRS}") + # target_link_libraries(lp_solver INTERFACE libsoplex) + # target_compile_definitions(lp_solver INTERFACE HAS_SOPLEX) + # target_sources(lp_solver INTERFACE lp/soplex_solver_interface.h lp/soplex_solver_interface.cc) + #endif() find_package(Gurobi QUIET) if(Gurobi_FOUND) diff --git a/src/search/lp/cplex_solver_interface.h b/src/search/lp/cplex_solver_interface.h index 1c2e171065..a572d0daaa 100644 --- a/src/search/lp/cplex_solver_interface.h +++ b/src/search/lp/cplex_solver_interface.h @@ -261,8 +261,8 @@ class CplexSolverInterface : public SolverInterface { const std::vector &coefficients) override; virtual void set_objective_coefficient( int index, double coefficient) override; - virtual void set_constraint_lower_bound(int index, double bound) override; - virtual void set_constraint_upper_bound(int index, double bound) override; + virtual void set_constraint_rhs(int index, double right_hand_side) override; + virtual void set_constraint_sense(int index, lp::Sense sense) override; virtual void set_variable_lower_bound(int index, double bound) override; virtual void set_variable_upper_bound(int index, double bound) override; virtual void set_mip_gap(double gap) override; diff --git a/src/search/lp/gurobi_solver_interface.cc b/src/search/lp/gurobi_solver_interface.cc index 2f045c25b9..2530953e3c 100644 --- a/src/search/lp/gurobi_solver_interface.cc +++ b/src/search/lp/gurobi_solver_interface.cc @@ -69,14 +69,6 @@ int objective_sense_to_gurobi(LPObjectiveSense sense) { } } -bool is_pos_infinity(double x) { - return x >= GRB_INFINITY; -} - -bool is_neg_infinity(double x) { - return x <= -GRB_INFINITY; -} - void add_constraint(GRBenv *env, GRBmodel *model, const LPConstraint &constraint) { const vector &indices = constraint.get_variables(); const vector &coefficients = constraint.get_coefficients(); @@ -84,37 +76,22 @@ void add_constraint(GRBenv *env, GRBmodel *model, const LPConstraint &constraint int *cind = numnz ? const_cast(indices.data()) : nullptr; double *cval = numnz ? const_cast(coefficients.data()) : nullptr; - double lb = constraint.get_lower_bound(); - double ub = constraint.get_upper_bound(); - - if (!is_neg_infinity(lb) && !is_pos_infinity(ub) && lb != ub) { - cerr << "Error: Two-sided constraints are not supported by Gurobi." << endl; - cerr << "Constraint: " << lb << " <= "; - for (size_t i = 0; i < indices.size(); ++i) { - if (i > 0) { - cerr << " + "; - } - cerr << "x" << indices[i] << " * " << coefficients[i]; - } - cerr << " <= " << ub << endl; - cerr << "Infinity value: " << GRB_INFINITY << endl; - cerr << "Lower bound is finite: " << !is_neg_infinity(lb) << endl; - cerr << "Upper bound is finite: " << !is_pos_infinity(ub) << endl; - utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); - } else if (is_neg_infinity(lb) && is_pos_infinity(ub)) { - GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, ub, nullptr); // We add a <= +infinity constraint whenever we have a free constraint. Whether the sense is '<=' or '>=' will be handled by the bounds. - }else if (is_neg_infinity(lb)) { - GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, ub, nullptr); - } else if (is_pos_infinity(ub)) { - GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_GREATER_EQUAL, lb, nullptr); - } else if (lb == ub) { - GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_EQUAL, lb, nullptr); + double rhs = constraint.get_right_hand_side(); + lp::Sense sense = constraint.get_sense(); + + //cerr << "Adding constraint with sense " << (sense == lp::Sense::GE ? "GE" : (sense == lp::Sense::LE ? "LE" : (sense == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) << " and right-hand side " << rhs << endl; + if (sense == lp::Sense::GE) { + GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_GREATER_EQUAL, rhs, nullptr); + } else if (sense == lp::Sense::LE) { + GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_LESS_EQUAL, rhs, nullptr); + } else if (sense == lp::Sense::EQ) { + GRB_CALL(env, GRBaddconstr, model, numnz, cind, cval, GRB_EQUAL, rhs, nullptr); } else { - cerr << "Two-sided constraints are not supported by Gurobi." << endl; + cerr << "Error: Unknown constraint sense." << endl; utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); } -} +} } // Have to add this otherwise the compiler complains. GurobiSolverInterface::GurobiSolverInterface(): env(nullptr), model(nullptr), num_permanent_constraints(0), num_temporary_constraints(0), model_dirty(false) { @@ -171,6 +148,30 @@ void GurobiSolverInterface::load_problem(const LinearProgram &lp) { } GRB_CALL(env, GRBupdatemodel, model); model_dirty = false; + + // Print model + //cerr << "Model loaded with " << num_vars << " variables and " << constraints.size() << " constraints." << endl; + //for (int i = 0; i < num_vars; ++i) { + // cerr << "Variable " << i << ": obj=" << variables[i].objective_coefficient + // << ", lb=" << variables[i].lower_bound + // << ", ub=" << variables[i].upper_bound + // << ", is_integer=" << variables[i].is_integer + // << endl; + //} + //for (int i = 0; i < constraints.size(); ++i) { + // const auto &c = constraints[i]; + // cerr << "Constraint " << i << ": sense=" + // << (c.get_sense() == lp::Sense::GE ? "GE" : (c.get_sense() == lp::Sense::LE ? "LE" : (c.get_sense() == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) + // << ", rhs=" << c.get_right_hand_side() + // << ", coefficients=["; + // for (size_t j = 0; j < c.get_variables().size(); ++j) { + // cerr << "(" << c.get_variables()[j] << ": " << c.get_coefficients()[j] << ")"; + // if (j + 1 < c.get_variables().size()) { + // cerr << ", "; + // } + // } + // cerr << "]" << endl; + //} } void GurobiSolverInterface::add_temporary_constraints(const named_vector::NamedVector &constraints) { @@ -179,6 +180,7 @@ void GurobiSolverInterface::add_temporary_constraints(const named_vector::NamedV } model_dirty = true; num_temporary_constraints += constraints.size(); + cerr << ">>>>> Added " << constraints.size() << " temporary constraints. Total temporary constraints: " << num_temporary_constraints << endl; } void GurobiSolverInterface::clear_temporary_constraints() { @@ -190,6 +192,7 @@ void GurobiSolverInterface::clear_temporary_constraints() { GRB_CALL(env, GRBdelconstrs, model, num_temporary_constraints, indices.data()); model_dirty = true; num_temporary_constraints = 0; + cerr << ">>>>> Cleared temporary constraints. Total temporary constraints: " << num_temporary_constraints << endl; } double GurobiSolverInterface::get_infinity() const { @@ -197,87 +200,67 @@ double GurobiSolverInterface::get_infinity() const { } void GurobiSolverInterface::set_objective_coefficients(const vector &coefficients) { - assert(coefficients.size() == get_num_variables()); + // assert(coefficients.size() == get_num_variables()); int num_coefficients = coefficients.size(); if (!num_coefficients) { return; } // TODO: is there a more elegant way to handle this? GRB_CALL(env, GRBsetdblattrarray, model, GRB_DBL_ATTR_OBJ, 0, num_coefficients, const_cast(coefficients.data())); model_dirty = true; + cerr << ">>>>> New objective coefficients: ["; + for (int i = 0; i < num_coefficients; ++i) { + cerr << coefficients[i]; + if (i + 1 < num_coefficients) { + cerr << ", "; + } + } + cerr << "]" << endl; } void GurobiSolverInterface::set_objective_coefficient(int index, double coefficient) { - assert(index >= 0 && index < get_num_variables()); + //assert(index >= 0 && index < get_num_variables()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_OBJ, index, coefficient); model_dirty = true; + cerr << ">>>>> New objective coefficient for variable " << index << ": " << coefficient << endl; } -void GurobiSolverInterface::set_constraint_lower_bound(int index, double bound) { +void GurobiSolverInterface::set_constraint_rhs(int index, double bound) { assert(index >= 0 && index < get_num_constraints()); - char sense; - double current_rhs; - GRB_CALL(env, GRBgetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, &sense); - GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); - if(sense == GRB_LESS_EQUAL) { // Constraint is of the form ax <= b - if (!is_pos_infinity(current_rhs)) { // Constraint is of the form ax <= finite value - cerr << "Error: cannot set lower bound on <= constraint to a finite value." << endl; - utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); - } else { // Constraint is of the form ax <= infty, which is essentially a free constraint - GRB_CALL(env, GRBsetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_GREATER_EQUAL); // change sense to >= - GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); - } - } - else if(sense == GRB_GREATER_EQUAL) { // constraint is of the form ax >= b - GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); - } - else if(sense == GRB_EQUAL) { - double current_rhs; - GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); - if (current_rhs != bound) { - cerr << "Error: cannot change lower bound on = constraint." << endl; - utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); - } - } + GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); model_dirty = true; + cerr << ">>>>> New right-hand side for constraint " << index << ": " << bound << endl; } -void GurobiSolverInterface::set_constraint_upper_bound(int index, double bound) { +void GurobiSolverInterface::set_constraint_sense(int index, lp::Sense sense) { assert(index >= 0 && index < get_num_constraints()); - char sense; - double current_rhs; - GRB_CALL(env, GRBgetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, &sense); - GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); - if(sense == GRB_LESS_EQUAL) { // constraint is of the form ax <= b - GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); - } else if(sense == GRB_GREATER_EQUAL) { // constraint is of the form ax >= b - if (!is_neg_infinity(current_rhs)) { // constraint is of the form ax >= finite_value - cerr << "Error: cannot set upper bound on >= constraint to a finite value." << endl; - utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); - }else { // constraint is of the form ax >= -infinity, we is essentially a free constraint - GRB_CALL(env, GRBsetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_LESS_EQUAL); // change sense to <= - GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); - } - } else if(sense == GRB_EQUAL) { // constraint is of the form ax = b - double current_rhs; - GRB_CALL(env, GRBgetdblattrelement, model, GRB_DBL_ATTR_RHS, index, ¤t_rhs); - if (current_rhs != bound) { - cerr << "Error: cannot change upper bound on = constraint." << endl; - utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); - } + if (sense == lp::Sense::GE) { + GRB_CALL(env, GRBsetdblattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_GREATER_EQUAL); + } else if (sense == lp::Sense::LE) { + GRB_CALL(env, GRBsetdblattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_LESS_EQUAL); + } else if (sense == lp::Sense::EQ) { + GRB_CALL(env, GRBsetdblattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_EQUAL); + } else { + cerr << "Error: Unknown constraint sense." << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); } model_dirty = true; + cerr << ">>>>> New sense for constraint " << index << ": " + << (sense == lp::Sense::GE ? "GE" : (sense == lp::Sense::LE ? "LE" : (sense == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) + << endl; } void GurobiSolverInterface::set_variable_lower_bound(int index, double bound) { - assert(index >= 0 && index < get_num_variables()); + //assert(index >= 0 && index < get_num_variables()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_LB, index, bound); model_dirty = true; + cerr << ">>>>> New lower bound for variable " << index << ": " << bound << endl; } void GurobiSolverInterface::set_variable_upper_bound(int index, double bound) { - assert(index >= 0 && index < get_num_variables()); + //assert(index >= 0 && index < get_num_variables()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_UB, index, bound); model_dirty = true; + cerr << ">>>>> New upper bound for variable " << index << ": " << bound << endl; } void GurobiSolverInterface::set_mip_gap(double gap) { @@ -306,6 +289,7 @@ void GurobiSolverInterface::print_failure_analysis() const { bool GurobiSolverInterface::is_infeasible() const { int status = 0; GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); + cerr << ">>>>> Is infeasible? " << (status == GRB_INFEASIBLE) << endl; return status == GRB_INFEASIBLE; } @@ -313,6 +297,7 @@ bool GurobiSolverInterface::is_infeasible() const { bool GurobiSolverInterface::is_unbounded() const { int status = 0; GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); + cerr << ">>>>> Is unbounded? " << (status == GRB_UNBOUNDED) << endl; return status == GRB_UNBOUNDED; } @@ -320,6 +305,7 @@ bool GurobiSolverInterface::is_unbounded() const { bool GurobiSolverInterface::has_optimal_solution() const { int status = 0; GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); + cerr << ">>>>> Has optimal solution? " << (status == GRB_OPTIMAL) << endl; return status == GRB_OPTIMAL; } @@ -327,30 +313,43 @@ bool GurobiSolverInterface::has_optimal_solution() const { double GurobiSolverInterface::get_objective_value() const { double value = 0.0; GRB_CALL(env, GRBgetdblattr, model, GRB_DBL_ATTR_OBJVAL, &value); + cerr << ">>>>> Objective value: " << value << endl; return value; } // TODO: check if an optimal solution is available vector GurobiSolverInterface::extract_solution() const { - int num_variables = get_num_variables(); + int num_variables = 0; + GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_NUMVARS, &num_variables); vector solution(num_variables); if (num_variables > 0) { GRB_CALL(env, GRBgetdblattrarray, model, GRB_DBL_ATTR_X, 0, num_variables, solution.data()); } + cerr << ">>>>> Extracted solution: ["; + for (int i = 0; i < num_variables; ++i) { + cerr << solution[i]; + if (i + 1 < num_variables) { + cerr << ", "; + } + } + cerr << "]" << endl; return solution; } int GurobiSolverInterface::get_num_variables() const { int num_variables = 0; GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_NUMVARS, &num_variables); + cerr << ">>>>> Number of variables: " << num_variables << endl; return num_variables; } int GurobiSolverInterface::get_num_constraints() const { + cerr << ">>>>> Number of constraints: " << (num_permanent_constraints + num_temporary_constraints) << endl; return num_permanent_constraints + num_temporary_constraints; } bool GurobiSolverInterface::has_temporary_constraints() const { + cerr << ">>>>> Has temporary constraints? " << (num_temporary_constraints > 0) << endl; return num_temporary_constraints > 0; } diff --git a/src/search/lp/gurobi_solver_interface.h b/src/search/lp/gurobi_solver_interface.h index b93456d53a..6583c6d3dc 100644 --- a/src/search/lp/gurobi_solver_interface.h +++ b/src/search/lp/gurobi_solver_interface.h @@ -26,8 +26,8 @@ class GurobiSolverInterface : public SolverInterface { virtual void set_objective_coefficients( const std::vector &coefficients) override; virtual void set_objective_coefficient(int index, double coefficient) override; - virtual void set_constraint_lower_bound(int index, double bound) override; - virtual void set_constraint_upper_bound(int index, double bound) override; + virtual void set_constraint_rhs(int index, double right_hand_side) override; + virtual void set_constraint_sense(int index, lp::Sense sense) override; virtual void set_variable_lower_bound(int index, double bound) override; virtual void set_variable_upper_bound(int index, double bound) override; diff --git a/src/search/lp/highs_solver_interface.cc b/src/search/lp/highs_solver_interface.cc index c766cbb5f3..ee29121077 100644 --- a/src/search/lp/highs_solver_interface.cc +++ b/src/search/lp/highs_solver_interface.cc @@ -8,6 +8,66 @@ using namespace std; namespace lp { +namespace { +struct RowBounds { + double lower; + double upper; +}; + +bool is_neg_inf(double value, double inf) { + return value <= -inf; +} + +bool is_pos_inf(double value, double inf) { + return value >= inf; +} + +RowBounds sense_rhs_to_bounds(const Highs &highs, Sense sense, double rhs) { + const double inf = highs.getInfinity(); + switch (sense) { + case Sense::GE: + return {rhs, inf}; + case Sense::LE: + return {-inf, rhs}; + case Sense::EQ: + return {rhs, rhs}; + } + throw std::runtime_error("Unknown constraint sense in HiGHS interface"); +} + +Sense bounds_to_sense(double lb, double ub, double inf) { + const bool lb_inf = is_neg_inf(lb, inf); + const bool ub_inf = is_pos_inf(ub, inf); + + if (lb_inf && !ub_inf) { + return Sense::LE; + } + if (!lb_inf && ub_inf) { + return Sense::GE; + } + if (!lb_inf && !ub_inf && lb == ub) { + return Sense::EQ; + } + throw std::runtime_error("HiGHS constraint has unsupported ranged bounds"); +} + +double bounds_to_rhs(double lb, double ub, double inf) { + const bool lb_inf = is_neg_inf(lb, inf); + const bool ub_inf = is_pos_inf(ub, inf); + + if (lb_inf && !ub_inf) { + return ub; + } + if (!lb_inf && ub_inf) { + return lb; + } + if (!lb_inf && !ub_inf && lb == ub) { + return lb; + } + throw std::runtime_error("HiGHS constraint has unsupported ranged bounds"); +} +} // namespace + static void highs_ok(HighsStatus s, const char* where) { if (s == HighsStatus::kOk) return; throw std::runtime_error(std::string("HiGHS error in ") + where); @@ -54,8 +114,9 @@ void HiGHSSolverInterface::load_problem(const LinearProgram &lp) { const auto &val = c.get_coefficients(); assert(idx.size() == val.size()); - highs_ok(highs_.addRow(c.get_lower_bound(), - c.get_upper_bound(), + const RowBounds bounds = sense_rhs_to_bounds(highs_, c.get_sense(), c.get_right_hand_side()); + highs_ok(highs_.addRow(bounds.lower, + bounds.upper, (int)idx.size(), idx.data(), val.data()), @@ -64,7 +125,29 @@ void HiGHSSolverInterface::load_problem(const LinearProgram &lp) { num_permanent_constraints = lp.get_constraints().size(); num_temporary_constraints = 0; - //cout << "so far so good" << endl; + // Print model + //cerr << "Model loaded with " << n << " variables and " << m << " constraints." << endl; + //for (int i = 0; i < n; ++i) { + // cerr << "Variable " << i << ": obj=" << vars[i].objective_coefficient + // << ", lb=" << vars[i].lower_bound + // << ", ub=" << vars[i].upper_bound + // << ", is_integer=" << vars[i].is_integer + // << endl; + //} + // for (int r = 0; r < m; ++r) { + // const auto &c = cons[r]; + // cerr << "Constraint " << r << ": sense=" + // << (c.get_sense() == lp::Sense::GE ? "GE" : (c.get_sense() == lp::Sense::LE ? "LE" : (c.get_sense() == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) + // << ", rhs=" << c.get_right_hand_side() + // << ", coefficients=["; + // for (size_t i = 0; i < c.get_variables().size(); ++i) { + // cerr << "(" << c.get_variables()[i] << ": " << c.get_coefficients()[i] << ")"; + // if (i + 1 < c.get_variables().size()) { + // cerr << ", "; + // } + // } + // cerr << "]" << endl; + //} } void HiGHSSolverInterface::add_temporary_constraints( @@ -76,13 +159,18 @@ void HiGHSSolverInterface::add_temporary_constraints( const auto &idx = c.get_variables(); const auto &val = c.get_coefficients(); - highs_ok(highs_.addRow(c.get_lower_bound(), - c.get_upper_bound(), + const RowBounds bounds = sense_rhs_to_bounds(highs_, c.get_sense(), c.get_right_hand_side()); + + highs_ok(highs_.addRow(bounds.lower, + bounds.upper, (int)idx.size(), idx.data(), val.data()), "addRow(temporary_constraints)"); } + + cerr << "@@@@@@@@@@@@@@@@@@@@@ Warning: add_temporary_constraints does not update the constraint right-hand side and sense. Make sure to call set_constraint_rhs and set_constraint_sense if the right-hand side or sense should be changed." << endl; + exit(1); } void HiGHSSolverInterface::clear_temporary_constraints() { @@ -98,10 +186,10 @@ void HiGHSSolverInterface::clear_temporary_constraints() { double HiGHSSolverInterface::get_infinity() const { return highs_.getInfinity(); + //return 1e100; } -void HiGHSSolverInterface::set_objective_coefficients( - const std::vector& coefficients) +void HiGHSSolverInterface::set_objective_coefficients(const std::vector& coefficients) { const int n = highs_.getNumCol(); @@ -118,30 +206,54 @@ void HiGHSSolverInterface::set_objective_coefficients( highs_ok(highs_.changeColCost(i, coefficients[i]), "changeColCost(vec)"); } + + cerr << ">>>>> New objective coefficients: ["; + for (int i = 0; i < n; ++i) { + cerr << coefficients[i]; + if (i + 1 < n) { + cerr << ", "; + } + } + cerr << "]" << endl; } void HiGHSSolverInterface::set_objective_coefficient(int index, double coefficient) { highs_ok(highs_.changeColCost(index, coefficient), "changeColCost"); + cerr << ">>>>> New objective coefficient for variable " << index << ": " << coefficient << endl; } -void HiGHSSolverInterface::set_constraint_lower_bound(int index, double bound) { +void HiGHSSolverInterface::set_constraint_rhs(int index, double right_hand_side) { + const double inf = highs_.getInfinity(); + const double lb = highs_.getLp().row_lower_[index]; const double ub = highs_.getLp().row_upper_[index]; - highs_ok(highs_.changeRowBounds(index, bound, ub), "changeRowBounds(lb)"); + const Sense sense = bounds_to_sense(lb, ub, inf); + const RowBounds bounds = sense_rhs_to_bounds(highs_, sense, right_hand_side); + highs_ok(highs_.changeRowBounds(index, bounds.lower, bounds.upper), "changeRowBounds(rhs)"); + cerr << ">>>>> New rhs for constraint " << index << ": " << right_hand_side << endl; } -void HiGHSSolverInterface::set_constraint_upper_bound(int index, double bound) { +void HiGHSSolverInterface::set_constraint_sense(int index, lp::Sense sense) { + const double inf = highs_.getInfinity(); const double lb = highs_.getLp().row_lower_[index]; - highs_ok(highs_.changeRowBounds(index, lb, bound), "changeRowBounds(ub)"); + const double ub = highs_.getLp().row_upper_[index]; + const double rhs = bounds_to_rhs(lb, ub, inf); + const RowBounds bounds = sense_rhs_to_bounds(highs_, sense, rhs); + highs_ok(highs_.changeRowBounds(index, bounds.lower, bounds.upper), "changeRowBounds(sense)"); + cerr << ">>>>> New sense for constraint " << index << ": " + << (sense == lp::Sense::GE ? "GE" : (sense == lp::Sense::LE ? "LE" : (sense == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) + << endl; } void HiGHSSolverInterface::set_variable_lower_bound(int index, double bound) { const double ub = highs_.getLp().col_upper_[index]; highs_ok(highs_.changeColBounds(index, bound, ub), "changeColBounds(lb)"); + cerr << ">>>>> New lower bound for variable " << index << ": " << bound << endl; } void HiGHSSolverInterface::set_variable_upper_bound(int index, double bound) { const double lb = highs_.getLp().col_lower_[index]; highs_ok(highs_.changeColBounds(index, lb, bound), "changeColBounds(ub)"); + cerr << ">>>>> New upper bound for variable " << index << ": " << bound << endl; } void HiGHSSolverInterface::set_mip_gap(double gap) { @@ -152,7 +264,6 @@ void HiGHSSolverInterface::solve() { highs_ok(highs_.run(), "run"); } - void HiGHSSolverInterface::write_lp(const std::string &filename) const { highs_ok(highs_.writeModel(filename), "writeModel"); } @@ -162,20 +273,24 @@ void HiGHSSolverInterface::print_failure_analysis() const { } bool HiGHSSolverInterface::is_infeasible() const { + cerr << ">>>>> Is infeasible? " << (highs_.getModelStatus() == HighsModelStatus::kInfeasible) << endl; return highs_.getModelStatus() == HighsModelStatus::kInfeasible; } bool HiGHSSolverInterface::is_unbounded() const { + cerr << ">>>>> Is unbounded? " << (highs_.getModelStatus() == HighsModelStatus::kUnbounded) << endl; return highs_.getModelStatus() == HighsModelStatus::kUnbounded; } bool HiGHSSolverInterface::has_optimal_solution() const { + cerr << ">>>>> Has optimal solution? " << (highs_.getModelStatus() == HighsModelStatus::kOptimal) << endl; return highs_.getModelStatus() == HighsModelStatus::kOptimal; } double HiGHSSolverInterface::get_objective_value() const { assert(has_optimal_solution()); const HighsInfo& info = highs_.getInfo(); + cerr << ">>>>> Objective value: " << info.objective_function_value << endl; return info.objective_function_value; } @@ -187,18 +302,29 @@ std::vector HiGHSSolverInterface::extract_solution() const { for (int i = 0; i < n; i++){ x[i] = sol.col_value[i]; } + cerr << ">>>>> Extracted solution: ["; + for (int i = 0; i < n; ++i) { + cerr << x[i]; + if (i + 1 < n) { + cerr << ", "; + } + } + cerr << "]" << endl; return x; } int HiGHSSolverInterface::get_num_variables() const { + cerr << ">>>>> Number of variables: " << highs_.getNumCol() << endl; return highs_.getNumCol(); } int HiGHSSolverInterface::get_num_constraints() const { + cerr << ">>>>> Number of constraints: " << highs_.getNumRow() << endl; return highs_.getNumRow(); } bool HiGHSSolverInterface::has_temporary_constraints() const { + cerr << ">>>>> Has temporary constraints? " << (num_temporary_constraints > 0) << endl; return num_temporary_constraints != 0; } diff --git a/src/search/lp/highs_solver_interface.h b/src/search/lp/highs_solver_interface.h index 1f6004a25d..8ed0019384 100644 --- a/src/search/lp/highs_solver_interface.h +++ b/src/search/lp/highs_solver_interface.h @@ -27,8 +27,8 @@ class HiGHSSolverInterface : public SolverInterface { const std::vector &coefficients) override; virtual void set_objective_coefficient( int index, double coefficient) override; - virtual void set_constraint_lower_bound(int index, double bound) override; - virtual void set_constraint_upper_bound(int index, double bound) override; + virtual void set_constraint_rhs(int index, double right_hand_side) override; + virtual void set_constraint_sense(int index, lp::Sense sense) override; virtual void set_variable_lower_bound(int index, double bound) override; virtual void set_variable_upper_bound(int index, double bound) override; diff --git a/src/search/lp/soplex_solver_interface.h b/src/search/lp/soplex_solver_interface.h index 6960f264e1..83ca0c706c 100644 --- a/src/search/lp/soplex_solver_interface.h +++ b/src/search/lp/soplex_solver_interface.h @@ -40,8 +40,8 @@ class SoPlexSolverInterface : public SolverInterface { const std::vector &coefficients) override; virtual void set_objective_coefficient( int index, double coefficient) override; - virtual void set_constraint_lower_bound(int index, double bound) override; - virtual void set_constraint_upper_bound(int index, double bound) override; + virtual void set_constraint_rhs(int index, double right_hand_side) override; + virtual void set_constraint_sense(int index, lp::Sense sense) override; virtual void set_variable_lower_bound(int index, double bound) override; virtual void set_variable_upper_bound(int index, double bound) override; From 2235161bf058072c1f7aa119290e47693ae734ae Mon Sep 17 00:00:00 2001 From: Travis Rivera Petit Date: Wed, 11 Feb 2026 11:11:33 +0100 Subject: [PATCH 10/15] small optimization --- src/search/lp/highs_solver_interface.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search/lp/highs_solver_interface.cc b/src/search/lp/highs_solver_interface.cc index ee29121077..e9f57412ce 100644 --- a/src/search/lp/highs_solver_interface.cc +++ b/src/search/lp/highs_solver_interface.cc @@ -90,7 +90,7 @@ void HiGHSSolverInterface::load_problem(const LinearProgram &lp) { highs_ok(highs_.setOptionValue("log_to_console", false), "setOptionValue(log_to_console)"); // variables - named_vector::NamedVector vars = lp.get_variables(); + const auto &vars = lp.get_variables(); const int n = static_cast(vars.size()); for (int i = 0; i < n; ++i) { const auto &v = vars[i]; From 91c5524b23600aad86489fb7ff4326114e320fe1 Mon Sep 17 00:00:00 2001 From: Travis Rivera Petit Date: Wed, 11 Feb 2026 14:47:41 +0100 Subject: [PATCH 11/15] Add SoPlex interface --- src/search/CMakeLists.txt | 32 +++---- src/search/lp/soplex_solver_interface.cc | 109 ++++++++++++++++++++--- 2 files changed, 112 insertions(+), 29 deletions(-) diff --git a/src/search/CMakeLists.txt b/src/search/CMakeLists.txt index 647867ee77..85ea088474 100644 --- a/src/search/CMakeLists.txt +++ b/src/search/CMakeLists.txt @@ -587,21 +587,22 @@ create_fast_downward_library( DEPENDENCY_ONLY ) if(USE_LP) - #find_package(Cplex 12) - #if(CPLEX_FOUND) - # target_compile_definitions(lp_solver INTERFACE HAS_CPLEX) - # target_link_libraries(lp_solver INTERFACE cplex::cplex) - # target_sources(lp_solver INTERFACE lp/cplex_solver_interface.h lp/cplex_solver_interface.cc) - #endif() - - - #find_package(soplex 7.1.0 QUIET) - #if (SOPLEX_FOUND) - # message(STATUS "Found SoPlex: ${SOPLEX_INCLUDE_DIRS}") - # target_link_libraries(lp_solver INTERFACE libsoplex) - # target_compile_definitions(lp_solver INTERFACE HAS_SOPLEX) - # target_sources(lp_solver INTERFACE lp/soplex_solver_interface.h lp/soplex_solver_interface.cc) - #endif() + find_package(Cplex 12) + if(CPLEX_FOUND) + message(STATUS "Found CPLEX: ${CPLEX_DIR}") + target_compile_definitions(lp_solver INTERFACE HAS_CPLEX) + target_link_libraries(lp_solver INTERFACE cplex::cplex) + target_sources(lp_solver INTERFACE lp/cplex_solver_interface.h lp/cplex_solver_interface.cc) + endif() + + + find_package(soplex 7.1.0 QUIET) + if (SOPLEX_FOUND) + message(STATUS "Found SoPlex: ${SOPLEX_DIR}") + target_link_libraries(lp_solver INTERFACE libsoplex) + target_compile_definitions(lp_solver INTERFACE HAS_SOPLEX) + target_sources(lp_solver INTERFACE lp/soplex_solver_interface.h lp/soplex_solver_interface.cc) + endif() find_package(Gurobi QUIET) if(Gurobi_FOUND) @@ -613,6 +614,7 @@ if(USE_LP) find_package(HiGHS CONFIG QUIET) if(HiGHS_FOUND) + message(STATUS "Found HiGHS: ${HiGHS_DIR}") target_compile_definitions(lp_solver INTERFACE HAS_HIGHS) target_link_libraries(lp_solver INTERFACE highs::highs) target_sources(lp_solver INTERFACE diff --git a/src/search/lp/soplex_solver_interface.cc b/src/search/lp/soplex_solver_interface.cc index c38f776368..133d7b9366 100644 --- a/src/search/lp/soplex_solver_interface.cc +++ b/src/search/lp/soplex_solver_interface.cc @@ -18,6 +18,7 @@ static int get_obj_sense(LPObjectiveSense sense) { static LPRowSetReal constraints_to_row_set( const named_vector::NamedVector &constraints) { + int num_rows = constraints.size(); int num_nonzeros = 0; for (const LPConstraint &constraint : constraints) { @@ -25,18 +26,40 @@ static LPRowSetReal constraints_to_row_set( } LPRowSetReal rows(num_rows, num_nonzeros); + for (const LPConstraint &constraint : constraints) { const vector variables = constraint.get_variables(); const vector coefficients = constraint.get_coefficients(); - int num_entries = coefficients.size(); + + const int num_entries = static_cast(coefficients.size()); soplex::DSVectorReal entries(num_entries); for (int i = 0; i < num_entries; ++i) { entries.add(variables[i], coefficients[i]); } - rows.add( - constraint.get_lower_bound(), entries, - constraint.get_upper_bound()); + + const double b = constraint.get_right_hand_side(); + const lp::Sense s = constraint.get_sense(); + + double lhs, rhs; + switch (s) { + case lp::Sense::LE: + lhs = -soplex::infinity; + rhs = b; + break; + case lp::Sense::GE: + lhs = b; + rhs = soplex::infinity; + break; + case lp::Sense::EQ: + lhs = b; + rhs = b; + break; + default: + throw std::logic_error("invalid sense"); + } + rows.add(lhs, entries, rhs); } + return rows; } @@ -89,7 +112,7 @@ void SoPlexSolverInterface::clear_temporary_constraints() { } double SoPlexSolverInterface::get_infinity() const { - return infinity; + return infinity; // soplex::infinity } void SoPlexSolverInterface::set_objective_coefficients( @@ -105,15 +128,15 @@ void SoPlexSolverInterface::set_objective_coefficient( soplex.changeObjReal(index, coefficient); } -void SoPlexSolverInterface::set_constraint_lower_bound( - int index, double bound) { - soplex.changeLhsReal(index, bound); -} - -void SoPlexSolverInterface::set_constraint_upper_bound( - int index, double bound) { - soplex.changeRhsReal(index, bound); -} +//void SoPlexSolverInterface::set_constraint_lower_bound( +// int index, double bound) { +// soplex.changeLhsReal(index, bound); +//} +// +//void SoPlexSolverInterface::set_constraint_upper_bound( +// int index, double bound) { +// soplex.changeRhsReal(index, bound); +//} void SoPlexSolverInterface::set_variable_lower_bound(int index, double bound) { soplex.changeLowerReal(index, bound); @@ -240,4 +263,62 @@ bool SoPlexSolverInterface::has_temporary_constraints() const { void SoPlexSolverInterface::print_statistics() const { soplex.printStatistics(cout); } + +void SoPlexSolverInterface::set_constraint_rhs(int index, double b) { + const double lhs = soplex.lhsReal(index); + const double rhs = soplex.rhsReal(index); + + const bool lhs_is_neginf = (lhs == -infinity); + const bool rhs_is_posinf = (rhs == infinity); + + if (lhs_is_neginf && !rhs_is_posinf) { + soplex.changeRhsReal(index, b); + } else if (!lhs_is_neginf && rhs_is_posinf) { + soplex.changeLhsReal(index, b); + } else if (!lhs_is_neginf && !rhs_is_posinf && lhs == rhs) { + soplex.changeLhsReal(index, b); + soplex.changeRhsReal(index, b); + } else { + throw std::logic_error("invalid constraint"); + } +} + +void SoPlexSolverInterface::set_constraint_sense(int index, lp::Sense sense) { + const double lhs = soplex.lhsReal(index); + const double rhs = soplex.rhsReal(index); + + const bool lhs_is_neginf = (lhs == -infinity); + const bool rhs_is_posinf = (rhs == infinity); + + double b; + if (lhs_is_neginf && !rhs_is_posinf) { + b = rhs; + } else if (!lhs_is_neginf && rhs_is_posinf) { + b = lhs; + } else if (!lhs_is_neginf && !rhs_is_posinf && lhs == rhs) { + b = rhs; + } else { + throw std::logic_error("invalid constraint"); + b = rhs; // keep compiler happy + } + + switch (sense) { + case lp::Sense::LE: + soplex.changeLhsReal(index, -infinity); + soplex.changeRhsReal(index, b); + break; + case lp::Sense::GE: + soplex.changeLhsReal(index, b); + soplex.changeRhsReal(index, infinity); + break; + case lp::Sense::EQ: + soplex.changeLhsReal(index, b); + soplex.changeRhsReal(index, b); + break; + } +} + + + + } From a008a04aa778bc5830043707b3bd9cd599abec51 Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Wed, 11 Feb 2026 14:59:07 +0100 Subject: [PATCH 12/15] update cplex interface --- src/search/lp/cplex_solver_interface.cc | 206 +++++------------------ src/search/lp/cplex_solver_interface.h | 59 +------ src/search/lp/gurobi_solver_interface.cc | 58 +++---- 3 files changed, 76 insertions(+), 247 deletions(-) diff --git a/src/search/lp/cplex_solver_interface.cc b/src/search/lp/cplex_solver_interface.cc index 54d77ae2a0..33e012b02d 100644 --- a/src/search/lp/cplex_solver_interface.cc +++ b/src/search/lp/cplex_solver_interface.cc @@ -67,23 +67,7 @@ static void freeProblem(CPXENVptr env, CPXLPptr *problem) { CPX_CALL(CPXfreeprob, env, problem); } -static tuple bounds_to_sense_rhs_range( - double lb, double ub) { - if (lb <= -CPX_INFBOUND && ub >= CPX_INFBOUND) { - // CPLEX does not support <= or >= constraints without bounds. - return {'R', -CPX_INFBOUND, 2 * CPX_INFBOUND}; - } else if (lb <= -CPX_INFBOUND) { - return {'L', ub, 0}; - } else if (ub >= CPX_INFBOUND) { - return {'G', lb, 0}; - } else if (lb == ub) { - return {'E', lb, 0}; - } else { - return {'R', lb, ub - lb}; - } -} - -static int sense_to_cplex_sense(LPObjectiveSense sense) { +static int model_sense_to_cplex_sense(LPObjectiveSense sense) { if (sense == LPObjectiveSense::MINIMIZE) { return CPX_MIN; } else { @@ -91,8 +75,27 @@ static int sense_to_cplex_sense(LPObjectiveSense sense) { } } -void CplexSolverInterface::CplexMatrix::assign_column_by_column( - const named_vector::NamedVector &constraints, int num_cols) { +static char constraint_sense_to_cplex_sense( + lp::Sense constraint_sense) { + switch (constraint_sense) { + case lp::Sense::LE: + return 'L'; + break; + case lp::Sense::GE: + return 'G'; + break; + case lp::Sense::EQ: + return 'E'; + break; + default: + cerr << "Unsupported constraint sense: " + << static_cast(constraint_sense) << endl; + utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); + } +} + + +void CplexSolverInterface::CplexMatrix::assign_column_by_column(const named_vector::NamedVector &constraints, int num_cols) { coefficients.clear(); indices.clear(); starts.clear(); @@ -152,8 +155,7 @@ void CplexSolverInterface::CplexMatrix::assign_column_by_column( assert(indices.size() == coefficients.size()); } -void CplexSolverInterface::CplexMatrix::assign_row_by_row( - const named_vector::NamedVector &constraints) { +void CplexSolverInterface::CplexMatrix::assign_row_by_row(const named_vector::NamedVector &constraints) { coefficients.clear(); indices.clear(); starts.clear(); @@ -173,8 +175,7 @@ void CplexSolverInterface::CplexMatrix::assign_row_by_row( assert(indices.size() == coefficients.size()); } -void CplexSolverInterface::CplexColumnsInfo::assign( - const named_vector::NamedVector &variables) { +void CplexSolverInterface::CplexColumnsInfo::assign(const named_vector::NamedVector &variables) { lb.clear(); ub.clear(); type.clear(); @@ -201,54 +202,31 @@ void CplexSolverInterface::CplexColumnsInfo::assign( assert(static_cast(objective.size()) == variables.size()); } -void CplexSolverInterface::CplexRowsInfo::assign( - const named_vector::NamedVector &constraints, int offset, - bool dense_range_values) { +void CplexSolverInterface::CplexRowsInfo::assign(const named_vector::NamedVector &constraints) { rhs.clear(); sense.clear(); - range_values.clear(); - range_indices.clear(); - int num_rows = constraints.size(); - sense.resize(num_rows); - rhs.resize(num_rows); - if (dense_range_values) { - range_values.resize(num_rows, 0); + if (num_rows > 0) { + sense.resize(num_rows); + rhs.resize(num_rows); } for (int row_index = 0; row_index < num_rows; ++row_index) { const LPConstraint &constraint = constraints[row_index]; - double lb = constraint.get_lower_bound(); - double ub = constraint.get_upper_bound(); - const auto &[sense_value, rhs_value, range_value] = - bounds_to_sense_rhs_range(lb, ub); - sense[row_index] = sense_value; + double rhs_value = constraint.get_right_hand_side(); + lp::Sense constraint_sense = constraint.get_sense(); + char cplex_sense = constraint_sense_to_cplex_sense(constraint_sense); + sense[row_index] = cplex_sense; rhs[row_index] = rhs_value; - if (sense_value == 'R') { - if (dense_range_values) { - range_values[row_index] = range_value; - } else { - range_values.push_back(range_value); - range_indices.push_back(offset + row_index); - } - } } assert(static_cast(rhs.size()) == constraints.size()); assert(static_cast(sense.size()) == constraints.size()); - assert(static_cast(range_values.size()) <= constraints.size()); - assert( - (dense_range_values && - (static_cast(range_values.size()) == constraints.size()) && - (range_indices.size() == 0)) || - (!dense_range_values && (range_values.size() == range_indices.size()))); } CplexSolverInterface::CplexSolverInterface() : env(nullptr), problem(nullptr), is_mip(false), - num_permanent_constraints(0), - num_unsatisfiable_constraints(0), - num_unsatisfiable_temp_constraints(0) { + num_permanent_constraints(0){ int status = 0; env = CPXopenCPLEX(&status); if (status) { @@ -269,41 +247,6 @@ CplexSolverInterface::~CplexSolverInterface() { } } -bool CplexSolverInterface::is_trivially_unsolvable() const { - return num_unsatisfiable_constraints + num_unsatisfiable_temp_constraints > - 0; -} - -void CplexSolverInterface::change_constraint_bounds( - int index, double lb, double ub) { - double current_lb = constraint_lower_bounds[index]; - double current_ub = constraint_upper_bounds[index]; - if (current_lb == lb && current_ub == ub) { - return; - } - const auto &[sense, rhs, range] = bounds_to_sense_rhs_range(lb, ub); - - CPX_CALL(CPXchgsense, env, problem, 1, &index, &sense); - CPX_CALL(CPXchgrhs, env, problem, 1, &index, &rhs); - CPX_CALL(CPXchgrngval, env, problem, 1, &index, &range); - - if (current_lb > current_ub && lb <= ub) { - if (index < num_permanent_constraints) { - --num_unsatisfiable_constraints; - } else { - --num_unsatisfiable_temp_constraints; - } - } else if (current_lb <= current_ub && lb > ub) { - if (index < num_permanent_constraints) { - ++num_unsatisfiable_constraints; - } else { - ++num_unsatisfiable_temp_constraints; - } - } - constraint_lower_bounds[index] = lb; - constraint_upper_bounds[index] = ub; -} - void CplexSolverInterface::load_problem(const LinearProgram &lp) { if (problem) { freeProblem(env, &problem); @@ -316,25 +259,17 @@ void CplexSolverInterface::load_problem(const LinearProgram &lp) { return v.is_integer; }); - const named_vector::NamedVector &constraints = - lp.get_constraints(); - num_permanent_constraints = constraints.size(); - num_unsatisfiable_constraints = 0; - for (const LPConstraint &constraint : constraints) { - if (constraint.get_lower_bound() > constraint.get_upper_bound()) { - ++num_unsatisfiable_constraints; - } - } - + const named_vector::NamedVector &constraints = lp.get_constraints(); + num_permanent_constraints = constraints.size(); matrix.assign_column_by_column(constraints, variables.size()); columns.assign(variables); rows.assign(constraints); CPX_CALL( CPXcopylp, env, problem, variables.size(), constraints.size(), - sense_to_cplex_sense(lp.get_sense()), columns.get_objective(), + model_sense_to_cplex_sense(lp.get_sense()), columns.get_objective(), rows.get_rhs(), rows.get_sense(), matrix.get_starts(), matrix.get_counts(), matrix.get_indices(), matrix.get_coefficients(), - columns.get_lb(), columns.get_ub(), rows.get_range_values()); + columns.get_lb(), columns.get_ub(), nullptr); if (is_mip) { CPX_CALL(CPXcopyctype, env, problem, columns.get_type()); @@ -342,13 +277,6 @@ void CplexSolverInterface::load_problem(const LinearProgram &lp) { assert(CPXgetprobtype(env, problem) == CPXPROB_LP); } - constraint_lower_bounds.clear(); - constraint_upper_bounds.clear(); - for (const LPConstraint &constraint : constraints) { - constraint_lower_bounds.push_back(constraint.get_lower_bound()); - constraint_upper_bounds.push_back(constraint.get_upper_bound()); - } - // Optionally set names. if (!lp.get_objective_name().empty()) { CPX_CALL(CPXchgprobname, env, problem, lp.get_objective_name().c_str()); @@ -369,14 +297,8 @@ void CplexSolverInterface::load_problem(const LinearProgram &lp) { void CplexSolverInterface::add_temporary_constraints( const named_vector::NamedVector &constraints) { - for (const LPConstraint &constraint : constraints) { - if (constraint.get_lower_bound() > constraint.get_upper_bound()) { - ++num_unsatisfiable_temp_constraints; - } - } - matrix.assign_row_by_row(constraints); - rows.assign(constraints, get_num_constraints(), false); + rows.assign(constraints); CplexNameData row_names(constraints); // CPXaddrows can add new variables as well, but we do not want any. static const int num_extra_columns = 0; @@ -386,21 +308,6 @@ void CplexSolverInterface::add_temporary_constraints( matrix.get_num_nonzeros(), rows.get_rhs(), rows.get_sense(), matrix.get_starts(), matrix.get_indices(), matrix.get_coefficients(), extra_column_names, row_names.get_names()); - - /* - If there are any ranged rows, we have to set up their ranges with a - separate call. - */ - if (rows.get_num_ranged_rows() > 0) { - CPX_CALL( - CPXchgrngval, env, problem, rows.get_num_ranged_rows(), - rows.get_range_indices(), rows.get_range_values()); - } - - for (const LPConstraint &constraint : constraints) { - constraint_lower_bounds.push_back(constraint.get_lower_bound()); - constraint_upper_bounds.push_back(constraint.get_upper_bound()); - } } void CplexSolverInterface::clear_temporary_constraints() { @@ -408,10 +315,6 @@ void CplexSolverInterface::clear_temporary_constraints() { int end = get_num_constraints() - 1; if (start <= end) { CPX_CALL(CPXdelrows, env, problem, start, end); - num_unsatisfiable_temp_constraints = 0; - - constraint_lower_bounds.resize(num_permanent_constraints); - constraint_upper_bounds.resize(num_permanent_constraints); } } @@ -434,12 +337,13 @@ void CplexSolverInterface::set_objective_coefficient( CPX_CALL(CPXchgobj, env, problem, 1, &index, &coefficient); } -void CplexSolverInterface::set_constraint_lower_bound(int index, double bound) { - change_constraint_bounds(index, bound, constraint_upper_bounds[index]); +void CplexSolverInterface::set_constraint_rhs(int index, double right_hand_side) { + CPX_CALL(CPXchgrhs, env, problem, 1, &index, &right_hand_side); } -void CplexSolverInterface::set_constraint_upper_bound(int index, double bound) { - change_constraint_bounds(index, constraint_lower_bounds[index], bound); +void CplexSolverInterface::set_constraint_sense(int index, lp::Sense sense) { + char cplex_sense = constraint_sense_to_cplex_sense(sense); + CPX_CALL(CPXchgsense, env, problem, 1, &index, &cplex_sense); } void CplexSolverInterface::set_variable_lower_bound(int index, double bound) { @@ -457,9 +361,7 @@ void CplexSolverInterface::set_mip_gap(double gap) { } void CplexSolverInterface::solve() { - if (is_trivially_unsolvable()) { - return; - } else if (is_mip) { + if (is_mip) { CPX_CALL(CPXmipopt, env, problem); } else { CPX_CALL(CPXlpopt, env, problem); @@ -467,23 +369,12 @@ void CplexSolverInterface::solve() { } void CplexSolverInterface::write_lp(const string &filename) const { - if (is_trivially_unsolvable()) { - cerr << "The LP has trivially unsatisfiable constraints that are not " - << "accurately represented in CPLEX. Writing it to a file would " - << "misrepresent the LP." << endl; - utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); - } // By not passing in a filetype, we let CPLEX infer it from the filename. static const char *filetype = nullptr; CPX_CALL(CPXwriteprob, env, problem, filename.c_str(), filetype); } void CplexSolverInterface::print_failure_analysis() const { - if (is_trivially_unsolvable()) { - cout << "LP/MIP is infeasible because of a trivially unsatisfiable " - << "constraint" << endl; - return; - } int status = CPXgetstat(env, problem); switch (status) { case CPX_STAT_OPTIMAL: @@ -515,25 +406,16 @@ void CplexSolverInterface::print_failure_analysis() const { } bool CplexSolverInterface::is_infeasible() const { - if (is_trivially_unsolvable()) { - return true; - } int status = CPXgetstat(env, problem); return status == CPX_STAT_INFEASIBLE || status == CPXMIP_INFEASIBLE; } bool CplexSolverInterface::is_unbounded() const { - if (is_trivially_unsolvable()) { - return false; - } int status = CPXgetstat(env, problem); return status == CPX_STAT_UNBOUNDED; } bool CplexSolverInterface::has_optimal_solution() const { - if (is_trivially_unsolvable()) { - return false; - } int status = CPXgetstat(env, problem); switch (status) { case CPX_STAT_OPTIMAL: diff --git a/src/search/lp/cplex_solver_interface.h b/src/search/lp/cplex_solver_interface.h index a572d0daaa..0d631b729e 100644 --- a/src/search/lp/cplex_solver_interface.h +++ b/src/search/lp/cplex_solver_interface.h @@ -31,24 +31,6 @@ class CplexSolverInterface : public SolverInterface { bool is_mip; int num_permanent_constraints; - /* - Our public interface allows using constraints of the form - LB <= expression <= UB - In cases where LB > UB, this constraint is trivially unsatisfiable. - CPLEX does not represent constraints like this and instead uses range - values, where the constraint is represented like this - expression - RNG = LB - where RNG is a variable restricted to take values from 0 to (UB - LB). - If LB > UB, the semantic instead is that RNG takes negative values between - (UB - LB) and 0. This means that in CPLEX, the constraint never is - trivially unsolvable. We still set the range value and the right-hand side - as described above but use negative range values to represent trivially - unsatisfiable constraints. The following two counters track how many such - constraints we have in the permanent and the temporary constraints. - */ - int num_unsatisfiable_constraints; - int num_unsatisfiable_temp_constraints; - /* Matrix data in CPLEX format for loading a new problem. Matrix entries are stored in sparse form: non-zero coefficients are either stored @@ -149,37 +131,17 @@ class CplexSolverInterface : public SolverInterface { class CplexRowsInfo { // Right-hand side value of a row std::vector rhs; - // Sense of a row (Greater or equal, Less or equal, Equal, or Range) + // Sense of a row (Greater or equal, Less or equal, or Equal) std::vector sense; - /* - If the sense of a row is Range, then its value is restricted to the - interval (RHS, RHS + range_value). - */ - std::vector range_values; - /* - In case not all rows specify a sense, this gives the indices of the - rows that are ranged rows. - */ - std::vector range_indices; public: - void assign( - const named_vector::NamedVector &constraints, - int offset = 0, bool dense_range_values = true); + void assign(const named_vector::NamedVector &constraints); + double *get_rhs() { return to_cplex_array(rhs); } char *get_sense() { return to_cplex_array(sense); } - double *get_range_values() { - return to_cplex_array(range_values); - } - int *get_range_indices() { - return to_cplex_array(range_indices); - } - int get_num_ranged_rows() { - return range_indices.size(); - } }; class CplexNameData { @@ -233,21 +195,6 @@ class CplexSolverInterface : public SolverInterface { CplexColumnsInfo columns; CplexRowsInfo rows; std::vector objective_indices; - - /* - We store a copy of the current constraint bounds. We need to know the - current bounds when changing bounds, and accessing them through the CPLEX - interface has a significant overhead. Storing these vectors overlaps with - storing CplexRowsInfo above. The difference is that CplexRowsInfo stores - more information and we reuse it for temporary constraints, while we want - to keep the following vectors always synchronized with the full LP - (permanent and temporary constraints). - */ - std::vector constraint_lower_bounds; - std::vector constraint_upper_bounds; - - bool is_trivially_unsolvable() const; - void change_constraint_bounds(int index, double lb, double ub); public: CplexSolverInterface(); virtual ~CplexSolverInterface() override; diff --git a/src/search/lp/gurobi_solver_interface.cc b/src/search/lp/gurobi_solver_interface.cc index 2530953e3c..f27f2703d3 100644 --- a/src/search/lp/gurobi_solver_interface.cc +++ b/src/search/lp/gurobi_solver_interface.cc @@ -180,7 +180,7 @@ void GurobiSolverInterface::add_temporary_constraints(const named_vector::NamedV } model_dirty = true; num_temporary_constraints += constraints.size(); - cerr << ">>>>> Added " << constraints.size() << " temporary constraints. Total temporary constraints: " << num_temporary_constraints << endl; + //cerr << ">>>>> Added " << constraints.size() << " temporary constraints. Total temporary constraints: " << num_temporary_constraints << endl; } void GurobiSolverInterface::clear_temporary_constraints() { @@ -192,7 +192,7 @@ void GurobiSolverInterface::clear_temporary_constraints() { GRB_CALL(env, GRBdelconstrs, model, num_temporary_constraints, indices.data()); model_dirty = true; num_temporary_constraints = 0; - cerr << ">>>>> Cleared temporary constraints. Total temporary constraints: " << num_temporary_constraints << endl; + //cerr << ">>>>> Cleared temporary constraints. Total temporary constraints: " << num_temporary_constraints << endl; } double GurobiSolverInterface::get_infinity() const { @@ -207,28 +207,28 @@ void GurobiSolverInterface::set_objective_coefficients(const vector &coe } // TODO: is there a more elegant way to handle this? GRB_CALL(env, GRBsetdblattrarray, model, GRB_DBL_ATTR_OBJ, 0, num_coefficients, const_cast(coefficients.data())); model_dirty = true; - cerr << ">>>>> New objective coefficients: ["; - for (int i = 0; i < num_coefficients; ++i) { - cerr << coefficients[i]; - if (i + 1 < num_coefficients) { - cerr << ", "; - } - } - cerr << "]" << endl; + //cerr << ">>>>> New objective coefficients: ["; + //for (int i = 0; i < num_coefficients; ++i) { + // cerr << coefficients[i]; + // if (i + 1 < num_coefficients) { + // cerr << ", "; + // } + //} + //cerr << "]" << endl; } void GurobiSolverInterface::set_objective_coefficient(int index, double coefficient) { //assert(index >= 0 && index < get_num_variables()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_OBJ, index, coefficient); model_dirty = true; - cerr << ">>>>> New objective coefficient for variable " << index << ": " << coefficient << endl; + //cerr << ">>>>> New objective coefficient for variable " << index << ": " << coefficient << endl; } void GurobiSolverInterface::set_constraint_rhs(int index, double bound) { assert(index >= 0 && index < get_num_constraints()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_RHS, index, bound); model_dirty = true; - cerr << ">>>>> New right-hand side for constraint " << index << ": " << bound << endl; + //cerr << ">>>>> New right-hand side for constraint " << index << ": " << bound << endl; } void GurobiSolverInterface::set_constraint_sense(int index, lp::Sense sense) { @@ -253,14 +253,14 @@ void GurobiSolverInterface::set_variable_lower_bound(int index, double bound) { //assert(index >= 0 && index < get_num_variables()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_LB, index, bound); model_dirty = true; - cerr << ">>>>> New lower bound for variable " << index << ": " << bound << endl; + //cerr << ">>>>> New lower bound for variable " << index << ": " << bound << endl; } void GurobiSolverInterface::set_variable_upper_bound(int index, double bound) { //assert(index >= 0 && index < get_num_variables()); GRB_CALL(env, GRBsetdblattrelement, model, GRB_DBL_ATTR_UB, index, bound); model_dirty = true; - cerr << ">>>>> New upper bound for variable " << index << ": " << bound << endl; + //cerr << ">>>>> New upper bound for variable " << index << ": " << bound << endl; } void GurobiSolverInterface::set_mip_gap(double gap) { @@ -289,7 +289,7 @@ void GurobiSolverInterface::print_failure_analysis() const { bool GurobiSolverInterface::is_infeasible() const { int status = 0; GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); - cerr << ">>>>> Is infeasible? " << (status == GRB_INFEASIBLE) << endl; + //cerr << ">>>>> Is infeasible? " << (status == GRB_INFEASIBLE) << endl; return status == GRB_INFEASIBLE; } @@ -297,7 +297,7 @@ bool GurobiSolverInterface::is_infeasible() const { bool GurobiSolverInterface::is_unbounded() const { int status = 0; GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); - cerr << ">>>>> Is unbounded? " << (status == GRB_UNBOUNDED) << endl; + //cerr << ">>>>> Is unbounded? " << (status == GRB_UNBOUNDED) << endl; return status == GRB_UNBOUNDED; } @@ -305,7 +305,7 @@ bool GurobiSolverInterface::is_unbounded() const { bool GurobiSolverInterface::has_optimal_solution() const { int status = 0; GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_STATUS, &status); - cerr << ">>>>> Has optimal solution? " << (status == GRB_OPTIMAL) << endl; + //cerr << ">>>>> Has optimal solution? " << (status == GRB_OPTIMAL) << endl; return status == GRB_OPTIMAL; } @@ -313,7 +313,7 @@ bool GurobiSolverInterface::has_optimal_solution() const { double GurobiSolverInterface::get_objective_value() const { double value = 0.0; GRB_CALL(env, GRBgetdblattr, model, GRB_DBL_ATTR_OBJVAL, &value); - cerr << ">>>>> Objective value: " << value << endl; + //cerr << ">>>>> Objective value: " << value << endl; return value; } @@ -325,31 +325,31 @@ vector GurobiSolverInterface::extract_solution() const { if (num_variables > 0) { GRB_CALL(env, GRBgetdblattrarray, model, GRB_DBL_ATTR_X, 0, num_variables, solution.data()); } - cerr << ">>>>> Extracted solution: ["; - for (int i = 0; i < num_variables; ++i) { - cerr << solution[i]; - if (i + 1 < num_variables) { - cerr << ", "; - } - } - cerr << "]" << endl; + //cerr << ">>>>> Extracted solution: ["; + //for (int i = 0; i < num_variables; ++i) { + // cerr << solution[i]; + // if (i + 1 < num_variables) { + // cerr << ", "; + // } + //} + //cerr << "]" << endl; return solution; } int GurobiSolverInterface::get_num_variables() const { int num_variables = 0; GRB_CALL(env, GRBgetintattr, model, GRB_INT_ATTR_NUMVARS, &num_variables); - cerr << ">>>>> Number of variables: " << num_variables << endl; + //cerr << ">>>>> Number of variables: " << num_variables << endl; return num_variables; } int GurobiSolverInterface::get_num_constraints() const { - cerr << ">>>>> Number of constraints: " << (num_permanent_constraints + num_temporary_constraints) << endl; + //cerr << ">>>>> Number of constraints: " << (num_permanent_constraints + num_temporary_constraints) << endl; return num_permanent_constraints + num_temporary_constraints; } bool GurobiSolverInterface::has_temporary_constraints() const { - cerr << ">>>>> Has temporary constraints? " << (num_temporary_constraints > 0) << endl; + //cerr << ">>>>> Has temporary constraints? " << (num_temporary_constraints > 0) << endl; return num_temporary_constraints > 0; } From a18f51c18a440845819dd4dc87cfc853b13a5e86 Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Wed, 11 Feb 2026 15:17:33 +0100 Subject: [PATCH 13/15] remove prints from the highs interface --- src/search/lp/highs_solver_interface.cc | 64 ++++++++++++------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/search/lp/highs_solver_interface.cc b/src/search/lp/highs_solver_interface.cc index e9f57412ce..38dc5a310c 100644 --- a/src/search/lp/highs_solver_interface.cc +++ b/src/search/lp/highs_solver_interface.cc @@ -169,8 +169,8 @@ void HiGHSSolverInterface::add_temporary_constraints( "addRow(temporary_constraints)"); } - cerr << "@@@@@@@@@@@@@@@@@@@@@ Warning: add_temporary_constraints does not update the constraint right-hand side and sense. Make sure to call set_constraint_rhs and set_constraint_sense if the right-hand side or sense should be changed." << endl; - exit(1); + //cerr << "@@@@@@@@@@@@@@@@@@@@@ Warning: add_temporary_constraints does not update the constraint right-hand side and sense. Make sure to call set_constraint_rhs and set_constraint_sense if the right-hand side or sense should be changed." << endl; + // exit(1); } void HiGHSSolverInterface::clear_temporary_constraints() { @@ -207,19 +207,19 @@ void HiGHSSolverInterface::set_objective_coefficients(const std::vector& "changeColCost(vec)"); } - cerr << ">>>>> New objective coefficients: ["; - for (int i = 0; i < n; ++i) { - cerr << coefficients[i]; - if (i + 1 < n) { - cerr << ", "; - } - } - cerr << "]" << endl; + //cerr << ">>>>> New objective coefficients: ["; + //for (int i = 0; i < n; ++i) { + // cerr << coefficients[i]; + // if (i + 1 < n) { + // cerr << ", "; + // } + //} + //cerr << "]" << endl; } void HiGHSSolverInterface::set_objective_coefficient(int index, double coefficient) { highs_ok(highs_.changeColCost(index, coefficient), "changeColCost"); - cerr << ">>>>> New objective coefficient for variable " << index << ": " << coefficient << endl; + //cerr << ">>>>> New objective coefficient for variable " << index << ": " << coefficient << endl; } void HiGHSSolverInterface::set_constraint_rhs(int index, double right_hand_side) { @@ -229,7 +229,7 @@ void HiGHSSolverInterface::set_constraint_rhs(int index, double right_hand_side) const Sense sense = bounds_to_sense(lb, ub, inf); const RowBounds bounds = sense_rhs_to_bounds(highs_, sense, right_hand_side); highs_ok(highs_.changeRowBounds(index, bounds.lower, bounds.upper), "changeRowBounds(rhs)"); - cerr << ">>>>> New rhs for constraint " << index << ": " << right_hand_side << endl; + //cerr << ">>>>> New rhs for constraint " << index << ": " << right_hand_side << endl; } void HiGHSSolverInterface::set_constraint_sense(int index, lp::Sense sense) { @@ -239,21 +239,21 @@ void HiGHSSolverInterface::set_constraint_sense(int index, lp::Sense sense) { const double rhs = bounds_to_rhs(lb, ub, inf); const RowBounds bounds = sense_rhs_to_bounds(highs_, sense, rhs); highs_ok(highs_.changeRowBounds(index, bounds.lower, bounds.upper), "changeRowBounds(sense)"); - cerr << ">>>>> New sense for constraint " << index << ": " - << (sense == lp::Sense::GE ? "GE" : (sense == lp::Sense::LE ? "LE" : (sense == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) - << endl; + //cerr << ">>>>> New sense for constraint " << index << ": " + // << (sense == lp::Sense::GE ? "GE" : (sense == lp::Sense::LE ? "LE" : (sense == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) + // << endl; } void HiGHSSolverInterface::set_variable_lower_bound(int index, double bound) { const double ub = highs_.getLp().col_upper_[index]; highs_ok(highs_.changeColBounds(index, bound, ub), "changeColBounds(lb)"); - cerr << ">>>>> New lower bound for variable " << index << ": " << bound << endl; + //cerr << ">>>>> New lower bound for variable " << index << ": " << bound << endl; } void HiGHSSolverInterface::set_variable_upper_bound(int index, double bound) { const double lb = highs_.getLp().col_lower_[index]; highs_ok(highs_.changeColBounds(index, lb, bound), "changeColBounds(ub)"); - cerr << ">>>>> New upper bound for variable " << index << ": " << bound << endl; + //cerr << ">>>>> New upper bound for variable " << index << ": " << bound << endl; } void HiGHSSolverInterface::set_mip_gap(double gap) { @@ -273,24 +273,24 @@ void HiGHSSolverInterface::print_failure_analysis() const { } bool HiGHSSolverInterface::is_infeasible() const { - cerr << ">>>>> Is infeasible? " << (highs_.getModelStatus() == HighsModelStatus::kInfeasible) << endl; + //cerr << ">>>>> Is infeasible? " << (highs_.getModelStatus() == HighsModelStatus::kInfeasible) << endl; return highs_.getModelStatus() == HighsModelStatus::kInfeasible; } bool HiGHSSolverInterface::is_unbounded() const { - cerr << ">>>>> Is unbounded? " << (highs_.getModelStatus() == HighsModelStatus::kUnbounded) << endl; + //cerr << ">>>>> Is unbounded? " << (highs_.getModelStatus() == HighsModelStatus::kUnbounded) << endl; return highs_.getModelStatus() == HighsModelStatus::kUnbounded; } bool HiGHSSolverInterface::has_optimal_solution() const { - cerr << ">>>>> Has optimal solution? " << (highs_.getModelStatus() == HighsModelStatus::kOptimal) << endl; + //cerr << ">>>>> Has optimal solution? " << (highs_.getModelStatus() == HighsModelStatus::kOptimal) << endl; return highs_.getModelStatus() == HighsModelStatus::kOptimal; } double HiGHSSolverInterface::get_objective_value() const { assert(has_optimal_solution()); const HighsInfo& info = highs_.getInfo(); - cerr << ">>>>> Objective value: " << info.objective_function_value << endl; + //cerr << ">>>>> Objective value: " << info.objective_function_value << endl; return info.objective_function_value; } @@ -302,29 +302,29 @@ std::vector HiGHSSolverInterface::extract_solution() const { for (int i = 0; i < n; i++){ x[i] = sol.col_value[i]; } - cerr << ">>>>> Extracted solution: ["; - for (int i = 0; i < n; ++i) { - cerr << x[i]; - if (i + 1 < n) { - cerr << ", "; - } - } - cerr << "]" << endl; + //cerr << ">>>>> Extracted solution: ["; + //for (int i = 0; i < n; ++i) { + // cerr << x[i]; + // if (i + 1 < n) { + // cerr << ", "; + // } + //} + //cerr << "]" << endl; return x; } int HiGHSSolverInterface::get_num_variables() const { - cerr << ">>>>> Number of variables: " << highs_.getNumCol() << endl; + //cerr << ">>>>> Number of variables: " << highs_.getNumCol() << endl; return highs_.getNumCol(); } int HiGHSSolverInterface::get_num_constraints() const { - cerr << ">>>>> Number of constraints: " << highs_.getNumRow() << endl; + //cerr << ">>>>> Number of constraints: " << highs_.getNumRow() << endl; return highs_.getNumRow(); } bool HiGHSSolverInterface::has_temporary_constraints() const { - cerr << ">>>>> Has temporary constraints? " << (num_temporary_constraints > 0) << endl; + //cerr << ">>>>> Has temporary constraints? " << (num_temporary_constraints > 0) << endl; return num_temporary_constraints != 0; } From 97ed437e9b00001b2245a77d296c0f387f77cfee Mon Sep 17 00:00:00 2001 From: Gustavo Delazeri Date: Wed, 11 Feb 2026 17:58:22 +0100 Subject: [PATCH 14/15] update highs interface; update gurobi interface --- src/search/lp/gurobi_solver_interface.cc | 12 ++-- src/search/lp/highs_solver_interface.cc | 80 ++++++++---------------- src/search/lp/highs_solver_interface.h | 2 +- 3 files changed, 34 insertions(+), 60 deletions(-) diff --git a/src/search/lp/gurobi_solver_interface.cc b/src/search/lp/gurobi_solver_interface.cc index f27f2703d3..a76f5bb151 100644 --- a/src/search/lp/gurobi_solver_interface.cc +++ b/src/search/lp/gurobi_solver_interface.cc @@ -234,19 +234,19 @@ void GurobiSolverInterface::set_constraint_rhs(int index, double bound) { void GurobiSolverInterface::set_constraint_sense(int index, lp::Sense sense) { assert(index >= 0 && index < get_num_constraints()); if (sense == lp::Sense::GE) { - GRB_CALL(env, GRBsetdblattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_GREATER_EQUAL); + GRB_CALL(env, GRBsetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_GREATER_EQUAL); } else if (sense == lp::Sense::LE) { - GRB_CALL(env, GRBsetdblattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_LESS_EQUAL); + GRB_CALL(env, GRBsetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_LESS_EQUAL); } else if (sense == lp::Sense::EQ) { - GRB_CALL(env, GRBsetdblattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_EQUAL); + GRB_CALL(env, GRBsetcharattrelement, model, GRB_CHAR_ATTR_SENSE, index, GRB_EQUAL); } else { cerr << "Error: Unknown constraint sense." << endl; utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); } model_dirty = true; - cerr << ">>>>> New sense for constraint " << index << ": " - << (sense == lp::Sense::GE ? "GE" : (sense == lp::Sense::LE ? "LE" : (sense == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) - << endl; + //cerr << ">>>>> New sense for constraint " << index << ": " + // << (sense == lp::Sense::GE ? "GE" : (sense == lp::Sense::LE ? "LE" : (sense == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) + // << endl; } void GurobiSolverInterface::set_variable_lower_bound(int index, double bound) { diff --git a/src/search/lp/highs_solver_interface.cc b/src/search/lp/highs_solver_interface.cc index 38dc5a310c..d436588741 100644 --- a/src/search/lp/highs_solver_interface.cc +++ b/src/search/lp/highs_solver_interface.cc @@ -14,13 +14,6 @@ struct RowBounds { double upper; }; -bool is_neg_inf(double value, double inf) { - return value <= -inf; -} - -bool is_pos_inf(double value, double inf) { - return value >= inf; -} RowBounds sense_rhs_to_bounds(const Highs &highs, Sense sense, double rhs) { const double inf = highs.getInfinity(); @@ -34,38 +27,6 @@ RowBounds sense_rhs_to_bounds(const Highs &highs, Sense sense, double rhs) { } throw std::runtime_error("Unknown constraint sense in HiGHS interface"); } - -Sense bounds_to_sense(double lb, double ub, double inf) { - const bool lb_inf = is_neg_inf(lb, inf); - const bool ub_inf = is_pos_inf(ub, inf); - - if (lb_inf && !ub_inf) { - return Sense::LE; - } - if (!lb_inf && ub_inf) { - return Sense::GE; - } - if (!lb_inf && !ub_inf && lb == ub) { - return Sense::EQ; - } - throw std::runtime_error("HiGHS constraint has unsupported ranged bounds"); -} - -double bounds_to_rhs(double lb, double ub, double inf) { - const bool lb_inf = is_neg_inf(lb, inf); - const bool ub_inf = is_pos_inf(ub, inf); - - if (lb_inf && !ub_inf) { - return ub; - } - if (!lb_inf && ub_inf) { - return lb; - } - if (!lb_inf && !ub_inf && lb == ub) { - return lb; - } - throw std::runtime_error("HiGHS constraint has unsupported ranged bounds"); -} } // namespace static void highs_ok(HighsStatus s, const char* where) { @@ -121,6 +82,7 @@ void HiGHSSolverInterface::load_problem(const LinearProgram &lp) { idx.data(), val.data()), "addRow"); + constraint_senses.push_back(c.get_sense()); } num_permanent_constraints = lp.get_constraints().size(); num_temporary_constraints = 0; @@ -167,6 +129,7 @@ void HiGHSSolverInterface::add_temporary_constraints( idx.data(), val.data()), "addRow(temporary_constraints)"); + constraint_senses.push_back(c.get_sense()); } //cerr << "@@@@@@@@@@@@@@@@@@@@@ Warning: add_temporary_constraints does not update the constraint right-hand side and sense. Make sure to call set_constraint_rhs and set_constraint_sense if the right-hand side or sense should be changed." << endl; @@ -180,7 +143,7 @@ void HiGHSSolverInterface::clear_temporary_constraints() { const int fst_temp_row = num_permanent_constraints; const int lst_temp_row = highs_.getNumRow() - 1; highs_ok(highs_.deleteRows(fst_temp_row, lst_temp_row), "deleteRows"); - + constraint_senses.resize(num_permanent_constraints); num_temporary_constraints = 0; } @@ -223,25 +186,36 @@ void HiGHSSolverInterface::set_objective_coefficient(int index, double coefficie } void HiGHSSolverInterface::set_constraint_rhs(int index, double right_hand_side) { - const double inf = highs_.getInfinity(); const double lb = highs_.getLp().row_lower_[index]; const double ub = highs_.getLp().row_upper_[index]; - const Sense sense = bounds_to_sense(lb, ub, inf); - const RowBounds bounds = sense_rhs_to_bounds(highs_, sense, right_hand_side); - highs_ok(highs_.changeRowBounds(index, bounds.lower, bounds.upper), "changeRowBounds(rhs)"); - //cerr << ">>>>> New rhs for constraint " << index << ": " << right_hand_side << endl; + const lp::Sense sense = constraint_senses[index]; + + if(sense == lp::Sense::GE) { + highs_ok(highs_.changeRowBounds(index, right_hand_side, ub), "changeRowBounds(rhs)"); + } + else if(sense == lp::Sense::LE) { + highs_ok(highs_.changeRowBounds(index, lb, right_hand_side), "changeRowBounds(rhs)"); + } + else if(sense == lp::Sense::EQ) { + highs_ok(highs_.changeRowBounds(index, right_hand_side, right_hand_side), "changeRowBounds(rhs)"); + } else { + throw std::runtime_error("Error: Unknown constraint sense."); + } } void HiGHSSolverInterface::set_constraint_sense(int index, lp::Sense sense) { - const double inf = highs_.getInfinity(); + // Get bounds const double lb = highs_.getLp().row_lower_[index]; - const double ub = highs_.getLp().row_upper_[index]; - const double rhs = bounds_to_rhs(lb, ub, inf); - const RowBounds bounds = sense_rhs_to_bounds(highs_, sense, rhs); - highs_ok(highs_.changeRowBounds(index, bounds.lower, bounds.upper), "changeRowBounds(sense)"); - //cerr << ">>>>> New sense for constraint " << index << ": " - // << (sense == lp::Sense::GE ? "GE" : (sense == lp::Sense::LE ? "LE" : (sense == lp::Sense::EQ ? "EQ" : "UNKNOWN"))) - // << endl; + const double ub = highs_.getLp().row_upper_[index]; + // Update vector with constraint senses + constraint_senses[index] = sense; + // Throw error if the new sense is unknown or the sense is equality but the bounds are not equal + if (sense == lp::Sense::EQ && lb != ub) { + throw std::runtime_error("Error: Cannot set sense to EQ for constraint " + std::to_string(index) + " because the lower and upper bounds are not equal."); + } + else if (sense != lp::Sense::GE && sense != lp::Sense::LE && sense != lp::Sense::EQ) { + throw std::runtime_error("Error: Unknown constraint sense."); + } } void HiGHSSolverInterface::set_variable_lower_bound(int index, double bound) { diff --git a/src/search/lp/highs_solver_interface.h b/src/search/lp/highs_solver_interface.h index 8ed0019384..a8b6acb982 100644 --- a/src/search/lp/highs_solver_interface.h +++ b/src/search/lp/highs_solver_interface.h @@ -11,9 +11,9 @@ namespace lp { class HiGHSSolverInterface : public SolverInterface { mutable Highs highs_; - HighsModelStatus model_status_ = HighsModelStatus::kNotset; int num_permanent_constraints; int num_temporary_constraints; + vector constraint_senses; public: HiGHSSolverInterface(); From 921139508d14d4c55ad374b4ad9a9cad6a8774c5 Mon Sep 17 00:00:00 2001 From: Travis Rivera Petit Date: Wed, 11 Feb 2026 18:15:39 +0100 Subject: [PATCH 15/15] Fix sense bug --- src/search/lp/soplex_solver_interface.cc | 36 ++++++++++++++++-------- src/search/lp/soplex_solver_interface.h | 1 + 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/search/lp/soplex_solver_interface.cc b/src/search/lp/soplex_solver_interface.cc index 133d7b9366..986133fcca 100644 --- a/src/search/lp/soplex_solver_interface.cc +++ b/src/search/lp/soplex_solver_interface.cc @@ -94,12 +94,25 @@ void SoPlexSolverInterface::load_problem(const LinearProgram &lp) { soplex.addRowsReal(constraints_to_row_set(lp.get_constraints())); num_permanent_constraints = lp.get_constraints().size(); num_temporary_constraints = 0; + + const auto &cons = lp.get_constraints(); + const int m = static_cast(cons.size()); + for (int r = 0; r < m; ++r) { + const auto &c = cons[r]; + constraint_senses.push_back(c.get_sense()); + } + } void SoPlexSolverInterface::add_temporary_constraints( const named_vector::NamedVector &constraints) { soplex.addRowsReal(constraints_to_row_set(constraints)); - num_temporary_constraints = constraints.size(); + num_temporary_constraints += constraints.size(); + + for (int i = 0; i < (int)constraints.size(); ++i) { + const auto &c = constraints[i]; + constraint_senses.push_back(c.get_sense()); + } } void SoPlexSolverInterface::clear_temporary_constraints() { @@ -107,6 +120,7 @@ void SoPlexSolverInterface::clear_temporary_constraints() { int first = num_permanent_constraints; int last = first + num_temporary_constraints - 1; soplex.removeRowRangeReal(first, last, nullptr); + constraint_senses.resize(num_permanent_constraints); num_temporary_constraints = 0; } } @@ -265,21 +279,19 @@ void SoPlexSolverInterface::print_statistics() const { } void SoPlexSolverInterface::set_constraint_rhs(int index, double b) { - const double lhs = soplex.lhsReal(index); - const double rhs = soplex.rhsReal(index); - - const bool lhs_is_neginf = (lhs == -infinity); - const bool rhs_is_posinf = (rhs == infinity); + const lp::Sense sense = constraint_senses[index]; - if (lhs_is_neginf && !rhs_is_posinf) { - soplex.changeRhsReal(index, b); - } else if (!lhs_is_neginf && rhs_is_posinf) { + if(sense == lp::Sense::GE) { soplex.changeLhsReal(index, b); - } else if (!lhs_is_neginf && !rhs_is_posinf && lhs == rhs) { + } + else if(sense == lp::Sense::LE) { + soplex.changeRhsReal(index, b); + } + else if(sense == lp::Sense::EQ) { soplex.changeLhsReal(index, b); soplex.changeRhsReal(index, b); } else { - throw std::logic_error("invalid constraint"); + throw std::runtime_error("Error: Unknown constraint sense."); } } @@ -290,6 +302,8 @@ void SoPlexSolverInterface::set_constraint_sense(int index, lp::Sense sense) { const bool lhs_is_neginf = (lhs == -infinity); const bool rhs_is_posinf = (rhs == infinity); + constraint_senses[index] = sense; + double b; if (lhs_is_neginf && !rhs_is_posinf) { b = rhs; diff --git a/src/search/lp/soplex_solver_interface.h b/src/search/lp/soplex_solver_interface.h index 83ca0c706c..7579d33218 100644 --- a/src/search/lp/soplex_solver_interface.h +++ b/src/search/lp/soplex_solver_interface.h @@ -27,6 +27,7 @@ class SoPlexSolverInterface : public SolverInterface { mutable soplex::SoPlex soplex; int num_permanent_constraints; int num_temporary_constraints; + std::vector constraint_senses; public: SoPlexSolverInterface();