diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6a2c02b1..4dd26138 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,10 @@ on: jobs: coverage: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + env: + CC: gcc-11 + CXX: g++-11 steps: - uses: actions/checkout@v3 - uses: turtlebrowser/get-conan@main @@ -55,7 +58,10 @@ jobs: - name: Run Tests run: ./build/test/Release/tests.exe test_without_conan: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + env: + CC: gcc-11 + CXX: g++-11 steps: - uses: actions/checkout@v3 - name: Install parallelism library for C++ @@ -74,11 +80,29 @@ jobs: make -j tests - name: Run tests run: ./test/tests - test_release_inx: - strategy: - matrix: - os: [ ubuntu-latest, macos-13 ] - runs-on: ${{ matrix.os }} + test_release_mac: + runs-on: macos-13 + steps: + - uses: actions/checkout@v3 + - uses: turtlebrowser/get-conan@main + - name: Run Conan Install + run: | + conan profile detect + pushd ~/.conan2/profiles + sed -i'' -e 's/gnu17/17/g' * + popd + conan install -of . -o with_tests=True --build=missing . + - name: Run CMake + run: cmake --preset conan-release . + - name: Compile + run: cmake --build build/Release --target tests + - name: Run Tests + run: ./build/Release/test/tests + test_release_linux: + runs-on: ubuntu-22.04 + env: + CC: gcc-11 + CXX: g++-11 steps: - uses: actions/checkout@v3 - uses: turtlebrowser/get-conan@main @@ -96,7 +120,10 @@ jobs: - name: Run Tests run: ./build/Release/test/tests clang_tidy: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + env: + CC: gcc-11 + CXX: g++-11 steps: - uses: actions/checkout@v3 - uses: turtlebrowser/get-conan@main @@ -112,7 +139,7 @@ jobs: - name: Run Clang-Tidy run: clang-tidy-14 -p build/Release source/*.cpp include/**/*.hpp clang_format: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: DoozyX/clang-format-lint-action@v0.16.2 @@ -121,7 +148,7 @@ jobs: extensions: 'hpp,cpp' clangFormatVersion: 17 doxygen: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: ssciwr/doxygen-install@v1 diff --git a/changelog.md b/changelog.md index 38ac2aca..15ee84f4 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Added +- [PR31](https://github.com/scipopt/SCIPpp/pull/31) Add `InitialSolution` and `Model::addSolution`. - [PR28](https://github.com/scipopt/SCIPpp/pull/28) Add `Var::getVar`. ## [1.2.0] - 2024-05-21 diff --git a/include/scippp/initial_solution.hpp b/include/scippp/initial_solution.hpp new file mode 100644 index 00000000..451acf0c --- /dev/null +++ b/include/scippp/initial_solution.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +namespace scippp { + +// forward declare +class Var; + +/** + * A primal solution to be added to %SCIP's solution pool. + * + * @since 1.3.0 + */ +class InitialSolution { + friend class Model; + //! Variable assignment in the initial solution. + std::map m_values {}; + +public: + /** + * Sets the value for a variable in the solution. + * + * @since 1.3.0 + * @param var Variable to assign a value to. + * @param value to assign to the variable. + */ + void setValue(const Var& var, double value); + + /** + * Access the mutable value assigned to the variable. + * + * Initializes the assigned value to 0 if no value was assigned to the variable so far. + * + * @since 1.3.0 + * @param var Variable to manipulate the value in the solution for. + * @return Mutable value assigned to the variable. + */ + double& operator()(const Var& var); +}; + +} diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index eba6b179..cac360ea 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -11,6 +11,7 @@ #include #include "scippp/constant_coefficient.hpp" +#include "scippp/initial_solution.hpp" #include "scippp/lin_expr.hpp" #include "scippp/lin_ineq.hpp" #include "scippp/param.hpp" @@ -326,5 +327,25 @@ class Model { That is only required for use-cases not supported by SCIP++. Consider adding the feature you are using to SCIP++!)")]] [[nodiscard]] Scip* scip() const; + + /** + * Adds a solution to %SCIP's solution pool. + * + * @since 1.3.0 + * @param initialSolution to add to the solution pool. + * @param printReason Should all reasons of violations be printed? + * @param completely Should all violations be checked if \p printReason is true? + * @param checkBounds Should the bounds of the variables be checked? + * @param checkIntegrality Should integrality be checked? + * @param checkLpRows Do constraints represented by rows in the current LP have to be checked? + * @return \c true iff the solution was feasible and stored in the solution storage (i.e, good enough to keep). + */ + bool addSolution( + const InitialSolution& initialSolution, + bool printReason = true, + bool completely = true, + bool checkBounds = true, + bool checkIntegrality = true, + bool checkLpRows = true); }; } diff --git a/source/initial_solution.cpp b/source/initial_solution.cpp new file mode 100644 index 00000000..1896a961 --- /dev/null +++ b/source/initial_solution.cpp @@ -0,0 +1,15 @@ +#include "scippp/initial_solution.hpp" + +namespace scippp { + +void InitialSolution::setValue(const Var& var, double value) +{ + m_values[&var] = value; +} + +double& InitialSolution::operator()(const Var& var) +{ + return m_values[&var]; +} + +} diff --git a/source/model.cpp b/source/model.cpp index 4553f92f..84117cbd 100644 --- a/source/model.cpp +++ b/source/model.cpp @@ -150,4 +150,29 @@ double Model::getPrimalbound() const return SCIPgetPrimalbound(m_scip); } +bool Model::addSolution( + const InitialSolution& initialSolution, + bool printReason, + bool completely, + bool checkBounds, + bool checkIntegrality, + bool checkLpRows) +{ + SCIP_Sol* sol { nullptr }; + m_scipCallWrapper(SCIPcreateSol(m_scip, &sol, nullptr)); + for (const auto& [var, value] : initialSolution.m_values) { + m_scipCallWrapper(SCIPsetSolVal(m_scip, sol, var->getVar(), value)); + } + SCIP_Bool isFeasible { false }; + m_scipCallWrapper(SCIPcheckSol( + m_scip, sol, printReason, completely, checkBounds, checkIntegrality, checkLpRows, &isFeasible)); + SCIP_Bool isStored { false }; + if (isFeasible) { + m_scipCallWrapper(SCIPaddSolFree(m_scip, &sol, &isStored)); + } else { + m_scipCallWrapper(SCIPfreeSol(m_scip, &sol)); + } + return isStored; +} + } diff --git a/test/test_initial_solution.cpp b/test/test_initial_solution.cpp new file mode 100644 index 00000000..1342492f --- /dev/null +++ b/test/test_initial_solution.cpp @@ -0,0 +1,57 @@ +#include + +#include + +#include "scippp/model.hpp" + +using namespace scippp; +using namespace std; +namespace bdata = boost::unit_test::data; + +BOOST_AUTO_TEST_SUITE(InitSolution) + +BOOST_DATA_TEST_CASE(InitialSolutionStored, bdata::make({ 1, 2, 3 }), indexSetToOne) +{ + Model model("Simple"); + auto x1 = model.addVar("x_1", 1, VarType::BINARY); + auto x2 = model.addVar("x_2", 1, VarType::BINARY); + auto x3 = model.addVar("x_3", 1, VarType::BINARY); + model.addConstr(x1 + x2 + x3 <= 1, "upperBound"); + model.setObjsense(Sense::MAXIMIZE); + + InitialSolution is; + is.setValue(x1, indexSetToOne == 1 ? 1 : 0); + is.setValue(x2, indexSetToOne == 2 ? 1 : 0); + is.setValue(x3, indexSetToOne == 3 ? 1 : 0); + BOOST_TEST(model.addSolution(is)); + + BOOST_REQUIRE(model.getNSols() > 0); + auto sol = model.getBestSol(); + BOOST_TEST(x1.getSolValAsInt(sol) == (indexSetToOne == 1 ? 1 : 0)); + BOOST_TEST(x2.getSolValAsInt(sol) == (indexSetToOne == 2 ? 1 : 0)); + BOOST_TEST(x3.getSolValAsInt(sol) == (indexSetToOne == 3 ? 1 : 0)); +} + +BOOST_AUTO_TEST_CASE(Infeasible) +{ + Model model("Simple"); + auto x1 = model.addVar("x_1", 1, VarType::BINARY); + model.setObjsense(Sense::MAXIMIZE); + InitialSolution is; + is.setValue(x1, 2); + BOOST_TEST(!model.addSolution(is)); +} + +BOOST_AUTO_TEST_CASE(UpdateSolution) +{ + Model model("Simple"); + const auto& [x1, x2] = model.addVars<2>("x_"); + InitialSolution is; + is(x1) = 4; + is(x1) += 38; + is(x2) += 42; + BOOST_TEST(is(x1) == 42); + BOOST_TEST(is(x2) == 42); +} + +BOOST_AUTO_TEST_SUITE_END()