From 21040e22afdcd26cd259c013183dd98f53983485 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 12:41:12 +0200 Subject: [PATCH 01/37] Add Model::setMessagehdlr to install custom message handlers Closes #29 Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 8 +++++ include/scippp/model.hpp | 13 ++++++++ readme.md | 12 +++++++ source/model.cpp | 13 ++++++++ test/test_objscip.cpp | 68 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+) diff --git a/changelog.md b/changelog.md index 5d10f554..c2280fdb 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - [Doc:Unreleased] +### Added + +- [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Custom message handlers derived from `scip::ObjMessagehdlr` + can be installed via `Model::setMessagehdlr` without accessing the raw SCIP object: + ```cpp + model.setMessagehdlr(std::make_unique()); + ``` + ## [1.4.0] - 2025-12-18 ### Added diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 3be8495b..d3c4aba1 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -356,5 +358,16 @@ class Model { * @return The generated %IIS. */ [[nodiscard]] IIS generateIIS() const; + + /** + * Installs a custom message handler, i.e., all info, warning, and dialog messages of %SCIP are passed to it. + * + * @since 1.5.0 + * @param handler to install, must not be \c nullptr. %SCIP takes ownership and deletes it when it is no longer + * used. + * @attention Must be called before solve(). Error messages are not passed to \p handler as %SCIP prints them via a + * global error printing function. + */ + void setMessagehdlr(std::unique_ptr handler) const; }; } diff --git a/readme.md b/readme.md index 8579ce33..83fe969d 100644 --- a/readme.md +++ b/readme.md @@ -155,6 +155,18 @@ model.solve(); auto pb { model.getSolvingStatistic(statistics::PRIMALBOUND) }; ``` +### Custom Message Handler + +Derive from `scip::ObjMessagehdlr` to process the output of SCIP, e.g., to write it to a logger, and install it via +`Model::setMessagehdlr` before calling `solve()`. SCIP takes ownership of the handler. + +```cpp +class MyMessageHandler : public scip::ObjMessagehdlr { ... }; +... +model.setMessagehdlr(std::make_unique()); +model.solve(); +``` + ### Features Not Yet Supported For features not yet supported by SCIP++, one can access the underlying raw SCIP object via diff --git a/source/model.cpp b/source/model.cpp index 5f29b308..fb6237bb 100644 --- a/source/model.cpp +++ b/source/model.cpp @@ -198,4 +198,17 @@ IIS Model::generateIIS() const return result; } +void Model::setMessagehdlr(std::unique_ptr handler) const +{ + assert(handler); // GCOVR_EXCL_LINE + SCIP_MESSAGEHDLR* messagehdlr { nullptr }; + // SCIP owns the handler object from now on, it even deletes it if the creation fails + m_scipCallWrapper(SCIPcreateObjMessagehdlr(&messagehdlr, handler.release(), TRUE)); + const auto RETCODE { SCIPsetMessagehdlr(m_scip, messagehdlr) }; + // SCIPsetMessagehdlr captures the message handler on success, so we always release our reference. Releasing before + // passing RETCODE to the call wrapper avoids a leak if the wrapper throws, and keeps RETCODE as the last return code. + m_scipCallWrapper(SCIPmessagehdlrRelease(&messagehdlr)); + m_scipCallWrapper(RETCODE); +} + } diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index f4085faf..57fd2bba 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -1,8 +1,15 @@ +#include +#include #include +#include +#include +#include #include "scippp/model.hpp" #include +#include +using namespace boost::algorithm; using namespace scippp; using namespace std; @@ -43,4 +50,65 @@ BOOST_AUTO_TEST_CASE(UseEventHandler) BOOST_TEST(model.getNSols() > 0); } +/** + * Forwards all messages to a file. + */ +class FileMessageHandler : public scip::ObjMessagehdlr { + ofstream m_file; + + void write(const char* msg) + { + if (msg) { + m_file << msg; + } else { + m_file.flush(); + } + } + +public: + explicit FileMessageHandler(const filesystem::path& path) + : scip::ObjMessagehdlr(false) + , m_file(path) + { + } + SCIP_DECL_MESSAGEWARNING(scip_warning) + override + { + write(msg); + } + SCIP_DECL_MESSAGEDIALOG(scip_dialog) + override + { + write(msg); + } + SCIP_DECL_MESSAGEINFO(scip_info) + override + { + write(msg); + } +}; + +BOOST_AUTO_TEST_CASE(UseMessageHandler) +{ + const auto LOG_FILE { filesystem::temp_directory_path() + / boost::filesystem::unique_path("%%%%-%%%%-%%%%-%%%%.log").string() }; + { // SCIP deletes the handler, and thereby closes the file, when the model is destructed + Model model("Simple"); + auto x1 = model.addVar("x_1", 1); + auto x2 = model.addVar("x_2", 1); + model.addConstr(x1 + x2 >= 1, "capacity"); + model.setMessagehdlr(make_unique(LOG_FILE)); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(model.getNSols() > 0); + } + ifstream file(LOG_FILE); + ostringstream content; + content << file.rdbuf(); + BOOST_TEST(contains(content.str(), "SCIP Status")); + BOOST_TEST(contains(content.str(), "optimal solution found")); + file.close(); + filesystem::remove(LOG_FILE); +} + BOOST_AUTO_TEST_SUITE_END() From 6732cae5adb9e968fe236cefc5d323cc1c3f2ccf Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 12:53:53 +0200 Subject: [PATCH 02/37] Add Model::includeEventhdlr --- changelog.md | 11 +++++++++-- include/scippp/model.hpp | 28 ++++++++++++++++++++++++++++ readme.md | 19 +++++++++++++++++++ test/test_objscip.cpp | 36 +++++++++++++++++++++++++++++------- 4 files changed, 85 insertions(+), 9 deletions(-) diff --git a/changelog.md b/changelog.md index c2280fdb..fb495d31 100644 --- a/changelog.md +++ b/changelog.md @@ -6,11 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Custom message handlers derived from `scip::ObjMessagehdlr` - can be installed via `Model::setMessagehdlr` without accessing the raw SCIP object: +- [PR44](https://github.com/scipopt/SCIPpp/pull/44), [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Custom + message handlers derived from `scip::ObjMessagehdlr` can be installed via `Model::setMessagehdlr` without accessing + the raw SCIP object: ```cpp model.setMessagehdlr(std::make_unique()); ``` +- [PR44](https://github.com/scipopt/SCIPpp/pull/44) Custom event handlers derived from `scip::ObjEventhdlr` can be + included via `Model::includeEventhdlr` without accessing the raw SCIP object. It forwards its arguments to the + constructor of the handler after the SCIP data structure: + ```cpp + model.includeEventhdlr(arg1, arg2); + ``` ## [1.4.0] - 2025-12-18 diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index d3c4aba1..efe8f57d 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -5,11 +5,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include "scippp/constant_coefficient.hpp" @@ -369,5 +371,31 @@ class Model { * global error printing function. */ void setMessagehdlr(std::unique_ptr handler) const; + + /** + * Includes a custom event handler. + * + * The handler is constructed by this method as scip::ObjEventhdlr requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * @since 1.5.0 + * @tparam Eventhdlr Type of the event handler, derived from scip::ObjEventhdlr. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Eventhdlr after the %SCIP data structure. + * @attention Must be called before solve(). + */ + template + void includeEventhdlr(Args&&... args) const + { + static_assert( + std::is_base_of_v, "Eventhdlr must derive from scip::ObjEventhdlr"); + auto handler { std::make_unique(m_scip, std::forward(args)...) }; + const auto RETCODE { SCIPincludeObjEventhdlr(m_scip, handler.get(), TRUE) }; + // SCIP owns the handler only on success, otherwise we still have to delete it. + if (RETCODE == SCIP_OKAY) { + handler.release(); + } + m_scipCallWrapper(RETCODE); + } }; } diff --git a/readme.md b/readme.md index 83fe969d..d7856ab1 100644 --- a/readme.md +++ b/readme.md @@ -167,6 +167,25 @@ model.setMessagehdlr(std::make_unique()); model.solve(); ``` +### Custom Event Handler + +Derive from `scip::ObjEventhdlr` to react to events of SCIP, e.g., to track new best solutions, and include it via +`Model::includeEventhdlr` before calling `solve()`. The handler is constructed by SCIP++ as its constructor requires +the SCIP data structure as first argument; all arguments of `includeEventhdlr` are forwarded after it. SCIP takes +ownership of the handler. + +```cpp +class MyEventHandler : public scip::ObjEventhdlr { +public: + MyEventHandler(SCIP* scip, int& counter); + ... +}; +... +int counter { 0 }; +model.includeEventhdlr(counter); +model.solve(); +``` + ### Features Not Yet Supported For features not yet supported by SCIP++, one can access the underlying raw SCIP object via diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 57fd2bba..722a61f8 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -16,38 +16,60 @@ using namespace std; BOOST_AUTO_TEST_SUITE(ObjSCIP) /** - * We check whether SCIP++ works with ObjSCIP + * Counts how often a new best solution is found. */ -class MyEventHandler : public scip::ObjEventhdlr { +class BestSolCounter : public scip::ObjEventhdlr { + int& m_nBestSols; + public: - MyEventHandler(SCIP* scip) - : scip::ObjEventhdlr(scip, "NAME", "DESC") + BestSolCounter(SCIP* scip, int& nBestSols) + : scip::ObjEventhdlr(scip, "bestsolcounter", "counts new best solutions") + , m_nBestSols(nBestSols) { } SCIP_DECL_EVENTINIT(scip_init) override { - clog << "MyEventHandler: Here I am" << endl; - return SCIP_OKAY; + return SCIPcatchEvent(scip, SCIP_EVENTTYPE_BESTSOLFOUND, eventhdlr, nullptr, nullptr); + } + SCIP_DECL_EVENTEXIT(scip_exit) + override + { + return SCIPdropEvent(scip, SCIP_EVENTTYPE_BESTSOLFOUND, eventhdlr, nullptr, -1); } SCIP_DECL_EVENTEXEC(scip_exec) override { + ++m_nBestSols; return SCIP_OKAY; } }; BOOST_AUTO_TEST_CASE(UseEventHandler) { + int nBestSols { 0 }; Model model("Simple"); auto x1 = model.addVar("x_1", 1); auto x2 = model.addVar("x_2", 1); model.addConstr(x1 + x2 >= 1, "capacity"); model.addConstr(x1 == x2, "equal"); - SCIPincludeObjEventhdlr(model.scip(), new MyEventHandler(model.scip()), false); + model.includeEventhdlr(nBestSols); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.setObjsense(Sense::MINIMIZE); model.solve(); BOOST_TEST(model.getNSols() > 0); + BOOST_TEST(nBestSols > 0); +} + +BOOST_AUTO_TEST_CASE(IncludeEventHandlerTwice) +{ + int nBestSols { 0 }; + Model model("Simple"); + model.includeEventhdlr(nBestSols); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + // SCIP rejects a second event handler with the same name, the handler is then deleted by SCIP++ + model.includeEventhdlr(nBestSols); + BOOST_TEST(model.getLastReturnCode() == SCIP_INVALIDDATA); } /** From 7a2d48b1daac9985eca9b7b7d4fb0e4762983007 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 13:18:42 +0200 Subject: [PATCH 03/37] Move usage documentation from readme to Doxygen Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 5 + include/scippp/model.hpp | 96 ++++++++++++++++- readme.md | 227 ++++++++++----------------------------- 3 files changed, 154 insertions(+), 174 deletions(-) diff --git a/changelog.md b/changelog.md index fb495d31..249abb33 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 model.includeEventhdlr(arg1, arg2); ``` +### Changed + +- [PR44](https://github.com/scipopt/SCIPpp/pull/44) The readme lists the supported features only. How to use them is + documented, including examples, in the Doxygen documentation. + ## [1.4.0] - 2025-12-18 ### Added diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index efe8f57d..6821d7f4 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -62,6 +62,21 @@ class Model { * * By default, all calls to the underlying %C %API are wrapped and the last return code is stored. * + * A model can be created without an existing %SCIP data structure, or with an existing one where the default + * plugins are added to or not: + * @code + * Model m1("ModelWithoutExistingSCIPEnvironment"); + * + * SCIP* scip2 { nullptr }; + * SCIPcreate(&scip2); + * Model m2("ModelWithExistingSCIPEnvironmentWhereDefaultPluginsWillBeAdded", scip2); + * + * SCIP* scip3 { nullptr }; + * SCIPcreate(&scip3); + * SCIPincludeDefaultPlugins(scip3); + * Model m3("ModelWithExistingSCIPEnvironmentWhereNoPluginsWillBeAdded", scip3, false); + * @endcode + * * @since 1.0.0 * @param name for the problem. * @param scip to create the problem in. If \c nullptr, a new %SCIP data structure will be created. @@ -108,6 +123,16 @@ class Model { /** * Adds multiple variables to the model. + * + * By default, all variables have a coefficient of zero in the objective function. Use scippp::COEFF_ONE for a + * coefficient of one, or any object providing double operator[](std::size_t index) const: + * @code + * auto x = model.addVars("x_", 42); + * auto y = model.addVars("y_", 42, COEFF_ONE); + * std::vector costs { 1.5, 2, 3 }; + * auto z = model.addVars("z_", costs.size(), costs, VarType::INTEGER); + * @endcode + * * @since 1.0.0 * @tparam CoeffType Type of the object holding the coefficients. They are accessed via \c [i] where i goes from 0 * to \p numVars - 1. @@ -140,7 +165,10 @@ class Model { * Adds multiple variables to the model. * * This method can be used when the number of variables to add is known at compile time. The result can be used in a - * structured binding. + * structured binding: + * @code + * const auto& [x0, x1] = model.addVars<2>("x_", COEFF_ONE); + * @endcode * * @since 1.0.0 * @tparam NumVars Number of variables to add. @@ -169,6 +197,22 @@ class Model { /** * Adds a constraint to the model. + * + * Linear inequalities are built from linear expressions, see LinExpr: + * @code + * const auto& [x0, x1, x2, x3] = model.addVars<4>("x_"); + * + * LinExpr sum1; + * sum1 += 42 * x0; + * sum1 += x1; + * model.addConstr(sum1 <= 0.5, "constraint1"); + * + * LinExpr sum2 = x1 + x2; + * model.addConstr(sum2 == 1.25, "constraint2"); + * + * model.addConstr(1 <= x2 + 2 * x3, "constraint3"); + * @endcode + * * @since 1.0.0 * @param ineq linear inequality to add. * @param name for the constraint when the model is written. @@ -228,6 +272,13 @@ class Model { /** * Query statistics about the solving process. * + * Use the scippp::statistics::Statistic objects from the header solving_statistics.hpp to access the statistics in + * a type-safe way: + * @code + * model.solve(); + * auto pb { model.getSolvingStatistic(statistics::PRIMALBOUND) }; + * @endcode + * * @tparam T Type of the statistics value. * @since 1.2.0 * @param statistic Statistics value to access. @@ -248,6 +299,18 @@ class Model { /** * Returns the best feasible primal solution found so far or best solution candidate. + * + * The values of the variables in the solution are accessed via Var::getSolVal, Var::getSolValAsInt, + * Var::getSolValAsLongInt, and Var::isZero: + * @code + * const auto& [x0, x1] = model.addVars<2>("x_"); + * model.solve(); + * if (model.getNSols() > 0 && model.getStatus() == SCIP_STATUS_OPTIMAL) { + * Solution sol { model.getBestSol() }; + * std::cout << "x0 + x1 = " << x0.getSolVal(sol) + x1.getSolVal(sol) << std::endl; + * } + * @endcode + * * @since 1.0.0 * @return best feasible primal solution. */ @@ -266,7 +329,15 @@ class Model { /** * Sets a parameter. * - * See the namespace scippp::params for a list of parameters, or create new ones using params::Param. + * The namespace scippp::params in the header parameters.hpp contains all parameters listed at + * https://www.scipopt.org/doc/html/PARAMETERS.php. They are strongly typed, so that, e.g., no string can be set as + * the value for a parameter expecting an integer. Instead of using the predefined parameters, one can also create + * new ones using params::Param: + * @code + * model.setParam(params::LIMITS::MAXSOL, 1); + * model.setParam(params::DISPLAY::VERBLEVEL, 0); + * model.setParam(params::Param("write/printzeros"), true); + * @endcode * * @since 1.0.0 * @tparam T Type of the value. @@ -364,6 +435,14 @@ class Model { /** * Installs a custom message handler, i.e., all info, warning, and dialog messages of %SCIP are passed to it. * + * Derive from scip::ObjMessagehdlr to process the output of %SCIP, e.g., to write it to a logger: + * @code + * class MyMessageHandler : public scip::ObjMessagehdlr { ... }; + * ... + * model.setMessagehdlr(std::make_unique()); + * model.solve(); + * @endcode + * * @since 1.5.0 * @param handler to install, must not be \c nullptr. %SCIP takes ownership and deletes it when it is no longer * used. @@ -378,6 +457,19 @@ class Model { * The handler is constructed by this method as scip::ObjEventhdlr requires the %SCIP data structure in its * constructor. %SCIP takes ownership and deletes it when the model is destructed. * + * Derive from scip::ObjEventhdlr to react to events of %SCIP, e.g., to track new best solutions: + * @code + * class MyEventHandler : public scip::ObjEventhdlr { + * public: + * MyEventHandler(SCIP* scip, int& counter); + * ... + * }; + * ... + * int counter { 0 }; + * model.includeEventhdlr(counter); + * model.solve(); + * @endcode + * * @since 1.5.0 * @tparam Eventhdlr Type of the event handler, derived from scip::ObjEventhdlr. * @tparam Args Types of the additional constructor arguments. diff --git a/readme.md b/readme.md index d7856ab1..89b62406 100644 --- a/readme.md +++ b/readme.md @@ -10,189 +10,72 @@ It automatically manages the memory, and provides a simple interface to create l ## Usage -The documentation can be found at https://scipopt.github.io/SCIPpp/ +The documentation, including examples for all features, can be found at https://scipopt.github.io/SCIPpp/ -Here is a simple example where we create a new model, add two variables, add a linear inequality as constraint, and ask -SCIP to solve the maximization problem. +Here is an example where we create a new model for a knapsack problem, add binary variables with their values as +coefficients in the objective function, add the capacity constraint built from a linear expression, ask SCIP to solve +the maximization problem, and print the packed items. ```cpp +#include #include +#include +#include + using namespace scippp; -int main() { - Model model("Simple"); - auto x1 = model.addVar("x_1", 1); - auto x2 = model.addVar("x_2", 1); - model.addConstr(3 * x1 + 2 * x2 <= 1, "capacity"); + +int main() +{ + std::vector weights { 3, 4, 3, 3, 3, 3, 4, 2, 4, 1 }; + std::vector values { 230, 134, 52, 60, 151, 95, 201, 245, 52, 55 }; + + Model model("Knapsack"); + auto x { model.addVars("x_", weights.size(), values, VarType::BINARY) }; + LinExpr weight; + for (size_t i { 0 }; i < weights.size(); ++i) { + weight += weights[i] * x[i]; + } + model.addConstr(weight <= 14, "capacity"); model.setObjsense(Sense::MAXIMIZE); + model.setParam(params::DISPLAY::VERBLEVEL, 0); model.solve(); -} -``` - -### Model Creation - -A model can be created - -* without an existing SCIP environment, -* with an existing SCIP environment where all default plugins should be added to, and -* with an existing SCIP environment where no additional plugins should be added to. - -```cpp -Model m1("ModelWithoutExistingSCIPEnvironment"); - -SCIP* scip2; -SCIPcreate(&scip2); -Model m2("ModelWithExistingSCIPEnvironmentWhereDefaultPluginsWillBeAdded", scip2); - -SCIP* scip3; -SCIPcreate(&scip3); -SCIPincludeDefaultPlugins(scip3); -Model m3("ModelWithExistingSCIPEnvironmentWhereNoPluginsWillBeAdded", scip3, false); -``` - -### Adding Variables - -Variables can be added - -* one at a time, -* multiple in a vector, and -* multiple when the number is known at compile time. - -```cpp -Model model("Example"); - -auto x = model.addVar("x"); -auto vec = model.addVars("x_", 42); -const auto& [x0, x1] = model.addVars<2>("x_"); -``` - -When adding multiple variables simultaneously to the model, they all have a coefficient of zero in the objective -function by default. -This can be changed to one by `scippp::COEFF_ONE`: -```cpp -const auto& [x1, x2] = model.addVars<2>("x_", COEFF_ONE); -``` - -For the coefficient, any object providing an index operator can be used: -```cpp -double operator[](std::size_t index) const -``` - -### Adding Constraints - -Linear inequalities can be added to the model. They can be built from linear expressions: - -```cpp -const auto& [x0, x1, x2, x3] = model.addVars<4>("x_"); - -LinExpr sum1; -sum1 += 42 * x0; -sum1 += x1; -model.addConstr(sum1 <= 0.5, "constraint1"); - -LinExpr sum2 = x1 + x2; -model.addConstr(sum2 == 1.25, "constraint2"); - -model.addConstr(1 <= x2 + 2 * x3, "constraint3"); -``` - -### Setting Parameters - -The namespace `scippp::params` contains all parameters shown https://www.scipopt.org/doc/html/PARAMETERS.php in the -header `parameters.hpp`. They are strongly typed, so that no string can be set as the value for a parameter expecting an -integer. Instead of using the predefined parameters, one can also use `scippp::params::Param` directly. -```cpp -model.setParam(params::LIMITS::MAXSOL, 1); -model.setParam(params::DISPLAY::VERBLEVEL, 0); -model.setParam(params::Param("write/printzeros"), true); -``` - -The optimization goal can be changed by: -```cpp -model.setObjsense(Sense::MAXIMIZE); -``` - -### Accessing a Solution - -A model can be asked for the status and the number of solutions. -A variable can be asked for its value in a given solution as - -* floating point number via `getSolVal(sol)`. -* integer via `getSolValAsInt(sol)`. -* long integer via `getSolValAsLongInt(sol)`, and -* it can be checked for zero via `isZero(sol)`. - -```cpp -const auto& [x0, x1] = model.addVars<2>("x_"); -model.solve(); -if (model.getNSols() > 0 && model.getStatus() == SCIP_STATUS_OPTIMAL) { - Solution sol { model.getBestSol() }; - cout << "x0 + x1 =" << x0.getSolVal(sol) + x1.getSolVal(sol) << endl; + if (model.getNSols() > 0) { + auto sol { model.getBestSol() }; + for (size_t i { 0 }; i < x.size(); ++i) { + if (x[i].getSolValAsInt(sol) == 1) { + std::cout << "pack item " << i << "\n"; + } + } + } } ``` -### IO - -A model can be written to file via `Model::writeOrigProblem` if a `std::filesystem::directory_entry` is given as -argument. If it is just a string representing a file extension, it is written to standard output. - -### Numerics - -The model exposes - -* `SCIPepsilon` via `epsilon()`, -* `SCIPround` via `round(double)`, and -* `SCIPisZero` via `isZero(double)` - -### Access Solving Statistics - -Use the `Statistics` objects from the header [solving_statistics.hpp](include/scippp/solving_statistics.hpp) to -access solving statistics in a type-safe way: - -```cpp -... -model.solve(); -auto pb { model.getSolvingStatistic(statistics::PRIMALBOUND) }; -``` - -### Custom Message Handler - -Derive from `scip::ObjMessagehdlr` to process the output of SCIP, e.g., to write it to a logger, and install it via -`Model::setMessagehdlr` before calling `solve()`. SCIP takes ownership of the handler. - -```cpp -class MyMessageHandler : public scip::ObjMessagehdlr { ... }; -... -model.setMessagehdlr(std::make_unique()); -model.solve(); -``` - -### Custom Event Handler - -Derive from `scip::ObjEventhdlr` to react to events of SCIP, e.g., to track new best solutions, and include it via -`Model::includeEventhdlr` before calling `solve()`. The handler is constructed by SCIP++ as its constructor requires -the SCIP data structure as first argument; all arguments of `includeEventhdlr` are forwarded after it. SCIP takes -ownership of the handler. - -```cpp -class MyEventHandler : public scip::ObjEventhdlr { -public: - MyEventHandler(SCIP* scip, int& counter); - ... -}; -... -int counter { 0 }; -model.includeEventhdlr(counter); -model.solve(); -``` - -### Features Not Yet Supported - -For features not yet supported by SCIP++, one can access the underlying raw SCIP object via - -```cpp -SCIP* scip = model.scip(); -``` +## Features + +* Create a model with a new SCIP data structure or with an existing one, with or without SCIP's default plugins. +* Add variables one at a time, as a vector, or as an array for structured bindings (`scippp::Model::addVar`, + `scippp::Model::addVars`). The objective coefficients can be given by any object providing an index operator. +* Build linear expressions (`scippp::LinExpr`) and add linear inequalities and equations as constraints + (`scippp::Model::addConstr`). +* Set the optimization goal (`scippp::Model::setObjsense`) and all SCIP parameters in a type-safe way + (`scippp::Model::setParam` with `scippp::params`). +* Solve the model and query its status, the number of solutions, the best solution, and the values of the variables in + a solution (`scippp::Model::solve`, `scippp::Model::getStatus`, `scippp::Model::getNSols`, + `scippp::Model::getBestSol`, `scippp::Var::getSolVal`). +* Add initial solutions to SCIP's solution pool (`scippp::InitialSolution`, `scippp::Model::addSolution`). +* Query solving statistics in a type-safe way (`scippp::Model::getSolvingStatistic` with `scippp::statistics`). +* Generate an Irreducible Infeasible Subsystem (`scippp::Model::generateIIS`). +* Write the original problem to a file or to standard output (`scippp::Model::writeOrigProblem`). +* Use SCIP's numerics (`scippp::Model::epsilon`, `scippp::Model::round`, `scippp::Model::isZero`, + `scippp::Model::infinity`). +* Install custom message handlers derived from + [`scip::ObjMessagehdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjMessagehdlr.php) + (`scippp::Model::setMessagehdlr`). +* Include custom event handlers derived from + [`scip::ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php) + (`scippp::Model::includeEventhdlr`). +* Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). ## Build From ba111d404878d3147f0dd9baa81436898b5fbccb Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 13:30:07 +0200 Subject: [PATCH 04/37] Move ObjSCIP stuff into own translation unit --- changelog.md | 4 +-- include/scippp/model.hpp | 43 ++++++++++++++++++++++--------- source/model.cpp | 13 ---------- source/model_objscip.cpp | 55 ++++++++++++++++++++++++++++++++++++++++ test/test_objscip.cpp | 6 ++--- 5 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 source/model_objscip.cpp diff --git a/changelog.md b/changelog.md index 249abb33..097d5f36 100644 --- a/changelog.md +++ b/changelog.md @@ -14,9 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ``` - [PR44](https://github.com/scipopt/SCIPpp/pull/44) Custom event handlers derived from `scip::ObjEventhdlr` can be included via `Model::includeEventhdlr` without accessing the raw SCIP object. It forwards its arguments to the - constructor of the handler after the SCIP data structure: + constructor of the handler after the SCIP data structure, and returns a non-owning pointer to the handler: ```cpp - model.includeEventhdlr(arg1, arg2); + MyEventHandler* handler = model.includeEventhdlr(arg1, arg2); ``` ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 6821d7f4..12653047 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -5,8 +5,7 @@ #include #include #include -#include -#include +#include #include #include #include @@ -56,6 +55,33 @@ class Model { //! Wrapper for every call to %SCIP's %C %API. std::function m_scipCallWrapper; + /** + * Constructs a plugin and includes it. + * + * @tparam Plugin Type of the plugin, derived from \p Base. + * @tparam Base objscip base class of the plugin. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Plugin after the %SCIP data structure. + * @return Non-owning pointer to the plugin, or \c nullptr if including failed. + */ + template + Plugin* constructAndInclude(Args&&... args) const + { + static_assert(std::is_base_of_v, "Plugin must derive from the objscip base class"); + auto plugin { std::make_unique(m_scip, std::forward(args)...) }; + auto* result { plugin.get() }; + // Converting to the base explicitly selects the matching overload, even if Plugin has multiple objscip bases. + return includeObj(std::unique_ptr(std::move(plugin))) ? result : nullptr; + } + + /** + * Includes an event handler, %SCIP takes ownership on success. + * + * @param eventhdlr to include, it is deleted if including fails. + * @return \c true iff including succeeded. + */ + bool includeObj(std::unique_ptr eventhdlr) const; + public: /** * Creates an empty problem and sets the optimization goal to Sense::MINIMIZE. @@ -474,20 +500,13 @@ class Model { * @tparam Eventhdlr Type of the event handler, derived from scip::ObjEventhdlr. * @tparam Args Types of the additional constructor arguments. * @param args passed to the constructor of \p Eventhdlr after the %SCIP data structure. + * @return Non-owning pointer to the handler, or \c nullptr if including failed. * @attention Must be called before solve(). */ template - void includeEventhdlr(Args&&... args) const + Eventhdlr* includeEventhdlr(Args&&... args) const { - static_assert( - std::is_base_of_v, "Eventhdlr must derive from scip::ObjEventhdlr"); - auto handler { std::make_unique(m_scip, std::forward(args)...) }; - const auto RETCODE { SCIPincludeObjEventhdlr(m_scip, handler.get(), TRUE) }; - // SCIP owns the handler only on success, otherwise we still have to delete it. - if (RETCODE == SCIP_OKAY) { - handler.release(); - } - m_scipCallWrapper(RETCODE); + return constructAndInclude(std::forward(args)...); } }; } diff --git a/source/model.cpp b/source/model.cpp index fb6237bb..5f29b308 100644 --- a/source/model.cpp +++ b/source/model.cpp @@ -198,17 +198,4 @@ IIS Model::generateIIS() const return result; } -void Model::setMessagehdlr(std::unique_ptr handler) const -{ - assert(handler); // GCOVR_EXCL_LINE - SCIP_MESSAGEHDLR* messagehdlr { nullptr }; - // SCIP owns the handler object from now on, it even deletes it if the creation fails - m_scipCallWrapper(SCIPcreateObjMessagehdlr(&messagehdlr, handler.release(), TRUE)); - const auto RETCODE { SCIPsetMessagehdlr(m_scip, messagehdlr) }; - // SCIPsetMessagehdlr captures the message handler on success, so we always release our reference. Releasing before - // passing RETCODE to the call wrapper avoids a leak if the wrapper throws, and keeps RETCODE as the last return code. - m_scipCallWrapper(SCIPmessagehdlrRelease(&messagehdlr)); - m_scipCallWrapper(RETCODE); -} - } diff --git a/source/model_objscip.cpp b/source/model_objscip.cpp new file mode 100644 index 00000000..5c7a6ca7 --- /dev/null +++ b/source/model_objscip.cpp @@ -0,0 +1,55 @@ +#include "scippp/model.hpp" + +#include + +namespace scippp { + +namespace { + + /** + * Includes an objscip plugin, %SCIP takes ownership on success. + * + * @tparam Base objscip base class of the plugin. + * @param scip %SCIP data structure to include the plugin in. + * @param scipCallWrapper Wrapper for the call to %SCIP's include function. + * @param scipInclude %SCIP's include function for \p Base, e.g., SCIPincludeObjEventhdlr. + * @param plugin to include, it is deleted if including fails. + * @return \c true iff including succeeded. + */ + template + bool includeObjPlugin( + SCIP* scip, + const std::function& scipCallWrapper, + SCIP_RETCODE (*scipInclude)(SCIP*, Base*, SCIP_Bool), + std::unique_ptr plugin) + { + const auto RETCODE { scipInclude(scip, plugin.get(), TRUE) }; + // SCIP owns the plugin only on success, otherwise it is deleted when leaving this function. + if (RETCODE == SCIP_OKAY) { + plugin.release(); + } + scipCallWrapper(RETCODE); + return RETCODE == SCIP_OKAY; + } + +} + +void Model::setMessagehdlr(std::unique_ptr handler) const +{ + assert(handler); // GCOVR_EXCL_LINE + SCIP_MESSAGEHDLR* messagehdlr { nullptr }; + // SCIP owns the handler object from now on, it even deletes it if the creation fails + m_scipCallWrapper(SCIPcreateObjMessagehdlr(&messagehdlr, handler.release(), TRUE)); + const auto RETCODE { SCIPsetMessagehdlr(m_scip, messagehdlr) }; + // SCIPsetMessagehdlr captures the message handler on success, so we always release our reference. Releasing before + // passing RETCODE to the call wrapper avoids a leak if the wrapper throws, and keeps RETCODE as the last return code. + m_scipCallWrapper(SCIPmessagehdlrRelease(&messagehdlr)); + m_scipCallWrapper(RETCODE); +} + +bool Model::includeObj(std::unique_ptr eventhdlr) const +{ + return includeObjPlugin(m_scip, m_scipCallWrapper, &SCIPincludeObjEventhdlr, std::move(eventhdlr)); +} + +} diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 722a61f8..ae98255a 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -53,7 +53,7 @@ BOOST_AUTO_TEST_CASE(UseEventHandler) auto x2 = model.addVar("x_2", 1); model.addConstr(x1 + x2 >= 1, "capacity"); model.addConstr(x1 == x2, "equal"); - model.includeEventhdlr(nBestSols); + BOOST_TEST(model.includeEventhdlr(nBestSols) != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.setObjsense(Sense::MINIMIZE); model.solve(); @@ -65,10 +65,10 @@ BOOST_AUTO_TEST_CASE(IncludeEventHandlerTwice) { int nBestSols { 0 }; Model model("Simple"); - model.includeEventhdlr(nBestSols); + BOOST_TEST(model.includeEventhdlr(nBestSols) != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); // SCIP rejects a second event handler with the same name, the handler is then deleted by SCIP++ - model.includeEventhdlr(nBestSols); + BOOST_TEST(model.includeEventhdlr(nBestSols) == nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_INVALIDDATA); } From 97b31175820568d5116e024ff448dcdcaa3faa5c Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 13:42:07 +0200 Subject: [PATCH 05/37] Add Conshdlr support --- changelog.md | 2 + include/scippp/model.hpp | 43 +++++++++++++++++++ readme.md | 10 +++-- source/model_objscip.cpp | 5 +++ test/test_objscip.cpp | 90 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 097d5f36..702f62b5 100644 --- a/changelog.md +++ b/changelog.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ```cpp MyEventHandler* handler = model.includeEventhdlr(arg1, arg2); ``` +- [PR44](https://github.com/scipopt/SCIPpp/pull/44) Custom constraint handlers derived from `scip::ObjConshdlr` can be + included via `Model::includeConshdlr`, analogous to `Model::includeEventhdlr`. ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 12653047..04698a4a 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -82,6 +82,14 @@ class Model { */ bool includeObj(std::unique_ptr eventhdlr) const; + /** + * Includes a constraint handler, %SCIP takes ownership on success. + * + * @param conshdlr to include, it is deleted if including fails. + * @return \c true iff including succeeded. + */ + bool includeObj(std::unique_ptr conshdlr) const; + public: /** * Creates an empty problem and sets the optimization goal to Sense::MINIMIZE. @@ -508,5 +516,40 @@ class Model { { return constructAndInclude(std::forward(args)...); } + + /** + * Includes a custom constraint handler. + * + * The handler is constructed by this method as scip::ObjConshdlr requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjConshdlr to enforce constraints that cannot be expressed linearly, e.g., lazily: + * @code + * class MyConstraintHandler : public scip::ObjConshdlr { + * public: + * MyConstraintHandler(SCIP* scip, const std::vector& vars); + * ... + * }; + * ... + * auto vars = model.addVars("x_", 42); + * model.includeConshdlr(vars); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Conshdlr Type of the constraint handler, derived from scip::ObjConshdlr. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Conshdlr after the %SCIP data structure. + * @return Non-owning pointer to the handler, or \c nullptr if including failed. + * @attention Must be called before solve(). + * @note SCIP++ cannot create constraints of a custom constraint handler. Thus, pass \c needscons = \c FALSE to the + * constructor of scip::ObjConshdlr, so that the handler is called without constraints, and lock the + * variables in \c scip_lock, which is then called with \c cons = \c nullptr. + */ + template + Conshdlr* includeConshdlr(Args&&... args) const + { + return constructAndInclude(std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 89b62406..6712cfc7 100644 --- a/readme.md +++ b/readme.md @@ -72,9 +72,13 @@ int main() * Install custom message handlers derived from [`scip::ObjMessagehdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjMessagehdlr.php) (`scippp::Model::setMessagehdlr`). -* Include custom event handlers derived from - [`scip::ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php) - (`scippp::Model::includeEventhdlr`). +* Include custom plugins: + * constraint handlers derived from + [`scip::ObjConshdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjConshdlr.php) + (`scippp::Model::includeConshdlr`), and + * event handlers derived from + [`scip::ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php) + (`scippp::Model::includeEventhdlr`). * Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). ## Build diff --git a/source/model_objscip.cpp b/source/model_objscip.cpp index 5c7a6ca7..ebf0dbbb 100644 --- a/source/model_objscip.cpp +++ b/source/model_objscip.cpp @@ -52,4 +52,9 @@ bool Model::includeObj(std::unique_ptr eventhdlr) const return includeObjPlugin(m_scip, m_scipCallWrapper, &SCIPincludeObjEventhdlr, std::move(eventhdlr)); } +bool Model::includeObj(std::unique_ptr conshdlr) const +{ + return includeObjPlugin(m_scip, m_scipCallWrapper, &SCIPincludeObjConshdlr, std::move(conshdlr)); +} + } diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index ae98255a..14f1fc18 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -6,6 +6,8 @@ #include #include "scippp/model.hpp" +#include "scippp/solving_statistics.hpp" +#include #include #include @@ -72,6 +74,94 @@ BOOST_AUTO_TEST_CASE(IncludeEventHandlerTwice) BOOST_TEST(model.getLastReturnCode() == SCIP_INVALIDDATA); } +/** + * Lazily enforces a non-negative variable to be zero, without any constraint. + */ +class ZeroVarConshdlr : public scip::ObjConshdlr { + SCIP_VAR* m_var; + + [[nodiscard]] bool isViolated(SCIP* scip, SCIP_SOL* sol) const + { + return SCIPisFeasPositive(scip, SCIPgetSolVal(scip, sol, m_var)); + } + + SCIP_RETCODE enforce(SCIP* scip, SCIP_RESULT* result) const + { + *result = SCIP_FEASIBLE; + if (isViolated(scip, nullptr)) { + SCIP_Bool infeasible { FALSE }; + SCIP_Bool tightened { FALSE }; + SCIP_CALL(SCIPtightenVarUb(scip, SCIPvarGetTransVar(m_var), 0.0, FALSE, &infeasible, &tightened)); + *result = infeasible ? SCIP_CUTOFF : SCIP_REDUCEDDOM; + } + return SCIP_OKAY; + } + +public: + ZeroVarConshdlr(SCIP* scip, SCIP_VAR* var) + : scip::ObjConshdlr( + scip, + "zerovar", + "enforces a variable to be zero", + 0, // sepapriority + -1, // enfopriority, negative to be called for integral solutions only + -1, // checkpriority + -1, // sepafreq + -1, // propfreq + -1, // eagerfreq + 0, // maxprerounds + FALSE, // delaysepa + FALSE, // delayprop + FALSE, // needscons + SCIP_PROPTIMING_BEFORELP, + SCIP_PRESOLTIMING_FAST) + , m_var(var) + { + } + SCIP_DECL_CONSTRANS(scip_trans) + override + { + // there are no constraints of this handler to transform + return SCIP_INVALIDCALL; + } + SCIP_DECL_CONSENFOLP(scip_enfolp) + override + { + return enforce(scip, result); + } + SCIP_DECL_CONSENFOPS(scip_enfops) + override + { + return enforce(scip, result); + } + SCIP_DECL_CONSCHECK(scip_check) + override + { + *result = isViolated(scip, sol) ? SCIP_INFEASIBLE : SCIP_FEASIBLE; + return SCIP_OKAY; + } + SCIP_DECL_CONSLOCK(scip_lock) + override + { + // called without constraint as this handler does not need constraints, increasing the variable may violate it + return SCIPaddVarLocksType(scip, SCIPvarGetTransVar(m_var), locktype, nlocksneg, nlockspos); + } +}; + +BOOST_AUTO_TEST_CASE(UseConstraintHandler) +{ + Model model("Simple"); + auto x1 = model.addVar("x_1", 1, VarType::BINARY); + auto x2 = model.addVar("x_2", 1, VarType::BINARY); + model.setObjsense(Sense::MAXIMIZE); + BOOST_TEST(model.includeConshdlr(x1.getVar()) != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); + BOOST_TEST(x1.getSolValAsInt(model.getBestSol()) == 0); + BOOST_TEST(x2.getSolValAsInt(model.getBestSol()) == 1); +} + /** * Forwards all messages to a file. */ From 237d2c8dc4bd73f656701a235c2c120b0431a962 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 13:44:46 +0200 Subject: [PATCH 06/37] Simplify changelog --- changelog.md | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/changelog.md b/changelog.md index 702f62b5..dd3b57b2 100644 --- a/changelog.md +++ b/changelog.md @@ -6,20 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- [PR44](https://github.com/scipopt/SCIPpp/pull/44), [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Custom - message handlers derived from `scip::ObjMessagehdlr` can be installed via `Model::setMessagehdlr` without accessing - the raw SCIP object: - ```cpp - model.setMessagehdlr(std::make_unique()); - ``` -- [PR44](https://github.com/scipopt/SCIPpp/pull/44) Custom event handlers derived from `scip::ObjEventhdlr` can be - included via `Model::includeEventhdlr` without accessing the raw SCIP object. It forwards its arguments to the - constructor of the handler after the SCIP data structure, and returns a non-owning pointer to the handler: - ```cpp - MyEventHandler* handler = model.includeEventhdlr(arg1, arg2); - ``` -- [PR44](https://github.com/scipopt/SCIPpp/pull/44) Custom constraint handlers derived from `scip::ObjConshdlr` can be - included via `Model::includeConshdlr`, analogous to `Model::includeEventhdlr`. +- [PR44](https://github.com/scipopt/SCIPpp/pull/44), [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Support + of ObjSCIP without accessing the raw SCIP object: + - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, + - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, and + - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`. ### Changed From 66d7271319a81d1a14d9e8ba78431c76a1e21cf8 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 13:55:34 +0200 Subject: [PATCH 07/37] Simplify include code --- include/scippp/model.hpp | 33 ++++++++++--------------------- source/model_objscip.cpp | 42 ---------------------------------------- 2 files changed, 10 insertions(+), 65 deletions(-) diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 04698a4a..82eec258 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -56,40 +56,27 @@ class Model { std::function m_scipCallWrapper; /** - * Constructs a plugin and includes it. + * Constructs a plugin and includes it, %SCIP takes ownership on success. * * @tparam Plugin Type of the plugin, derived from \p Base. * @tparam Base objscip base class of the plugin. * @tparam Args Types of the additional constructor arguments. + * @param scipInclude %SCIP's include function for \p Base, e.g., SCIPincludeObjEventhdlr. * @param args passed to the constructor of \p Plugin after the %SCIP data structure. * @return Non-owning pointer to the plugin, or \c nullptr if including failed. */ template - Plugin* constructAndInclude(Args&&... args) const + Plugin* constructAndInclude(SCIP_RETCODE (*scipInclude)(SCIP*, Base*, SCIP_Bool), Args&&... args) const { static_assert(std::is_base_of_v, "Plugin must derive from the objscip base class"); auto plugin { std::make_unique(m_scip, std::forward(args)...) }; - auto* result { plugin.get() }; - // Converting to the base explicitly selects the matching overload, even if Plugin has multiple objscip bases. - return includeObj(std::unique_ptr(std::move(plugin))) ? result : nullptr; + const auto RETCODE { scipInclude(m_scip, plugin.get(), TRUE) }; + // SCIP owns the plugin only on success, otherwise it is deleted when leaving this method. + Plugin* result { RETCODE == SCIP_OKAY ? plugin.release() : nullptr }; + m_scipCallWrapper(RETCODE); + return result; } - /** - * Includes an event handler, %SCIP takes ownership on success. - * - * @param eventhdlr to include, it is deleted if including fails. - * @return \c true iff including succeeded. - */ - bool includeObj(std::unique_ptr eventhdlr) const; - - /** - * Includes a constraint handler, %SCIP takes ownership on success. - * - * @param conshdlr to include, it is deleted if including fails. - * @return \c true iff including succeeded. - */ - bool includeObj(std::unique_ptr conshdlr) const; - public: /** * Creates an empty problem and sets the optimization goal to Sense::MINIMIZE. @@ -514,7 +501,7 @@ class Model { template Eventhdlr* includeEventhdlr(Args&&... args) const { - return constructAndInclude(std::forward(args)...); + return constructAndInclude(&SCIPincludeObjEventhdlr, std::forward(args)...); } /** @@ -549,7 +536,7 @@ class Model { template Conshdlr* includeConshdlr(Args&&... args) const { - return constructAndInclude(std::forward(args)...); + return constructAndInclude(&SCIPincludeObjConshdlr, std::forward(args)...); } }; } diff --git a/source/model_objscip.cpp b/source/model_objscip.cpp index ebf0dbbb..647ee534 100644 --- a/source/model_objscip.cpp +++ b/source/model_objscip.cpp @@ -1,39 +1,7 @@ #include "scippp/model.hpp" -#include - namespace scippp { -namespace { - - /** - * Includes an objscip plugin, %SCIP takes ownership on success. - * - * @tparam Base objscip base class of the plugin. - * @param scip %SCIP data structure to include the plugin in. - * @param scipCallWrapper Wrapper for the call to %SCIP's include function. - * @param scipInclude %SCIP's include function for \p Base, e.g., SCIPincludeObjEventhdlr. - * @param plugin to include, it is deleted if including fails. - * @return \c true iff including succeeded. - */ - template - bool includeObjPlugin( - SCIP* scip, - const std::function& scipCallWrapper, - SCIP_RETCODE (*scipInclude)(SCIP*, Base*, SCIP_Bool), - std::unique_ptr plugin) - { - const auto RETCODE { scipInclude(scip, plugin.get(), TRUE) }; - // SCIP owns the plugin only on success, otherwise it is deleted when leaving this function. - if (RETCODE == SCIP_OKAY) { - plugin.release(); - } - scipCallWrapper(RETCODE); - return RETCODE == SCIP_OKAY; - } - -} - void Model::setMessagehdlr(std::unique_ptr handler) const { assert(handler); // GCOVR_EXCL_LINE @@ -47,14 +15,4 @@ void Model::setMessagehdlr(std::unique_ptr handler) const m_scipCallWrapper(RETCODE); } -bool Model::includeObj(std::unique_ptr eventhdlr) const -{ - return includeObjPlugin(m_scip, m_scipCallWrapper, &SCIPincludeObjEventhdlr, std::move(eventhdlr)); -} - -bool Model::includeObj(std::unique_ptr conshdlr) const -{ - return includeObjPlugin(m_scip, m_scipCallWrapper, &SCIPincludeObjConshdlr, std::move(conshdlr)); -} - } From bb092136168847acc67719478d4e9c77d31deeff Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 13:59:29 +0200 Subject: [PATCH 08/37] Add Model::includeHeur Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 5 ++-- include/scippp/model.hpp | 32 +++++++++++++++++++++ readme.md | 7 +++-- test/test_objscip.cpp | 62 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index dd3b57b2..32990b24 100644 --- a/changelog.md +++ b/changelog.md @@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [PR44](https://github.com/scipopt/SCIPpp/pull/44), [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Support of ObjSCIP without accessing the raw SCIP object: - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, - - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, and - - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`. + - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, + - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, and + - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`. ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 82eec258..e20c8dd3 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -538,5 +538,37 @@ class Model { { return constructAndInclude(&SCIPincludeObjConshdlr, std::forward(args)...); } + + /** + * Includes a custom primal heuristic. + * + * The heuristic is constructed by this method as scip::ObjHeur requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjHeur to construct solutions from problem-specific knowledge: + * @code + * class MyHeuristic : public scip::ObjHeur { + * public: + * MyHeuristic(SCIP* scip, const std::vector& vars); + * ... + * }; + * ... + * auto vars = model.addVars("x_", 42); + * model.includeHeur(vars); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Heur Type of the heuristic, derived from scip::ObjHeur. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Heur after the %SCIP data structure. + * @return Non-owning pointer to the heuristic, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Heur* includeHeur(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjHeur, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 6712cfc7..99de1475 100644 --- a/readme.md +++ b/readme.md @@ -75,10 +75,13 @@ int main() * Include custom plugins: * constraint handlers derived from [`scip::ObjConshdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjConshdlr.php) - (`scippp::Model::includeConshdlr`), and + (`scippp::Model::includeConshdlr`), * event handlers derived from [`scip::ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php) - (`scippp::Model::includeEventhdlr`). + (`scippp::Model::includeEventhdlr`), and + * primal heuristics derived from + [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) + (`scippp::Model::includeHeur`). * Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). ## Build diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 14f1fc18..9ee2a1b8 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -9,6 +9,7 @@ #include "scippp/solving_statistics.hpp" #include #include +#include #include using namespace boost::algorithm; @@ -162,6 +163,67 @@ BOOST_AUTO_TEST_CASE(UseConstraintHandler) BOOST_TEST(x2.getSolValAsInt(model.getBestSol()) == 1); } +/** + * Proposes the solution where a given binary variable is one and all others are zero, before presolving. + */ +class OneVarHeur : public scip::ObjHeur { + SCIP_VAR* m_var; + int m_nFound { 0 }; + +public: + OneVarHeur(SCIP* scip, SCIP_VAR* var) + : scip::ObjHeur( + scip, + "onevar", + "proposes a solution where a given binary variable is one", + 'O', // dispchar + 20000, // priority, higher than the one of the trivial heuristic to be called first + 1, // freq + 0, // freqofs + -1, // maxdepth + SCIP_HEURTIMING_BEFOREPRESOL, + FALSE) // usessubscip + , m_var(var) + { + } + [[nodiscard]] int getNFound() const + { + return m_nFound; + } + SCIP_DECL_HEUREXEC(scip_exec) + override + { + SCIP_SOL* sol { nullptr }; + SCIP_CALL(SCIPcreateSol(scip, &sol, heur)); + SCIP_CALL(SCIPsetSolVal(scip, sol, SCIPvarGetTransVar(m_var), 1.0)); + SCIP_Bool stored { FALSE }; + SCIP_CALL(SCIPtrySolFree(scip, &sol, FALSE, FALSE, TRUE, TRUE, TRUE, &stored)); + if (stored) { + ++m_nFound; + *result = SCIP_FOUNDSOL; + } else { + *result = SCIP_DIDNOTFIND; + } + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseHeuristic) +{ + Model model("Simple"); + auto x1 = model.addVar("x_1", 1, VarType::BINARY); + auto x2 = model.addVar("x_2", 1, VarType::BINARY); + model.addConstr(x1 + x2 <= 1, "capacity"); + model.setObjsense(Sense::MAXIMIZE); + const auto* heur { model.includeHeur(x1.getVar()) }; + BOOST_REQUIRE(heur != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + // the heuristic is called first, so its solution is stored as there is no other solution yet + BOOST_TEST(heur->getNFound() == 1); + BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); +} + /** * Forwards all messages to a file. */ From d163a3d60f98d8c34d644f4329281be07d9344fd Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:03:30 +0200 Subject: [PATCH 09/37] Add Model::includePresol Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 5 ++-- include/scippp/model.hpp | 32 ++++++++++++++++++++++++ readme.md | 7 ++++-- test/test_objscip.cpp | 53 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 32990b24..9f8d0b5a 100644 --- a/changelog.md +++ b/changelog.md @@ -10,8 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 of ObjSCIP without accessing the raw SCIP object: - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, - - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, and - - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`. + - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, + - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, and + - presolvers derived from `scip::ObjPresol` via `Model::includePresol`. ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index e20c8dd3..4c16291b 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -570,5 +570,37 @@ class Model { { return constructAndInclude(&SCIPincludeObjHeur, std::forward(args)...); } + + /** + * Includes a custom presolver. + * + * The presolver is constructed by this method as scip::ObjPresol requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjPresol to reduce the problem before solving it, e.g., based on problem-specific knowledge: + * @code + * class MyPresolver : public scip::ObjPresol { + * public: + * MyPresolver(SCIP* scip, const std::vector& vars); + * ... + * }; + * ... + * auto vars = model.addVars("x_", 42); + * model.includePresol(vars); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Presol Type of the presolver, derived from scip::ObjPresol. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Presol after the %SCIP data structure. + * @return Non-owning pointer to the presolver, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Presol* includePresol(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjPresol, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 99de1475..43f56d4a 100644 --- a/readme.md +++ b/readme.md @@ -78,10 +78,13 @@ int main() (`scippp::Model::includeConshdlr`), * event handlers derived from [`scip::ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php) - (`scippp::Model::includeEventhdlr`), and + (`scippp::Model::includeEventhdlr`), * primal heuristics derived from [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) - (`scippp::Model::includeHeur`). + (`scippp::Model::includeHeur`), and + * presolvers derived from + [`scip::ObjPresol`](https://www.scipopt.org/doc/html/classscip_1_1ObjPresol.php) + (`scippp::Model::includePresol`). * Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). ## Build diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 9ee2a1b8..c6ebe810 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -11,6 +11,7 @@ #include #include #include +#include using namespace boost::algorithm; using namespace scippp; @@ -224,6 +225,58 @@ BOOST_AUTO_TEST_CASE(UseHeuristic) BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); } +/** + * Fixes a variable to zero, e.g., based on problem-specific knowledge. + */ +class ZeroVarPresol : public scip::ObjPresol { + SCIP_VAR* m_var; + +public: + ZeroVarPresol(SCIP* scip, SCIP_VAR* var) + : scip::ObjPresol( + scip, + "zerovar", + "fixes a variable to zero", + 10000000, // priority, higher than the ones of the default presolvers to be called first + -1, // maxrounds + SCIP_PRESOLTIMING_FAST) + , m_var(var) + { + } + SCIP_DECL_PRESOLEXEC(scip_exec) + override + { + SCIP_Bool infeasible { FALSE }; + SCIP_Bool fixed { FALSE }; + SCIP_CALL(SCIPfixVar(scip, SCIPvarGetTransVar(m_var), 0.0, &infeasible, &fixed)); + if (infeasible) { + *result = SCIP_CUTOFF; + } else if (fixed) { + ++(*nfixedvars); + *result = SCIP_SUCCESS; + } else { + *result = SCIP_DIDNOTFIND; + } + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UsePresolver) +{ + Model model("Simple"); + auto x1 = model.addVar("x_1", 2, VarType::BINARY); + auto x2 = model.addVar("x_2", 1, VarType::BINARY); + model.addConstr(x1 + x2 <= 1, "capacity"); + model.setObjsense(Sense::MAXIMIZE); + BOOST_TEST(model.includePresol(x1.getVar()) != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + // without the presolver, x_1 = 1 would be optimal with objective 2 + BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); + BOOST_TEST(x1.getSolValAsInt(model.getBestSol()) == 0); + BOOST_TEST(x2.getSolValAsInt(model.getBestSol()) == 1); +} + /** * Forwards all messages to a file. */ From 32e0c703de387eb500cb384ce78d132a504fa1c3 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:09:32 +0200 Subject: [PATCH 10/37] Add Model::includeProp Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 5 ++-- include/scippp/model.hpp | 33 +++++++++++++++++++++++++ readme.md | 7 ++++-- test/test_objscip.cpp | 52 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 9f8d0b5a..59c6c90f 100644 --- a/changelog.md +++ b/changelog.md @@ -11,8 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, - - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, and - - presolvers derived from `scip::ObjPresol` via `Model::includePresol`. + - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, + - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, and + - propagators derived from `scip::ObjProp` via `Model::includeProp`. ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 4c16291b..0c7ab4c2 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -602,5 +602,38 @@ class Model { { return constructAndInclude(&SCIPincludeObjPresol, std::forward(args)...); } + + /** + * Includes a custom propagator. + * + * The propagator is constructed by this method as scip::ObjProp requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjProp to tighten the domains of variables during the search, e.g., based on + * problem-specific knowledge: + * @code + * class MyPropagator : public scip::ObjProp { + * public: + * MyPropagator(SCIP* scip, const std::vector& vars); + * ... + * }; + * ... + * auto vars = model.addVars("x_", 42); + * model.includeProp(vars); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Prop Type of the propagator, derived from scip::ObjProp. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Prop after the %SCIP data structure. + * @return Non-owning pointer to the propagator, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Prop* includeProp(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjProp, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 43f56d4a..24719d60 100644 --- a/readme.md +++ b/readme.md @@ -81,10 +81,13 @@ int main() (`scippp::Model::includeEventhdlr`), * primal heuristics derived from [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) - (`scippp::Model::includeHeur`), and + (`scippp::Model::includeHeur`), * presolvers derived from [`scip::ObjPresol`](https://www.scipopt.org/doc/html/classscip_1_1ObjPresol.php) - (`scippp::Model::includePresol`). + (`scippp::Model::includePresol`), and + * propagators derived from + [`scip::ObjProp`](https://www.scipopt.org/doc/html/classscip_1_1ObjProp.php) + (`scippp::Model::includeProp`). * Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). ## Build diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index c6ebe810..ae3084f5 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -6,12 +6,14 @@ #include #include "scippp/model.hpp" +#include "scippp/parameters.hpp" #include "scippp/solving_statistics.hpp" #include #include #include #include #include +#include using namespace boost::algorithm; using namespace scippp; @@ -277,6 +279,56 @@ BOOST_AUTO_TEST_CASE(UsePresolver) BOOST_TEST(x2.getSolValAsInt(model.getBestSol()) == 1); } +/** + * Counts how often it is called for domain propagation. + */ +class CountingProp : public scip::ObjProp { + int m_nCalls { 0 }; + +public: + explicit CountingProp(SCIP* scip) + : scip::ObjProp( + scip, + "counting", + "counts how often it is called", + 0, // priority + 1, // freq + FALSE, // delay + SCIP_PROPTIMING_BEFORELP, + 0, // presolpriority + -1, // presolmaxrounds + SCIP_PRESOLTIMING_NONE) + { + } + [[nodiscard]] int getNCalls() const + { + return m_nCalls; + } + SCIP_DECL_PROPEXEC(scip_exec) + override + { + ++m_nCalls; + *result = SCIP_DIDNOTFIND; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UsePropagator) +{ + Model model("Simple"); + auto x1 = model.addVar("x_1", 2, VarType::BINARY); + auto x2 = model.addVar("x_2", 1, VarType::BINARY); + model.addConstr(x1 + x2 <= 1, "capacity"); + model.setObjsense(Sense::MAXIMIZE); + // otherwise presolving solves the problem and no node is processed + model.setParam(params::PRESOLVING::MAXROUNDS, 0); + const auto* prop { model.includeProp() }; + BOOST_REQUIRE(prop != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(prop->getNCalls() > 0); +} + /** * Forwards all messages to a file. */ From bd40390fc10de163abf6688a3632e368587bcc45 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:12:53 +0200 Subject: [PATCH 11/37] Add Model::includeSepa Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 5 +-- include/scippp/model.hpp | 33 ++++++++++++++++++ readme.md | 7 ++-- test/test_objscip.cpp | 73 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 59c6c90f..341674d5 100644 --- a/changelog.md +++ b/changelog.md @@ -12,8 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, and - - propagators derived from `scip::ObjProp` via `Model::includeProp`. + - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, + - propagators derived from `scip::ObjProp` via `Model::includeProp`, and + - separators derived from `scip::ObjSepa` via `Model::includeSepa`. ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 0c7ab4c2..4817cb82 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -635,5 +635,38 @@ class Model { { return constructAndInclude(&SCIPincludeObjProp, std::forward(args)...); } + + /** + * Includes a custom separator. + * + * The separator is constructed by this method as scip::ObjSepa requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjSepa to cut off fractional solutions of the LP relaxation, e.g., with problem-specific + * valid inequalities: + * @code + * class MySeparator : public scip::ObjSepa { + * public: + * MySeparator(SCIP* scip, const std::vector& vars); + * ... + * }; + * ... + * auto vars = model.addVars("x_", 42); + * model.includeSepa(vars); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Sepa Type of the separator, derived from scip::ObjSepa. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Sepa after the %SCIP data structure. + * @return Non-owning pointer to the separator, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Sepa* includeSepa(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjSepa, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 24719d60..a615c06b 100644 --- a/readme.md +++ b/readme.md @@ -84,10 +84,13 @@ int main() (`scippp::Model::includeHeur`), * presolvers derived from [`scip::ObjPresol`](https://www.scipopt.org/doc/html/classscip_1_1ObjPresol.php) - (`scippp::Model::includePresol`), and + (`scippp::Model::includePresol`), * propagators derived from [`scip::ObjProp`](https://www.scipopt.org/doc/html/classscip_1_1ObjProp.php) - (`scippp::Model::includeProp`). + (`scippp::Model::includeProp`), and + * separators derived from + [`scip::ObjSepa`](https://www.scipopt.org/doc/html/classscip_1_1ObjSepa.php) + (`scippp::Model::includeSepa`). * Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). ## Build diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index ae3084f5..6cff30fa 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include using namespace boost::algorithm; using namespace scippp; @@ -329,6 +331,77 @@ BOOST_AUTO_TEST_CASE(UsePropagator) BOOST_TEST(prop->getNCalls() > 0); } +/** + * Adds max 1.1 x_1 + x_2 s.t. 2 x_1 + 3 x_2 <= 4 with binary variables, whose LP relaxation has the fractional + * solution x_1 = 1, x_2 = 2/3, while x_1 = 1, x_2 = 0 is optimal. + */ +array addFractionalProblem(Model& model) +{ + auto x1 = model.addVar("x_1", 1.1, VarType::BINARY); + auto x2 = model.addVar("x_2", 1, VarType::BINARY); + model.addConstr(2 * x1 + 3 * x2 <= 4, "capacity"); + model.setObjsense(Sense::MAXIMIZE); + // otherwise presolving or propagation solve the problem before the LP relaxation is solved + model.setParam(params::PRESOLVING::MAXROUNDS, 0); + model.setParam(params::PROPAGATING::MAXROUNDSROOT, 0); + return { x1, x2 }; +} + +/** + * Separates the cut x_1 + x_2 <= 1 at the root, which is valid for binary variables with 2 x_1 + 3 x_2 <= 4. + */ +class CapacitySepa : public scip::ObjSepa { + array m_vars; + int m_nCuts { 0 }; + +public: + CapacitySepa(SCIP* scip, SCIP_VAR* x1, SCIP_VAR* x2) + : scip::ObjSepa( + scip, + "capacity", + "separates x_1 + x_2 <= 1", + 1000000, // priority, higher than the ones of the default separators to be called first + 0, // freq + 1.0, // maxbounddist + FALSE, // usessubscip + FALSE) // delay + , m_vars { x1, x2 } + { + } + [[nodiscard]] int getNCuts() const + { + return m_nCuts; + } + SCIP_DECL_SEPAEXECLP(scip_execlp) + override + { + *result = SCIP_DIDNOTFIND; + SCIP_ROW* row { nullptr }; + SCIP_CALL(SCIPcreateEmptyRowSepa(scip, &row, sepa, "capacity", -SCIPinfinity(scip), 1.0, FALSE, FALSE, TRUE)); + for (auto* var : m_vars) { + SCIP_CALL(SCIPaddVarToRow(scip, row, SCIPvarGetTransVar(var), 1.0)); + } + if (SCIPisCutEfficacious(scip, nullptr, row)) { + SCIP_Bool infeasible { FALSE }; + SCIP_CALL(SCIPaddRow(scip, row, FALSE, &infeasible)); + ++m_nCuts; + *result = infeasible ? SCIP_CUTOFF : SCIP_SEPARATED; + } + return SCIPreleaseRow(scip, &row); + } +}; + +BOOST_AUTO_TEST_CASE(UseSeparator) +{ + Model model("Simple"); + auto [x1, x2] = addFractionalProblem(model); + const auto* sepa { model.includeSepa(x1.getVar(), x2.getVar()) }; + BOOST_REQUIRE(sepa != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(sepa->getNCuts() > 0); +} + /** * Forwards all messages to a file. */ From dc20cf0a40f096e2628bb048084a0087bc8f1478 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:14:02 +0200 Subject: [PATCH 12/37] Add Model::includeCutsel Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 31 +++++++++++++++++++++++++++ readme.md | 3 +++ test/test_objscip.cpp | 45 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 341674d5..02687ac6 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 of ObjSCIP without accessing the raw SCIP object: - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, + - cut selectors derived from `scip::ObjCutsel` via `Model::includeCutsel`, - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 4817cb82..135d96f8 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -668,5 +668,36 @@ class Model { { return constructAndInclude(&SCIPincludeObjSepa, std::forward(args)...); } + + /** + * Includes a custom cut selector. + * + * The cut selector is constructed by this method as scip::ObjCutsel requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjCutsel to decide which of the separated cuts are added to the LP relaxation: + * @code + * class MyCutSelector : public scip::ObjCutsel { + * public: + * MyCutSelector(SCIP* scip, double minEfficacy); + * ... + * }; + * ... + * model.includeCutsel(0.1); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Cutsel Type of the cut selector, derived from scip::ObjCutsel. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Cutsel after the %SCIP data structure. + * @return Non-owning pointer to the cut selector, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Cutsel* includeCutsel(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjCutsel, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index a615c06b..6e9e0a5b 100644 --- a/readme.md +++ b/readme.md @@ -76,6 +76,9 @@ int main() * constraint handlers derived from [`scip::ObjConshdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjConshdlr.php) (`scippp::Model::includeConshdlr`), + * cut selectors derived from + [`scip::ObjCutsel`](https://www.scipopt.org/doc/html/classscip_1_1ObjCutsel.php) + (`scippp::Model::includeCutsel`), * event handlers derived from [`scip::ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php) (`scippp::Model::includeEventhdlr`), diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 6cff30fa..573b403e 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -1,6 +1,7 @@ +#include +#include #include #include -#include #include #include #include @@ -10,6 +11,7 @@ #include "scippp/parameters.hpp" #include "scippp/solving_statistics.hpp" #include +#include #include #include #include @@ -402,6 +404,47 @@ BOOST_AUTO_TEST_CASE(UseSeparator) BOOST_TEST(sepa->getNCuts() > 0); } +/** + * Selects all cuts, up to the maximal number of cuts to select. + */ +class AllCutsel : public scip::ObjCutsel { + int m_nCalls { 0 }; + +public: + explicit AllCutsel(SCIP* scip) + : scip::ObjCutsel( + scip, + "all", + "selects all cuts", + 1000000) // priority, higher than the ones of the default cut selectors to be used + { + } + [[nodiscard]] int getNCalls() const + { + return m_nCalls; + } + SCIP_DECL_CUTSELSELECT(scip_select) + override + { + ++m_nCalls; + *nselectedcuts = min(ncuts, maxnselectedcuts); + *result = SCIP_SUCCESS; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseCutSelector) +{ + Model model("Simple"); + auto [x1, x2] = addFractionalProblem(model); + BOOST_REQUIRE(model.includeSepa(x1.getVar(), x2.getVar()) != nullptr); + const auto* cutsel { model.includeCutsel() }; + BOOST_REQUIRE(cutsel != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(cutsel->getNCalls() > 0); +} + /** * Forwards all messages to a file. */ From ea45075014857cccfac77b16eb7776698946dcc4 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:16:05 +0200 Subject: [PATCH 13/37] Add Model::includeBranchrule Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 33 +++++++++++++++++++++++++ readme.md | 3 +++ test/test_objscip.cpp | 53 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/changelog.md b/changelog.md index 02687ac6..7fe1544b 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [PR44](https://github.com/scipopt/SCIPpp/pull/44), [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Support of ObjSCIP without accessing the raw SCIP object: - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, + - branching rules derived from `scip::ObjBranchrule` via `Model::includeBranchrule`, - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, - cut selectors derived from `scip::ObjCutsel` via `Model::includeCutsel`, - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 135d96f8..98d467bb 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -699,5 +699,38 @@ class Model { { return constructAndInclude(&SCIPincludeObjCutsel, std::forward(args)...); } + + /** + * Includes a custom branching rule. + * + * The branching rule is constructed by this method as scip::ObjBranchrule requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjBranchrule to decide how to split the search space, e.g., based on problem-specific + * knowledge: + * @code + * class MyBranchingRule : public scip::ObjBranchrule { + * public: + * MyBranchingRule(SCIP* scip, const std::vector& vars); + * ... + * }; + * ... + * auto vars = model.addVars("x_", 42); + * model.includeBranchrule(vars); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Branchrule Type of the branching rule, derived from scip::ObjBranchrule. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Branchrule after the %SCIP data structure. + * @return Non-owning pointer to the branching rule, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Branchrule* includeBranchrule(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjBranchrule, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 6e9e0a5b..d3bce4cf 100644 --- a/readme.md +++ b/readme.md @@ -73,6 +73,9 @@ int main() [`scip::ObjMessagehdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjMessagehdlr.php) (`scippp::Model::setMessagehdlr`). * Include custom plugins: + * branching rules derived from + [`scip::ObjBranchrule`](https://www.scipopt.org/doc/html/classscip_1_1ObjBranchrule.php) + (`scippp::Model::includeBranchrule`), * constraint handlers derived from [`scip::ObjConshdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjConshdlr.php) (`scippp::Model::includeConshdlr`), diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 573b403e..43e7ef6e 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -10,6 +10,7 @@ #include "scippp/model.hpp" #include "scippp/parameters.hpp" #include "scippp/solving_statistics.hpp" +#include #include #include #include @@ -445,6 +446,58 @@ BOOST_AUTO_TEST_CASE(UseCutSelector) BOOST_TEST(cutsel->getNCalls() > 0); } +/** + * Branches on the first fractional variable of the LP solution. + */ +class FirstFracBranchrule : public scip::ObjBranchrule { + int m_nCalls { 0 }; + +public: + explicit FirstFracBranchrule(SCIP* scip) + : scip::ObjBranchrule( + scip, + "firstfrac", + "branches on the first fractional variable", + 1000000, // priority, higher than the ones of the default branching rules to be used + -1, // maxdepth + 1.0) // maxbounddist + { + } + [[nodiscard]] int getNCalls() const + { + return m_nCalls; + } + SCIP_DECL_BRANCHEXECLP(scip_execlp) + override + { + ++m_nCalls; + SCIP_VAR** cands { nullptr }; + int nCands { 0 }; + SCIP_CALL(SCIPgetLPBranchCands(scip, &cands, nullptr, nullptr, &nCands, nullptr, nullptr)); + if (nCands == 0) { + *result = SCIP_DIDNOTRUN; + return SCIP_OKAY; + } + SCIP_CALL(SCIPbranchVar(scip, cands[0], nullptr, nullptr, nullptr)); + *result = SCIP_BRANCHED; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseBranchingRule) +{ + Model model("Simple"); + addFractionalProblem(model); + // otherwise the problem is solved at the root by separation or conflict analysis + model.setParam(params::SEPARATING::MAXROUNDSROOT, 0); + model.setParam(params::CONFLICT::ENABLE, false); + const auto* branchrule { model.includeBranchrule() }; + BOOST_REQUIRE(branchrule != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(branchrule->getNCalls() > 0); +} + /** * Forwards all messages to a file. */ From 33fbdaf7ab535bad22d958438bafeff21ec7ab99 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:16:57 +0200 Subject: [PATCH 14/37] Add Model::includeNodesel Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 31 ++++++++++++++++++++++ readme.md | 3 +++ test/test_objscip.cpp | 56 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/changelog.md b/changelog.md index 7fe1544b..9288cae9 100644 --- a/changelog.md +++ b/changelog.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - cut selectors derived from `scip::ObjCutsel` via `Model::includeCutsel`, - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, + - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, - propagators derived from `scip::ObjProp` via `Model::includeProp`, and - separators derived from `scip::ObjSepa` via `Model::includeSepa`. diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 98d467bb..b6b346f4 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -732,5 +732,36 @@ class Model { { return constructAndInclude(&SCIPincludeObjBranchrule, std::forward(args)...); } + + /** + * Includes a custom node selector. + * + * The node selector is constructed by this method as scip::ObjNodesel requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjNodesel to decide which open node of the search tree is processed next: + * @code + * class MyNodeSelector : public scip::ObjNodesel { + * public: + * explicit MyNodeSelector(SCIP* scip); + * ... + * }; + * ... + * model.includeNodesel(); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Nodesel Type of the node selector, derived from scip::ObjNodesel. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Nodesel after the %SCIP data structure. + * @return Non-owning pointer to the node selector, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Nodesel* includeNodesel(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjNodesel, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index d3bce4cf..b026fdc7 100644 --- a/readme.md +++ b/readme.md @@ -88,6 +88,9 @@ int main() * primal heuristics derived from [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) (`scippp::Model::includeHeur`), + * node selectors derived from + [`scip::ObjNodesel`](https://www.scipopt.org/doc/html/classscip_1_1ObjNodesel.php) + (`scippp::Model::includeNodesel`), * presolvers derived from [`scip::ObjPresol`](https://www.scipopt.org/doc/html/classscip_1_1ObjPresol.php) (`scippp::Model::includePresol`), diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 43e7ef6e..59eb4c88 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -498,6 +499,61 @@ BOOST_AUTO_TEST_CASE(UseBranchingRule) BOOST_TEST(branchrule->getNCalls() > 0); } +/** + * Selects nodes in depth-first order. + */ +class DepthFirstNodesel : public scip::ObjNodesel { + int m_nCalls { 0 }; + +public: + explicit DepthFirstNodesel(SCIP* scip) + : scip::ObjNodesel( + scip, + "depthfirst", + "selects nodes in depth-first order", + 1000000, // stdpriority, higher than the ones of the default node selectors to be used + 0) // memsavepriority + { + } + [[nodiscard]] int getNCalls() const + { + return m_nCalls; + } + SCIP_DECL_NODESELSELECT(scip_select) + override + { + ++m_nCalls; + *selnode = SCIPgetPrioChild(scip); + if (*selnode == nullptr) { + *selnode = SCIPgetPrioSibling(scip); + } + if (*selnode == nullptr) { + *selnode = SCIPgetBestLeaf(scip); + } + return SCIP_OKAY; + } + SCIP_DECL_NODESELCOMP(scip_comp) + override + { + // deeper nodes first + return SCIPnodeGetDepth(node2) - SCIPnodeGetDepth(node1); + } +}; + +BOOST_AUTO_TEST_CASE(UseNodeSelector) +{ + Model model("Simple"); + addFractionalProblem(model); + // otherwise the problem is solved at the root by separation or conflict analysis + model.setParam(params::SEPARATING::MAXROUNDSROOT, 0); + model.setParam(params::CONFLICT::ENABLE, false); + const auto* nodesel { model.includeNodesel() }; + BOOST_REQUIRE(nodesel != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(nodesel->getNCalls() > 0); +} + /** * Forwards all messages to a file. */ From 4f05ceae18458fcaefd1cdd575cd61c8ba67ac94 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:17:37 +0200 Subject: [PATCH 15/37] Add Model::includeRelax Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 3 ++- include/scippp/model.hpp | 32 ++++++++++++++++++++++++++++++ readme.md | 5 ++++- test/test_objscip.cpp | 42 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 9288cae9..1ba9b7c9 100644 --- a/changelog.md +++ b/changelog.md @@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, - - propagators derived from `scip::ObjProp` via `Model::includeProp`, and + - propagators derived from `scip::ObjProp` via `Model::includeProp`, + - relaxators derived from `scip::ObjRelax` via `Model::includeRelax`, and - separators derived from `scip::ObjSepa` via `Model::includeSepa`. ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index b6b346f4..f10dbcec 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -763,5 +763,37 @@ class Model { { return constructAndInclude(&SCIPincludeObjNodesel, std::forward(args)...); } + + /** + * Includes a custom relaxator. + * + * The relaxator is constructed by this method as scip::ObjRelax requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjRelax to solve problem-specific relaxations in addition to the LP relaxation: + * @code + * class MyRelaxator : public scip::ObjRelax { + * public: + * MyRelaxator(SCIP* scip, const std::vector& vars); + * ... + * }; + * ... + * auto vars = model.addVars("x_", 42); + * model.includeRelax(vars); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Relax Type of the relaxator, derived from scip::ObjRelax. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Relax after the %SCIP data structure. + * @return Non-owning pointer to the relaxator, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Relax* includeRelax(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjRelax, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index b026fdc7..94c725a0 100644 --- a/readme.md +++ b/readme.md @@ -96,7 +96,10 @@ int main() (`scippp::Model::includePresol`), * propagators derived from [`scip::ObjProp`](https://www.scipopt.org/doc/html/classscip_1_1ObjProp.php) - (`scippp::Model::includeProp`), and + (`scippp::Model::includeProp`), + * relaxators derived from + [`scip::ObjRelax`](https://www.scipopt.org/doc/html/classscip_1_1ObjRelax.php) + (`scippp::Model::includeRelax`), and * separators derived from [`scip::ObjSepa`](https://www.scipopt.org/doc/html/classscip_1_1ObjSepa.php) (`scippp::Model::includeSepa`). diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 59eb4c88..b3371ca9 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include using namespace boost::algorithm; @@ -554,6 +555,47 @@ BOOST_AUTO_TEST_CASE(UseNodeSelector) BOOST_TEST(nodesel->getNCalls() > 0); } +/** + * Counts how often it is called before the LP relaxation is solved. + */ +class CountingRelax : public scip::ObjRelax { + int m_nCalls { 0 }; + +public: + explicit CountingRelax(SCIP* scip) + : scip::ObjRelax( + scip, + "counting", + "counts how often it is called", + 0, // priority, non-negative to be called before the LP relaxation is solved + 1, // freq + FALSE) // includeslp + { + } + [[nodiscard]] int getNCalls() const + { + return m_nCalls; + } + SCIP_DECL_RELAXEXEC(scip_exec) + override + { + ++m_nCalls; + *result = SCIP_DIDNOTRUN; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseRelaxator) +{ + Model model("Simple"); + addFractionalProblem(model); + const auto* relax { model.includeRelax() }; + BOOST_REQUIRE(relax != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(relax->getNCalls() > 0); +} + /** * Forwards all messages to a file. */ From aa78943d2f4ce2faddf458b70220d1462597ae3d Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:18:22 +0200 Subject: [PATCH 16/37] Add Model::includeDisp Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 31 +++++++++++++++++++++++++++ readme.md | 3 +++ test/test_objscip.cpp | 45 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/changelog.md b/changelog.md index 1ba9b7c9..5f5f715f 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - branching rules derived from `scip::ObjBranchrule` via `Model::includeBranchrule`, - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, - cut selectors derived from `scip::ObjCutsel` via `Model::includeCutsel`, + - display columns derived from `scip::ObjDisp` via `Model::includeDisp`, - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index f10dbcec..fba7cca0 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -795,5 +795,36 @@ class Model { { return constructAndInclude(&SCIPincludeObjRelax, std::forward(args)...); } + + /** + * Includes a custom display column. + * + * The display column is constructed by this method as scip::ObjDisp requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjDisp to show additional information in the lines %SCIP prints during the solving process: + * @code + * class MyDisplayColumn : public scip::ObjDisp { + * public: + * explicit MyDisplayColumn(SCIP* scip); + * ... + * }; + * ... + * model.includeDisp(); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Disp Type of the display column, derived from scip::ObjDisp. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Disp after the %SCIP data structure. + * @return Non-owning pointer to the display column, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Disp* includeDisp(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjDisp, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 94c725a0..81472668 100644 --- a/readme.md +++ b/readme.md @@ -82,6 +82,9 @@ int main() * cut selectors derived from [`scip::ObjCutsel`](https://www.scipopt.org/doc/html/classscip_1_1ObjCutsel.php) (`scippp::Model::includeCutsel`), + * display columns derived from + [`scip::ObjDisp`](https://www.scipopt.org/doc/html/classscip_1_1ObjDisp.php) + (`scippp::Model::includeDisp`), * event handlers derived from [`scip::ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php) (`scippp::Model::includeEventhdlr`), diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index b3371ca9..24538bfb 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -596,6 +597,50 @@ BOOST_AUTO_TEST_CASE(UseRelaxator) BOOST_TEST(relax->getNCalls() > 0); } +/** + * Displays how often it was displayed. + */ +class CountingDisp : public scip::ObjDisp { + int m_nCalls { 0 }; + +public: + explicit CountingDisp(SCIP* scip) + : scip::ObjDisp( + scip, + "counting", + "displays how often it was displayed", + "calls", // header + 5, // width + 1000000, // priority, higher than the ones of the default display columns to be displayed + 100000, // position, higher than the ones of the default display columns to be the last column + TRUE) // stripline + { + } + [[nodiscard]] int getNCalls() const + { + return m_nCalls; + } + SCIP_DECL_DISPOUTPUT(scip_output) + override + { + ++m_nCalls; + SCIPinfoMessage(scip, file, "%5d", m_nCalls); + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseDisplayColumn) +{ + Model model("Simple"); + // nodes have to be processed, otherwise no display line is printed + addFractionalProblem(model); + const auto* disp { model.includeDisp() }; + BOOST_REQUIRE(disp != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(disp->getNCalls() > 0); +} + /** * Forwards all messages to a file. */ From 2d633a8fce3a7a633ae41fd44be8aca0aa08a590 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:19:51 +0200 Subject: [PATCH 17/37] Add Model::includeTable Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 5 +++-- include/scippp/model.hpp | 33 ++++++++++++++++++++++++++++ readme.md | 7 ++++-- test/test_objscip.cpp | 46 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 5f5f715f..efa89114 100644 --- a/changelog.md +++ b/changelog.md @@ -18,8 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, - propagators derived from `scip::ObjProp` via `Model::includeProp`, - - relaxators derived from `scip::ObjRelax` via `Model::includeRelax`, and - - separators derived from `scip::ObjSepa` via `Model::includeSepa`. + - relaxators derived from `scip::ObjRelax` via `Model::includeRelax`, + - separators derived from `scip::ObjSepa` via `Model::includeSepa`, and + - statistics tables derived from `scip::ObjTable` via `Model::includeTable`. ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index fba7cca0..9cbdeb1b 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -6,6 +6,7 @@ #include #include #include +#include // not included by objscip.h #include #include #include @@ -826,5 +827,37 @@ class Model { { return constructAndInclude(&SCIPincludeObjDisp, std::forward(args)...); } + + /** + * Includes a custom statistics table. + * + * The statistics table is constructed by this method as scip::ObjTable requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjTable to add problem-specific information to the statistics %SCIP prints: + * @code + * class MyStatisticsTable : public scip::ObjTable { + * public: + * explicit MyStatisticsTable(SCIP* scip); + * ... + * }; + * ... + * model.includeTable(); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Table Type of the statistics table, derived from scip::ObjTable. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Table after the %SCIP data structure. + * @return Non-owning pointer to the statistics table, or \c nullptr if including failed. + * @attention Must be called before solve(). + * @note SCIP++ does not print statistics, this requires %SCIP's %C %API, e.g., SCIPprintStatistics. + */ + template + Table* includeTable(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjTable, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 81472668..0e69084f 100644 --- a/readme.md +++ b/readme.md @@ -102,10 +102,13 @@ int main() (`scippp::Model::includeProp`), * relaxators derived from [`scip::ObjRelax`](https://www.scipopt.org/doc/html/classscip_1_1ObjRelax.php) - (`scippp::Model::includeRelax`), and + (`scippp::Model::includeRelax`), * separators derived from [`scip::ObjSepa`](https://www.scipopt.org/doc/html/classscip_1_1ObjSepa.php) - (`scippp::Model::includeSepa`). + (`scippp::Model::includeSepa`), and + * statistics tables derived from + [`scip::ObjTable`](https://www.scipopt.org/doc/html/classscip_1_1ObjTable.php) + (`scippp::Model::includeTable`). * Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). ## Build diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 24538bfb..b57bca97 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include using namespace boost::algorithm; using namespace scippp; @@ -641,6 +643,50 @@ BOOST_AUTO_TEST_CASE(UseDisplayColumn) BOOST_TEST(disp->getNCalls() > 0); } +/** + * Prints how often it was printed. + */ +class CountingTable : public scip::ObjTable { + int m_nCalls { 0 }; + +public: + explicit CountingTable(SCIP* scip) + : scip::ObjTable( + scip, + "counting", + "prints how often it was printed", + 100000, // position, higher than the ones of the default statistics tables to be the last table + SCIP_STAGE_TRANSFORMED) // earlieststage + { + } + [[nodiscard]] int getNCalls() const + { + return m_nCalls; + } + SCIP_DECL_TABLEOUTPUT(scip_output) + override + { + ++m_nCalls; + SCIPinfoMessage(scip, file, "Counting : %d\n", m_nCalls); + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseStatisticsTable) +{ + Model model("Simple"); + const auto* table { model.includeTable() }; + BOOST_REQUIRE(table != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + // SCIP++ does not print statistics, so we have to use the raw SCIP object + auto* file { tmpfile() }; + BOOST_REQUIRE(file != nullptr); + BOOST_TEST(SCIPprintStatistics(model.scip(), file) == SCIP_OKAY); + fclose(file); + BOOST_TEST(table->getNCalls() == 1); +} + /** * Forwards all messages to a file. */ From 264da1126dadb45c45ccd5afb443c3822d440a27 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:20:43 +0200 Subject: [PATCH 18/37] Add Model::includeReader Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 31 +++++++++++++++++++++++++++++++ readme.md | 3 +++ test/test_objscip.cpp | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/changelog.md b/changelog.md index efa89114..c744f35d 100644 --- a/changelog.md +++ b/changelog.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - cut selectors derived from `scip::ObjCutsel` via `Model::includeCutsel`, - display columns derived from `scip::ObjDisp` via `Model::includeDisp`, - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, + - file readers derived from `scip::ObjReader` via `Model::includeReader`, - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 9cbdeb1b..f05699c9 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -859,5 +859,36 @@ class Model { { return constructAndInclude
(&SCIPincludeObjTable, std::forward(args)...); } + + /** + * Includes a custom file reader. + * + * The file reader is constructed by this method as scip::ObjReader requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjReader to write the problem in a custom file format via writeOrigProblem(), which selects + * the file reader by the extension of the file name: + * @code + * class MyFileReader : public scip::ObjReader { + * public: + * explicit MyFileReader(SCIP* scip); // passes "myext" as extension to scip::ObjReader + * ... + * }; + * ... + * model.includeReader(); + * model.writeOrigProblem(std::filesystem::directory_entry("problem.myext")); + * @endcode + * + * @since 1.5.0 + * @tparam Reader Type of the file reader, derived from scip::ObjReader. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Reader after the %SCIP data structure. + * @return Non-owning pointer to the file reader, or \c nullptr if including failed. + */ + template + Reader* includeReader(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjReader, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 0e69084f..247da628 100644 --- a/readme.md +++ b/readme.md @@ -88,6 +88,9 @@ int main() * event handlers derived from [`scip::ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php) (`scippp::Model::includeEventhdlr`), + * file readers derived from + [`scip::ObjReader`](https://www.scipopt.org/doc/html/classscip_1_1ObjReader.php) + (`scippp::Model::includeReader`), * primal heuristics derived from [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) (`scippp::Model::includeHeur`), diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index b57bca97..79144b15 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -687,6 +688,44 @@ BOOST_AUTO_TEST_CASE(UseStatisticsTable) BOOST_TEST(table->getNCalls() == 1); } +/** + * Writes the number of variables and constraints of a problem to files with extension cnt. + */ +class CountReader : public scip::ObjReader { +public: + explicit CountReader(SCIP* scip) + : scip::ObjReader(scip, "count", "writes the number of variables and constraints", "cnt") + { + } + SCIP_DECL_READERWRITE(scip_write) + override + { + SCIPinfoMessage(scip, file, "%d %d\n", nvars, nconss); + *result = SCIP_SUCCESS; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseReader) +{ + Model model("Simple"); + auto x1 = model.addVar("x_1", 1); + auto x2 = model.addVar("x_2", 1); + model.addConstr(x1 + x2 >= 1, "capacity"); + BOOST_TEST(model.includeReader() != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + const auto FILE_NAME { filesystem::temp_directory_path() + / boost::filesystem::unique_path("%%%%-%%%%-%%%%-%%%%.cnt").string() }; + model.writeOrigProblem(filesystem::directory_entry(FILE_NAME)); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + ifstream file(FILE_NAME); + ostringstream content; + content << file.rdbuf(); + file.close(); + filesystem::remove(FILE_NAME); + BOOST_TEST(content.str() == "2 1\n"); +} + /** * Forwards all messages to a file. */ From bf9d00217ca06d1c10b04983ecf20723ef0c826b Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:21:42 +0200 Subject: [PATCH 19/37] Add Model::includeIISfinder Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 32 ++++++++++++++++++++++++++++ readme.md | 3 +++ test/test_objscip.cpp | 46 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+) diff --git a/changelog.md b/changelog.md index c744f35d..b7afc11e 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - display columns derived from `scip::ObjDisp` via `Model::includeDisp`, - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, - file readers derived from `scip::ObjReader` via `Model::includeReader`, + - IIS finders derived from `scip::ObjIISfinder` via `Model::includeIISfinder`, - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index f05699c9..7f01694e 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -890,5 +890,37 @@ class Model { { return constructAndInclude(&SCIPincludeObjReader, std::forward(args)...); } + + /** + * Includes a custom %IIS finder. + * + * The %IIS finder is constructed by this method as scip::ObjIISfinder requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjIISfinder to compute an %IIS of an infeasible problem via generateIIS(), e.g., based on + * problem-specific knowledge: + * @code + * class MyIISFinder : public scip::ObjIISfinder { + * public: + * explicit MyIISFinder(SCIP* scip); + * ... + * }; + * ... + * model.includeIISfinder(); + * model.solve(); + * auto iis { model.generateIIS() }; + * @endcode + * + * @since 1.5.0 + * @tparam IISfinder Type of the %IIS finder, derived from scip::ObjIISfinder. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p IISfinder after the %SCIP data structure. + * @return Non-owning pointer to the %IIS finder, or \c nullptr if including failed. + */ + template + IISfinder* includeIISfinder(Args&&... args) const + { + return constructAndInclude(&SCIPincludeObjIISfinder, std::forward(args)...); + } }; } diff --git a/readme.md b/readme.md index 247da628..556f90a3 100644 --- a/readme.md +++ b/readme.md @@ -91,6 +91,9 @@ int main() * file readers derived from [`scip::ObjReader`](https://www.scipopt.org/doc/html/classscip_1_1ObjReader.php) (`scippp::Model::includeReader`), + * IIS finders derived from + [`scip::ObjIISfinder`](https://www.scipopt.org/doc/html/classscip_1_1ObjIISfinder.php) + (`scippp::Model::includeIISfinder`), * primal heuristics derived from [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) (`scippp::Model::includeHeur`), diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 79144b15..7790b069 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -726,6 +727,51 @@ BOOST_AUTO_TEST_CASE(UseReader) BOOST_TEST(content.str() == "2 1\n"); } +/** + * Counts how often it is called and leaves the work to the other %IIS finders. + */ +class CountingIISfinder : public scip::ObjIISfinder { + int m_nCalls { 0 }; + +public: + explicit CountingIISfinder(SCIP* scip) + : scip::ObjIISfinder( + scip, + "counting", + "counts how often it is called", + 1000000) // priority, higher than the ones of the default IIS finders to be called first + { + } + [[nodiscard]] int getNCalls() const + { + return m_nCalls; + } + SCIP_DECL_IISFINDEREXEC(scip_exec) + override + { + ++m_nCalls; + *result = SCIP_DIDNOTRUN; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseIISFinder) +{ + Model model("Simple"); + const auto& [x1, x2] = model.addVars<2>("x_"); + model.addConstr(x1 + x2 >= 2, "lower"); + model.addConstr(x1 + x2 <= 1, "upper"); + model.setParam(params::IIS::SILENT, true); + const auto* iisfinder { model.includeIISfinder() }; + BOOST_REQUIRE(iisfinder != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_REQUIRE(model.getStatus() == SCIP_STATUS_INFEASIBLE); + // the default IIS finder still computes the IIS + BOOST_TEST(model.generateIIS().consIds.size() == 2); + BOOST_TEST(iisfinder->getNCalls() == 1); +} + /** * Forwards all messages to a file. */ From 589c44e4d04272817b027702f2975cd5f2354abf Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:22:46 +0200 Subject: [PATCH 20/37] Add Model::includePricer Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 44 ++++++++++++++++++++++++++++++++++++++ readme.md | 3 +++ source/model_objscip.cpp | 5 +++++ test/test_objscip.cpp | 46 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 99 insertions(+) diff --git a/changelog.md b/changelog.md index b7afc11e..860c2fb8 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, + - pricers derived from `scip::ObjPricer` via `Model::includePricer`, - propagators derived from `scip::ObjProp` via `Model::includeProp`, - relaxators derived from `scip::ObjRelax` via `Model::includeRelax`, - separators derived from `scip::ObjSepa` via `Model::includeSepa`, and diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 7f01694e..1b8282d5 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -78,6 +78,13 @@ class Model { return result; } + /** + * Activates an included variable pricer, so that it is used when solving the model. + * + * @param pricer to activate. + */ + void activatePricer(const scip::ObjPricer& pricer) const; + public: /** * Creates an empty problem and sets the optimization goal to Sense::MINIMIZE. @@ -922,5 +929,42 @@ class Model { { return constructAndInclude(&SCIPincludeObjIISfinder, std::forward(args)...); } + + /** + * Includes and activates a custom variable pricer. + * + * The pricer is constructed by this method as scip::ObjPricer requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjPricer to add variables with negative reduced costs during the solving process, i.e., to + * implement column generation: + * @code + * class MyPricer : public scip::ObjPricer { + * public: + * MyPricer(SCIP* scip, const std::vector& conss); + * ... + * }; + * ... + * model.includePricer(conss); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Pricer Type of the pricer, derived from scip::ObjPricer. + * @tparam Args Types of the additional constructor arguments. + * @param args passed to the constructor of \p Pricer after the %SCIP data structure. + * @return Non-owning pointer to the pricer, or \c nullptr if including failed. + * @attention Must be called before solve(). + * @note Constraints added via addConstr() are not modifiable, i.e., priced variables cannot be added to them. + */ + template + Pricer* includePricer(Args&&... args) const + { + auto* pricer { constructAndInclude(&SCIPincludeObjPricer, std::forward(args)...) }; + if (pricer != nullptr) { + activatePricer(*pricer); + } + return pricer; + } }; } diff --git a/readme.md b/readme.md index 556f90a3..3432e772 100644 --- a/readme.md +++ b/readme.md @@ -103,6 +103,9 @@ int main() * presolvers derived from [`scip::ObjPresol`](https://www.scipopt.org/doc/html/classscip_1_1ObjPresol.php) (`scippp::Model::includePresol`), + * pricers derived from + [`scip::ObjPricer`](https://www.scipopt.org/doc/html/classscip_1_1ObjPricer.php) + (`scippp::Model::includePricer`), * propagators derived from [`scip::ObjProp`](https://www.scipopt.org/doc/html/classscip_1_1ObjProp.php) (`scippp::Model::includeProp`), diff --git a/source/model_objscip.cpp b/source/model_objscip.cpp index 647ee534..fb1aacd0 100644 --- a/source/model_objscip.cpp +++ b/source/model_objscip.cpp @@ -15,4 +15,9 @@ void Model::setMessagehdlr(std::unique_ptr handler) const m_scipCallWrapper(RETCODE); } +void Model::activatePricer(const scip::ObjPricer& pricer) const +{ + m_scipCallWrapper(SCIPactivatePricer(m_scip, SCIPfindPricer(m_scip, pricer.scip_name_))); +} + } diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 7790b069..d4be5252 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -772,6 +773,51 @@ BOOST_AUTO_TEST_CASE(UseIISFinder) BOOST_TEST(iisfinder->getNCalls() == 1); } +/** + * Counts how often it is called for pricing, but never adds a variable. + */ +class CountingPricer : public scip::ObjPricer { + int m_nCalls { 0 }; + +public: + explicit CountingPricer(SCIP* scip) + : scip::ObjPricer( + scip, + "counting", + "counts how often it is called", + 0, // priority + FALSE) // delay + { + } + [[nodiscard]] int getNCalls() const + { + return m_nCalls; + } + SCIP_DECL_PRICERREDCOST(scip_redcost) + override + { + ++m_nCalls; + *result = SCIP_SUCCESS; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UsePricer) +{ + Model model("Simple"); + auto x1 = model.addVar("x_1", 1); + auto x2 = model.addVar("x_2", 1); + model.addConstr(x1 + x2 >= 1, "capacity"); + // otherwise presolving solves the problem and no LP is solved + model.setParam(params::PRESOLVING::MAXROUNDS, 0); + const auto* pricer { model.includePricer() }; + BOOST_REQUIRE(pricer != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(pricer->getNCalls() > 0); + BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); +} + /** * Forwards all messages to a file. */ From 6a82d5d50e590eff6e5b14219e8622b0236df275 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:37:33 +0200 Subject: [PATCH 21/37] Add Model::includeBenders Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 57 +++++++++++++++++++++++++++++++ readme.md | 3 ++ source/model_objscip.cpp | 16 +++++++++ test/test_objscip.cpp | 72 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 149 insertions(+) diff --git a/changelog.md b/changelog.md index 860c2fb8..f6ff7e6b 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [PR44](https://github.com/scipopt/SCIPpp/pull/44), [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Support of ObjSCIP without accessing the raw SCIP object: - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, + - Benders' decompositions derived from `scip::ObjBenders` via `Model::includeBenders`, - branching rules derived from `scip::ObjBranchrule` via `Model::includeBranchrule`, - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, - cut selectors derived from `scip::ObjCutsel` via `Model::includeCutsel`, diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 1b8282d5..d023adc1 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -85,6 +85,15 @@ class Model { */ void activatePricer(const scip::ObjPricer& pricer) const; + /** + * Includes the default Benders' cuts for an included Benders' decomposition and activates it together with the + * constraint handlers enforcing it, so that it is used when solving the model. + * + * @param benders Benders' decomposition to activate. + * @param nSubproblems number of subproblems of the Benders' decomposition. + */ + void activateBenders(const scip::ObjBenders& benders, int nSubproblems) const; + public: /** * Creates an empty problem and sets the optimization goal to Sense::MINIMIZE. @@ -966,5 +975,53 @@ class Model { } return pricer; } + + /** + * Includes and activates a custom Benders' decomposition together with %SCIP's default Benders' cuts. + * + * The Benders' decomposition is constructed by this method as scip::ObjBenders requires the %SCIP data structure + * in its constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjBenders to solve the model as master problem of a Benders' decomposition. The + * decomposition creates the subproblems, solves them, and maps the variables between the master problem and the + * subproblems: + * @code + * class MyBenders : public scip::ObjBenders { + * public: + * MyBenders(SCIP* scip, const std::vector& masterVars); + * ... + * }; + * ... + * auto masterVars = model.addVars("x_", 42); + * model.includeBenders(nSubproblems, masterVars); + * model.solve(); + * @endcode + * + * Activating the Benders' decomposition also activates the constraint handlers \c benders and \c benderslp, which + * enforce it. If \p Benders is not cloneable, copying Benders' decompositions to sub-SCIPs is disabled via the + * parameter \c benders/copybenders, as %SCIP cannot copy it. + * + * @since 1.5.0 + * @tparam Benders Type of the Benders' decomposition, derived from scip::ObjBenders. + * @tparam Args Types of the additional constructor arguments. + * @param nSubproblems number of subproblems of the Benders' decomposition. + * @param args passed to the constructor of \p Benders after the %SCIP data structure. + * @return Non-owning pointer to the Benders' decomposition, or \c nullptr if including failed. + * @attention Must be called before solve(). The priority of \p Benders has to be positive, as %SCIP expects + * active Benders' decompositions to have higher priorities than the inactive default one. + * @attention scip::ObjBenders always provides the callbacks to solve subproblems, so %SCIP does not solve them + * itself. Hence, \p Benders has to implement \c scip_solvesubconvex or \c scip_solvesub. + * @note The default Benders' cuts can be disabled via the parameters + * benders/\/benderscut/\/enabled. + */ + template + Benders* includeBenders(int nSubproblems, Args&&... args) const + { + auto* benders { constructAndInclude(&SCIPincludeObjBenders, std::forward(args)...) }; + if (benders != nullptr) { + activateBenders(*benders, nSubproblems); + } + return benders; + } }; } diff --git a/readme.md b/readme.md index 3432e772..085796a8 100644 --- a/readme.md +++ b/readme.md @@ -73,6 +73,9 @@ int main() [`scip::ObjMessagehdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjMessagehdlr.php) (`scippp::Model::setMessagehdlr`). * Include custom plugins: + * Benders' decompositions derived from + [`scip::ObjBenders`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenders.php) + (`scippp::Model::includeBenders`), * branching rules derived from [`scip::ObjBranchrule`](https://www.scipopt.org/doc/html/classscip_1_1ObjBranchrule.php) (`scippp::Model::includeBranchrule`), diff --git a/source/model_objscip.cpp b/source/model_objscip.cpp index fb1aacd0..6728f038 100644 --- a/source/model_objscip.cpp +++ b/source/model_objscip.cpp @@ -1,5 +1,7 @@ #include "scippp/model.hpp" +#include + namespace scippp { void Model::setMessagehdlr(std::unique_ptr handler) const @@ -20,4 +22,18 @@ void Model::activatePricer(const scip::ObjPricer& pricer) const m_scipCallWrapper(SCIPactivatePricer(m_scip, SCIPfindPricer(m_scip, pricer.scip_name_))); } +void Model::activateBenders(const scip::ObjBenders& benders, int nSubproblems) const +{ + auto* scipBenders { SCIPfindBenders(m_scip, benders.scip_name_) }; + m_scipCallWrapper(SCIPincludeBendersDefaultCuts(m_scip, scipBenders)); + m_scipCallWrapper(SCIPactivateBenders(m_scip, scipBenders, nSubproblems)); + // otherwise the constraint handlers do not enforce the Benders' decomposition + m_scipCallWrapper(SCIPsetBoolParam(m_scip, "constraints/benders/active", TRUE)); + m_scipCallWrapper(SCIPsetBoolParam(m_scip, "constraints/benderslp/active", TRUE)); + if (!benders.iscloneable()) { + // SCIP crashes when copying the model to a sub-SCIP, e.g., in a heuristic, as it expects the copy to succeed + m_scipCallWrapper(SCIPsetBoolParam(m_scip, "benders/copybenders", FALSE)); + } +} + } diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index d4be5252..548aacb6 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -11,6 +11,7 @@ #include "scippp/model.hpp" #include "scippp/parameters.hpp" #include "scippp/solving_statistics.hpp" +#include #include #include #include @@ -818,6 +819,77 @@ BOOST_AUTO_TEST_CASE(UsePricer) BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); } +/** + * Decomposes min x + y s.t. y >= 2 - c x with binary x and continuous y >= 0 into the master problem min x, which is the + * model, and the subproblem min y s.t. y >= 2 - c x, y >= 0, where x is fixed to its value in the master problem. The + * decomposition computes the optimal value max(0, 2 - c x) of the subproblem itself, so there is no SCIP instance for it. + */ +class DemandBenders : public scip::ObjBenders { + SCIP_VAR* m_masterX; + double m_coverage; + int m_nSolves { 0 }; + +public: + DemandBenders(SCIP* scip, SCIP_VAR* masterX, double coverage) + : scip::ObjBenders( + scip, + "demand", + "decomposes the demand problem", + 1, // priority, positive to be higher than the one of the inactive default Benders' decomposition + TRUE, // cutlp + TRUE, // cutpseudo + TRUE, // cutrelax + FALSE) // shareauxvars + , m_masterX(masterX) + , m_coverage(coverage) + { + } + [[nodiscard]] int getNSolves() const + { + return m_nSolves; + } + SCIP_DECL_BENDERSCREATESUB(scip_createsub) + override + { + SCIP_CALL(SCIPaddBendersSubproblem(scip, benders, nullptr)); + // required as the subproblem is solved by this decomposition + SCIPbendersSetSubproblemType(benders, probnumber, SCIP_BENDERSSUBTYPE_CONVEXCONT); + // the smallest optimal value of the subproblem, which is attained for x = 1 + SCIPbendersUpdateSubproblemLowerbound(benders, probnumber, max(0.0, 2.0 - m_coverage)); + return SCIP_OKAY; + } + SCIP_DECL_BENDERSSOLVESUBCONVEX(scip_solvesubconvex) + override + { + ++m_nSolves; + *objective = max(0.0, 2.0 - m_coverage * SCIPgetSolVal(scip, sol, m_masterX)); + *result = SCIP_FEASIBLE; + return SCIP_OKAY; + } + SCIP_DECL_BENDERSGETVAR(scip_getvar) + override + { + // there is no SCIP instance for the subproblem, hence no variables to map + *mappedvar = nullptr; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseBenders) +{ + Model model("Master"); + auto x = model.addVar("x", 1, VarType::BINARY); + // without coverage, the subproblem has the constant optimal value 2, which is its lower bound, i.e., no cut is needed + const auto* benders { model.includeBenders(1, x.getVar(), 0.0) }; + BOOST_REQUIRE(benders != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(model.getStatus() == SCIP_STATUS_OPTIMAL); + BOOST_TEST(benders->getNSolves() > 0); + BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 2.0, boost::test_tools::tolerance(1e-6)); + BOOST_TEST(x.getSolValAsInt(model.getBestSol()) == 0); +} + /** * Forwards all messages to a file. */ From a74e301570019f68db1cfd0b84a5065405ec2300 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:39:51 +0200 Subject: [PATCH 22/37] Add Model::includeBenderscut Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 42 +++++++++++++++++++++++++ readme.md | 3 ++ test/test_objscip.cpp | 68 +++++++++++++++++++++++++++++++++++++--- 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index f6ff7e6b..fdb0445b 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 of ObjSCIP without accessing the raw SCIP object: - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, - Benders' decompositions derived from `scip::ObjBenders` via `Model::includeBenders`, + - Benders' decomposition cuts derived from `scip::ObjBenderscut` via `Model::includeBenderscut`, - branching rules derived from `scip::ObjBranchrule` via `Model::includeBranchrule`, - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, - cut selectors derived from `scip::ObjCutsel` via `Model::includeCutsel`, diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index d023adc1..858ddf01 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -1023,5 +1023,47 @@ class Model { } return benders; } + + /** + * Includes a custom Benders' decomposition cut for a Benders' decomposition. + * + * The cut is constructed by this method as scip::ObjBenderscut requires the %SCIP data structure in its + * constructor. %SCIP takes ownership and deletes it when the model is destructed. + * + * Derive from scip::ObjBenderscut to generate problem-specific cuts from the subproblems of a Benders' + * decomposition: + * @code + * class MyBendersCut : public scip::ObjBenderscut { + * public: + * explicit MyBendersCut(SCIP* scip); + * ... + * }; + * ... + * auto* benders { model.includeBenders(nSubproblems) }; + * model.includeBenderscut(*benders); + * model.solve(); + * @endcode + * + * @since 1.5.0 + * @tparam Benderscut Type of the cut, derived from scip::ObjBenderscut. + * @tparam Args Types of the additional constructor arguments. + * @param benders Benders' decomposition to generate the cuts for, see includeBenders(). + * @param args passed to the constructor of \p Benderscut after the %SCIP data structure. + * @return Non-owning pointer to the cut, or \c nullptr if including failed. + * @attention Must be called before solve(). + */ + template + Benderscut* includeBenderscut(scip::ObjBenders& benders, Args&&... args) const + { + // like constructAndInclude, but the include function additionally requires the Benders' decomposition + static_assert( + std::is_base_of_v, "Benderscut must derive from scip::ObjBenderscut"); + auto cut { std::make_unique(m_scip, std::forward(args)...) }; + const auto RETCODE { SCIPincludeObjBenderscut(m_scip, &benders, cut.get(), TRUE) }; + // SCIP owns the cut only on success, otherwise it is deleted when leaving this method. + Benderscut* result { RETCODE == SCIP_OKAY ? cut.release() : nullptr }; + m_scipCallWrapper(RETCODE); + return result; + } }; } diff --git a/readme.md b/readme.md index 085796a8..18cafbdc 100644 --- a/readme.md +++ b/readme.md @@ -76,6 +76,9 @@ int main() * Benders' decompositions derived from [`scip::ObjBenders`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenders.php) (`scippp::Model::includeBenders`), + * Benders' decomposition cuts derived from + [`scip::ObjBenderscut`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenderscut.php) + (`scippp::Model::includeBenderscut`), * branching rules derived from [`scip::ObjBranchrule`](https://www.scipopt.org/doc/html/classscip_1_1ObjBranchrule.php) (`scippp::Model::includeBranchrule`), diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 548aacb6..5a41a3a6 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -12,6 +12,7 @@ #include "scippp/parameters.hpp" #include "scippp/solving_statistics.hpp" #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include using namespace boost::algorithm; using namespace scippp; @@ -820,9 +822,10 @@ BOOST_AUTO_TEST_CASE(UsePricer) } /** - * Decomposes min x + y s.t. y >= 2 - c x with binary x and continuous y >= 0 into the master problem min x, which is the - * model, and the subproblem min y s.t. y >= 2 - c x, y >= 0, where x is fixed to its value in the master problem. The - * decomposition computes the optimal value max(0, 2 - c x) of the subproblem itself, so there is no SCIP instance for it. + * Decomposes min x + y s.t. y >= 2 - c x with binary x and continuous y >= 0 into the master problem min x, which is + * the model, and the subproblem min y s.t. y >= 2 - c x, y >= 0, where x is fixed to its value in the master problem. + * The decomposition computes the optimal value max(0, 2 - c x) of the subproblem itself, so there is no SCIP instance + * for it. */ class DemandBenders : public scip::ObjBenders { SCIP_VAR* m_masterX; @@ -879,7 +882,7 @@ BOOST_AUTO_TEST_CASE(UseBenders) { Model model("Master"); auto x = model.addVar("x", 1, VarType::BINARY); - // without coverage, the subproblem has the constant optimal value 2, which is its lower bound, i.e., no cut is needed + // without coverage, the subproblem has the constant optimal value 2, which is its lower bound, so no cut is needed const auto* benders { model.includeBenders(1, x.getVar(), 0.0) }; BOOST_REQUIRE(benders != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); @@ -890,6 +893,63 @@ BOOST_AUTO_TEST_CASE(UseBenders) BOOST_TEST(x.getSolValAsInt(model.getBestSol()) == 0); } +/** + * Adds the optimality cut theta + c x >= 2 for the subproblem of DemandBenders, where theta is the auxiliary variable + * of the subproblem in the master problem. + */ +class DemandBenderscut : public scip::ObjBenderscut { + SCIP_VAR* m_masterX; + double m_coverage; + int m_nCuts { 0 }; + +public: + DemandBenderscut(SCIP* scip, SCIP_VAR* masterX, double coverage) + : scip::ObjBenderscut( + scip, + "demand", + "adds the optimality cut of the demand subproblem", + 1000000, // priority, higher than the ones of the default Benders' cuts to be called first + TRUE) // islpcut + , m_masterX(masterX) + , m_coverage(coverage) + { + } + [[nodiscard]] int getNCuts() const + { + return m_nCuts; + } + SCIP_DECL_BENDERSCUTEXEC(scip_exec) + override + { + SCIP_CONS* cons { nullptr }; + SCIP_CALL(SCIPcreateConsBasicLinear(scip, &cons, "demand", 0, nullptr, nullptr, 2.0, SCIPinfinity(scip))); + SCIP_CALL(SCIPaddCoefLinear(scip, cons, SCIPbendersGetAuxiliaryVar(benders, probnumber), 1.0)); + SCIP_CALL(SCIPaddCoefLinear(scip, cons, SCIPvarGetTransVar(m_masterX), m_coverage)); + SCIP_CALL(SCIPaddCons(scip, cons)); + SCIP_CALL(SCIPreleaseCons(scip, &cons)); + ++m_nCuts; + *result = SCIP_CONSADDED; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(UseBendersCut) +{ + Model model("Master"); + auto x = model.addVar("x", 1, VarType::BINARY); + // x = 1 costs 1 in the master problem, whereas x = 0 costs 2 in the subproblem + auto* benders { model.includeBenders(1, x.getVar(), 2.0) }; + BOOST_REQUIRE(benders != nullptr); + const auto* cut { model.includeBenderscut(*benders, x.getVar(), 2.0) }; + BOOST_REQUIRE(cut != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.solve(); + BOOST_TEST(model.getStatus() == SCIP_STATUS_OPTIMAL); + BOOST_TEST(cut->getNCuts() > 0); + BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1.0, boost::test_tools::tolerance(1e-6)); + BOOST_TEST(x.getSolValAsInt(model.getBestSol()) == 1); +} + /** * Forwards all messages to a file. */ From 19255439bf1e8ac88b1d9ef4fc222f8c75631552 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:42:30 +0200 Subject: [PATCH 23/37] Add Model constructor with custom problem data Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 3 ++- include/scippp/model.hpp | 31 +++++++++++++++++++++++++++++++ readme.md | 10 ++++++---- source/model.cpp | 7 ++++++- source/model_objscip.cpp | 16 ++++++++++++++++ test/test_objscip.cpp | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index fdb0445b..c8c9991b 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [PR44](https://github.com/scipopt/SCIPpp/pull/44), [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Support of ObjSCIP without accessing the raw SCIP object: - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, + - problem data derived from `scip::ObjProbData` via a new constructor of `Model`, - Benders' decompositions derived from `scip::ObjBenders` via `Model::includeBenders`, - Benders' decomposition cuts derived from `scip::ObjBenderscut` via `Model::includeBenderscut`, - branching rules derived from `scip::ObjBranchrule` via `Model::includeBranchrule`, @@ -18,10 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, - file readers derived from `scip::ObjReader` via `Model::includeReader`, - IIS finders derived from `scip::ObjIISfinder` via `Model::includeIISfinder`, - - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, - pricers derived from `scip::ObjPricer` via `Model::includePricer`, + - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - propagators derived from `scip::ObjProp` via `Model::includeProp`, - relaxators derived from `scip::ObjRelax` via `Model::includeRelax`, - separators derived from `scip::ObjSepa` via `Model::includeSepa`, and diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 858ddf01..b6b7d08f 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -94,6 +94,14 @@ class Model { */ void activateBenders(const scip::ObjBenders& benders, int nSubproblems) const; + /** + * Prepares the %SCIP data structure without creating a problem in it. + * + * @param scip to use. If \c nullptr, a new %SCIP data structure will be created. + * @param includeDefaultPlugins if \c true, the default plugins are added to \p scip. + */ + Model(SCIP* scip, bool includeDefaultPlugins); + public: /** * Creates an empty problem and sets the optimization goal to Sense::MINIMIZE. @@ -122,6 +130,29 @@ class Model { */ explicit Model(const std::string& name, SCIP* scip = nullptr, bool includeDefaultPlugins = true); + /** + * Creates an empty problem with custom problem data and sets the optimization goal to Sense::MINIMIZE. + * + * Derive from scip::ObjProbData to store problem-specific data, which plugins can access via SCIPgetObjProbData: + * @code + * class MyProblemData : public scip::ObjProbData { ... }; + * ... + * Model model("Problem", std::make_unique()); + * @endcode + * + * @since 1.5.0 + * @param name for the problem. + * @param probData to store in the problem, must not be \c nullptr. %SCIP takes ownership and deletes it when the + * problem is freed. + * @param scip to create the problem in. If \c nullptr, a new %SCIP data structure will be created. + * @param includeDefaultPlugins if \c true, the default plugins are added to \p scip. + */ + Model( + const std::string& name, + std::unique_ptr probData, + SCIP* scip = nullptr, + bool includeDefaultPlugins = true); + /** * Releases the variables and constraints. * @since 1.0.0 diff --git a/readme.md b/readme.md index 18cafbdc..6061e04f 100644 --- a/readme.md +++ b/readme.md @@ -53,7 +53,9 @@ int main() ## Features -* Create a model with a new SCIP data structure or with an existing one, with or without SCIP's default plugins. +* Create a model with a new SCIP data structure or with an existing one, with or without SCIP's default plugins, and + optionally with custom problem data derived from + [`scip::ObjProbData`](https://www.scipopt.org/doc/html/classscip_1_1ObjProbData.php) (`scippp::Model`). * Add variables one at a time, as a vector, or as an array for structured bindings (`scippp::Model::addVar`, `scippp::Model::addVars`). The objective coefficients can be given by any object providing an index operator. * Build linear expressions (`scippp::LinExpr`) and add linear inequalities and equations as constraints @@ -100,9 +102,6 @@ int main() * IIS finders derived from [`scip::ObjIISfinder`](https://www.scipopt.org/doc/html/classscip_1_1ObjIISfinder.php) (`scippp::Model::includeIISfinder`), - * primal heuristics derived from - [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) - (`scippp::Model::includeHeur`), * node selectors derived from [`scip::ObjNodesel`](https://www.scipopt.org/doc/html/classscip_1_1ObjNodesel.php) (`scippp::Model::includeNodesel`), @@ -112,6 +111,9 @@ int main() * pricers derived from [`scip::ObjPricer`](https://www.scipopt.org/doc/html/classscip_1_1ObjPricer.php) (`scippp::Model::includePricer`), + * primal heuristics derived from + [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) + (`scippp::Model::includeHeur`), * propagators derived from [`scip::ObjProp`](https://www.scipopt.org/doc/html/classscip_1_1ObjProp.php) (`scippp::Model::includeProp`), diff --git a/source/model.cpp b/source/model.cpp index 5f29b308..4769c7ff 100644 --- a/source/model.cpp +++ b/source/model.cpp @@ -28,7 +28,7 @@ Var& Model::addVar( return m_vars.back(); } -Model::Model(const std::string& name, SCIP* scip, bool withDefaultPlugins) +Model::Model(SCIP* scip, bool withDefaultPlugins) { m_scipCallWrapper = [this](SCIP_Retcode r) { m_lastReturnCode = r; }; if (scip) { @@ -40,6 +40,11 @@ Model::Model(const std::string& name, SCIP* scip, bool withDefaultPlugins) if (withDefaultPlugins) { m_scipCallWrapper(SCIPincludeDefaultPlugins(m_scip)); } +} + +Model::Model(const std::string& name, SCIP* scip, bool withDefaultPlugins) + : Model(scip, withDefaultPlugins) +{ m_scipCallWrapper(SCIPcreateProbBasic(m_scip, name.c_str())); } diff --git a/source/model_objscip.cpp b/source/model_objscip.cpp index 6728f038..93b6d2fd 100644 --- a/source/model_objscip.cpp +++ b/source/model_objscip.cpp @@ -4,6 +4,22 @@ namespace scippp { +Model::Model( + const std::string& name, + std::unique_ptr probData, + SCIP* scip, + bool withDefaultPlugins) + : Model(scip, withDefaultPlugins) +{ + assert(probData); // GCOVR_EXCL_LINE + const auto RETCODE { SCIPcreateObjProb(m_scip, name.c_str(), probData.get(), TRUE) }; + // SCIP owns the problem data only on success, otherwise it is deleted when leaving this constructor. + if (RETCODE == SCIP_OKAY) { + probData.release(); + } + m_scipCallWrapper(RETCODE); +} + void Model::setMessagehdlr(std::unique_ptr handler) const { assert(handler); // GCOVR_EXCL_LINE diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 5a41a3a6..9f5f6257 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -950,6 +951,40 @@ BOOST_AUTO_TEST_CASE(UseBendersCut) BOOST_TEST(x.getSolValAsInt(model.getBestSol()) == 1); } +/** + * Tracks whether it was deleted. + */ +class TrackingProbData : public scip::ObjProbData { + bool& m_deleted; + +public: + explicit TrackingProbData(bool& deleted) + : m_deleted(deleted) + { + } + ~TrackingProbData() override + { + m_deleted = true; + } +}; + +BOOST_AUTO_TEST_CASE(UseProblemData) +{ + bool deleted { false }; + { + Model model("Simple", make_unique(deleted)); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.addVar("x", 1); + model.solve(); + BOOST_TEST(model.getStatus() == SCIP_STATUS_OPTIMAL); + // plugins access the problem data via SCIP + BOOST_TEST(dynamic_cast(SCIPgetObjProbData(model.scip())) != nullptr); + BOOST_TEST(!deleted); + } + // SCIP deletes the problem data when the model is destructed + BOOST_TEST(deleted); +} + /** * Forwards all messages to a file. */ From 34a569e0734c5520d23d9fb5cc797105fbd8a868 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:44:11 +0200 Subject: [PATCH 24/37] Add Model::addVar with custom variable data Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 1 + include/scippp/model.hpp | 28 ++++++++++++++++++++++++++++ readme.md | 4 +++- source/model_objscip.cpp | 33 +++++++++++++++++++++++++++++++++ test/test_objscip.cpp | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index c8c9991b..4f162e5a 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 of ObjSCIP without accessing the raw SCIP object: - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, - problem data derived from `scip::ObjProbData` via a new constructor of `Model`, + - variable data derived from `scip::ObjVardata` via a new overload of `Model::addVar`, - Benders' decompositions derived from `scip::ObjBenders` via `Model::includeBenders`, - Benders' decomposition cuts derived from `scip::ObjBenderscut` via `Model::includeBenderscut`, - branching rules derived from `scip::ObjBranchrule` via `Model::includeBranchrule`, diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index b6b7d08f..abe507c0 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -190,6 +190,34 @@ class Model { std::optional lb = 0.0, std::optional ub = 1.0); + /** + * Adds a variable with custom variable data to the model. + * + * Derive from scip::ObjVardata to store variable-specific data, which plugins can access via SCIPgetObjVardata: + * @code + * class MyVariableData : public scip::ObjVardata { ... }; + * ... + * auto& x = model.addVar("x", std::make_unique(), 1.0, VarType::BINARY); + * @endcode + * + * @since 1.5.0 + * @param name of the variable when the model is written. + * @param vardata to store in the variable, must not be \c nullptr. %SCIP takes ownership and deletes it when the + * variable is freed. + * @param coeff Coefficient in the objective function. + * @param varType variable type. + * @param lb lower bound. \c std::nullopt is interpreted as -infinity. + * @param ub upper bound. \c std::nullopt is interpreted as infinity. + * @return Reference to the newly created variable. + */ + Var& addVar( + const std::string& name, + std::unique_ptr vardata, + SCIP_Real coeff = 0.0, + VarType varType = VarType::CONTINUOUS, + std::optional lb = 0.0, + std::optional ub = 1.0); + /** * Adds multiple variables to the model. * diff --git a/readme.md b/readme.md index 6061e04f..bf5a7291 100644 --- a/readme.md +++ b/readme.md @@ -57,7 +57,9 @@ int main() optionally with custom problem data derived from [`scip::ObjProbData`](https://www.scipopt.org/doc/html/classscip_1_1ObjProbData.php) (`scippp::Model`). * Add variables one at a time, as a vector, or as an array for structured bindings (`scippp::Model::addVar`, - `scippp::Model::addVars`). The objective coefficients can be given by any object providing an index operator. + `scippp::Model::addVars`). The objective coefficients can be given by any object providing an index operator. A + single variable can carry custom variable data derived from + [`scip::ObjVardata`](https://www.scipopt.org/doc/html/classscip_1_1ObjVardata.php). * Build linear expressions (`scippp::LinExpr`) and add linear inequalities and equations as constraints (`scippp::Model::addConstr`). * Set the optimization goal (`scippp::Model::setObjsense`) and all SCIP parameters in a type-safe way diff --git a/source/model_objscip.cpp b/source/model_objscip.cpp index 93b6d2fd..8715902d 100644 --- a/source/model_objscip.cpp +++ b/source/model_objscip.cpp @@ -20,6 +20,39 @@ Model::Model( m_scipCallWrapper(RETCODE); } +Var& Model::addVar( + const std::string& name, + std::unique_ptr vardata, + SCIP_Real coeff, + VarType varType, + std::optional lb, + std::optional ub) +{ + assert(vardata); // GCOVR_EXCL_LINE + SCIP_VAR* var { nullptr }; + const auto RETCODE { SCIPcreateObjVar( + m_scip, /* SCIP environment */ + &var, /* reference to the variable */ + name.c_str(), /* name of the variable */ + lb != std::nullopt ? lb.value() : -SCIPinfinity(m_scip), /* lower bound of the variable */ + ub != std::nullopt ? ub.value() : SCIPinfinity(m_scip), /* upper bound of the variable */ + coeff, /* obj. coefficient. */ + static_cast(varType), /* variable type */ + TRUE, /* initial */ + FALSE, /* removable */ + vardata.get(), /* variable data */ + TRUE /* delete the variable data when the variable is freed */ + ) }; + // SCIP owns the variable data only on success, otherwise it is deleted when leaving this method. + if (RETCODE == SCIP_OKAY) { + vardata.release(); + } + m_scipCallWrapper(RETCODE); + m_scipCallWrapper(SCIPaddVar(m_scip, var)); + m_vars.emplace_back(Var { var }); + return m_vars.back(); +} + void Model::setMessagehdlr(std::unique_ptr handler) const { assert(handler); // GCOVR_EXCL_LINE diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 9f5f6257..9fd00011 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include using namespace boost::algorithm; @@ -985,6 +986,42 @@ BOOST_AUTO_TEST_CASE(UseProblemData) BOOST_TEST(deleted); } +/** + * Tracks whether it was deleted. + */ +class TrackingVardata : public scip::ObjVardata { + bool& m_deleted; + +public: + explicit TrackingVardata(bool& deleted) + : m_deleted(deleted) + { + } + ~TrackingVardata() override + { + m_deleted = true; + } +}; + +BOOST_AUTO_TEST_CASE(UseVariableData) +{ + bool deleted { false }; + { + Model model("Simple"); + auto& x = model.addVar("x", make_unique(deleted), 1, VarType::BINARY); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + model.addConstr(x >= 1, "lower"); + model.solve(); + BOOST_TEST(model.getStatus() == SCIP_STATUS_OPTIMAL); + BOOST_TEST(x.getSolValAsInt(model.getBestSol()) == 1); + // plugins access the variable data via SCIP + BOOST_TEST(dynamic_cast(SCIPgetObjVardata(model.scip(), x.getVar())) != nullptr); + BOOST_TEST(!deleted); + } + // SCIP deletes the variable data when the model is destructed + BOOST_TEST(deleted); +} + /** * Forwards all messages to a file. */ From 21620ffd7486f63e3bec3c0f7a9adb7a4c3ccb6e Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:56:16 +0200 Subject: [PATCH 25/37] List all supported ObjSCIP features in one list Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 10 +++++----- readme.md | 26 ++++++++++++++------------ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/changelog.md b/changelog.md index 4f162e5a..a1b42fde 100644 --- a/changelog.md +++ b/changelog.md @@ -8,9 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [PR44](https://github.com/scipopt/SCIPpp/pull/44), [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Support of ObjSCIP without accessing the raw SCIP object: - - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, - - problem data derived from `scip::ObjProbData` via a new constructor of `Model`, - - variable data derived from `scip::ObjVardata` via a new overload of `Model::addVar`, - Benders' decompositions derived from `scip::ObjBenders` via `Model::includeBenders`, - Benders' decomposition cuts derived from `scip::ObjBenderscut` via `Model::includeBenderscut`, - branching rules derived from `scip::ObjBranchrule` via `Model::includeBranchrule`, @@ -20,14 +17,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, - file readers derived from `scip::ObjReader` via `Model::includeReader`, - IIS finders derived from `scip::ObjIISfinder` via `Model::includeIISfinder`, + - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, - pricers derived from `scip::ObjPricer` via `Model::includePricer`, - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, + - problem data derived from `scip::ObjProbData` via a new constructor of `Model`, - propagators derived from `scip::ObjProp` via `Model::includeProp`, - relaxators derived from `scip::ObjRelax` via `Model::includeRelax`, - - separators derived from `scip::ObjSepa` via `Model::includeSepa`, and - - statistics tables derived from `scip::ObjTable` via `Model::includeTable`. + - separators derived from `scip::ObjSepa` via `Model::includeSepa`, + - statistics tables derived from `scip::ObjTable` via `Model::includeTable`, and + - variable data derived from `scip::ObjVardata` via a new overload of `Model::addVar`. ### Changed diff --git a/readme.md b/readme.md index bf5a7291..7cf75c39 100644 --- a/readme.md +++ b/readme.md @@ -53,13 +53,9 @@ int main() ## Features -* Create a model with a new SCIP data structure or with an existing one, with or without SCIP's default plugins, and - optionally with custom problem data derived from - [`scip::ObjProbData`](https://www.scipopt.org/doc/html/classscip_1_1ObjProbData.php) (`scippp::Model`). +* Create a model with a new SCIP data structure or with an existing one, with or without SCIP's default plugins. * Add variables one at a time, as a vector, or as an array for structured bindings (`scippp::Model::addVar`, - `scippp::Model::addVars`). The objective coefficients can be given by any object providing an index operator. A - single variable can carry custom variable data derived from - [`scip::ObjVardata`](https://www.scipopt.org/doc/html/classscip_1_1ObjVardata.php). + `scippp::Model::addVars`). The objective coefficients can be given by any object providing an index operator. * Build linear expressions (`scippp::LinExpr`) and add linear inequalities and equations as constraints (`scippp::Model::addConstr`). * Set the optimization goal (`scippp::Model::setObjsense`) and all SCIP parameters in a type-safe way @@ -73,10 +69,7 @@ int main() * Write the original problem to a file or to standard output (`scippp::Model::writeOrigProblem`). * Use SCIP's numerics (`scippp::Model::epsilon`, `scippp::Model::round`, `scippp::Model::isZero`, `scippp::Model::infinity`). -* Install custom message handlers derived from - [`scip::ObjMessagehdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjMessagehdlr.php) - (`scippp::Model::setMessagehdlr`). -* Include custom plugins: +* Use ObjSCIP without accessing the raw SCIP object: * Benders' decompositions derived from [`scip::ObjBenders`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenders.php) (`scippp::Model::includeBenders`), @@ -104,6 +97,9 @@ int main() * IIS finders derived from [`scip::ObjIISfinder`](https://www.scipopt.org/doc/html/classscip_1_1ObjIISfinder.php) (`scippp::Model::includeIISfinder`), + * message handlers derived from + [`scip::ObjMessagehdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjMessagehdlr.php) + (`scippp::Model::setMessagehdlr`), * node selectors derived from [`scip::ObjNodesel`](https://www.scipopt.org/doc/html/classscip_1_1ObjNodesel.php) (`scippp::Model::includeNodesel`), @@ -116,6 +112,9 @@ int main() * primal heuristics derived from [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) (`scippp::Model::includeHeur`), + * problem data derived from + [`scip::ObjProbData`](https://www.scipopt.org/doc/html/classscip_1_1ObjProbData.php) + (`scippp::Model`), * propagators derived from [`scip::ObjProp`](https://www.scipopt.org/doc/html/classscip_1_1ObjProp.php) (`scippp::Model::includeProp`), @@ -124,10 +123,13 @@ int main() (`scippp::Model::includeRelax`), * separators derived from [`scip::ObjSepa`](https://www.scipopt.org/doc/html/classscip_1_1ObjSepa.php) - (`scippp::Model::includeSepa`), and + (`scippp::Model::includeSepa`), * statistics tables derived from [`scip::ObjTable`](https://www.scipopt.org/doc/html/classscip_1_1ObjTable.php) - (`scippp::Model::includeTable`). + (`scippp::Model::includeTable`), and + * variable data derived from + [`scip::ObjVardata`](https://www.scipopt.org/doc/html/classscip_1_1ObjVardata.php) + (`scippp::Model::addVar`). * Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). ## Build From 8034824e3da4edba8f9f304ef1bed9e09e5ee217 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 14:56:42 +0200 Subject: [PATCH 26/37] Format test_objscip.cpp with clang-format 17 Co-Authored-By: Claude Opus 5.5 (1M context) --- test/test_objscip.cpp | 210 +++++++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 9fd00011..90a9e53d 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -122,21 +122,21 @@ class ZeroVarConshdlr : public scip::ObjConshdlr { public: ZeroVarConshdlr(SCIP* scip, SCIP_VAR* var) : scip::ObjConshdlr( - scip, - "zerovar", - "enforces a variable to be zero", - 0, // sepapriority - -1, // enfopriority, negative to be called for integral solutions only - -1, // checkpriority - -1, // sepafreq - -1, // propfreq - -1, // eagerfreq - 0, // maxprerounds - FALSE, // delaysepa - FALSE, // delayprop - FALSE, // needscons - SCIP_PROPTIMING_BEFORELP, - SCIP_PRESOLTIMING_FAST) + scip, + "zerovar", + "enforces a variable to be zero", + 0, // sepapriority + -1, // enfopriority, negative to be called for integral solutions only + -1, // checkpriority + -1, // sepafreq + -1, // propfreq + -1, // eagerfreq + 0, // maxprerounds + FALSE, // delaysepa + FALSE, // delayprop + FALSE, // needscons + SCIP_PROPTIMING_BEFORELP, + SCIP_PRESOLTIMING_FAST) , m_var(var) { } @@ -194,16 +194,16 @@ class OneVarHeur : public scip::ObjHeur { public: OneVarHeur(SCIP* scip, SCIP_VAR* var) : scip::ObjHeur( - scip, - "onevar", - "proposes a solution where a given binary variable is one", - 'O', // dispchar - 20000, // priority, higher than the one of the trivial heuristic to be called first - 1, // freq - 0, // freqofs - -1, // maxdepth - SCIP_HEURTIMING_BEFOREPRESOL, - FALSE) // usessubscip + scip, + "onevar", + "proposes a solution where a given binary variable is one", + 'O', // dispchar + 20000, // priority, higher than the one of the trivial heuristic to be called first + 1, // freq + 0, // freqofs + -1, // maxdepth + SCIP_HEURTIMING_BEFOREPRESOL, + FALSE) // usessubscip , m_var(var) { } @@ -254,12 +254,12 @@ class ZeroVarPresol : public scip::ObjPresol { public: ZeroVarPresol(SCIP* scip, SCIP_VAR* var) : scip::ObjPresol( - scip, - "zerovar", - "fixes a variable to zero", - 10000000, // priority, higher than the ones of the default presolvers to be called first - -1, // maxrounds - SCIP_PRESOLTIMING_FAST) + scip, + "zerovar", + "fixes a variable to zero", + 10000000, // priority, higher than the ones of the default presolvers to be called first + -1, // maxrounds + SCIP_PRESOLTIMING_FAST) , m_var(var) { } @@ -306,16 +306,16 @@ class CountingProp : public scip::ObjProp { public: explicit CountingProp(SCIP* scip) : scip::ObjProp( - scip, - "counting", - "counts how often it is called", - 0, // priority - 1, // freq - FALSE, // delay - SCIP_PROPTIMING_BEFORELP, - 0, // presolpriority - -1, // presolmaxrounds - SCIP_PRESOLTIMING_NONE) + scip, + "counting", + "counts how often it is called", + 0, // priority + 1, // freq + FALSE, // delay + SCIP_PROPTIMING_BEFORELP, + 0, // presolpriority + -1, // presolmaxrounds + SCIP_PRESOLTIMING_NONE) { } [[nodiscard]] int getNCalls() const @@ -373,14 +373,14 @@ class CapacitySepa : public scip::ObjSepa { public: CapacitySepa(SCIP* scip, SCIP_VAR* x1, SCIP_VAR* x2) : scip::ObjSepa( - scip, - "capacity", - "separates x_1 + x_2 <= 1", - 1000000, // priority, higher than the ones of the default separators to be called first - 0, // freq - 1.0, // maxbounddist - FALSE, // usessubscip - FALSE) // delay + scip, + "capacity", + "separates x_1 + x_2 <= 1", + 1000000, // priority, higher than the ones of the default separators to be called first + 0, // freq + 1.0, // maxbounddist + FALSE, // usessubscip + FALSE) // delay , m_vars { x1, x2 } { } @@ -427,10 +427,10 @@ class AllCutsel : public scip::ObjCutsel { public: explicit AllCutsel(SCIP* scip) : scip::ObjCutsel( - scip, - "all", - "selects all cuts", - 1000000) // priority, higher than the ones of the default cut selectors to be used + scip, + "all", + "selects all cuts", + 1000000) // priority, higher than the ones of the default cut selectors to be used { } [[nodiscard]] int getNCalls() const @@ -468,12 +468,12 @@ class FirstFracBranchrule : public scip::ObjBranchrule { public: explicit FirstFracBranchrule(SCIP* scip) : scip::ObjBranchrule( - scip, - "firstfrac", - "branches on the first fractional variable", - 1000000, // priority, higher than the ones of the default branching rules to be used - -1, // maxdepth - 1.0) // maxbounddist + scip, + "firstfrac", + "branches on the first fractional variable", + 1000000, // priority, higher than the ones of the default branching rules to be used + -1, // maxdepth + 1.0) // maxbounddist { } [[nodiscard]] int getNCalls() const @@ -520,11 +520,11 @@ class DepthFirstNodesel : public scip::ObjNodesel { public: explicit DepthFirstNodesel(SCIP* scip) : scip::ObjNodesel( - scip, - "depthfirst", - "selects nodes in depth-first order", - 1000000, // stdpriority, higher than the ones of the default node selectors to be used - 0) // memsavepriority + scip, + "depthfirst", + "selects nodes in depth-first order", + 1000000, // stdpriority, higher than the ones of the default node selectors to be used + 0) // memsavepriority { } [[nodiscard]] int getNCalls() const @@ -575,12 +575,12 @@ class CountingRelax : public scip::ObjRelax { public: explicit CountingRelax(SCIP* scip) : scip::ObjRelax( - scip, - "counting", - "counts how often it is called", - 0, // priority, non-negative to be called before the LP relaxation is solved - 1, // freq - FALSE) // includeslp + scip, + "counting", + "counts how often it is called", + 0, // priority, non-negative to be called before the LP relaxation is solved + 1, // freq + FALSE) // includeslp { } [[nodiscard]] int getNCalls() const @@ -616,14 +616,14 @@ class CountingDisp : public scip::ObjDisp { public: explicit CountingDisp(SCIP* scip) : scip::ObjDisp( - scip, - "counting", - "displays how often it was displayed", - "calls", // header - 5, // width - 1000000, // priority, higher than the ones of the default display columns to be displayed - 100000, // position, higher than the ones of the default display columns to be the last column - TRUE) // stripline + scip, + "counting", + "displays how often it was displayed", + "calls", // header + 5, // width + 1000000, // priority, higher than the ones of the default display columns to be displayed + 100000, // position, higher than the ones of the default display columns to be the last column + TRUE) // stripline { } [[nodiscard]] int getNCalls() const @@ -660,11 +660,11 @@ class CountingTable : public scip::ObjTable { public: explicit CountingTable(SCIP* scip) : scip::ObjTable( - scip, - "counting", - "prints how often it was printed", - 100000, // position, higher than the ones of the default statistics tables to be the last table - SCIP_STAGE_TRANSFORMED) // earlieststage + scip, + "counting", + "prints how often it was printed", + 100000, // position, higher than the ones of the default statistics tables to be the last table + SCIP_STAGE_TRANSFORMED) // earlieststage { } [[nodiscard]] int getNCalls() const @@ -742,10 +742,10 @@ class CountingIISfinder : public scip::ObjIISfinder { public: explicit CountingIISfinder(SCIP* scip) : scip::ObjIISfinder( - scip, - "counting", - "counts how often it is called", - 1000000) // priority, higher than the ones of the default IIS finders to be called first + scip, + "counting", + "counts how often it is called", + 1000000) // priority, higher than the ones of the default IIS finders to be called first { } [[nodiscard]] int getNCalls() const @@ -787,11 +787,11 @@ class CountingPricer : public scip::ObjPricer { public: explicit CountingPricer(SCIP* scip) : scip::ObjPricer( - scip, - "counting", - "counts how often it is called", - 0, // priority - FALSE) // delay + scip, + "counting", + "counts how often it is called", + 0, // priority + FALSE) // delay { } [[nodiscard]] int getNCalls() const @@ -837,14 +837,14 @@ class DemandBenders : public scip::ObjBenders { public: DemandBenders(SCIP* scip, SCIP_VAR* masterX, double coverage) : scip::ObjBenders( - scip, - "demand", - "decomposes the demand problem", - 1, // priority, positive to be higher than the one of the inactive default Benders' decomposition - TRUE, // cutlp - TRUE, // cutpseudo - TRUE, // cutrelax - FALSE) // shareauxvars + scip, + "demand", + "decomposes the demand problem", + 1, // priority, positive to be higher than the one of the inactive default Benders' decomposition + TRUE, // cutlp + TRUE, // cutpseudo + TRUE, // cutrelax + FALSE) // shareauxvars , m_masterX(masterX) , m_coverage(coverage) { @@ -907,11 +907,11 @@ class DemandBenderscut : public scip::ObjBenderscut { public: DemandBenderscut(SCIP* scip, SCIP_VAR* masterX, double coverage) : scip::ObjBenderscut( - scip, - "demand", - "adds the optimality cut of the demand subproblem", - 1000000, // priority, higher than the ones of the default Benders' cuts to be called first - TRUE) // islpcut + scip, + "demand", + "adds the optimality cut of the demand subproblem", + 1000000, // priority, higher than the ones of the default Benders' cuts to be called first + TRUE) // islpcut , m_masterX(masterX) , m_coverage(coverage) { From aed5e423665c93d08a20f9fc358e005a16a4124c Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Thu, 24 Sep 2026 15:41:41 +0200 Subject: [PATCH 27/37] Omit ObjTable support due to scipopt/scip#222 and cover ObjSCIP failure branches Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.md | 3 +- include/scippp/model.hpp | 33 -------- readme.md | 10 ++- test/test_objscip.cpp | 163 ++++++++++++++++++++++++++++----------- 4 files changed, 124 insertions(+), 85 deletions(-) diff --git a/changelog.md b/changelog.md index a1b42fde..8211ce40 100644 --- a/changelog.md +++ b/changelog.md @@ -25,8 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - problem data derived from `scip::ObjProbData` via a new constructor of `Model`, - propagators derived from `scip::ObjProp` via `Model::includeProp`, - relaxators derived from `scip::ObjRelax` via `Model::includeRelax`, - - separators derived from `scip::ObjSepa` via `Model::includeSepa`, - - statistics tables derived from `scip::ObjTable` via `Model::includeTable`, and + - separators derived from `scip::ObjSepa` via `Model::includeSepa`, and - variable data derived from `scip::ObjVardata` via a new overload of `Model::addVar`. ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index abe507c0..f7178fe8 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -6,7 +6,6 @@ #include #include #include -#include // not included by objscip.h #include #include #include @@ -903,38 +902,6 @@ class Model { return constructAndInclude(&SCIPincludeObjDisp, std::forward(args)...); } - /** - * Includes a custom statistics table. - * - * The statistics table is constructed by this method as scip::ObjTable requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjTable to add problem-specific information to the statistics %SCIP prints: - * @code - * class MyStatisticsTable : public scip::ObjTable { - * public: - * explicit MyStatisticsTable(SCIP* scip); - * ... - * }; - * ... - * model.includeTable(); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Table Type of the statistics table, derived from scip::ObjTable. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Table after the %SCIP data structure. - * @return Non-owning pointer to the statistics table, or \c nullptr if including failed. - * @attention Must be called before solve(). - * @note SCIP++ does not print statistics, this requires %SCIP's %C %API, e.g., SCIPprintStatistics. - */ - template - Table* includeTable(Args&&... args) const - { - return constructAndInclude
(&SCIPincludeObjTable, std::forward(args)...); - } - /** * Includes a custom file reader. * diff --git a/readme.md b/readme.md index 7cf75c39..a32f9b85 100644 --- a/readme.md +++ b/readme.md @@ -123,13 +123,15 @@ int main() (`scippp::Model::includeRelax`), * separators derived from [`scip::ObjSepa`](https://www.scipopt.org/doc/html/classscip_1_1ObjSepa.php) - (`scippp::Model::includeSepa`), - * statistics tables derived from - [`scip::ObjTable`](https://www.scipopt.org/doc/html/classscip_1_1ObjTable.php) - (`scippp::Model::includeTable`), and + (`scippp::Model::includeSepa`), and * variable data derived from [`scip::ObjVardata`](https://www.scipopt.org/doc/html/classscip_1_1ObjVardata.php) (`scippp::Model::addVar`). + + Statistics tables derived from + [`scip::ObjTable`](https://www.scipopt.org/doc/html/classscip_1_1ObjTable.php) + are not supported, as SCIP does not export `scip::ObjTable` from its shared library, see + [SCIP issue 222](https://github.com/scipopt/scip/issues/222). * Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). ## Build diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 90a9e53d..7da7d8f6 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -3,10 +3,10 @@ #include #include #include -#include #include #include #include +#include #include "scippp/model.hpp" #include "scippp/parameters.hpp" @@ -29,7 +29,6 @@ #include #include #include -#include #include #include @@ -651,50 +650,6 @@ BOOST_AUTO_TEST_CASE(UseDisplayColumn) BOOST_TEST(disp->getNCalls() > 0); } -/** - * Prints how often it was printed. - */ -class CountingTable : public scip::ObjTable { - int m_nCalls { 0 }; - -public: - explicit CountingTable(SCIP* scip) - : scip::ObjTable( - scip, - "counting", - "prints how often it was printed", - 100000, // position, higher than the ones of the default statistics tables to be the last table - SCIP_STAGE_TRANSFORMED) // earlieststage - { - } - [[nodiscard]] int getNCalls() const - { - return m_nCalls; - } - SCIP_DECL_TABLEOUTPUT(scip_output) - override - { - ++m_nCalls; - SCIPinfoMessage(scip, file, "Counting : %d\n", m_nCalls); - return SCIP_OKAY; - } -}; - -BOOST_AUTO_TEST_CASE(UseStatisticsTable) -{ - Model model("Simple"); - const auto* table { model.includeTable() }; - BOOST_REQUIRE(table != nullptr); - BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); - model.solve(); - // SCIP++ does not print statistics, so we have to use the raw SCIP object - auto* file { tmpfile() }; - BOOST_REQUIRE(file != nullptr); - BOOST_TEST(SCIPprintStatistics(model.scip(), file) == SCIP_OKAY); - fclose(file); - BOOST_TEST(table->getNCalls() == 1); -} - /** * Writes the number of variables and constraints of a problem to files with extension cnt. */ @@ -823,6 +778,16 @@ BOOST_AUTO_TEST_CASE(UsePricer) BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); } +BOOST_AUTO_TEST_CASE(IncludePricerTwice) +{ + Model model("Simple"); + BOOST_TEST(model.includePricer() != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + // SCIP rejects a second pricer with the same name, the pricer is then deleted by SCIP++ without being activated + BOOST_TEST(model.includePricer() == nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_INVALIDDATA); +} + /** * Decomposes min x + y s.t. y >= 2 - c x with binary x and continuous y >= 0 into the master problem min x, which is * the model, and the subproblem min y s.t. y >= 2 - c x, y >= 0, where x is fixed to its value in the master problem. @@ -895,6 +860,73 @@ BOOST_AUTO_TEST_CASE(UseBenders) BOOST_TEST(x.getSolValAsInt(model.getBestSol()) == 0); } +BOOST_AUTO_TEST_CASE(IncludeBendersTwice) +{ + Model model("Master"); + auto x = model.addVar("x", 1, VarType::BINARY); + BOOST_TEST(model.includeBenders(1, x.getVar(), 0.0) != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + // SCIP rejects a second Benders' decomposition with the same name, it is then deleted by SCIP++ without being + // activated + BOOST_TEST(model.includeBenders(1, x.getVar(), 0.0) == nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_INVALIDDATA); +} + +/** + * Can be copied to sub-SCIPs. It is never solved, hence it creates no subproblems. + */ +class CloneableBenders : public scip::ObjBenders { +public: + explicit CloneableBenders(SCIP* scip) + : scip::ObjBenders( + scip, + "cloneable", + "can be copied to sub-SCIPs", + 1, // priority, positive to be higher than the one of the inactive default Benders' decomposition + TRUE, // cutlp + TRUE, // cutpseudo + TRUE, // cutrelax + FALSE) // shareauxvars + { + } + SCIP_DECL_OBJCLONEABLECLONE(ObjCloneable* clone) + override + { + return new CloneableBenders(scip); + } + SCIP_DECL_OBJCLONEABLEISCLONEABLE(iscloneable) + override + { + return TRUE; + } + SCIP_DECL_BENDERSCREATESUB(scip_createsub) + override + { + return SCIP_OKAY; + } + SCIP_DECL_BENDERSGETVAR(scip_getvar) + override + { + *mappedvar = nullptr; + return SCIP_OKAY; + } +}; + +BOOST_AUTO_TEST_CASE(CopyBendersOnlyIfCloneable) +{ + SCIP_Bool copyBenders { FALSE }; + Model cloneable("Master"); + BOOST_REQUIRE(cloneable.includeBenders(1) != nullptr); + BOOST_TEST(SCIPgetBoolParam(cloneable.scip(), "benders/copybenders", ©Benders) == SCIP_OKAY); + BOOST_TEST(copyBenders); + + Model notCloneable("Master"); + auto x = notCloneable.addVar("x", 1, VarType::BINARY); + BOOST_REQUIRE(notCloneable.includeBenders(1, x.getVar(), 0.0) != nullptr); + BOOST_TEST(SCIPgetBoolParam(notCloneable.scip(), "benders/copybenders", ©Benders) == SCIP_OKAY); + BOOST_TEST(!copyBenders); +} + /** * Adds the optimality cut theta + c x >= 2 for the subproblem of DemandBenders, where theta is the auxiliary variable * of the subproblem in the master problem. @@ -952,6 +984,20 @@ BOOST_AUTO_TEST_CASE(UseBendersCut) BOOST_TEST(x.getSolValAsInt(model.getBestSol()) == 1); } +BOOST_AUTO_TEST_CASE(IncludeBenderscutTwice) +{ + Model model("Master"); + auto x = model.addVar("x", 1, VarType::BINARY); + auto* benders { model.includeBenders(1, x.getVar(), 2.0) }; + BOOST_REQUIRE(benders != nullptr); + BOOST_TEST(model.includeBenderscut(*benders, x.getVar(), 2.0) != nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + // SCIP rejects a second cut with the same name for the same Benders' decomposition, the cut is then deleted by + // SCIP++ + BOOST_TEST(model.includeBenderscut(*benders, x.getVar(), 2.0) == nullptr); + BOOST_TEST(model.getLastReturnCode() == SCIP_INVALIDDATA); +} + /** * Tracks whether it was deleted. */ @@ -1022,6 +1068,31 @@ BOOST_AUTO_TEST_CASE(UseVariableData) BOOST_TEST(deleted); } +BOOST_AUTO_TEST_CASE(AddVariableDataWithBounds) +{ + Model model("Simple"); + auto& x = model.addVar("x", make_unique(), 1, VarType::CONTINUOUS, 2.0, 3.0); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + BOOST_TEST(SCIPvarGetLbOriginal(x.getVar()) == 2.0); + BOOST_TEST(SCIPvarGetUbOriginal(x.getVar()) == 3.0); +} + +BOOST_AUTO_TEST_CASE(AddVariableDataFails) +{ + bool deleted { false }; + Model model("Simple"); + // by default, SCIP++ continues after a failed call, but a variable that was not created cannot be added + model.setScipCallWrapper([](SCIP_Retcode retcode) { + if (retcode != SCIP_OKAY) { + throw runtime_error("SCIP call failed"); + } + }); + // SCIP rejects an infinite objective coefficient, the variable data is then deleted by SCIP++ + BOOST_CHECK_THROW( + model.addVar("x", make_unique(deleted), SCIPinfinity(model.scip())), runtime_error); + BOOST_TEST(deleted); +} + /** * Forwards all messages to a file. */ From 658948319bce2fde04cc4dca07c11e0acef995a9 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Mon, 5 Oct 2026 10:14:06 +0200 Subject: [PATCH 28/37] Simplify include code --- changelog.md | 27 +-- include/scippp/model.hpp | 510 ++++++--------------------------------- readme.md | 117 ++++----- test/test_objscip.cpp | 60 ++--- 4 files changed, 149 insertions(+), 565 deletions(-) diff --git a/changelog.md b/changelog.md index e540e96d..0211eb44 100644 --- a/changelog.md +++ b/changelog.md @@ -8,25 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [PR44](https://github.com/scipopt/SCIPpp/pull/44), [Issue29](https://github.com/scipopt/SCIPpp/issues/29) Support of ObjSCIP without accessing the raw SCIP object: - - Benders' decompositions derived from `scip::ObjBenders` via `Model::includeBenders`, - - Benders' decomposition cuts derived from `scip::ObjBenderscut` via `Model::includeBenderscut`, - - branching rules derived from `scip::ObjBranchrule` via `Model::includeBranchrule`, - - constraint handlers derived from `scip::ObjConshdlr` via `Model::includeConshdlr`, - - cut selectors derived from `scip::ObjCutsel` via `Model::includeCutsel`, - - display columns derived from `scip::ObjDisp` via `Model::includeDisp`, - - event handlers derived from `scip::ObjEventhdlr` via `Model::includeEventhdlr`, - - file readers derived from `scip::ObjReader` via `Model::includeReader`, - - IIS finders derived from `scip::ObjIISfinder` via `Model::includeIISfinder`, - - message handlers derived from `scip::ObjMessagehdlr` via `Model::setMessagehdlr`, - - node selectors derived from `scip::ObjNodesel` via `Model::includeNodesel`, - - presolvers derived from `scip::ObjPresol` via `Model::includePresol`, - - pricers derived from `scip::ObjPricer` via `Model::includePricer`, - - primal heuristics derived from `scip::ObjHeur` via `Model::includeHeur`, - - problem data derived from `scip::ObjProbData` via a new constructor of `Model`, - - propagators derived from `scip::ObjProp` via `Model::includeProp`, - - relaxators derived from `scip::ObjRelax` via `Model::includeRelax`, - - separators derived from `scip::ObjSepa` via `Model::includeSepa`, and - - variable data derived from `scip::ObjVardata` via a new overload of `Model::addVar`. + - `Model::include` includes plugins derived from `ObjBranchrule`, `ObjConshdlr`, `ObjCutsel`, + `ObjDisp`, `ObjEventhdlr`, `ObjHeur`, `ObjIISfinder`, `ObjNodesel`, + `ObjPresol`, `ObjPricer`, `ObjProp`, `ObjReader`, `ObjRelax`, and `ObjSepa`, + - `Model::includeBenders` and `Model::includeBenderscut` include Benders' decompositions and cuts derived from + `ObjBenders` and `ObjBenderscut`, + - `Model::setMessagehdlr` installs message handlers derived from `ObjMessagehdlr`, + - a new constructor of `Model` takes problem data derived from `ObjProbData`, and + - a new overload of `Model::addVar` takes variable data derived from `ObjVardata`. ### Changed diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index f7178fe8..a1c0cbba 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -548,458 +548,104 @@ class Model { void setMessagehdlr(std::unique_ptr handler) const; /** - * Includes a custom event handler. + * Includes a custom plugin, e.g., an event handler, a primal heuristic, or a separator. + * + * The plugin is constructed by this method as the objscip base classes require the %SCIP data structure in their + * constructors. %SCIP takes ownership and deletes it when the model is destructed. The kind of the plugin is + * determined by its objscip base class, which has to be exactly one of + * - scip::ObjBranchrule for branching rules, which decide how to split the search space, + * - scip::ObjConshdlr for constraint handlers, which enforce constraints that cannot be expressed linearly, + * - scip::ObjCutsel for cut selectors, which decide which of the separated cuts are added to the LP relaxation, + * - scip::ObjDisp for display columns, which show additional information in the lines %SCIP prints during the + * solving process, + * - scip::ObjEventhdlr for event handlers, which react to events of %SCIP, e.g., new best solutions, + * - scip::ObjHeur for primal heuristics, which construct solutions from problem-specific knowledge, + * - scip::ObjIISfinder for %IIS finders, which compute an %IIS of an infeasible problem via generateIIS(), + * - scip::ObjNodesel for node selectors, which decide which open node of the search tree is processed next, + * - scip::ObjPresol for presolvers, which reduce the problem before solving it, + * - scip::ObjPricer for variable pricers, which add variables with negative reduced costs during the solving + * process, i.e., implement column generation. The pricer is activated after including it, + * - scip::ObjProp for propagators, which tighten the domains of variables during the search, + * - scip::ObjReader for file readers, which write the problem in a custom file format via writeOrigProblem(), + * selected by the extension of the file name, + * - scip::ObjRelax for relaxators, which solve problem-specific relaxations in addition to the LP relaxation, or + * - scip::ObjSepa for separators, which cut off fractional solutions of the LP relaxation, e.g., with + * problem-specific valid inequalities. + * + * Benders' decompositions and their cuts need additional arguments, see includeBenders() and includeBenderscut(). * - * The handler is constructed by this method as scip::ObjEventhdlr requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjEventhdlr to react to events of %SCIP, e.g., to track new best solutions: * @code - * class MyEventHandler : public scip::ObjEventhdlr { - * public: - * MyEventHandler(SCIP* scip, int& counter); - * ... - * }; - * ... - * int counter { 0 }; - * model.includeEventhdlr(counter); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Eventhdlr Type of the event handler, derived from scip::ObjEventhdlr. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Eventhdlr after the %SCIP data structure. - * @return Non-owning pointer to the handler, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Eventhdlr* includeEventhdlr(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjEventhdlr, std::forward(args)...); - } - - /** - * Includes a custom constraint handler. - * - * The handler is constructed by this method as scip::ObjConshdlr requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjConshdlr to enforce constraints that cannot be expressed linearly, e.g., lazily: - * @code - * class MyConstraintHandler : public scip::ObjConshdlr { + * class MyHeuristic : public scip::ObjHeur { * public: - * MyConstraintHandler(SCIP* scip, const std::vector& vars); + * MyHeuristic(SCIP* scip, const std::vector& vars); * ... * }; * ... * auto vars = model.addVars("x_", 42); - * model.includeConshdlr(vars); + * model.include(vars); * model.solve(); * @endcode * * @since 1.5.0 - * @tparam Conshdlr Type of the constraint handler, derived from scip::ObjConshdlr. + * @tparam Plugin Type of the plugin, derived from exactly one of the objscip base classes listed above. * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Conshdlr after the %SCIP data structure. - * @return Non-owning pointer to the handler, or \c nullptr if including failed. + * @param args passed to the constructor of \p Plugin after the %SCIP data structure. + * @return Non-owning pointer to the plugin, or \c nullptr if including failed. * @attention Must be called before solve(). * @note SCIP++ cannot create constraints of a custom constraint handler. Thus, pass \c needscons = \c FALSE to the * constructor of scip::ObjConshdlr, so that the handler is called without constraints, and lock the * variables in \c scip_lock, which is then called with \c cons = \c nullptr. - */ - template - Conshdlr* includeConshdlr(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjConshdlr, std::forward(args)...); - } - - /** - * Includes a custom primal heuristic. - * - * The heuristic is constructed by this method as scip::ObjHeur requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjHeur to construct solutions from problem-specific knowledge: - * @code - * class MyHeuristic : public scip::ObjHeur { - * public: - * MyHeuristic(SCIP* scip, const std::vector& vars); - * ... - * }; - * ... - * auto vars = model.addVars("x_", 42); - * model.includeHeur(vars); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Heur Type of the heuristic, derived from scip::ObjHeur. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Heur after the %SCIP data structure. - * @return Non-owning pointer to the heuristic, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Heur* includeHeur(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjHeur, std::forward(args)...); - } - - /** - * Includes a custom presolver. - * - * The presolver is constructed by this method as scip::ObjPresol requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjPresol to reduce the problem before solving it, e.g., based on problem-specific knowledge: - * @code - * class MyPresolver : public scip::ObjPresol { - * public: - * MyPresolver(SCIP* scip, const std::vector& vars); - * ... - * }; - * ... - * auto vars = model.addVars("x_", 42); - * model.includePresol(vars); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Presol Type of the presolver, derived from scip::ObjPresol. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Presol after the %SCIP data structure. - * @return Non-owning pointer to the presolver, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Presol* includePresol(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjPresol, std::forward(args)...); - } - - /** - * Includes a custom propagator. - * - * The propagator is constructed by this method as scip::ObjProp requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjProp to tighten the domains of variables during the search, e.g., based on - * problem-specific knowledge: - * @code - * class MyPropagator : public scip::ObjProp { - * public: - * MyPropagator(SCIP* scip, const std::vector& vars); - * ... - * }; - * ... - * auto vars = model.addVars("x_", 42); - * model.includeProp(vars); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Prop Type of the propagator, derived from scip::ObjProp. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Prop after the %SCIP data structure. - * @return Non-owning pointer to the propagator, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Prop* includeProp(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjProp, std::forward(args)...); - } - - /** - * Includes a custom separator. - * - * The separator is constructed by this method as scip::ObjSepa requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjSepa to cut off fractional solutions of the LP relaxation, e.g., with problem-specific - * valid inequalities: - * @code - * class MySeparator : public scip::ObjSepa { - * public: - * MySeparator(SCIP* scip, const std::vector& vars); - * ... - * }; - * ... - * auto vars = model.addVars("x_", 42); - * model.includeSepa(vars); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Sepa Type of the separator, derived from scip::ObjSepa. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Sepa after the %SCIP data structure. - * @return Non-owning pointer to the separator, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Sepa* includeSepa(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjSepa, std::forward(args)...); - } - - /** - * Includes a custom cut selector. - * - * The cut selector is constructed by this method as scip::ObjCutsel requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjCutsel to decide which of the separated cuts are added to the LP relaxation: - * @code - * class MyCutSelector : public scip::ObjCutsel { - * public: - * MyCutSelector(SCIP* scip, double minEfficacy); - * ... - * }; - * ... - * model.includeCutsel(0.1); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Cutsel Type of the cut selector, derived from scip::ObjCutsel. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Cutsel after the %SCIP data structure. - * @return Non-owning pointer to the cut selector, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Cutsel* includeCutsel(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjCutsel, std::forward(args)...); - } - - /** - * Includes a custom branching rule. - * - * The branching rule is constructed by this method as scip::ObjBranchrule requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjBranchrule to decide how to split the search space, e.g., based on problem-specific - * knowledge: - * @code - * class MyBranchingRule : public scip::ObjBranchrule { - * public: - * MyBranchingRule(SCIP* scip, const std::vector& vars); - * ... - * }; - * ... - * auto vars = model.addVars("x_", 42); - * model.includeBranchrule(vars); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Branchrule Type of the branching rule, derived from scip::ObjBranchrule. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Branchrule after the %SCIP data structure. - * @return Non-owning pointer to the branching rule, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Branchrule* includeBranchrule(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjBranchrule, std::forward(args)...); - } - - /** - * Includes a custom node selector. - * - * The node selector is constructed by this method as scip::ObjNodesel requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjNodesel to decide which open node of the search tree is processed next: - * @code - * class MyNodeSelector : public scip::ObjNodesel { - * public: - * explicit MyNodeSelector(SCIP* scip); - * ... - * }; - * ... - * model.includeNodesel(); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Nodesel Type of the node selector, derived from scip::ObjNodesel. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Nodesel after the %SCIP data structure. - * @return Non-owning pointer to the node selector, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Nodesel* includeNodesel(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjNodesel, std::forward(args)...); - } - - /** - * Includes a custom relaxator. - * - * The relaxator is constructed by this method as scip::ObjRelax requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjRelax to solve problem-specific relaxations in addition to the LP relaxation: - * @code - * class MyRelaxator : public scip::ObjRelax { - * public: - * MyRelaxator(SCIP* scip, const std::vector& vars); - * ... - * }; - * ... - * auto vars = model.addVars("x_", 42); - * model.includeRelax(vars); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Relax Type of the relaxator, derived from scip::ObjRelax. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Relax after the %SCIP data structure. - * @return Non-owning pointer to the relaxator, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Relax* includeRelax(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjRelax, std::forward(args)...); - } - - /** - * Includes a custom display column. - * - * The display column is constructed by this method as scip::ObjDisp requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjDisp to show additional information in the lines %SCIP prints during the solving process: - * @code - * class MyDisplayColumn : public scip::ObjDisp { - * public: - * explicit MyDisplayColumn(SCIP* scip); - * ... - * }; - * ... - * model.includeDisp(); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Disp Type of the display column, derived from scip::ObjDisp. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Disp after the %SCIP data structure. - * @return Non-owning pointer to the display column, or \c nullptr if including failed. - * @attention Must be called before solve(). - */ - template - Disp* includeDisp(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjDisp, std::forward(args)...); - } - - /** - * Includes a custom file reader. - * - * The file reader is constructed by this method as scip::ObjReader requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjReader to write the problem in a custom file format via writeOrigProblem(), which selects - * the file reader by the extension of the file name: - * @code - * class MyFileReader : public scip::ObjReader { - * public: - * explicit MyFileReader(SCIP* scip); // passes "myext" as extension to scip::ObjReader - * ... - * }; - * ... - * model.includeReader(); - * model.writeOrigProblem(std::filesystem::directory_entry("problem.myext")); - * @endcode - * - * @since 1.5.0 - * @tparam Reader Type of the file reader, derived from scip::ObjReader. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Reader after the %SCIP data structure. - * @return Non-owning pointer to the file reader, or \c nullptr if including failed. - */ - template - Reader* includeReader(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjReader, std::forward(args)...); - } - - /** - * Includes a custom %IIS finder. - * - * The %IIS finder is constructed by this method as scip::ObjIISfinder requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjIISfinder to compute an %IIS of an infeasible problem via generateIIS(), e.g., based on - * problem-specific knowledge: - * @code - * class MyIISFinder : public scip::ObjIISfinder { - * public: - * explicit MyIISFinder(SCIP* scip); - * ... - * }; - * ... - * model.includeIISfinder(); - * model.solve(); - * auto iis { model.generateIIS() }; - * @endcode - * - * @since 1.5.0 - * @tparam IISfinder Type of the %IIS finder, derived from scip::ObjIISfinder. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p IISfinder after the %SCIP data structure. - * @return Non-owning pointer to the %IIS finder, or \c nullptr if including failed. - */ - template - IISfinder* includeIISfinder(Args&&... args) const - { - return constructAndInclude(&SCIPincludeObjIISfinder, std::forward(args)...); - } - - /** - * Includes and activates a custom variable pricer. - * - * The pricer is constructed by this method as scip::ObjPricer requires the %SCIP data structure in its - * constructor. %SCIP takes ownership and deletes it when the model is destructed. - * - * Derive from scip::ObjPricer to add variables with negative reduced costs during the solving process, i.e., to - * implement column generation: - * @code - * class MyPricer : public scip::ObjPricer { - * public: - * MyPricer(SCIP* scip, const std::vector& conss); - * ... - * }; - * ... - * model.includePricer(conss); - * model.solve(); - * @endcode - * - * @since 1.5.0 - * @tparam Pricer Type of the pricer, derived from scip::ObjPricer. - * @tparam Args Types of the additional constructor arguments. - * @param args passed to the constructor of \p Pricer after the %SCIP data structure. - * @return Non-owning pointer to the pricer, or \c nullptr if including failed. - * @attention Must be called before solve(). * @note Constraints added via addConstr() are not modifiable, i.e., priced variables cannot be added to them. */ - template - Pricer* includePricer(Args&&... args) const + template + Plugin* include(Args&&... args) const { - auto* pricer { constructAndInclude(&SCIPincludeObjPricer, std::forward(args)...) }; - if (pricer != nullptr) { - activatePricer(*pricer); + static_assert(!std::is_base_of_v, "Use includeBenders() for Benders' decompositions"); + static_assert(!std::is_base_of_v, "Use includeBenderscut() for Benders' cuts"); + // a plugin with several base classes would be included as only one of them, depending on the order below + constexpr int N_BASES { std::is_base_of_v + + std::is_base_of_v + + std::is_base_of_v + std::is_base_of_v + + std::is_base_of_v + std::is_base_of_v + + std::is_base_of_v + std::is_base_of_v + + std::is_base_of_v + std::is_base_of_v + + std::is_base_of_v + std::is_base_of_v + + std::is_base_of_v + std::is_base_of_v }; + static_assert(N_BASES == 1, "Plugin must derive from exactly one supported objscip base class"); + if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjBranchrule, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjConshdlr, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjCutsel, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjDisp, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjEventhdlr, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjHeur, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjIISfinder, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjNodesel, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjPresol, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + auto* pricer { constructAndInclude(&SCIPincludeObjPricer, std::forward(args)...) }; + if (pricer != nullptr) { + activatePricer(*pricer); + } + return pricer; + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjProp, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjReader, std::forward(args)...); + } else if constexpr (std::is_base_of_v) { + return constructAndInclude(&SCIPincludeObjRelax, std::forward(args)...); + } else { + // the only remaining base class, constructAndInclude checks it + return constructAndInclude(&SCIPincludeObjSepa, std::forward(args)...); } - return pricer; } /** diff --git a/readme.md b/readme.md index a32f9b85..5c65e083 100644 --- a/readme.md +++ b/readme.md @@ -54,85 +54,52 @@ int main() ## Features * Create a model with a new SCIP data structure or with an existing one, with or without SCIP's default plugins. -* Add variables one at a time, as a vector, or as an array for structured bindings (`scippp::Model::addVar`, - `scippp::Model::addVars`). The objective coefficients can be given by any object providing an index operator. -* Build linear expressions (`scippp::LinExpr`) and add linear inequalities and equations as constraints - (`scippp::Model::addConstr`). -* Set the optimization goal (`scippp::Model::setObjsense`) and all SCIP parameters in a type-safe way - (`scippp::Model::setParam` with `scippp::params`). +* Add variables one at a time, as a vector, or as an array for structured bindings (`Model::addVar`, + `Model::addVars`). The objective coefficients can be given by any object providing an index operator. +* Build linear expressions (`LinExpr`) and add linear inequalities and equations as constraints + (`Model::addConstr`). +* Set the optimization goal (`Model::setObjsense`) and all SCIP parameters in a type-safe way + (`Model::setParam` with `scippp::params`). * Solve the model and query its status, the number of solutions, the best solution, and the values of the variables in - a solution (`scippp::Model::solve`, `scippp::Model::getStatus`, `scippp::Model::getNSols`, - `scippp::Model::getBestSol`, `scippp::Var::getSolVal`). -* Add initial solutions to SCIP's solution pool (`scippp::InitialSolution`, `scippp::Model::addSolution`). -* Query solving statistics in a type-safe way (`scippp::Model::getSolvingStatistic` with `scippp::statistics`). -* Generate an Irreducible Infeasible Subsystem (`scippp::Model::generateIIS`). -* Write the original problem to a file or to standard output (`scippp::Model::writeOrigProblem`). -* Use SCIP's numerics (`scippp::Model::epsilon`, `scippp::Model::round`, `scippp::Model::isZero`, - `scippp::Model::infinity`). -* Use ObjSCIP without accessing the raw SCIP object: - * Benders' decompositions derived from - [`scip::ObjBenders`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenders.php) - (`scippp::Model::includeBenders`), - * Benders' decomposition cuts derived from - [`scip::ObjBenderscut`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenderscut.php) - (`scippp::Model::includeBenderscut`), - * branching rules derived from - [`scip::ObjBranchrule`](https://www.scipopt.org/doc/html/classscip_1_1ObjBranchrule.php) - (`scippp::Model::includeBranchrule`), - * constraint handlers derived from - [`scip::ObjConshdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjConshdlr.php) - (`scippp::Model::includeConshdlr`), - * cut selectors derived from - [`scip::ObjCutsel`](https://www.scipopt.org/doc/html/classscip_1_1ObjCutsel.php) - (`scippp::Model::includeCutsel`), - * display columns derived from - [`scip::ObjDisp`](https://www.scipopt.org/doc/html/classscip_1_1ObjDisp.php) - (`scippp::Model::includeDisp`), - * event handlers derived from - [`scip::ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php) - (`scippp::Model::includeEventhdlr`), - * file readers derived from - [`scip::ObjReader`](https://www.scipopt.org/doc/html/classscip_1_1ObjReader.php) - (`scippp::Model::includeReader`), - * IIS finders derived from - [`scip::ObjIISfinder`](https://www.scipopt.org/doc/html/classscip_1_1ObjIISfinder.php) - (`scippp::Model::includeIISfinder`), - * message handlers derived from - [`scip::ObjMessagehdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjMessagehdlr.php) - (`scippp::Model::setMessagehdlr`), - * node selectors derived from - [`scip::ObjNodesel`](https://www.scipopt.org/doc/html/classscip_1_1ObjNodesel.php) - (`scippp::Model::includeNodesel`), - * presolvers derived from - [`scip::ObjPresol`](https://www.scipopt.org/doc/html/classscip_1_1ObjPresol.php) - (`scippp::Model::includePresol`), - * pricers derived from - [`scip::ObjPricer`](https://www.scipopt.org/doc/html/classscip_1_1ObjPricer.php) - (`scippp::Model::includePricer`), - * primal heuristics derived from - [`scip::ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php) - (`scippp::Model::includeHeur`), - * problem data derived from - [`scip::ObjProbData`](https://www.scipopt.org/doc/html/classscip_1_1ObjProbData.php) - (`scippp::Model`), - * propagators derived from - [`scip::ObjProp`](https://www.scipopt.org/doc/html/classscip_1_1ObjProp.php) - (`scippp::Model::includeProp`), - * relaxators derived from - [`scip::ObjRelax`](https://www.scipopt.org/doc/html/classscip_1_1ObjRelax.php) - (`scippp::Model::includeRelax`), - * separators derived from - [`scip::ObjSepa`](https://www.scipopt.org/doc/html/classscip_1_1ObjSepa.php) - (`scippp::Model::includeSepa`), and - * variable data derived from - [`scip::ObjVardata`](https://www.scipopt.org/doc/html/classscip_1_1ObjVardata.php) - (`scippp::Model::addVar`). + a solution (`Model::solve`, `Model::getStatus`, `Model::getNSols`, + `Model::getBestSol`, `Var::getSolVal`). +* Add initial solutions to SCIP's solution pool (`InitialSolution`, `Model::addSolution`). +* Query solving statistics in a type-safe way (`Model::getSolvingStatistic` with `scippp::statistics`). +* Generate an Irreducible Infeasible Subsystem (`Model::generateIIS`). +* Write the original problem to a file or to standard output (`Model::writeOrigProblem`). +* Use SCIP's numerics (`Model::epsilon`, `Model::round`, `Model::isZero`, + `Model::infinity`). +* Use ObjSCIP: + * `Model::include` includes plugins derived from + [`ObjBranchrule`](https://www.scipopt.org/doc/html/classscip_1_1ObjBranchrule.php), + [`ObjConshdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjConshdlr.php), + [`ObjCutsel`](https://www.scipopt.org/doc/html/classscip_1_1ObjCutsel.php), + [`ObjDisp`](https://www.scipopt.org/doc/html/classscip_1_1ObjDisp.php), + [`ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php), + [`ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php), + [`ObjIISfinder`](https://www.scipopt.org/doc/html/classscip_1_1ObjIISfinder.php), + [`ObjNodesel`](https://www.scipopt.org/doc/html/classscip_1_1ObjNodesel.php), + [`ObjPresol`](https://www.scipopt.org/doc/html/classscip_1_1ObjPresol.php), + [`ObjPricer`](https://www.scipopt.org/doc/html/classscip_1_1ObjPricer.php), + [`ObjProp`](https://www.scipopt.org/doc/html/classscip_1_1ObjProp.php), + [`ObjReader`](https://www.scipopt.org/doc/html/classscip_1_1ObjReader.php), + [`ObjRelax`](https://www.scipopt.org/doc/html/classscip_1_1ObjRelax.php), and + [`ObjSepa`](https://www.scipopt.org/doc/html/classscip_1_1ObjSepa.php), + * `Model::includeBenders` and `Model::includeBenderscut` include Benders' decompositions and cuts + derived from [`ObjBenders`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenders.php) and + [`ObjBenderscut`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenderscut.php), + * `Model::setMessagehdlr` installs message handlers derived from + [`ObjMessagehdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjMessagehdlr.php), + * a constructor of `Model` takes problem data derived from + [`ObjProbData`](https://www.scipopt.org/doc/html/classscip_1_1ObjProbData.php), and + * `Model::addVar` takes variable data derived from + [`ObjVardata`](https://www.scipopt.org/doc/html/classscip_1_1ObjVardata.php). Statistics tables derived from - [`scip::ObjTable`](https://www.scipopt.org/doc/html/classscip_1_1ObjTable.php) - are not supported, as SCIP does not export `scip::ObjTable` from its shared library, see + [`ObjTable`](https://www.scipopt.org/doc/html/classscip_1_1ObjTable.php) + are not supported, as SCIP does not export `ObjTable` from its shared library, see [SCIP issue 222](https://github.com/scipopt/scip/issues/222). -* Access the raw SCIP object for features not yet supported (`scippp::Model::scip`). +* Access the raw SCIP object for features not yet supported (`Model::scip`). ## Build diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 7da7d8f6..e5f6993a 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -5,32 +5,14 @@ #include #include #include +#include +#include #include #include #include "scippp/model.hpp" #include "scippp/parameters.hpp" #include "scippp/solving_statistics.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include using namespace boost::algorithm; using namespace scippp; @@ -76,7 +58,7 @@ BOOST_AUTO_TEST_CASE(UseEventHandler) auto x2 = model.addVar("x_2", 1); model.addConstr(x1 + x2 >= 1, "capacity"); model.addConstr(x1 == x2, "equal"); - BOOST_TEST(model.includeEventhdlr(nBestSols) != nullptr); + BOOST_TEST(model.include(nBestSols) != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.setObjsense(Sense::MINIMIZE); model.solve(); @@ -88,10 +70,10 @@ BOOST_AUTO_TEST_CASE(IncludeEventHandlerTwice) { int nBestSols { 0 }; Model model("Simple"); - BOOST_TEST(model.includeEventhdlr(nBestSols) != nullptr); + BOOST_TEST(model.include(nBestSols) != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); // SCIP rejects a second event handler with the same name, the handler is then deleted by SCIP++ - BOOST_TEST(model.includeEventhdlr(nBestSols) == nullptr); + BOOST_TEST(model.include(nBestSols) == nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_INVALIDDATA); } @@ -175,7 +157,7 @@ BOOST_AUTO_TEST_CASE(UseConstraintHandler) auto x1 = model.addVar("x_1", 1, VarType::BINARY); auto x2 = model.addVar("x_2", 1, VarType::BINARY); model.setObjsense(Sense::MAXIMIZE); - BOOST_TEST(model.includeConshdlr(x1.getVar()) != nullptr); + BOOST_TEST(model.include(x1.getVar()) != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); @@ -235,7 +217,7 @@ BOOST_AUTO_TEST_CASE(UseHeuristic) auto x2 = model.addVar("x_2", 1, VarType::BINARY); model.addConstr(x1 + x2 <= 1, "capacity"); model.setObjsense(Sense::MAXIMIZE); - const auto* heur { model.includeHeur(x1.getVar()) }; + const auto* heur { model.include(x1.getVar()) }; BOOST_REQUIRE(heur != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -287,7 +269,7 @@ BOOST_AUTO_TEST_CASE(UsePresolver) auto x2 = model.addVar("x_2", 1, VarType::BINARY); model.addConstr(x1 + x2 <= 1, "capacity"); model.setObjsense(Sense::MAXIMIZE); - BOOST_TEST(model.includePresol(x1.getVar()) != nullptr); + BOOST_TEST(model.include(x1.getVar()) != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); // without the presolver, x_1 = 1 would be optimal with objective 2 @@ -339,7 +321,7 @@ BOOST_AUTO_TEST_CASE(UsePropagator) model.setObjsense(Sense::MAXIMIZE); // otherwise presolving solves the problem and no node is processed model.setParam(params::PRESOLVING::MAXROUNDS, 0); - const auto* prop { model.includeProp() }; + const auto* prop { model.include() }; BOOST_REQUIRE(prop != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -410,7 +392,7 @@ BOOST_AUTO_TEST_CASE(UseSeparator) { Model model("Simple"); auto [x1, x2] = addFractionalProblem(model); - const auto* sepa { model.includeSepa(x1.getVar(), x2.getVar()) }; + const auto* sepa { model.include(x1.getVar(), x2.getVar()) }; BOOST_REQUIRE(sepa != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -450,8 +432,8 @@ BOOST_AUTO_TEST_CASE(UseCutSelector) { Model model("Simple"); auto [x1, x2] = addFractionalProblem(model); - BOOST_REQUIRE(model.includeSepa(x1.getVar(), x2.getVar()) != nullptr); - const auto* cutsel { model.includeCutsel() }; + BOOST_REQUIRE(model.include(x1.getVar(), x2.getVar()) != nullptr); + const auto* cutsel { model.include() }; BOOST_REQUIRE(cutsel != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -503,7 +485,7 @@ BOOST_AUTO_TEST_CASE(UseBranchingRule) // otherwise the problem is solved at the root by separation or conflict analysis model.setParam(params::SEPARATING::MAXROUNDSROOT, 0); model.setParam(params::CONFLICT::ENABLE, false); - const auto* branchrule { model.includeBranchrule() }; + const auto* branchrule { model.include() }; BOOST_REQUIRE(branchrule != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -558,7 +540,7 @@ BOOST_AUTO_TEST_CASE(UseNodeSelector) // otherwise the problem is solved at the root by separation or conflict analysis model.setParam(params::SEPARATING::MAXROUNDSROOT, 0); model.setParam(params::CONFLICT::ENABLE, false); - const auto* nodesel { model.includeNodesel() }; + const auto* nodesel { model.include() }; BOOST_REQUIRE(nodesel != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -599,7 +581,7 @@ BOOST_AUTO_TEST_CASE(UseRelaxator) { Model model("Simple"); addFractionalProblem(model); - const auto* relax { model.includeRelax() }; + const auto* relax { model.include() }; BOOST_REQUIRE(relax != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -643,7 +625,7 @@ BOOST_AUTO_TEST_CASE(UseDisplayColumn) Model model("Simple"); // nodes have to be processed, otherwise no display line is printed addFractionalProblem(model); - const auto* disp { model.includeDisp() }; + const auto* disp { model.include() }; BOOST_REQUIRE(disp != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -674,7 +656,7 @@ BOOST_AUTO_TEST_CASE(UseReader) auto x1 = model.addVar("x_1", 1); auto x2 = model.addVar("x_2", 1); model.addConstr(x1 + x2 >= 1, "capacity"); - BOOST_TEST(model.includeReader() != nullptr); + BOOST_TEST(model.include() != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); const auto FILE_NAME { filesystem::temp_directory_path() / boost::filesystem::unique_path("%%%%-%%%%-%%%%-%%%%.cnt").string() }; @@ -723,7 +705,7 @@ BOOST_AUTO_TEST_CASE(UseIISFinder) model.addConstr(x1 + x2 >= 2, "lower"); model.addConstr(x1 + x2 <= 1, "upper"); model.setParam(params::IIS::SILENT, true); - const auto* iisfinder { model.includeIISfinder() }; + const auto* iisfinder { model.include() }; BOOST_REQUIRE(iisfinder != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -770,7 +752,7 @@ BOOST_AUTO_TEST_CASE(UsePricer) model.addConstr(x1 + x2 >= 1, "capacity"); // otherwise presolving solves the problem and no LP is solved model.setParam(params::PRESOLVING::MAXROUNDS, 0); - const auto* pricer { model.includePricer() }; + const auto* pricer { model.include() }; BOOST_REQUIRE(pricer != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); @@ -781,10 +763,10 @@ BOOST_AUTO_TEST_CASE(UsePricer) BOOST_AUTO_TEST_CASE(IncludePricerTwice) { Model model("Simple"); - BOOST_TEST(model.includePricer() != nullptr); + BOOST_TEST(model.include() != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); // SCIP rejects a second pricer with the same name, the pricer is then deleted by SCIP++ without being activated - BOOST_TEST(model.includePricer() == nullptr); + BOOST_TEST(model.include() == nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_INVALIDDATA); } From 65d88d154688e27889d448b0b5c49a89d22efd7c Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Mon, 5 Oct 2026 10:41:59 +0200 Subject: [PATCH 29/37] Update codecov action from v3 to v7 --- .github/workflows/main.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 32d274b1..7aa9832e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,10 +45,23 @@ jobs: run: pip install gcovr - name: Extract Coverage run: gcovr -r . -e ./test --object-directory=./build/Debug/CMakeFiles --exclude-unreachable-branches --exclude-throw-branches -b --sonarqube coverage.xml --xml cobertura.xml - - uses: codecov/codecov-action@v3 + - name: Upload Coverage + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./boosttest.xml,./coverage.xml,./cobertura.xml + files: ./coverage.xml,./cobertura.xml + disable_search: true + verbose: true # optional (default = false) + fail_ci_if_error: true + # Codecov treats JUnit test results as a separate report type, so they need their own upload + - name: Upload Test Results + if: ${{ !cancelled() }} + uses: codecov/codecov-action@v7 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./boosttest.xml + disable_search: true + report_type: test_results verbose: true # optional (default = false) fail_ci_if_error: true test_release_win: From a77f1166572817bb7d24ffdfe00f74b016188b31 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Mon, 5 Oct 2026 16:23:31 +0200 Subject: [PATCH 30/37] Fix false positive branch coverage --- source/model_objscip.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/model_objscip.cpp b/source/model_objscip.cpp index 8715902d..aacb2ca1 100644 --- a/source/model_objscip.cpp +++ b/source/model_objscip.cpp @@ -14,7 +14,9 @@ Model::Model( assert(probData); // GCOVR_EXCL_LINE const auto RETCODE { SCIPcreateObjProb(m_scip, name.c_str(), probData.get(), TRUE) }; // SCIP owns the problem data only on success, otherwise it is deleted when leaving this constructor. - if (RETCODE == SCIP_OKAY) { + // SCIPcreateObjProb only fails in an invalid stage, when out of memory, or when freeing a previous problem fails, + // which a test cannot provoke without leaving the SCIP object in a corrupted state. + if (RETCODE == SCIP_OKAY) { // GCOVR_EXCL_BR_LINE probData.release(); } m_scipCallWrapper(RETCODE); @@ -30,6 +32,9 @@ Var& Model::addVar( { assert(vardata); // GCOVR_EXCL_LINE SCIP_VAR* var { nullptr }; + // Both cases of the bounds are tested, the only untaken branches GCC reports here are exception edges of the calls. + // They are still counted as partially covered by gcovr despite --exclude-throw-branches. + // GCOVR_EXCL_BR_START const auto RETCODE { SCIPcreateObjVar( m_scip, /* SCIP environment */ &var, /* reference to the variable */ @@ -43,6 +48,7 @@ Var& Model::addVar( vardata.get(), /* variable data */ TRUE /* delete the variable data when the variable is freed */ ) }; + // GCOVR_EXCL_BR_STOP // SCIP owns the variable data only on success, otherwise it is deleted when leaving this method. if (RETCODE == SCIP_OKAY) { vardata.release(); From d402e29f7381381cab7140d6f5dab602ab209e83 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Tue, 6 Oct 2026 11:10:39 +0200 Subject: [PATCH 31/37] Restore old docu --- changelog.md | 2 - include/scippp/model.hpp | 166 +----------------------- readme.md | 264 +++++++++++++++++++++++++++------------ 3 files changed, 191 insertions(+), 241 deletions(-) diff --git a/changelog.md b/changelog.md index 0211eb44..dcf439ab 100644 --- a/changelog.md +++ b/changelog.md @@ -20,8 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [PR45](https://github.com/scipopt/SCIPpp/pull/45) Update to SCIP 10.1.0. -- [PR44](https://github.com/scipopt/SCIPpp/pull/44) The readme lists the supported features only. How to use them is - documented, including examples, in the Doxygen documentation. ## [1.4.0] - 2025-12-18 diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index a1c0cbba..554cf6a7 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -107,21 +107,6 @@ class Model { * * By default, all calls to the underlying %C %API are wrapped and the last return code is stored. * - * A model can be created without an existing %SCIP data structure, or with an existing one where the default - * plugins are added to or not: - * @code - * Model m1("ModelWithoutExistingSCIPEnvironment"); - * - * SCIP* scip2 { nullptr }; - * SCIPcreate(&scip2); - * Model m2("ModelWithExistingSCIPEnvironmentWhereDefaultPluginsWillBeAdded", scip2); - * - * SCIP* scip3 { nullptr }; - * SCIPcreate(&scip3); - * SCIPincludeDefaultPlugins(scip3); - * Model m3("ModelWithExistingSCIPEnvironmentWhereNoPluginsWillBeAdded", scip3, false); - * @endcode - * * @since 1.0.0 * @param name for the problem. * @param scip to create the problem in. If \c nullptr, a new %SCIP data structure will be created. @@ -131,14 +116,6 @@ class Model { /** * Creates an empty problem with custom problem data and sets the optimization goal to Sense::MINIMIZE. - * - * Derive from scip::ObjProbData to store problem-specific data, which plugins can access via SCIPgetObjProbData: - * @code - * class MyProblemData : public scip::ObjProbData { ... }; - * ... - * Model model("Problem", std::make_unique()); - * @endcode - * * @since 1.5.0 * @param name for the problem. * @param probData to store in the problem, must not be \c nullptr. %SCIP takes ownership and deletes it when the @@ -191,14 +168,6 @@ class Model { /** * Adds a variable with custom variable data to the model. - * - * Derive from scip::ObjVardata to store variable-specific data, which plugins can access via SCIPgetObjVardata: - * @code - * class MyVariableData : public scip::ObjVardata { ... }; - * ... - * auto& x = model.addVar("x", std::make_unique(), 1.0, VarType::BINARY); - * @endcode - * * @since 1.5.0 * @param name of the variable when the model is written. * @param vardata to store in the variable, must not be \c nullptr. %SCIP takes ownership and deletes it when the @@ -219,16 +188,6 @@ class Model { /** * Adds multiple variables to the model. - * - * By default, all variables have a coefficient of zero in the objective function. Use scippp::COEFF_ONE for a - * coefficient of one, or any object providing double operator[](std::size_t index) const: - * @code - * auto x = model.addVars("x_", 42); - * auto y = model.addVars("y_", 42, COEFF_ONE); - * std::vector costs { 1.5, 2, 3 }; - * auto z = model.addVars("z_", costs.size(), costs, VarType::INTEGER); - * @endcode - * * @since 1.0.0 * @tparam CoeffType Type of the object holding the coefficients. They are accessed via \c [i] where i goes from 0 * to \p numVars - 1. @@ -261,10 +220,7 @@ class Model { * Adds multiple variables to the model. * * This method can be used when the number of variables to add is known at compile time. The result can be used in a - * structured binding: - * @code - * const auto& [x0, x1] = model.addVars<2>("x_", COEFF_ONE); - * @endcode + * structured binding. * * @since 1.0.0 * @tparam NumVars Number of variables to add. @@ -293,22 +249,6 @@ class Model { /** * Adds a constraint to the model. - * - * Linear inequalities are built from linear expressions, see LinExpr: - * @code - * const auto& [x0, x1, x2, x3] = model.addVars<4>("x_"); - * - * LinExpr sum1; - * sum1 += 42 * x0; - * sum1 += x1; - * model.addConstr(sum1 <= 0.5, "constraint1"); - * - * LinExpr sum2 = x1 + x2; - * model.addConstr(sum2 == 1.25, "constraint2"); - * - * model.addConstr(1 <= x2 + 2 * x3, "constraint3"); - * @endcode - * * @since 1.0.0 * @param ineq linear inequality to add. * @param name for the constraint when the model is written. @@ -368,13 +308,6 @@ class Model { /** * Query statistics about the solving process. * - * Use the scippp::statistics::Statistic objects from the header solving_statistics.hpp to access the statistics in - * a type-safe way: - * @code - * model.solve(); - * auto pb { model.getSolvingStatistic(statistics::PRIMALBOUND) }; - * @endcode - * * @tparam T Type of the statistics value. * @since 1.2.0 * @param statistic Statistics value to access. @@ -395,18 +328,6 @@ class Model { /** * Returns the best feasible primal solution found so far or best solution candidate. - * - * The values of the variables in the solution are accessed via Var::getSolVal, Var::getSolValAsInt, - * Var::getSolValAsLongInt, and Var::isZero: - * @code - * const auto& [x0, x1] = model.addVars<2>("x_"); - * model.solve(); - * if (model.getNSols() > 0 && model.getStatus() == SCIP_STATUS_OPTIMAL) { - * Solution sol { model.getBestSol() }; - * std::cout << "x0 + x1 = " << x0.getSolVal(sol) + x1.getSolVal(sol) << std::endl; - * } - * @endcode - * * @since 1.0.0 * @return best feasible primal solution. */ @@ -425,15 +346,7 @@ class Model { /** * Sets a parameter. * - * The namespace scippp::params in the header parameters.hpp contains all parameters listed at - * https://www.scipopt.org/doc/html/PARAMETERS.php. They are strongly typed, so that, e.g., no string can be set as - * the value for a parameter expecting an integer. Instead of using the predefined parameters, one can also create - * new ones using params::Param: - * @code - * model.setParam(params::LIMITS::MAXSOL, 1); - * model.setParam(params::DISPLAY::VERBLEVEL, 0); - * model.setParam(params::Param("write/printzeros"), true); - * @endcode + * See the namespace scippp::params for a list of parameters, or create new ones using params::Param. * * @since 1.0.0 * @tparam T Type of the value. @@ -530,15 +443,6 @@ class Model { /** * Installs a custom message handler, i.e., all info, warning, and dialog messages of %SCIP are passed to it. - * - * Derive from scip::ObjMessagehdlr to process the output of %SCIP, e.g., to write it to a logger: - * @code - * class MyMessageHandler : public scip::ObjMessagehdlr { ... }; - * ... - * model.setMessagehdlr(std::make_unique()); - * model.solve(); - * @endcode - * * @since 1.5.0 * @param handler to install, must not be \c nullptr. %SCIP takes ownership and deletes it when it is no longer * used. @@ -552,42 +456,11 @@ class Model { * * The plugin is constructed by this method as the objscip base classes require the %SCIP data structure in their * constructors. %SCIP takes ownership and deletes it when the model is destructed. The kind of the plugin is - * determined by its objscip base class, which has to be exactly one of - * - scip::ObjBranchrule for branching rules, which decide how to split the search space, - * - scip::ObjConshdlr for constraint handlers, which enforce constraints that cannot be expressed linearly, - * - scip::ObjCutsel for cut selectors, which decide which of the separated cuts are added to the LP relaxation, - * - scip::ObjDisp for display columns, which show additional information in the lines %SCIP prints during the - * solving process, - * - scip::ObjEventhdlr for event handlers, which react to events of %SCIP, e.g., new best solutions, - * - scip::ObjHeur for primal heuristics, which construct solutions from problem-specific knowledge, - * - scip::ObjIISfinder for %IIS finders, which compute an %IIS of an infeasible problem via generateIIS(), - * - scip::ObjNodesel for node selectors, which decide which open node of the search tree is processed next, - * - scip::ObjPresol for presolvers, which reduce the problem before solving it, - * - scip::ObjPricer for variable pricers, which add variables with negative reduced costs during the solving - * process, i.e., implement column generation. The pricer is activated after including it, - * - scip::ObjProp for propagators, which tighten the domains of variables during the search, - * - scip::ObjReader for file readers, which write the problem in a custom file format via writeOrigProblem(), - * selected by the extension of the file name, - * - scip::ObjRelax for relaxators, which solve problem-specific relaxations in addition to the LP relaxation, or - * - scip::ObjSepa for separators, which cut off fractional solutions of the LP relaxation, e.g., with - * problem-specific valid inequalities. - * - * Benders' decompositions and their cuts need additional arguments, see includeBenders() and includeBenderscut(). - * - * @code - * class MyHeuristic : public scip::ObjHeur { - * public: - * MyHeuristic(SCIP* scip, const std::vector& vars); - * ... - * }; - * ... - * auto vars = model.addVars("x_", 42); - * model.include(vars); - * model.solve(); - * @endcode + * determined by its objscip base class. Variable pricers are activated after including them. For Benders' + * decompositions and their cuts, use includeBenders() and includeBenderscut(). * * @since 1.5.0 - * @tparam Plugin Type of the plugin, derived from exactly one of the objscip base classes listed above. + * @tparam Plugin Type of the plugin, derived from exactly one of the supported objscip base classes, see readme. * @tparam Args Types of the additional constructor arguments. * @param args passed to the constructor of \p Plugin after the %SCIP data structure. * @return Non-owning pointer to the plugin, or \c nullptr if including failed. @@ -654,21 +527,6 @@ class Model { * The Benders' decomposition is constructed by this method as scip::ObjBenders requires the %SCIP data structure * in its constructor. %SCIP takes ownership and deletes it when the model is destructed. * - * Derive from scip::ObjBenders to solve the model as master problem of a Benders' decomposition. The - * decomposition creates the subproblems, solves them, and maps the variables between the master problem and the - * subproblems: - * @code - * class MyBenders : public scip::ObjBenders { - * public: - * MyBenders(SCIP* scip, const std::vector& masterVars); - * ... - * }; - * ... - * auto masterVars = model.addVars("x_", 42); - * model.includeBenders(nSubproblems, masterVars); - * model.solve(); - * @endcode - * * Activating the Benders' decomposition also activates the constraint handlers \c benders and \c benderslp, which * enforce it. If \p Benders is not cloneable, copying Benders' decompositions to sub-SCIPs is disabled via the * parameter \c benders/copybenders, as %SCIP cannot copy it. @@ -702,20 +560,6 @@ class Model { * The cut is constructed by this method as scip::ObjBenderscut requires the %SCIP data structure in its * constructor. %SCIP takes ownership and deletes it when the model is destructed. * - * Derive from scip::ObjBenderscut to generate problem-specific cuts from the subproblems of a Benders' - * decomposition: - * @code - * class MyBendersCut : public scip::ObjBenderscut { - * public: - * explicit MyBendersCut(SCIP* scip); - * ... - * }; - * ... - * auto* benders { model.includeBenders(nSubproblems) }; - * model.includeBenderscut(*benders); - * model.solve(); - * @endcode - * * @since 1.5.0 * @tparam Benderscut Type of the cut, derived from scip::ObjBenderscut. * @tparam Args Types of the additional constructor arguments. diff --git a/readme.md b/readme.md index 5c65e083..96d55e09 100644 --- a/readme.md +++ b/readme.md @@ -10,96 +10,204 @@ It automatically manages the memory, and provides a simple interface to create l ## Usage -The documentation, including examples for all features, can be found at https://scipopt.github.io/SCIPpp/ +The documentation can be found at https://scipopt.github.io/SCIPpp/ -Here is an example where we create a new model for a knapsack problem, add binary variables with their values as -coefficients in the objective function, add the capacity constraint built from a linear expression, ask SCIP to solve -the maximization problem, and print the packed items. +Here is a simple example where we create a new model, add two variables, add a linear inequality as constraint, and ask +SCIP to solve the maximization problem. ```cpp -#include #include -#include -#include - using namespace scippp; - -int main() -{ - std::vector weights { 3, 4, 3, 3, 3, 3, 4, 2, 4, 1 }; - std::vector values { 230, 134, 52, 60, 151, 95, 201, 245, 52, 55 }; - - Model model("Knapsack"); - auto x { model.addVars("x_", weights.size(), values, VarType::BINARY) }; - LinExpr weight; - for (size_t i { 0 }; i < weights.size(); ++i) { - weight += weights[i] * x[i]; - } - model.addConstr(weight <= 14, "capacity"); +int main() { + Model model("Simple"); + auto x1 = model.addVar("x_1", 1); + auto x2 = model.addVar("x_2", 1); + model.addConstr(3 * x1 + 2 * x2 <= 1, "capacity"); model.setObjsense(Sense::MAXIMIZE); - model.setParam(params::DISPLAY::VERBLEVEL, 0); model.solve(); +} +``` - if (model.getNSols() > 0) { - auto sol { model.getBestSol() }; - for (size_t i { 0 }; i < x.size(); ++i) { - if (x[i].getSolValAsInt(sol) == 1) { - std::cout << "pack item " << i << "\n"; - } - } - } +### Model Creation + +A model can be created + +* without an existing SCIP environment, +* with an existing SCIP environment where all default plugins should be added to, and +* with an existing SCIP environment where no additional plugins should be added to. + +```cpp +Model m1("ModelWithoutExistingSCIPEnvironment"); + +SCIP* scip2; +SCIPcreate(&scip2); +Model m2("ModelWithExistingSCIPEnvironmentWhereDefaultPluginsWillBeAdded", scip2); + +SCIP* scip3; +SCIPcreate(&scip3); +SCIPincludeDefaultPlugins(scip3); +Model m3("ModelWithExistingSCIPEnvironmentWhereNoPluginsWillBeAdded", scip3, false); +``` + +### Adding Variables + +Variables can be added + +* one at a time, +* multiple in a vector, and +* multiple when the number is known at compile time. + +```cpp +Model model("Example"); + +auto x = model.addVar("x"); +auto vec = model.addVars("x_", 42); +const auto& [x0, x1] = model.addVars<2>("x_"); +``` + +When adding multiple variables simultaneously to the model, they all have a coefficient of zero in the objective +function by default. +This can be changed to one by `scippp::COEFF_ONE`: +```cpp +const auto& [x1, x2] = model.addVars<2>("x_", COEFF_ONE); +``` + +For the coefficient, any object providing an index operator can be used: +```cpp +double operator[](std::size_t index) const +``` + +### Adding Constraints + +Linear inequalities can be added to the model. They can be built from linear expressions: + +```cpp +const auto& [x0, x1, x2, x3] = model.addVars<4>("x_"); + +LinExpr sum1; +sum1 += 42 * x0; +sum1 += x1; +model.addConstr(sum1 <= 0.5, "constraint1"); + +LinExpr sum2 = x1 + x2; +model.addConstr(sum2 == 1.25, "constraint2"); + +model.addConstr(1 <= x2 + 2 * x3, "constraint3"); +``` + +### Setting Parameters + +The namespace `scippp::params` contains all parameters shown https://www.scipopt.org/doc/html/PARAMETERS.php in the +header `parameters.hpp`. They are strongly typed, so that no string can be set as the value for a parameter expecting an +integer. Instead of using the predefined parameters, one can also use `scippp::params::Param` directly. + +```cpp +model.setParam(params::LIMITS::MAXSOL, 1); +model.setParam(params::DISPLAY::VERBLEVEL, 0); +model.setParam(params::Param("write/printzeros"), true); +``` + +The optimization goal can be changed by: +```cpp +model.setObjsense(Sense::MAXIMIZE); +``` + +### Accessing a Solution + +A model can be asked for the status and the number of solutions. +A variable can be asked for its value in a given solution as + +* floating point number via `getSolVal(sol)`. +* integer via `getSolValAsInt(sol)`. +* long integer via `getSolValAsLongInt(sol)`, and +* it can be checked for zero via `isZero(sol)`. + +```cpp +const auto& [x0, x1] = model.addVars<2>("x_"); +model.solve(); +if (model.getNSols() > 0 && model.getStatus() == SCIP_STATUS_OPTIMAL) { + Solution sol { model.getBestSol() }; + cout << "x0 + x1 =" << x0.getSolVal(sol) + x1.getSolVal(sol) << endl; } ``` -## Features - -* Create a model with a new SCIP data structure or with an existing one, with or without SCIP's default plugins. -* Add variables one at a time, as a vector, or as an array for structured bindings (`Model::addVar`, - `Model::addVars`). The objective coefficients can be given by any object providing an index operator. -* Build linear expressions (`LinExpr`) and add linear inequalities and equations as constraints - (`Model::addConstr`). -* Set the optimization goal (`Model::setObjsense`) and all SCIP parameters in a type-safe way - (`Model::setParam` with `scippp::params`). -* Solve the model and query its status, the number of solutions, the best solution, and the values of the variables in - a solution (`Model::solve`, `Model::getStatus`, `Model::getNSols`, - `Model::getBestSol`, `Var::getSolVal`). -* Add initial solutions to SCIP's solution pool (`InitialSolution`, `Model::addSolution`). -* Query solving statistics in a type-safe way (`Model::getSolvingStatistic` with `scippp::statistics`). -* Generate an Irreducible Infeasible Subsystem (`Model::generateIIS`). -* Write the original problem to a file or to standard output (`Model::writeOrigProblem`). -* Use SCIP's numerics (`Model::epsilon`, `Model::round`, `Model::isZero`, - `Model::infinity`). -* Use ObjSCIP: - * `Model::include` includes plugins derived from - [`ObjBranchrule`](https://www.scipopt.org/doc/html/classscip_1_1ObjBranchrule.php), - [`ObjConshdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjConshdlr.php), - [`ObjCutsel`](https://www.scipopt.org/doc/html/classscip_1_1ObjCutsel.php), - [`ObjDisp`](https://www.scipopt.org/doc/html/classscip_1_1ObjDisp.php), - [`ObjEventhdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjEventhdlr.php), - [`ObjHeur`](https://www.scipopt.org/doc/html/classscip_1_1ObjHeur.php), - [`ObjIISfinder`](https://www.scipopt.org/doc/html/classscip_1_1ObjIISfinder.php), - [`ObjNodesel`](https://www.scipopt.org/doc/html/classscip_1_1ObjNodesel.php), - [`ObjPresol`](https://www.scipopt.org/doc/html/classscip_1_1ObjPresol.php), - [`ObjPricer`](https://www.scipopt.org/doc/html/classscip_1_1ObjPricer.php), - [`ObjProp`](https://www.scipopt.org/doc/html/classscip_1_1ObjProp.php), - [`ObjReader`](https://www.scipopt.org/doc/html/classscip_1_1ObjReader.php), - [`ObjRelax`](https://www.scipopt.org/doc/html/classscip_1_1ObjRelax.php), and - [`ObjSepa`](https://www.scipopt.org/doc/html/classscip_1_1ObjSepa.php), - * `Model::includeBenders` and `Model::includeBenderscut` include Benders' decompositions and cuts - derived from [`ObjBenders`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenders.php) and - [`ObjBenderscut`](https://www.scipopt.org/doc/html/classscip_1_1ObjBenderscut.php), - * `Model::setMessagehdlr` installs message handlers derived from - [`ObjMessagehdlr`](https://www.scipopt.org/doc/html/classscip_1_1ObjMessagehdlr.php), - * a constructor of `Model` takes problem data derived from - [`ObjProbData`](https://www.scipopt.org/doc/html/classscip_1_1ObjProbData.php), and - * `Model::addVar` takes variable data derived from - [`ObjVardata`](https://www.scipopt.org/doc/html/classscip_1_1ObjVardata.php). - - Statistics tables derived from - [`ObjTable`](https://www.scipopt.org/doc/html/classscip_1_1ObjTable.php) - are not supported, as SCIP does not export `ObjTable` from its shared library, see +### IO + +A model can be written to file via `Model::writeOrigProblem` if a `std::filesystem::directory_entry` is given as +argument. If it is just a string representing a file extension, it is written to standard output. + +### Numerics + +The model exposes + +* `SCIPepsilon` via `epsilon()`, +* `SCIPround` via `round(double)`, and +* `SCIPisZero` via `isZero(double)` + +### Access Solving Statistics + +Use the `Statistics` objects from the header [solving_statistics.hpp](include/scippp/solving_statistics.hpp) to +access solving statistics in a type-safe way: + +```cpp +... +model.solve(); +auto pb { model.getSolvingStatistic(statistics::PRIMALBOUND) }; +``` + +### Custom Plugins (ObjSCIP) + +Plugins derived from SCIP's [ObjSCIP](https://www.scipopt.org/doc/html/OBJ.php) base classes are included via +`Model::include`. The plugin is constructed by SCIP++, because the base classes need the SCIP data structure in their +constructors. Its constructor gets the `SCIP*` as first argument, followed by the arguments passed to `include`. SCIP +takes ownership of the plugin, `include` returns a non-owning pointer to it. Plugins have to be included before solving. + +```cpp +class MyHeuristic : public scip::ObjHeur { +public: + MyHeuristic(SCIP* scip, const std::vector& vars) + : ObjHeur(scip, "myheur", "my heuristic", 'x', 1000, 1, 0, -1, SCIP_HEURTIMING_AFTERNODE, FALSE) + , m_vars(vars) + { + } + SCIP_DECL_HEUREXEC(scip_exec) override; +private: + std::vector m_vars; +}; + +auto vars = model.addVars("x_", 42); +auto* heur = model.include(vars); +model.solve(); +``` + +This works the same way for plugins derived from `ObjBranchrule`, `ObjConshdlr`, `ObjCutsel`, `ObjDisp`, +`ObjEventhdlr`, `ObjHeur`, `ObjIISfinder`, `ObjNodesel`, `ObjPresol`, `ObjPricer`, `ObjProp`, `ObjReader`, `ObjRelax`, +and `ObjSepa`, with the following exceptions: + +* `ObjConshdlr`: SCIP++ cannot create constraints of a custom constraint handler. Pass `needscons = FALSE` to the + constructor, so that the handler is called without constraints, and lock the variables in `scip_lock`, which is then + called with `cons = nullptr`. +* `ObjPricer`: The pricer is activated after including it. Constraints added via `Model::addConstr` are not + modifiable, i.e., priced variables cannot be added to them. +* `ObjBenders`: Use `model.includeBenders(nSubproblems, args...)`. It activates the decomposition together + with SCIP's default Benders' cuts. The priority has to be positive, and `scip_solvesubconvex` or `scip_solvesub` has + to be implemented, as SCIP does not solve the subproblems itself. +* `ObjBenderscut`: Use `model.includeBenderscut(*benders, args...)` with the pointer returned by + `includeBenders`. +* `ObjMessagehdlr`: Is not constructed by SCIP++, install it via + `model.setMessagehdlr(std::make_unique())`. Error messages are not passed to it. +* `ObjProbData`: Pass it to the constructor, `Model model("name", std::make_unique())`. +* `ObjVardata`: Pass it when adding a variable, `model.addVar("x", std::make_unique(), 1.0)`. +* `ObjTable`: Is not supported, as SCIP does not export it from its shared library, see [SCIP issue 222](https://github.com/scipopt/scip/issues/222). -* Access the raw SCIP object for features not yet supported (`Model::scip`). + +### Features Not Yet Supported + +For features not yet supported by SCIP++, one can access the underlying raw SCIP object via + +```cpp +SCIP* scip = model.scip(); +``` ## Build From 8f212f7dbd143bdd1ac0adcd24b3bf2e975d00bf Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Fri, 16 Oct 2026 09:10:13 +0200 Subject: [PATCH 32/37] Select the include function by overload resolution Co-Authored-By: Claude Fable 5.1 --- include/scippp/model.hpp | 74 ++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 44 deletions(-) diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 554cf6a7..bbcabfd6 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -77,6 +77,29 @@ class Model { return result; } + /** + * Maps an objscip base class to %SCIP's include function for it, so that include() can select it by overload + * resolution on a pointer to the plugin. A plugin deriving from several base classes is rejected as ambiguous. + * @{ + */ + static auto includeFunction(scip::ObjBranchrule*) { return &SCIPincludeObjBranchrule; } + static auto includeFunction(scip::ObjConshdlr*) { return &SCIPincludeObjConshdlr; } + static auto includeFunction(scip::ObjCutsel*) { return &SCIPincludeObjCutsel; } + static auto includeFunction(scip::ObjDisp*) { return &SCIPincludeObjDisp; } + static auto includeFunction(scip::ObjEventhdlr*) { return &SCIPincludeObjEventhdlr; } + static auto includeFunction(scip::ObjHeur*) { return &SCIPincludeObjHeur; } + static auto includeFunction(scip::ObjIISfinder*) { return &SCIPincludeObjIISfinder; } + static auto includeFunction(scip::ObjNodesel*) { return &SCIPincludeObjNodesel; } + static auto includeFunction(scip::ObjPresol*) { return &SCIPincludeObjPresol; } + static auto includeFunction(scip::ObjPricer*) { return &SCIPincludeObjPricer; } + static auto includeFunction(scip::ObjProp*) { return &SCIPincludeObjProp; } + static auto includeFunction(scip::ObjReader*) { return &SCIPincludeObjReader; } + static auto includeFunction(scip::ObjRelax*) { return &SCIPincludeObjRelax; } + static auto includeFunction(scip::ObjSepa*) { return &SCIPincludeObjSepa; } + static auto includeFunction(scip::ObjBenders*) = delete; // use includeBenders() + static auto includeFunction(scip::ObjBenderscut*) = delete; // use includeBenderscut() + /** @} */ + /** * Activates an included variable pricer, so that it is used when solving the model. * @@ -473,52 +496,15 @@ class Model { template Plugin* include(Args&&... args) const { - static_assert(!std::is_base_of_v, "Use includeBenders() for Benders' decompositions"); - static_assert(!std::is_base_of_v, "Use includeBenderscut() for Benders' cuts"); - // a plugin with several base classes would be included as only one of them, depending on the order below - constexpr int N_BASES { std::is_base_of_v - + std::is_base_of_v - + std::is_base_of_v + std::is_base_of_v - + std::is_base_of_v + std::is_base_of_v - + std::is_base_of_v + std::is_base_of_v - + std::is_base_of_v + std::is_base_of_v - + std::is_base_of_v + std::is_base_of_v - + std::is_base_of_v + std::is_base_of_v }; - static_assert(N_BASES == 1, "Plugin must derive from exactly one supported objscip base class"); - if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjBranchrule, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjConshdlr, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjCutsel, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjDisp, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjEventhdlr, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjHeur, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjIISfinder, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjNodesel, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjPresol, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - auto* pricer { constructAndInclude(&SCIPincludeObjPricer, std::forward(args)...) }; - if (pricer != nullptr) { - activatePricer(*pricer); + // overload resolution picks the include function of the base class, see includeFunction + auto* plugin { constructAndInclude( + includeFunction(static_cast(nullptr)), std::forward(args)...) }; + if constexpr (std::is_base_of_v) { + if (plugin != nullptr) { + activatePricer(*plugin); } - return pricer; - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjProp, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjReader, std::forward(args)...); - } else if constexpr (std::is_base_of_v) { - return constructAndInclude(&SCIPincludeObjRelax, std::forward(args)...); - } else { - // the only remaining base class, constructAndInclude checks it - return constructAndInclude(&SCIPincludeObjSepa, std::forward(args)...); } + return plugin; } /** From bba7de021ec96000e5e984801bf561ababcfa24e Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Fri, 16 Oct 2026 09:31:01 +0200 Subject: [PATCH 33/37] Share the tail of both addVar overloads Co-Authored-By: Claude Fable 5.1 --- include/scippp/model.hpp | 8 ++++++++ source/model.cpp | 13 ++++++++++--- source/model_objscip.cpp | 14 +++++--------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index bbcabfd6..0aac2ff9 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -100,6 +100,14 @@ class Model { static auto includeFunction(scip::ObjBenderscut*) = delete; // use includeBenderscut() /** @} */ + /** + * Adds a created variable to the problem and stores it. + * + * @param var created variable, %SCIP's reference to it is kept by this model. + * @return Reference to the stored variable. + */ + Var& addCreatedVar(SCIP_VAR* var); + /** * Activates an included variable pricer, so that it is used when solving the model. * diff --git a/source/model.cpp b/source/model.cpp index 4769c7ff..b516becc 100644 --- a/source/model.cpp +++ b/source/model.cpp @@ -13,16 +13,23 @@ Var& Model::addVar( std::optional lb, std::optional ub) { + const auto LB { lb.value_or(-SCIPinfinity(m_scip)) }; + const auto UB { ub.value_or(SCIPinfinity(m_scip)) }; SCIP_VAR* var { nullptr }; m_scipCallWrapper(SCIPcreateVarBasic( m_scip, /* SCIP environment */ &var, /* reference to the variable */ name.c_str(), /* name of the variable */ - lb != std::nullopt ? lb.value() : -SCIPinfinity(m_scip), /* lower bound of the variable */ - ub != std::nullopt ? ub.value() : SCIPinfinity(m_scip), /* upper bound of the variable */ + LB, /* lower bound of the variable */ + UB, /* upper bound of the variable */ coeff, /* obj. coefficient. */ - static_cast(varType) /* variable is binary */ + static_cast(varType) /* variable type */ )); + return addCreatedVar(var); +} + +Var& Model::addCreatedVar(SCIP_VAR* var) +{ m_scipCallWrapper(SCIPaddVar(m_scip, var)); m_vars.emplace_back(Var { var }); return m_vars.back(); diff --git a/source/model_objscip.cpp b/source/model_objscip.cpp index aacb2ca1..b4620829 100644 --- a/source/model_objscip.cpp +++ b/source/model_objscip.cpp @@ -31,16 +31,15 @@ Var& Model::addVar( std::optional ub) { assert(vardata); // GCOVR_EXCL_LINE + const auto LB { lb.value_or(-SCIPinfinity(m_scip)) }; + const auto UB { ub.value_or(SCIPinfinity(m_scip)) }; SCIP_VAR* var { nullptr }; - // Both cases of the bounds are tested, the only untaken branches GCC reports here are exception edges of the calls. - // They are still counted as partially covered by gcovr despite --exclude-throw-branches. - // GCOVR_EXCL_BR_START const auto RETCODE { SCIPcreateObjVar( m_scip, /* SCIP environment */ &var, /* reference to the variable */ name.c_str(), /* name of the variable */ - lb != std::nullopt ? lb.value() : -SCIPinfinity(m_scip), /* lower bound of the variable */ - ub != std::nullopt ? ub.value() : SCIPinfinity(m_scip), /* upper bound of the variable */ + LB, /* lower bound of the variable */ + UB, /* upper bound of the variable */ coeff, /* obj. coefficient. */ static_cast(varType), /* variable type */ TRUE, /* initial */ @@ -48,15 +47,12 @@ Var& Model::addVar( vardata.get(), /* variable data */ TRUE /* delete the variable data when the variable is freed */ ) }; - // GCOVR_EXCL_BR_STOP // SCIP owns the variable data only on success, otherwise it is deleted when leaving this method. if (RETCODE == SCIP_OKAY) { vardata.release(); } m_scipCallWrapper(RETCODE); - m_scipCallWrapper(SCIPaddVar(m_scip, var)); - m_vars.emplace_back(Var { var }); - return m_vars.back(); + return addCreatedVar(var); } void Model::setMessagehdlr(std::unique_ptr handler) const From a777b996b6b096346abb05143ae8604eb17e1c15 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Fri, 16 Oct 2026 09:36:06 +0200 Subject: [PATCH 34/37] Use public counters in the test plugins Co-Authored-By: Claude Fable 5.1 --- test/test_objscip.cpp | 162 +++++++++++++++--------------------------- 1 file changed, 58 insertions(+), 104 deletions(-) diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index e5f6993a..9de526c1 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -24,12 +24,11 @@ BOOST_AUTO_TEST_SUITE(ObjSCIP) * Counts how often a new best solution is found. */ class BestSolCounter : public scip::ObjEventhdlr { - int& m_nBestSols; - public: - BestSolCounter(SCIP* scip, int& nBestSols) + int nBestSols { 0 }; + + explicit BestSolCounter(SCIP* scip) : scip::ObjEventhdlr(scip, "bestsolcounter", "counts new best solutions") - , m_nBestSols(nBestSols) { } SCIP_DECL_EVENTINIT(scip_init) @@ -45,35 +44,34 @@ class BestSolCounter : public scip::ObjEventhdlr { SCIP_DECL_EVENTEXEC(scip_exec) override { - ++m_nBestSols; + ++nBestSols; return SCIP_OKAY; } }; BOOST_AUTO_TEST_CASE(UseEventHandler) { - int nBestSols { 0 }; Model model("Simple"); auto x1 = model.addVar("x_1", 1); auto x2 = model.addVar("x_2", 1); model.addConstr(x1 + x2 >= 1, "capacity"); model.addConstr(x1 == x2, "equal"); - BOOST_TEST(model.include(nBestSols) != nullptr); + const auto* counter { model.include() }; + BOOST_REQUIRE(counter != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.setObjsense(Sense::MINIMIZE); model.solve(); BOOST_TEST(model.getNSols() > 0); - BOOST_TEST(nBestSols > 0); + BOOST_TEST(counter->nBestSols > 0); } BOOST_AUTO_TEST_CASE(IncludeEventHandlerTwice) { - int nBestSols { 0 }; Model model("Simple"); - BOOST_TEST(model.include(nBestSols) != nullptr); + BOOST_TEST(model.include() != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); // SCIP rejects a second event handler with the same name, the handler is then deleted by SCIP++ - BOOST_TEST(model.include(nBestSols) == nullptr); + BOOST_TEST(model.include() == nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_INVALIDDATA); } @@ -170,9 +168,10 @@ BOOST_AUTO_TEST_CASE(UseConstraintHandler) */ class OneVarHeur : public scip::ObjHeur { SCIP_VAR* m_var; - int m_nFound { 0 }; public: + int nFound { 0 }; + OneVarHeur(SCIP* scip, SCIP_VAR* var) : scip::ObjHeur( scip, @@ -188,10 +187,6 @@ class OneVarHeur : public scip::ObjHeur { , m_var(var) { } - [[nodiscard]] int getNFound() const - { - return m_nFound; - } SCIP_DECL_HEUREXEC(scip_exec) override { @@ -201,7 +196,7 @@ class OneVarHeur : public scip::ObjHeur { SCIP_Bool stored { FALSE }; SCIP_CALL(SCIPtrySolFree(scip, &sol, FALSE, FALSE, TRUE, TRUE, TRUE, &stored)); if (stored) { - ++m_nFound; + ++nFound; *result = SCIP_FOUNDSOL; } else { *result = SCIP_DIDNOTFIND; @@ -222,7 +217,7 @@ BOOST_AUTO_TEST_CASE(UseHeuristic) BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); // the heuristic is called first, so its solution is stored as there is no other solution yet - BOOST_TEST(heur->getNFound() == 1); + BOOST_TEST(heur->nFound == 1); BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); } @@ -282,9 +277,9 @@ BOOST_AUTO_TEST_CASE(UsePresolver) * Counts how often it is called for domain propagation. */ class CountingProp : public scip::ObjProp { - int m_nCalls { 0 }; - public: + int nCalls { 0 }; + explicit CountingProp(SCIP* scip) : scip::ObjProp( scip, @@ -299,14 +294,10 @@ class CountingProp : public scip::ObjProp { SCIP_PRESOLTIMING_NONE) { } - [[nodiscard]] int getNCalls() const - { - return m_nCalls; - } SCIP_DECL_PROPEXEC(scip_exec) override { - ++m_nCalls; + ++nCalls; *result = SCIP_DIDNOTFIND; return SCIP_OKAY; } @@ -325,7 +316,7 @@ BOOST_AUTO_TEST_CASE(UsePropagator) BOOST_REQUIRE(prop != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); - BOOST_TEST(prop->getNCalls() > 0); + BOOST_TEST(prop->nCalls > 0); } /** @@ -349,9 +340,10 @@ array addFractionalProblem(Model& model) */ class CapacitySepa : public scip::ObjSepa { array m_vars; - int m_nCuts { 0 }; public: + int nCuts { 0 }; + CapacitySepa(SCIP* scip, SCIP_VAR* x1, SCIP_VAR* x2) : scip::ObjSepa( scip, @@ -365,10 +357,6 @@ class CapacitySepa : public scip::ObjSepa { , m_vars { x1, x2 } { } - [[nodiscard]] int getNCuts() const - { - return m_nCuts; - } SCIP_DECL_SEPAEXECLP(scip_execlp) override { @@ -381,7 +369,7 @@ class CapacitySepa : public scip::ObjSepa { if (SCIPisCutEfficacious(scip, nullptr, row)) { SCIP_Bool infeasible { FALSE }; SCIP_CALL(SCIPaddRow(scip, row, FALSE, &infeasible)); - ++m_nCuts; + ++nCuts; *result = infeasible ? SCIP_CUTOFF : SCIP_SEPARATED; } return SCIPreleaseRow(scip, &row); @@ -396,16 +384,16 @@ BOOST_AUTO_TEST_CASE(UseSeparator) BOOST_REQUIRE(sepa != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); - BOOST_TEST(sepa->getNCuts() > 0); + BOOST_TEST(sepa->nCuts > 0); } /** * Selects all cuts, up to the maximal number of cuts to select. */ class AllCutsel : public scip::ObjCutsel { - int m_nCalls { 0 }; - public: + int nCalls { 0 }; + explicit AllCutsel(SCIP* scip) : scip::ObjCutsel( scip, @@ -414,14 +402,10 @@ class AllCutsel : public scip::ObjCutsel { 1000000) // priority, higher than the ones of the default cut selectors to be used { } - [[nodiscard]] int getNCalls() const - { - return m_nCalls; - } SCIP_DECL_CUTSELSELECT(scip_select) override { - ++m_nCalls; + ++nCalls; *nselectedcuts = min(ncuts, maxnselectedcuts); *result = SCIP_SUCCESS; return SCIP_OKAY; @@ -437,16 +421,16 @@ BOOST_AUTO_TEST_CASE(UseCutSelector) BOOST_REQUIRE(cutsel != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); - BOOST_TEST(cutsel->getNCalls() > 0); + BOOST_TEST(cutsel->nCalls > 0); } /** * Branches on the first fractional variable of the LP solution. */ class FirstFracBranchrule : public scip::ObjBranchrule { - int m_nCalls { 0 }; - public: + int nCalls { 0 }; + explicit FirstFracBranchrule(SCIP* scip) : scip::ObjBranchrule( scip, @@ -457,14 +441,10 @@ class FirstFracBranchrule : public scip::ObjBranchrule { 1.0) // maxbounddist { } - [[nodiscard]] int getNCalls() const - { - return m_nCalls; - } SCIP_DECL_BRANCHEXECLP(scip_execlp) override { - ++m_nCalls; + ++nCalls; SCIP_VAR** cands { nullptr }; int nCands { 0 }; SCIP_CALL(SCIPgetLPBranchCands(scip, &cands, nullptr, nullptr, &nCands, nullptr, nullptr)); @@ -489,16 +469,16 @@ BOOST_AUTO_TEST_CASE(UseBranchingRule) BOOST_REQUIRE(branchrule != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); - BOOST_TEST(branchrule->getNCalls() > 0); + BOOST_TEST(branchrule->nCalls > 0); } /** * Selects nodes in depth-first order. */ class DepthFirstNodesel : public scip::ObjNodesel { - int m_nCalls { 0 }; - public: + int nCalls { 0 }; + explicit DepthFirstNodesel(SCIP* scip) : scip::ObjNodesel( scip, @@ -508,14 +488,10 @@ class DepthFirstNodesel : public scip::ObjNodesel { 0) // memsavepriority { } - [[nodiscard]] int getNCalls() const - { - return m_nCalls; - } SCIP_DECL_NODESELSELECT(scip_select) override { - ++m_nCalls; + ++nCalls; *selnode = SCIPgetPrioChild(scip); if (*selnode == nullptr) { *selnode = SCIPgetPrioSibling(scip); @@ -544,16 +520,16 @@ BOOST_AUTO_TEST_CASE(UseNodeSelector) BOOST_REQUIRE(nodesel != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); - BOOST_TEST(nodesel->getNCalls() > 0); + BOOST_TEST(nodesel->nCalls > 0); } /** * Counts how often it is called before the LP relaxation is solved. */ class CountingRelax : public scip::ObjRelax { - int m_nCalls { 0 }; - public: + int nCalls { 0 }; + explicit CountingRelax(SCIP* scip) : scip::ObjRelax( scip, @@ -564,14 +540,10 @@ class CountingRelax : public scip::ObjRelax { FALSE) // includeslp { } - [[nodiscard]] int getNCalls() const - { - return m_nCalls; - } SCIP_DECL_RELAXEXEC(scip_exec) override { - ++m_nCalls; + ++nCalls; *result = SCIP_DIDNOTRUN; return SCIP_OKAY; } @@ -585,16 +557,16 @@ BOOST_AUTO_TEST_CASE(UseRelaxator) BOOST_REQUIRE(relax != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); - BOOST_TEST(relax->getNCalls() > 0); + BOOST_TEST(relax->nCalls > 0); } /** * Displays how often it was displayed. */ class CountingDisp : public scip::ObjDisp { - int m_nCalls { 0 }; - public: + int nCalls { 0 }; + explicit CountingDisp(SCIP* scip) : scip::ObjDisp( scip, @@ -607,15 +579,11 @@ class CountingDisp : public scip::ObjDisp { TRUE) // stripline { } - [[nodiscard]] int getNCalls() const - { - return m_nCalls; - } SCIP_DECL_DISPOUTPUT(scip_output) override { - ++m_nCalls; - SCIPinfoMessage(scip, file, "%5d", m_nCalls); + ++nCalls; + SCIPinfoMessage(scip, file, "%5d", nCalls); return SCIP_OKAY; } }; @@ -629,7 +597,7 @@ BOOST_AUTO_TEST_CASE(UseDisplayColumn) BOOST_REQUIRE(disp != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); - BOOST_TEST(disp->getNCalls() > 0); + BOOST_TEST(disp->nCalls > 0); } /** @@ -674,9 +642,9 @@ BOOST_AUTO_TEST_CASE(UseReader) * Counts how often it is called and leaves the work to the other %IIS finders. */ class CountingIISfinder : public scip::ObjIISfinder { - int m_nCalls { 0 }; - public: + int nCalls { 0 }; + explicit CountingIISfinder(SCIP* scip) : scip::ObjIISfinder( scip, @@ -685,14 +653,10 @@ class CountingIISfinder : public scip::ObjIISfinder { 1000000) // priority, higher than the ones of the default IIS finders to be called first { } - [[nodiscard]] int getNCalls() const - { - return m_nCalls; - } SCIP_DECL_IISFINDEREXEC(scip_exec) override { - ++m_nCalls; + ++nCalls; *result = SCIP_DIDNOTRUN; return SCIP_OKAY; } @@ -712,16 +676,16 @@ BOOST_AUTO_TEST_CASE(UseIISFinder) BOOST_REQUIRE(model.getStatus() == SCIP_STATUS_INFEASIBLE); // the default IIS finder still computes the IIS BOOST_TEST(model.generateIIS().consIds.size() == 2); - BOOST_TEST(iisfinder->getNCalls() == 1); + BOOST_TEST(iisfinder->nCalls == 1); } /** * Counts how often it is called for pricing, but never adds a variable. */ class CountingPricer : public scip::ObjPricer { - int m_nCalls { 0 }; - public: + int nCalls { 0 }; + explicit CountingPricer(SCIP* scip) : scip::ObjPricer( scip, @@ -731,14 +695,10 @@ class CountingPricer : public scip::ObjPricer { FALSE) // delay { } - [[nodiscard]] int getNCalls() const - { - return m_nCalls; - } SCIP_DECL_PRICERREDCOST(scip_redcost) override { - ++m_nCalls; + ++nCalls; *result = SCIP_SUCCESS; return SCIP_OKAY; } @@ -756,7 +716,7 @@ BOOST_AUTO_TEST_CASE(UsePricer) BOOST_REQUIRE(pricer != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); - BOOST_TEST(pricer->getNCalls() > 0); + BOOST_TEST(pricer->nCalls > 0); BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1); } @@ -779,9 +739,10 @@ BOOST_AUTO_TEST_CASE(IncludePricerTwice) class DemandBenders : public scip::ObjBenders { SCIP_VAR* m_masterX; double m_coverage; - int m_nSolves { 0 }; public: + int nSolves { 0 }; + DemandBenders(SCIP* scip, SCIP_VAR* masterX, double coverage) : scip::ObjBenders( scip, @@ -796,10 +757,6 @@ class DemandBenders : public scip::ObjBenders { , m_coverage(coverage) { } - [[nodiscard]] int getNSolves() const - { - return m_nSolves; - } SCIP_DECL_BENDERSCREATESUB(scip_createsub) override { @@ -813,7 +770,7 @@ class DemandBenders : public scip::ObjBenders { SCIP_DECL_BENDERSSOLVESUBCONVEX(scip_solvesubconvex) override { - ++m_nSolves; + ++nSolves; *objective = max(0.0, 2.0 - m_coverage * SCIPgetSolVal(scip, sol, m_masterX)); *result = SCIP_FEASIBLE; return SCIP_OKAY; @@ -837,7 +794,7 @@ BOOST_AUTO_TEST_CASE(UseBenders) BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); BOOST_TEST(model.getStatus() == SCIP_STATUS_OPTIMAL); - BOOST_TEST(benders->getNSolves() > 0); + BOOST_TEST(benders->nSolves > 0); BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 2.0, boost::test_tools::tolerance(1e-6)); BOOST_TEST(x.getSolValAsInt(model.getBestSol()) == 0); } @@ -916,9 +873,10 @@ BOOST_AUTO_TEST_CASE(CopyBendersOnlyIfCloneable) class DemandBenderscut : public scip::ObjBenderscut { SCIP_VAR* m_masterX; double m_coverage; - int m_nCuts { 0 }; public: + int nCuts { 0 }; + DemandBenderscut(SCIP* scip, SCIP_VAR* masterX, double coverage) : scip::ObjBenderscut( scip, @@ -930,10 +888,6 @@ class DemandBenderscut : public scip::ObjBenderscut { , m_coverage(coverage) { } - [[nodiscard]] int getNCuts() const - { - return m_nCuts; - } SCIP_DECL_BENDERSCUTEXEC(scip_exec) override { @@ -943,7 +897,7 @@ class DemandBenderscut : public scip::ObjBenderscut { SCIP_CALL(SCIPaddCoefLinear(scip, cons, SCIPvarGetTransVar(m_masterX), m_coverage)); SCIP_CALL(SCIPaddCons(scip, cons)); SCIP_CALL(SCIPreleaseCons(scip, &cons)); - ++m_nCuts; + ++nCuts; *result = SCIP_CONSADDED; return SCIP_OKAY; } @@ -961,7 +915,7 @@ BOOST_AUTO_TEST_CASE(UseBendersCut) BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); model.solve(); BOOST_TEST(model.getStatus() == SCIP_STATUS_OPTIMAL); - BOOST_TEST(cut->getNCuts() > 0); + BOOST_TEST(cut->nCuts > 0); BOOST_TEST(model.getSolvingStatistic(statistics::PRIMALBOUND) == 1.0, boost::test_tools::tolerance(1e-6)); BOOST_TEST(x.getSolValAsInt(model.getBestSol()) == 1); } From c4f81d7fc0ce51761e4360c6b7120dbda6a6f133 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Fri, 16 Oct 2026 09:41:23 +0200 Subject: [PATCH 35/37] Derive the cloneable Benders test plugin from the existing one Co-Authored-By: Claude Fable 5.1 --- test/test_objscip.cpp | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index 9de526c1..c8a6f738 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -812,56 +812,35 @@ BOOST_AUTO_TEST_CASE(IncludeBendersTwice) } /** - * Can be copied to sub-SCIPs. It is never solved, hence it creates no subproblems. + * Can be copied to sub-SCIPs. The copy is never solved, so it does not map the master variable. */ -class CloneableBenders : public scip::ObjBenders { +class CloneableDemandBenders : public DemandBenders { public: - explicit CloneableBenders(SCIP* scip) - : scip::ObjBenders( - scip, - "cloneable", - "can be copied to sub-SCIPs", - 1, // priority, positive to be higher than the one of the inactive default Benders' decomposition - TRUE, // cutlp - TRUE, // cutpseudo - TRUE, // cutrelax - FALSE) // shareauxvars - { - } + using DemandBenders::DemandBenders; SCIP_DECL_OBJCLONEABLECLONE(ObjCloneable* clone) override { - return new CloneableBenders(scip); + return new CloneableDemandBenders(scip, nullptr, 0.0); } SCIP_DECL_OBJCLONEABLEISCLONEABLE(iscloneable) override { return TRUE; } - SCIP_DECL_BENDERSCREATESUB(scip_createsub) - override - { - return SCIP_OKAY; - } - SCIP_DECL_BENDERSGETVAR(scip_getvar) - override - { - *mappedvar = nullptr; - return SCIP_OKAY; - } }; BOOST_AUTO_TEST_CASE(CopyBendersOnlyIfCloneable) { SCIP_Bool copyBenders { FALSE }; Model cloneable("Master"); - BOOST_REQUIRE(cloneable.includeBenders(1) != nullptr); + auto x = cloneable.addVar("x", 1, VarType::BINARY); + BOOST_REQUIRE(cloneable.includeBenders(1, x.getVar(), 0.0) != nullptr); BOOST_TEST(SCIPgetBoolParam(cloneable.scip(), "benders/copybenders", ©Benders) == SCIP_OKAY); BOOST_TEST(copyBenders); Model notCloneable("Master"); - auto x = notCloneable.addVar("x", 1, VarType::BINARY); - BOOST_REQUIRE(notCloneable.includeBenders(1, x.getVar(), 0.0) != nullptr); + auto y = notCloneable.addVar("y", 1, VarType::BINARY); + BOOST_REQUIRE(notCloneable.includeBenders(1, y.getVar(), 0.0) != nullptr); BOOST_TEST(SCIPgetBoolParam(notCloneable.scip(), "benders/copybenders", ©Benders) == SCIP_OKAY); BOOST_TEST(!copyBenders); } From b6b4a0efebe8fc3e988ed40b79bf8a00c6a3b9cf Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Fri, 16 Oct 2026 09:43:25 +0200 Subject: [PATCH 36/37] Share file handling between the reader and message handler tests Co-Authored-By: Claude Fable 5.1 --- test/test_objscip.cpp | 47 ++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/test/test_objscip.cpp b/test/test_objscip.cpp index c8a6f738..4dea8f89 100644 --- a/test/test_objscip.cpp +++ b/test/test_objscip.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -14,12 +13,33 @@ #include "scippp/parameters.hpp" #include "scippp/solving_statistics.hpp" -using namespace boost::algorithm; using namespace scippp; using namespace std; BOOST_AUTO_TEST_SUITE(ObjSCIP) +/** + * Creates a path to a not yet existing file with the given extension in the temporary directory. + */ +filesystem::path uniqueTempPath(const string& extension) +{ + return filesystem::temp_directory_path() + / boost::filesystem::unique_path("%%%%-%%%%-%%%%-%%%%." + extension).string(); +} + +/** + * Reads the whole file and removes it. + */ +string readAndRemove(const filesystem::path& path) +{ + ifstream file(path); + ostringstream content; + content << file.rdbuf(); + file.close(); + filesystem::remove(path); + return content.str(); +} + /** * Counts how often a new best solution is found. */ @@ -626,16 +646,10 @@ BOOST_AUTO_TEST_CASE(UseReader) model.addConstr(x1 + x2 >= 1, "capacity"); BOOST_TEST(model.include() != nullptr); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); - const auto FILE_NAME { filesystem::temp_directory_path() - / boost::filesystem::unique_path("%%%%-%%%%-%%%%-%%%%.cnt").string() }; + const auto FILE_NAME { uniqueTempPath("cnt") }; model.writeOrigProblem(filesystem::directory_entry(FILE_NAME)); BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); - ifstream file(FILE_NAME); - ostringstream content; - content << file.rdbuf(); - file.close(); - filesystem::remove(FILE_NAME); - BOOST_TEST(content.str() == "2 1\n"); + BOOST_TEST(readAndRemove(FILE_NAME) == "2 1\n"); } /** @@ -1048,8 +1062,7 @@ class FileMessageHandler : public scip::ObjMessagehdlr { BOOST_AUTO_TEST_CASE(UseMessageHandler) { - const auto LOG_FILE { filesystem::temp_directory_path() - / boost::filesystem::unique_path("%%%%-%%%%-%%%%-%%%%.log").string() }; + const auto LOG_FILE { uniqueTempPath("log") }; { // SCIP deletes the handler, and thereby closes the file, when the model is destructed Model model("Simple"); auto x1 = model.addVar("x_1", 1); @@ -1060,13 +1073,9 @@ BOOST_AUTO_TEST_CASE(UseMessageHandler) model.solve(); BOOST_TEST(model.getNSols() > 0); } - ifstream file(LOG_FILE); - ostringstream content; - content << file.rdbuf(); - BOOST_TEST(contains(content.str(), "SCIP Status")); - BOOST_TEST(contains(content.str(), "optimal solution found")); - file.close(); - filesystem::remove(LOG_FILE); + const auto LOG { readAndRemove(LOG_FILE) }; + BOOST_TEST(LOG.find("SCIP Status") != string::npos); + BOOST_TEST(LOG.find("optimal solution found") != string::npos); } BOOST_AUTO_TEST_SUITE_END() From 57437afc57c36be5020705e56ad7313dd95d7648 Mon Sep 17 00:00:00 2001 From: Ivo Hedtke Date: Fri, 16 Oct 2026 09:44:16 +0200 Subject: [PATCH 37/37] Drop copied comments from the coverage upload steps Co-Authored-By: Claude Fable 5.1 --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7aa9832e..246f9e4a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,7 +51,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml,./cobertura.xml disable_search: true - verbose: true # optional (default = false) + verbose: true fail_ci_if_error: true # Codecov treats JUnit test results as a separate report type, so they need their own upload - name: Upload Test Results @@ -62,7 +62,7 @@ jobs: files: ./boosttest.xml disable_search: true report_type: test_results - verbose: true # optional (default = false) + verbose: true fail_ci_if_error: true test_release_win: strategy: