From 2c67dfe539f19da957e7f91c832c6d234482f169 Mon Sep 17 00:00:00 2001 From: Sukrit Kalra Date: Mon, 25 Sep 2023 20:29:10 -0700 Subject: [PATCH] Implement Google-OR Tools backend. --- schedulers/tetrisched/CMakeLists.txt | 21 ++- .../include/tetrisched/GoogleCPSolver.hpp | 50 ++++++ .../include/tetrisched/SolverModel.hpp | 4 + .../tetrisched/include/tetrisched/Types.hpp | 1 + schedulers/tetrisched/src/GoogleCPSolver.cpp | 165 ++++++++++++++++++ schedulers/tetrisched/test/test_solver.cpp | 64 ++++--- 6 files changed, 281 insertions(+), 24 deletions(-) create mode 100644 schedulers/tetrisched/include/tetrisched/GoogleCPSolver.hpp create mode 100644 schedulers/tetrisched/src/GoogleCPSolver.cpp diff --git a/schedulers/tetrisched/CMakeLists.txt b/schedulers/tetrisched/CMakeLists.txt index e2f331b4..96366cbe 100644 --- a/schedulers/tetrisched/CMakeLists.txt +++ b/schedulers/tetrisched/CMakeLists.txt @@ -21,6 +21,7 @@ set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) set(CPLEX_DIR "/opt/ibm/ILOG/CPLEX_Studio2211") set(GUROBI_DIR "/opt/gurobi1001/linux64") +set(ORTOOLS_DIR "/usr/local/include/ortools") if (EXISTS "${CPLEX_DIR}") message("-- Adding CPLEX : ${CPLEX_DIR}") @@ -45,6 +46,7 @@ else() set(CPLEX_LINK_DIRS "") set(CPLEX_LINK_LIBRARIES "") endif() + if (EXISTS "${GUROBI_DIR}") message("-- Adding GUROBI : ${GUROBI_DIR}") add_compile_definitions(_TETRISCHED_WITH_GUROBI_) @@ -56,15 +58,23 @@ if (EXISTS "${GUROBI_DIR}") set(GUROBI_LINK_LIBRARIES "gurobi_c++" "gurobi100") - else() message("-- Not Adding GUROBI") set(GUROBI_INCLUDE_DIRS "") set(GUROBI_LINK_DIRS "") set(GUROBI_LINK_LIBRARIES "") - endif() +if (EXISTS "${ORTOOLS_DIR}") + message("-- Adding Google OR-Tools : ${ORTOOLS_DIR}") + add_compile_definitions(_TETRISCHED_WITH_ORTOOLS_) + + set(ORTOOLS_LINK_LIBRARIES + "ortools") +else() + message("-- Not adding OR-Tools") + set(ORTOOLS_LINK_LIBRARIES "") +endif() set(TETRISCHED_SOURCE "src/Expression.cpp" @@ -78,6 +88,9 @@ endif() if (EXISTS "${GUROBI_DIR}") LIST(APPEND TETRISCHED_SOURCE "src/GurobiSolver.cpp") endif() +if (EXISTS "${ORTOOLS_DIR}") + LIST(APPEND TETRISCHED_SOURCE "src/GoogleCPSolver.cpp") +endif() add_library(tetrisched SHARED ${TETRISCHED_SOURCE}) target_include_directories(tetrisched PRIVATE include) @@ -93,6 +106,9 @@ target_include_directories(tetrisched PRIVATE ${GUROBI_INCLUDE_DIRS}) target_link_directories(tetrisched PRIVATE ${GUROBI_LINK_DIRS}) target_link_libraries(tetrisched PRIVATE ${GUROBI_LINK_LIBRARIES}) +# Include and link Google-ORTools. +target_link_libraries(tetrisched PRIVATE ${ORTOOLS_LINK_LIBRARIES}) + # add_executable(tetrisched_main ${TETRISCHED_SOURCE}) # target_include_directories(tetrisched_main PRIVATE ${CPLEX_INCLUDE_DIRS}) @@ -127,6 +143,7 @@ target_link_libraries( tetrisched ${CPLEX_LINK_LIBRARIES} ${GUROBI_LINK_LIBRARIES} + ${ORTOOLS_LINK_LIBRARIES} ) include(GoogleTest) diff --git a/schedulers/tetrisched/include/tetrisched/GoogleCPSolver.hpp b/schedulers/tetrisched/include/tetrisched/GoogleCPSolver.hpp new file mode 100644 index 00000000..e4aec033 --- /dev/null +++ b/schedulers/tetrisched/include/tetrisched/GoogleCPSolver.hpp @@ -0,0 +1,50 @@ +#ifndef _TETRISCHED_GOOGLE_CP_SOLVER_HPP_ +#define _TETRISCHED_GOOGLE_CP_SOLVER_HPP_ + +#include + +#include "ortools/sat/cp_model.h" +#include "tetrisched/Solver.hpp" + +namespace tetrisched { +// Import the relevant names from the ORTools namespace. +using operations_research::sat::BoolVar; +using operations_research::sat::CpModelBuilder; +using operations_research::sat::IntVar; +using operations_research::sat::LinearExpr; + +class GoogleCPSolver : public Solver { + using GoogleCPVarType = std::variant; + + private: + /// The SolverModelPtr associated with this GoogleCPSolver. + SolverModelPtr solverModel; + /// The ORTools model associated with this GoogleCPSolver. + std::unique_ptr cpModel; + /// A map from the Variable ID to the ORTools variable. + std::unordered_map cpVariables; + + /// Translates the VariablePtr into an IntVar / BoolVar. + GoogleCPVarType translateVariable(const VariablePtr& variable) const; + /// Translates the ConstraintPtr into a Constraint and adds it to the model. + operations_research::sat::Constraint translateConstraint( + const ConstraintPtr& constraint); + /// Translates the ObjectiveFunctionPtr into an Expression in Google OR-Tools. + LinearExpr translateObjectiveFunction( + const ObjectiveFunctionPtr& objectiveFunction) const; + + public: + /// Create a new CP-SAT solver. + GoogleCPSolver(); + + /// Retrieve a pointer to the SolverModel. + SolverModelPtr getModel() override; + + /// Translates the SolverModel into a CP-SAT model. + void translateModel() override; + + /// Export the constructed model to the given file. + void exportModel(const std::string& fileName) override; +}; +} // namespace tetrisched +#endif // _TETRISCHED_GOOGLE_CP_SOLVER_HPP_ diff --git a/schedulers/tetrisched/include/tetrisched/SolverModel.hpp b/schedulers/tetrisched/include/tetrisched/SolverModel.hpp index 44d019ce..85f3dc9f 100644 --- a/schedulers/tetrisched/include/tetrisched/SolverModel.hpp +++ b/schedulers/tetrisched/include/tetrisched/SolverModel.hpp @@ -81,6 +81,7 @@ class VariableT { /// Annotate friend classes for Solvers so that they have access to internals. friend tetrisched::CPLEXSolver; friend tetrisched::GurobiSolver; + friend tetrisched::GoogleCPSolver; }; // Specialize the VariableT class for Integer type. @@ -147,6 +148,7 @@ class ConstraintT { /// Annotate friend classes for Solvers so that they have access to internals. friend tetrisched::CPLEXSolver; friend tetrisched::GurobiSolver; + friend tetrisched::GoogleCPSolver; }; // Specialize the Constraint class for Integer. @@ -191,6 +193,7 @@ class ObjectiveFunctionT { /// Annotate friend classes for Solvers so that they have access to internals. friend tetrisched::CPLEXSolver; friend tetrisched::GurobiSolver; + friend tetrisched::GoogleCPSolver; }; // Specialize the ObjectiveFunction class for Integer. @@ -242,6 +245,7 @@ class SolverModelT { /// back to the user. friend tetrisched::CPLEXSolver; friend tetrisched::GurobiSolver; + friend tetrisched::GoogleCPSolver; }; // Specialize the SolverModel class for Integer. diff --git a/schedulers/tetrisched/include/tetrisched/Types.hpp b/schedulers/tetrisched/include/tetrisched/Types.hpp index be4e95ce..ef8d2d18 100644 --- a/schedulers/tetrisched/include/tetrisched/Types.hpp +++ b/schedulers/tetrisched/include/tetrisched/Types.hpp @@ -62,6 +62,7 @@ using ExpressionPtr = std::unique_ptr; /// them as friend classes in the model. class CPLEXSolver; class GurobiSolver; +class GoogleCPSolver; } // namespace tetrisched #endif diff --git a/schedulers/tetrisched/src/GoogleCPSolver.cpp b/schedulers/tetrisched/src/GoogleCPSolver.cpp new file mode 100644 index 00000000..ceebe0e0 --- /dev/null +++ b/schedulers/tetrisched/src/GoogleCPSolver.cpp @@ -0,0 +1,165 @@ +#include "tetrisched/GoogleCPSolver.hpp" + +namespace tetrisched { +GoogleCPSolver::GoogleCPSolver() : cpModel(new CpModelBuilder()) {} + +SolverModelPtr GoogleCPSolver::getModel() { + if (!solverModel) { + solverModel = std::shared_ptr(new SolverModel()); + } + return solverModel; +} + +GoogleCPSolver::GoogleCPVarType GoogleCPSolver::translateVariable( + const VariablePtr& variable) const { + // Check that a continuous variable is not passed in. + if (variable->variableType == VariableType::VAR_CONTINUOUS) { + throw exceptions::SolverException( + "Cannot construct a continuous variable in ORTools."); + } else if (variable->variableType == VariableType::VAR_INTEGER) { + // Check that the Variable has been given both a lower and an upper bound. + // This is required to set a Domain in ORTools. + if (!(variable->lowerBound.has_value() && + variable->upperBound.has_value())) { + throw exceptions::SolverException( + "Cannot construct a variable without a lower and upper bound in " + "ORTools: " + + variable->toString()); + } + + // Construct the domain for the variable. + const operations_research::Domain domain(variable->lowerBound.value(), + variable->upperBound.value()); + return cpModel->NewIntVar(domain).WithName(variable->variableName); + } else if (variable->variableType == VariableType::VAR_INDICATOR) { + // Construct the Indicator variable. + return cpModel->NewBoolVar().WithName(variable->variableName); + } else { + throw exceptions::SolverException("Cannot construct a variable of type " + + std::to_string(variable->variableType) + + " in ORTools."); + } +} + +operations_research::sat::Constraint GoogleCPSolver::translateConstraint( + const ConstraintPtr& constraint) { + // TODO (Sukrit): We are currently assuming that all constraints and + // objectives are linear. We may need to support quadratic constraints. + operations_research::sat::LinearExpr constraintExpr; + + // Construct all the terms. + for (const auto& [coefficient, variable] : constraint->terms) { + if (variable) { + switch (variable->variableType) { + case VariableType::VAR_INTEGER: + constraintExpr += + coefficient * std::get(cpVariables.at(variable->getId())); + break; + case VariableType::VAR_INDICATOR: + constraintExpr += + coefficient * + std::get(cpVariables.at(variable->getId())); + break; + default: + throw exceptions::SolverException( + "Cannot construct a constraint with a variable of type " + + std::to_string(variable->variableType) + " in ORTools."); + } + } else { + constraintExpr += coefficient; + } + } + + // Translate the constraint. + switch (constraint->constraintType) { + case ConstraintType::CONSTR_EQ: + return cpModel->AddEquality(constraintExpr, constraint->rightHandSide) + .WithName(constraint->getName()); + case ConstraintType::CONSTR_GE: + return cpModel + ->AddGreaterOrEqual(constraintExpr, constraint->rightHandSide) + .WithName(constraint->getName()); + case ConstraintType::CONSTR_LE: + return cpModel->AddLessOrEqual(constraintExpr, constraint->rightHandSide) + .WithName(constraint->getName()); + default: + throw exceptions::SolverException( + "Invalid constraint type: " + + std::to_string(constraint->constraintType)); + } +} + +LinearExpr GoogleCPSolver::translateObjectiveFunction( + const ObjectiveFunctionPtr& objectiveFunction) const { + LinearExpr objectiveExpr; + + // Construct all the terms. + for (const auto& [coefficient, variable] : objectiveFunction->terms) { + if (variable) { + switch (variable->variableType) { + case VariableType::VAR_INTEGER: + objectiveExpr += + coefficient * std::get(cpVariables.at(variable->getId())); + break; + case VariableType::VAR_INDICATOR: + objectiveExpr += coefficient * + std::get(cpVariables.at(variable->getId())); + break; + default: + throw exceptions::SolverException( + "Cannot construct an objective function with a variable of " + "type " + + std::to_string(variable->variableType) + " in ORTools."); + } + } else { + objectiveExpr += coefficient; + } + } + return objectiveExpr; +} + +void GoogleCPSolver::translateModel() { + if (!solverModel) { + throw tetrisched::exceptions::SolverException( + "Empty SolverModel for GurobiSolver. Nothing to translate!"); + } + + // Generate all the variables and keep a cache of the variable indices + // to the ORTools variables. + for (const auto& [variableId, variable] : solverModel->variables) { + TETRISCHED_DEBUG("Adding variable " << variable->getName() << "(" + << variable->getId() + << ") to ORTools model."); + cpVariables[variableId] = translateVariable(variable); + } + + // Generate all the constraints. + for (const auto& [constraintId, constraint] : solverModel->constraints) { + TETRISCHED_DEBUG("Adding constraint " << constraint->getName() << "(" + << constraint->getId() + << ") to ORTools model."); + auto _ = translateConstraint(constraint); + } + + // Translate the objective function. + auto objectiveExpr = + translateObjectiveFunction(solverModel->objectiveFunction); + switch (solverModel->objectiveFunction->objectiveType) { + case ObjectiveType::OBJ_MINIMIZE: + cpModel->Minimize(objectiveExpr); + break; + case ObjectiveType::OBJ_MAXIMIZE: + cpModel->Maximize(objectiveExpr); + break; + default: + throw exceptions::SolverException( + "Invalid objective type: " + + std::to_string(solverModel->objectiveFunction->objectiveType)); + } +} + +void GoogleCPSolver::exportModel(const std::string& fileName) { + cpModel->ExportToFile(fileName); +} + +} // namespace tetrisched diff --git a/schedulers/tetrisched/test/test_solver.cpp b/schedulers/tetrisched/test/test_solver.cpp index 4a278938..3794c086 100644 --- a/schedulers/tetrisched/test/test_solver.cpp +++ b/schedulers/tetrisched/test/test_solver.cpp @@ -4,10 +4,13 @@ #include #ifdef _TETRISCHED_WITH_CPLEX_ #include "tetrisched/CPLEXSolver.hpp" -#endif //_TETRISCHED_WITH_CPLEX_ +#endif //_TETRISCHED_WITH_CPLEX_ #ifdef _TETRISCHED_WITH_GUROBI_ #include "tetrisched/GurobiSolver.hpp" -#endif //_TETRISCHED_WITH_GUROBI_ +#endif //_TETRISCHED_WITH_GUROBI_ +// #ifdef _TETRISCHED_WITH_OR_TOOLS_ +// #endif //_TETRISCHED_WITH_OR_TOOLS_ +#include "tetrisched/GoogleCPSolver.hpp" #include "tetrisched/Solver.hpp" #include "tetrisched/SolverModel.hpp" @@ -47,6 +50,21 @@ TEST(SolverModelTypes, TestObjectiveFnConstruction) { EXPECT_EQ(objectiveFn.size(), 1); } +void constructModel(tetrisched::SolverModelPtr& solverModelPtr) { + auto intVar = std::make_shared(tetrisched::VAR_INTEGER, + "intVar", 0, 100); + solverModelPtr->addVariable(intVar); + auto constraint = std::make_unique( + "TestConstraint", tetrisched::ConstraintType::CONSTR_LE, 10); + constraint->addTerm(2, intVar); + constraint->addTerm(5); + solverModelPtr->addConstraint(std::move(constraint)); + auto objectiveFunction = std::make_unique( + tetrisched::ObjectiveType::OBJ_MAXIMIZE); + objectiveFunction->addTerm(1, intVar); + solverModelPtr->setObjectiveFunction(std::move(objectiveFunction)); +} + #ifdef _TETRISCHED_WITH_CPLEX_ TEST(SolverModelTypes, TestSolverModel) { tetrisched::CPLEXSolver cplexSolver; @@ -73,21 +91,6 @@ TEST(SolverModelTypes, TestSolverModel) { std::filesystem::remove("test.lp"); } -void constructModel(tetrisched::SolverModelPtr& solverModelPtr) { - auto intVar = - std::make_shared(tetrisched::VAR_INTEGER, "intVar"); - solverModelPtr->addVariable(intVar); - auto constraint = std::make_unique( - "TestConstraint", tetrisched::ConstraintType::CONSTR_LE, 10); - constraint->addTerm(2, intVar); - constraint->addTerm(5); - solverModelPtr->addConstraint(std::move(constraint)); - auto objectiveFunction = std::make_unique( - tetrisched::ObjectiveType::OBJ_MAXIMIZE); - objectiveFunction->addTerm(1, intVar); - solverModelPtr->setObjectiveFunction(std::move(objectiveFunction)); -} - TEST(SolverModel, TestCPLEXSolverTranslation) { tetrisched::CPLEXSolver cplexSolver; auto solverModelPtr = cplexSolver.getModel(); @@ -100,9 +103,9 @@ TEST(SolverModel, TestCPLEXSolverTranslation) { cplexSolver.exportModel("test_cplexmodel.lp"); EXPECT_TRUE(std::filesystem::exists("test_cplexmodel.lp")) << "The file test_cplexmodel.lp was not created."; -// std::filesystem::remove("test_cplexmodel.lp"); + std::filesystem::remove("test_cplexmodel.lp"); } -#endif //_TETRISCHED_WITH_CPLEX_ +#endif //_TETRISCHED_WITH_CPLEX_ #ifdef _TETRISCHED_WITH_GUROBI_ TEST(SolverModel, TestGurobiSolverTranslation) { @@ -112,11 +115,28 @@ TEST(SolverModel, TestGurobiSolverTranslation) { solverModelPtr->exportModel("test_solvermodel.lp"); EXPECT_TRUE(std::filesystem::exists("test_solvermodel.lp")) << "The file test_solvermodel.lp was not created."; - // std::filesystem::remove("test_solvermodel.lp"); + std::filesystem::remove("test_solvermodel.lp"); gurobiSolver.translateModel(); gurobiSolver.exportModel("test_gurobimodel.lp"); EXPECT_TRUE(std::filesystem::exists("test_gurobimodel.lp")) << "The file test_gurobimodel.lp was not created."; - // std::filesystem::remove("test_gurobimodel.lp"); + std::filesystem::remove("test_gurobimodel.lp"); +} +#endif //_TETRISCHED_WITH_GUROBI_ + +#ifdef _TETRISCHED_WITH_ORTOOLS_ +TEST(SolverModel, TestOrToolsSolverTranslation) { + tetrisched::GoogleCPSolver googleCPSolver; + auto solverModelPtr = googleCPSolver.getModel(); + constructModel(solverModelPtr); + solverModelPtr->exportModel("test_solvermodel.lp"); + EXPECT_TRUE(std::filesystem::exists("test_solvermodel.lp")) + << "The file test_solvermodel.lp was not created."; + std::filesystem::remove("test_solvermodel.lp"); + googleCPSolver.translateModel(); + googleCPSolver.exportModel("test_ortoolsmodel.lp"); + EXPECT_TRUE(std::filesystem::exists("test_ortoolsmodel.lp")) + << "The file test_ortoolsmodel.lp was not created."; + std::filesystem::remove("test_ortoolsmodel.lp"); } -#endif //_TETRISCHED_WITH_GUROBI_ \ No newline at end of file +#endif //_TETRISCHED_WITH_ORTOOLS_