diff --git a/include/scippp/model.hpp b/include/scippp/model.hpp index 47bd7915..8b8e05a7 100644 --- a/include/scippp/model.hpp +++ b/include/scippp/model.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -273,6 +274,28 @@ class Model { } } + /** + * Writes original problem to file. + * + * @since 1.1.0 + * @param filename output file name including extension + * @param genericNames using generic variable (x0, x1, ...) and constraint names (c0, c1, ...) instead of the + * user-given names? + * @attention Do not use an std::string or std::filesystem::path as argument \p filename, + * as this will call the other overload instead! + */ + void writeOrigProblem(const std::filesystem::directory_entry& filename, bool genericNames = false) const; + + /** + * Writes original problem to standard output. + * + * @since 1.1.0 + * @param extension file extension to derive the output format from + * @param genericNames using generic variable (x0, x1, ...) and constraint names (c0, c1, ...) instead of the + * user-given names? + */ + void writeOrigProblem(const std::string& extension, bool genericNames = false) const; + /** * Returns a pointer to the underlying %SCIP object. * diff --git a/source/model.cpp b/source/model.cpp index 5999a8f4..bd504ee3 100644 --- a/source/model.cpp +++ b/source/model.cpp @@ -115,6 +115,16 @@ void Model::setObjsense(Sense objsense) m_scipCallWrapper(SCIPsetObjsense(m_scip, static_cast(objsense))); } +void Model::writeOrigProblem(const std::filesystem::directory_entry& filename, bool genericNames) const +{ + m_scipCallWrapper(SCIPwriteOrigProblem(m_scip, filename.path().string().c_str(), nullptr, genericNames)); +} + +void Model::writeOrigProblem(const std::string& extension, bool genericNames) const +{ + m_scipCallWrapper(SCIPwriteOrigProblem(m_scip, nullptr, extension.data(), genericNames)); +} + Scip* Model::scip() const { return m_scip; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3d98cf46..72026e0c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,11 +1,16 @@ -find_package(Boost CONFIG REQUIRED COMPONENTS unit_test_framework) +find_package(Boost CONFIG REQUIRED COMPONENTS unit_test_framework filesystem) if (NOT TARGET Boost::unit_test_framework) add_library(Boost::unit_test_framework IMPORTED INTERFACE) set_property(TARGET Boost::unit_test_framework PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${Boost_INCLUDE_DIR}) set_property(TARGET Boost::unit_test_framework PROPERTY INTERFACE_LINK_LIBRARIES ${Boost_LIBRARIES}) endif () +if (NOT TARGET Boost::filesystem) + add_library(Boost::filesystem IMPORTED INTERFACE) + set_property(TARGET Boost::filesystem PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${Boost_INCLUDE_DIR}) + set_property(TARGET Boost::filesystem PROPERTY INTERFACE_LINK_LIBRARIES ${Boost_LIBRARIES}) +endif () file(GLOB TEST_SOURCES *.cpp) add_executable(tests ${TEST_SOURCES}) target_include_directories(tests SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) -target_link_libraries(tests PRIVATE ScipPP Boost::unit_test_framework) +target_link_libraries(tests PRIVATE ScipPP Boost::unit_test_framework Boost::filesystem) diff --git a/test/test_io.cpp b/test/test_io.cpp new file mode 100644 index 00000000..798d828e --- /dev/null +++ b/test/test_io.cpp @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include + +#include "scippp/model.hpp" + +using namespace boost::algorithm; +using namespace scippp; +using namespace std; + +class TempFile { + filesystem::path m_path { filesystem::temp_directory_path() }; + +public: + explicit TempFile(const string& extension) + { + m_path += boost::filesystem::unique_path("/%%%%-%%%%-%%%%-%%%%." + extension).string(); + } + ~TempFile() + { + filesystem::remove(m_path); + } + [[nodiscard]] filesystem::directory_entry path() const + { + return filesystem::directory_entry(m_path); + } + [[nodiscard]] string content() const + { + ifstream t(m_path); + ostringstream sstr; + sstr << t.rdbuf(); + return sstr.str(); + } +}; + +auto createModel() +{ + 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"); + model.setObjsense(Sense::MINIMIZE); + return model; +} + +BOOST_AUTO_TEST_SUITE(IO) + +BOOST_AUTO_TEST_CASE(FileLP) +{ + auto model { createModel() }; + + TempFile tf("lp"); + model.writeOrigProblem(tf.path()); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + auto content { tf.content() }; + BOOST_TEST(contains(content, "Obj: +1 x_1 +1 x_2")); + BOOST_TEST(contains(content, "capacity:")); + BOOST_TEST(contains(content, "equal:")); + BOOST_TEST(contains(content, "Minimize")); +} + +BOOST_AUTO_TEST_CASE(FileLPGenericNames) +{ + auto model { createModel() }; + + TempFile tf("lp"); + model.writeOrigProblem(tf.path(), true); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + auto content { tf.content() }; + BOOST_TEST(contains(content, "Obj: +1 x0 +1 x1")); + BOOST_TEST(contains(content, "c0:")); + BOOST_TEST(contains(content, "c1:")); + BOOST_TEST(contains(content, "Minimize")); +} + +BOOST_AUTO_TEST_CASE(FileMPS) +{ + auto model { createModel() }; + + TempFile tf("mps"); + model.writeOrigProblem(tf.path()); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); + auto content { tf.content() }; + BOOST_TEST(contains(content, "G capacity")); + BOOST_TEST(contains(content, "E equal")); + BOOST_TEST(contains(content, "\n MIN")); +} + +BOOST_AUTO_TEST_CASE(StdoutLP) +{ + auto model { createModel() }; + model.writeOrigProblem("lp"); + BOOST_TEST(model.getLastReturnCode() == SCIP_OKAY); +} + +BOOST_AUTO_TEST_CASE(StdoutWithInvalidExtension) +{ + auto model { createModel() }; + model.writeOrigProblem("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + BOOST_TEST(model.getLastReturnCode() == SCIP_PLUGINNOTFOUND); +} + +BOOST_AUTO_TEST_SUITE_END()