From 92ffa8f52519bba5f39b20d30fbf73d2ac015b9f Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Fri, 15 May 2026 18:57:16 +0000 Subject: [PATCH 01/30] Add LP format reader; accept .lp wherever .mps is accepted cuOptReadProblem, cuopt_cli, the Python ParseLp() wrapper, and the self-hosted client now dispatch on the input filename: a case-insensitive ".lp" suffix routes to a new LP parser; everything else (including .mps, .mps.gz, .mps.bz2, and extensionless inputs) continues to use parse_mps. The LP parser supports LP, MIP, and QP problems in the conventional LP dialect used by most commercial solvers (not the lpsolve variant). SOS, PWL, semi-continuous, user-cut, and general-constraint sections raise a ValidationError rather than silently mis-parsing. Quadratic-constraint support (QCMATRIX) remains MPS-only. Co-Authored-By: Claude Opus 4.7 (1M context) Signed-off-by: Miles Lubin --- cpp/cuopt_cli.cpp | 29 +- .../cuopt/linear_programming/cuopt_c.h | 12 +- .../cuopt/linear_programming/io/parser.hpp | 88 + ...ython_mps_parser.hpp => cython_parser.hpp} | 3 + cpp/src/io/CMakeLists.txt | 4 +- cpp/src/io/file_to_string.cpp | 246 ++ cpp/src/io/file_to_string.hpp | 24 + cpp/src/io/lp_parser.cpp | 1393 +++++++++ cpp/src/io/lp_parser.hpp | 84 + cpp/src/io/mps_parser.cpp | 226 +- cpp/src/io/mps_parser_internal.hpp | 6 - cpp/src/io/parser_finalize.hpp | 255 ++ ...ython_mps_parser.cpp => cython_parser.cpp} | 9 +- cpp/src/pdlp/cuopt_c.cpp | 11 +- cpp/tests/linear_programming/CMakeLists.txt | 4 +- .../c_api_tests/c_api_tests.cpp | 39 + .../linear_programming/mps_parser_test.cpp | 1401 --------- cpp/tests/linear_programming/parser_test.cpp | 2751 +++++++++++++++++ datasets/linear_programming/good-mps-1.lp | 12 + datasets/linear_programming/good-mps-1.lp.bz2 | Bin 0 -> 215 bytes datasets/linear_programming/good-mps-1.lp.gz | Bin 0 -> 199 bytes .../linear_programming/good-mps-fixed-var.lp | 11 + .../linear_programming/good-mps-free-var.lp | 11 + .../good-mps-lower-bound-inf-var.lp | 11 + .../good-mps-some-var-bounds.lp | 12 + .../good-mps-upper-bound-inf-var.lp | 12 + .../lp_model_with_var_bounds.lp | 14 + .../good-mip-mps-1.lp | 17 + .../good-mip-mps-no-bounds.lp | 12 + .../good-mip-mps-partial-bounds.lp | 15 + .../lp-qp-milp/examples/lp_file_example.c | 179 ++ .../cuopt-c/lp-qp-milp/examples/sample.lp | 9 + .../cuopt-c/lp-qp-milp/lp-qp-example.rst | 42 + docs/cuopt/source/cuopt-cli/cli-examples.rst | 16 + docs/cuopt/source/cuopt-cli/index.rst | 2 +- .../cuopt-server/examples/lp-examples.rst | 9 +- docs/cuopt/source/hidden/mps-api.rst | 7 +- .../cuopt/linear_programming/__init__.py | 2 +- .../linear_programming/mps_parser/__init__.py | 2 +- .../linear_programming/mps_parser/parser.pxd | 6 +- .../linear_programming/mps_parser/parser.py | 48 + .../mps_parser/parser_wrapper.pyx | 109 +- .../tests/linear_programming/test_parser.py | 117 + .../cuopt_sh_client/cuopt_self_host_client.py | 55 +- 44 files changed, 5584 insertions(+), 1731 deletions(-) rename cpp/include/cuopt/linear_programming/io/utilities/{cython_mps_parser.hpp => cython_parser.hpp} (80%) create mode 100644 cpp/src/io/file_to_string.cpp create mode 100644 cpp/src/io/file_to_string.hpp create mode 100644 cpp/src/io/lp_parser.cpp create mode 100644 cpp/src/io/lp_parser.hpp create mode 100644 cpp/src/io/parser_finalize.hpp rename cpp/src/io/utilities/{cython_mps_parser.cpp => cython_parser.cpp} (64%) delete mode 100644 cpp/tests/linear_programming/mps_parser_test.cpp create mode 100644 cpp/tests/linear_programming/parser_test.cpp create mode 100644 datasets/linear_programming/good-mps-1.lp create mode 100644 datasets/linear_programming/good-mps-1.lp.bz2 create mode 100644 datasets/linear_programming/good-mps-1.lp.gz create mode 100644 datasets/linear_programming/good-mps-fixed-var.lp create mode 100644 datasets/linear_programming/good-mps-free-var.lp create mode 100644 datasets/linear_programming/good-mps-lower-bound-inf-var.lp create mode 100644 datasets/linear_programming/good-mps-some-var-bounds.lp create mode 100644 datasets/linear_programming/good-mps-upper-bound-inf-var.lp create mode 100644 datasets/linear_programming/lp_model_with_var_bounds.lp create mode 100644 datasets/mixed_integer_programming/good-mip-mps-1.lp create mode 100644 datasets/mixed_integer_programming/good-mip-mps-no-bounds.lp create mode 100644 datasets/mixed_integer_programming/good-mip-mps-partial-bounds.lp create mode 100644 docs/cuopt/source/cuopt-c/lp-qp-milp/examples/lp_file_example.c create mode 100644 docs/cuopt/source/cuopt-c/lp-qp-milp/examples/sample.lp diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index cbfc0b6b9f..5a325f2a0f 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -38,17 +38,18 @@ static char cuda_module_loading_env[] = "CUDA_MODULE_LOADING=EAGER"; * @brief Command line interface for solving Linear Programming (LP) and Mixed Integer Programming * (MIP) problems using cuOpt * - * This CLI provides a simple interface to solve LP/MIP problems using cuOpt. It accepts MPS format - * input files and various solver parameters. + * This CLI provides a simple interface to solve LP/MIP problems using cuOpt. It accepts MPS, QPS, + * or LP format input files (dispatched automatically by extension; see run_single_file below for + * the full list of supported suffixes) and various solver parameters. * * Usage: * ``` - * cuopt_cli [OPTIONS] - * cuopt_cli [OPTIONS] + * cuopt_cli [OPTIONS] + * cuopt_cli [OPTIONS] * ``` * * Required arguments: - * - : Path to the MPS format input file containing the optimization problem + * - : Path to the MPS or LP format input file containing the optimization problem * * Optional arguments: * - --initial-solution: Path to initial solution file in SOL format @@ -84,7 +85,10 @@ inline cuopt::init_logger_t dummy_logger( /** * @brief Run a single file - * @param file_path Path to the MPS format input file containing the optimization problem + * @param file_path Path to the input file. Dispatched by extension: + * .lp/.lp.gz/.lp.bz2 → LP parser; + * .mps/.qps and their .gz/.bz2 variants → MPS parser; + * anything else is rejected. * @param initial_solution_file Path to initial solution file in SOL format * @param settings Merged solver settings (config file loaded in main, then CLI overrides applied) */ @@ -98,23 +102,21 @@ int run_single_file(const std::string& file_path, std::string base_filename = file_path.substr(file_path.find_last_of("/\\") + 1); - constexpr bool input_mps_strict = false; cuopt::linear_programming::io::mps_data_model_t mps_data_model; bool parsing_failed = false; auto timer = cuopt::timer_t(settings.get_parameter(CUOPT_TIME_LIMIT)); { CUOPT_LOG_INFO("Reading file %s", base_filename.c_str()); try { - mps_data_model = - cuopt::linear_programming::io::parse_mps(file_path, input_mps_strict); + mps_data_model = cuopt::linear_programming::io::parse_problem(file_path); } catch (const std::logic_error& e) { - CUOPT_LOG_ERROR("MPS parser execption: %s", e.what()); + CUOPT_LOG_ERROR("Parser exception: %s", e.what()); parsing_failed = true; } } if (parsing_failed) { auto log = dummy_logger(settings); - CUOPT_LOG_ERROR("Parsing MPS failed. Exiting!"); + CUOPT_LOG_ERROR("Parsing input file failed. Exiting!"); return -1; } CUOPT_LOG_INFO("Read file %s in %.2f seconds", base_filename.c_str(), timer.elapsed_time()); @@ -279,7 +281,10 @@ int main(int argc, char* argv[]) argparse::ArgumentParser program("cuopt_cli", version_string); // Define all arguments with appropriate defaults and help messages - program.add_argument("filename").help("input mps file").nargs(1).required(); + program.add_argument("filename") + .help("input MPS or LP file (dispatched by .lp / .mps extension)") + .nargs(1) + .required(); // FIXME: use a standard format for initial solution file program.add_argument("--initial-solution") diff --git a/cpp/include/cuopt/linear_programming/cuopt_c.h b/cpp/include/cuopt/linear_programming/cuopt_c.h index 4c4d44c764..6665ba0fac 100644 --- a/cpp/include/cuopt/linear_programming/cuopt_c.h +++ b/cpp/include/cuopt/linear_programming/cuopt_c.h @@ -100,12 +100,18 @@ cuopt_int_t cuOptGetVersion(cuopt_int_t* version_major, cuopt_int_t* version_patch); /** - * @brief Read an optimization problem from an MPS file. + * @brief Read an optimization problem from an MPS, QPS, or LP file. * - * @param[in] filename - The path to the MPS file. + * The file format is dispatched on the filename extension + * (case-insensitive): + * - ".lp", ".lp.gz", ".lp.bz2" → LP parser + * - ".mps", ".mps.gz", ".mps.bz2", ".qps", ".qps.gz", ".qps.bz2" → MPS parser + * - anything else (including no extension) is rejected. + * + * @param[in] filename - The path to the MPS, QPS, or LP file. * * @param[out] problem_ptr - A pointer to a cuOptOptimizationProblem. On output - * the problem will be created and initialized with the data from the MPS file + * the problem will be created and initialized with the data from the input file. * * @return A status code indicating success or failure. */ diff --git a/cpp/include/cuopt/linear_programming/io/parser.hpp b/cpp/include/cuopt/linear_programming/io/parser.hpp index ef55dabf52..434efa5f97 100644 --- a/cpp/include/cuopt/linear_programming/io/parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/parser.hpp @@ -9,6 +9,9 @@ #include +#include +#include +#include #include #include @@ -55,4 +58,89 @@ template mps_data_model_t parse_mps_from_string(std::string_view mps_contents, bool fixed_mps_format = false); +/** + * @brief Reads a linear, mixed-integer, or quadratic optimization problem from + * a file in LP format. + * + * The LP format is a human-readable alternative to MPS format. This parser + * supports the conventional LP dialect implemented by most commercial + * optimization solvers (not the lpsolve variant, which has a different + * syntax). + * + * Scope: LP, MIP, and QP problems are supported, plus semi-continuous + * variables (via a Semi-Continuous section; finite upper bound required) + * and quadratic constraints (QCQP; `<=` only). + * + * Quadratic terms appear inside `[ ... ]` blocks. The convention differs + * between objective and constraints: + * - Objective bracket: MUST be followed by `/ 2` (the LP file states + * coefficients in the `0.5 x^T Q x` convention). + * - Constraint bracket: MUST NOT be followed by `/ 2`; coefficients are + * taken at face value (`x^T Q x`). + * + * SOS constraints, PWL objectives, general constraints, and user cuts cause + * a ValidationError when encountered. + * + * Compressed inputs (.lp.gz, .lp.bz2) are supported when zlib / libbzip2 + * are installed (same dispatching as parse_mps). + * + * @param[in] lp_file_path Path to the LP file. + * @return mps_data_model_t A fully formed LP/MIP/QP problem representing the + * given file. + */ +template +mps_data_model_t parse_lp(const std::string& lp_file_path); + +/** + * @brief Reads an LP, MIP, or QP problem from in-memory file contents. + * + * This parses the same plain-text LP format as parse_lp(), but the input is + * already loaded in memory. Compressed .lp.gz/.lp.bz2 inputs are only + * supported by parse_lp() because compression is detected from the file + * path. Supports the same scope as parse_lp() (LP, MIP, QP, plus + * semi-continuous variables). + * + * @param[in] lp_contents LP file contents. + * @return mps_data_model_t A fully formed LP/MIP/QP problem representing the + * given content. + */ +template +mps_data_model_t parse_lp_from_string(std::string_view lp_contents); + +/** + * @brief Reads an optimization problem from a file, dispatching on the file + * extension. Extension matching is case-insensitive. + * + * Routing: + * - .mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2 → parse_mps() + * - .lp, .lp.gz, .lp.bz2 → parse_lp() + * - anything else → std::logic_error + * + * This is the entry point of choice for user-facing tools (CLI, C API) that + * want both formats to "just work" without an explicit format flag. + * + * @param[in] path Path to the input file. + * @return mps_data_model_t The parsed problem. + */ +template +inline mps_data_model_t parse_problem(const std::string& path) +{ + std::string lower(path); + std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + if (lower.ends_with(".mps") || lower.ends_with(".mps.gz") || lower.ends_with(".mps.bz2") || + lower.ends_with(".qps") || lower.ends_with(".qps.gz") || lower.ends_with(".qps.bz2")) { + return parse_mps(path); + } + if (lower.ends_with(".lp") || lower.ends_with(".lp.gz") || lower.ends_with(".lp.bz2")) { + return parse_lp(path); + } + throw std::logic_error( + "parse_problem: unrecognized input file extension. Supported (case-insensitive): " + ".mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2, .lp, .lp.gz, .lp.bz2. " + "Given path: " + + path); +} + } // namespace cuopt::linear_programming::io diff --git a/cpp/include/cuopt/linear_programming/io/utilities/cython_mps_parser.hpp b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp similarity index 80% rename from cpp/include/cuopt/linear_programming/io/utilities/cython_mps_parser.hpp rename to cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp index d787eb2dcf..eb4044d1d0 100644 --- a/cpp/include/cuopt/linear_programming/io/utilities/cython_mps_parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp @@ -17,5 +17,8 @@ namespace cython { std::unique_ptr> call_parse_mps( const std::string& mps_file_path, bool fixed_mps_format); +std::unique_ptr> call_parse_lp( + const std::string& lp_file_path); + } // namespace cython } // namespace cuopt diff --git a/cpp/src/io/CMakeLists.txt b/cpp/src/io/CMakeLists.txt index d91350a222..cc4affa890 100644 --- a/cpp/src/io/CMakeLists.txt +++ b/cpp/src/io/CMakeLists.txt @@ -5,12 +5,14 @@ set(PARSERS_SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/data_model_view.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/file_to_string.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/lp_parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mps_data_model.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mps_parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mps_writer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/writer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/utilities/cython_mps_parser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/utilities/cython_parser.cpp ) set(CUOPT_SRC_FILES ${CUOPT_SRC_FILES} ${PARSERS_SRC_FILES} PARENT_SCOPE) diff --git a/cpp/src/io/file_to_string.cpp b/cpp/src/io/file_to_string.cpp new file mode 100644 index 0000000000..f910eb977e --- /dev/null +++ b/cpp/src/io/file_to_string.cpp @@ -0,0 +1,246 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include + +#include + +#include +#include +#include +#include + +#ifdef MPS_PARSER_WITH_BZIP2 +#include +#endif // MPS_PARSER_WITH_BZIP2 + +#ifdef MPS_PARSER_WITH_ZLIB +#include +#endif // MPS_PARSER_WITH_ZLIB + +#if defined(MPS_PARSER_WITH_BZIP2) || defined(MPS_PARSER_WITH_ZLIB) +#include +#endif // MPS_PARSER_WITH_BZIP2 || MPS_PARSER_WITH_ZLIB + +namespace { +using cuopt::linear_programming::io::error_type_t; +using cuopt::linear_programming::io::mps_parser_expects; +using cuopt::linear_programming::io::mps_parser_expects_fatal; + +struct FcloseDeleter { + void operator()(FILE* fp) + { + mps_parser_expects_fatal( + fclose(fp) == 0, error_type_t::ValidationError, "Error closing input file!"); + } +}; +} // end namespace + +#ifdef MPS_PARSER_WITH_BZIP2 +namespace { +using BZ2_bzReadOpen_t = decltype(&BZ2_bzReadOpen); +using BZ2_bzReadClose_t = decltype(&BZ2_bzReadClose); +using BZ2_bzRead_t = decltype(&BZ2_bzRead); + +std::vector bz2_file_to_string(const std::string& file) +{ + struct DlCloseDeleter { + void operator()(void* fp) + { + mps_parser_expects_fatal( + dlclose(fp) == 0, error_type_t::ValidationError, "Error closing libbz2.so!"); + } + }; + struct BzReadCloseDeleter { + void operator()(void* f) + { + int bzerror; + if (f != nullptr) fptr(&bzerror, f); + mps_parser_expects_fatal( + bzerror == BZ_OK, error_type_t::ValidationError, "Error closing bzip2 file!"); + } + BZ2_bzReadClose_t fptr = nullptr; + }; + + std::unique_ptr lbz2handle{dlopen("libbz2.so", RTLD_LAZY)}; + mps_parser_expects( + lbz2handle != nullptr, + error_type_t::ValidationError, + "Could not open .bz2 file since libbz2.so was not found. In order to open .bz2 files " + "directly, please ensure libbzip2 is installed. Alternatively, decompress the .bz2 file " + "manually and open the uncompressed file. Given path: %s", + file.c_str()); + + BZ2_bzReadOpen_t BZ2_bzReadOpen = + reinterpret_cast(dlsym(lbz2handle.get(), "BZ2_bzReadOpen")); + BZ2_bzReadClose_t BZ2_bzReadClose = + reinterpret_cast(dlsym(lbz2handle.get(), "BZ2_bzReadClose")); + BZ2_bzRead_t BZ2_bzRead = reinterpret_cast(dlsym(lbz2handle.get(), "BZ2_bzRead")); + mps_parser_expects( + BZ2_bzReadOpen != nullptr && BZ2_bzReadClose != nullptr && BZ2_bzRead != nullptr, + error_type_t::ValidationError, + "Error loading libbzip2! Library version might be incompatible. Please decompress the .bz2 " + "file manually and open the uncompressed file. Given path: %s", + file.c_str()); + + std::unique_ptr fp{fopen(file.c_str(), "rb")}; + mps_parser_expects(fp != nullptr, + error_type_t::ValidationError, + "Error opening input file! Given path: %s", + file.c_str()); + int bzerror = BZ_OK; + std::unique_ptr bzfile{ + BZ2_bzReadOpen(&bzerror, fp.get(), 0, 0, nullptr, 0), {BZ2_bzReadClose}}; + mps_parser_expects(bzerror == BZ_OK, + error_type_t::ValidationError, + "Could not open bzip2 compressed file! Given path: %s", + file.c_str()); + + std::vector buf; + const size_t readbufsize = 1ull << 24; // 16MiB - just a guess. + std::vector readbuf(readbufsize); + while (bzerror == BZ_OK) { + const size_t bytes_read = BZ2_bzRead(&bzerror, bzfile.get(), readbuf.data(), readbuf.size()); + if (bzerror == BZ_OK || bzerror == BZ_STREAM_END) { + buf.insert(buf.end(), begin(readbuf), begin(readbuf) + bytes_read); + } + } + buf.push_back('\0'); + mps_parser_expects(bzerror == BZ_STREAM_END, + error_type_t::ValidationError, + "Error in bzip2 decompression of input file! Given path: %s", + file.c_str()); + return buf; +} +} // end namespace +#endif // MPS_PARSER_WITH_BZIP2 + +#ifdef MPS_PARSER_WITH_ZLIB +namespace { +using gzopen_t = decltype(&gzopen); +using gzclose_r_t = decltype(&gzclose_r); +using gzbuffer_t = decltype(&gzbuffer); +using gzread_t = decltype(&gzread); +using gzerror_t = decltype(&gzerror); + +std::vector zlib_file_to_string(const std::string& file) +{ + struct DlCloseDeleter { + void operator()(void* fp) + { + mps_parser_expects_fatal( + dlclose(fp) == 0, error_type_t::ValidationError, "Error closing libz.so!"); + } + }; + struct GzCloseDeleter { + void operator()(gzFile_s* f) + { + int err = fptr(f); + mps_parser_expects_fatal( + err == Z_OK, error_type_t::ValidationError, "Error closing gz file!"); + } + gzclose_r_t fptr = nullptr; + }; + + std::unique_ptr lzhandle{dlopen("libz.so.1", RTLD_LAZY)}; + mps_parser_expects( + lzhandle != nullptr, + error_type_t::ValidationError, + "Could not open .gz file since libz.so was not found. In order to open .gz files " + "directly, please ensure zlib is installed. Alternatively, decompress the .gz file " + "manually and open the uncompressed file. Given path: %s", + file.c_str()); + gzopen_t gzopen = reinterpret_cast(dlsym(lzhandle.get(), "gzopen")); + gzclose_r_t gzclose_r = reinterpret_cast(dlsym(lzhandle.get(), "gzclose_r")); + gzbuffer_t gzbuffer = reinterpret_cast(dlsym(lzhandle.get(), "gzbuffer")); + gzread_t gzread = reinterpret_cast(dlsym(lzhandle.get(), "gzread")); + gzerror_t gzerror = reinterpret_cast(dlsym(lzhandle.get(), "gzerror")); + mps_parser_expects( + gzopen != nullptr && gzclose_r != nullptr && gzbuffer != nullptr && gzread != nullptr && + gzerror != nullptr, + error_type_t::ValidationError, + "Error loading zlib! Library version might be incompatible. Please decompress the .gz file " + "manually and open the uncompressed file. Given path: %s", + file.c_str()); + std::unique_ptr gzfp{gzopen(file.c_str(), "rb"), {gzclose_r}}; + mps_parser_expects(gzfp != nullptr, + error_type_t::ValidationError, + "Error opening compressed input file! Given path: %s", + file.c_str()); + int zlib_status = gzbuffer(gzfp.get(), 1 << 20); // 1 MiB + mps_parser_expects(zlib_status == Z_OK, + error_type_t::ValidationError, + "Could not set zlib internal buffer size for decompression! Given path: %s", + file.c_str()); + std::vector buf; + const size_t readbufsize = 1ull << 24; // 16MiB + std::vector readbuf(readbufsize); + int bytes_read = -1; + while (bytes_read != 0) { + bytes_read = gzread(gzfp.get(), readbuf.data(), readbuf.size()); + if (bytes_read > 0) { buf.insert(buf.end(), begin(readbuf), begin(readbuf) + bytes_read); } + if (bytes_read < 0) { + gzerror(gzfp.get(), &zlib_status); + break; + } + } + buf.push_back('\0'); + mps_parser_expects(zlib_status == Z_OK, + error_type_t::ValidationError, + "Error in zlib decompression of input file! Given path: %s", + file.c_str()); + return buf; +} +} // end namespace +#endif // MPS_PARSER_WITH_ZLIB + +namespace cuopt::linear_programming::io::detail { + +std::vector file_to_string(const std::string& file) +{ +#ifdef MPS_PARSER_WITH_BZIP2 + if (file.size() > 4 && file.substr(file.size() - 4, 4) == ".bz2") { + return bz2_file_to_string(file); + } +#endif // MPS_PARSER_WITH_BZIP2 + +#ifdef MPS_PARSER_WITH_ZLIB + if (file.size() > 3 && file.substr(file.size() - 3, 3) == ".gz") { + return zlib_file_to_string(file); + } +#endif // MPS_PARSER_WITH_ZLIB + + // Faster than using C++ I/O + std::unique_ptr fp{fopen(file.c_str(), "r")}; + mps_parser_expects(fp != nullptr, + error_type_t::ValidationError, + "Error opening input file! Given path: %s", + file.c_str()); + + mps_parser_expects(fseek(fp.get(), 0L, SEEK_END) == 0, + error_type_t::ValidationError, + "Error seeking input file! Given path: %s", + file.c_str()); + const long bufsize = ftell(fp.get()); + mps_parser_expects(bufsize != -1L, + error_type_t::ValidationError, + "Error sizing input file! Given path: %s", + file.c_str()); + std::vector buf(bufsize + 1); + rewind(fp.get()); + + mps_parser_expects( + fread(buf.data(), sizeof(char), bufsize, fp.get()) == static_cast(bufsize), + error_type_t::ValidationError, + "Error reading input file! Given path: %s", + file.c_str()); + buf[bufsize] = '\0'; + + return buf; +} + +} // namespace cuopt::linear_programming::io::detail diff --git a/cpp/src/io/file_to_string.hpp b/cpp/src/io/file_to_string.hpp new file mode 100644 index 0000000000..94b2df821d --- /dev/null +++ b/cpp/src/io/file_to_string.hpp @@ -0,0 +1,24 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include + +namespace cuopt::linear_programming::io::detail { + +// Reads `file` into a buffer and appends a trailing '\0'. +// +// The dispatcher looks at the extension: +// - ".bz2" → libbz2 (dlopen'd at runtime), if MPS_PARSER_WITH_BZIP2. +// - ".gz" → libz (dlopen'd at runtime), if MPS_PARSER_WITH_ZLIB. +// - otherwise → plain fopen. +// The returned buffer's size includes the null terminator. +std::vector file_to_string(const std::string& file); + +} // namespace cuopt::linear_programming::io::detail diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp new file mode 100644 index 0000000000..7341c7fa9e --- /dev/null +++ b/cpp/src/io/lp_parser.cpp @@ -0,0 +1,1393 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cuopt::linear_programming::io { + +namespace { + +// =========================================================================== +// Small character / string helpers +// =========================================================================== + +// Per the LP-format convention, variable names may use letters and a specific +// set of punctuation characters. Characters used by the grammar (+, -, *, ^, +// :, =, <, >, [, ], \, whitespace) are excluded. Digits and '.' are valid +// mid-name but not as the starting character. +bool is_name_start_char(char c) +{ + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) return true; + switch (c) { + case '!': + case '"': + case '#': + case '$': + case '%': + case '&': + case '(': + case ')': + case ',': + case ';': + case '?': + case '@': + case '_': + case '`': + case '\'': + case '{': + case '}': + case '|': + case '~': return true; + default: return false; + } +} + +bool is_name_char(char c) +{ + if (is_name_start_char(c)) return true; + return (c >= '0' && c <= '9') || c == '.' || c == '/'; +} + +char to_lower(char c) +{ + if (c >= 'A' && c <= 'Z') return static_cast(c - 'A' + 'a'); + return c; +} + +std::string lowercase(std::string_view s) +{ + std::string out; + out.reserve(s.size()); + for (char c : s) + out.push_back(to_lower(c)); + return out; +} + +// =========================================================================== +// LP section-keyword classifiers (case-insensitive; callers pass lowercased) +// =========================================================================== + +bool is_objective_min_keyword(std::string_view lower) +{ + return lower == "minimize" || lower == "minimum" || lower == "min"; +} + +bool is_objective_max_keyword(std::string_view lower) +{ + return lower == "maximize" || lower == "maximum" || lower == "max"; +} + +bool is_bounds_keyword(std::string_view lower) { return lower == "bounds" || lower == "bound"; } + +bool is_generals_keyword(std::string_view lower) +{ + return lower == "generals" || lower == "general" || lower == "gen" || lower == "integer" || + lower == "integers"; +} + +bool is_binaries_keyword(std::string_view lower) +{ + return lower == "binaries" || lower == "binary" || lower == "bin"; +} + +bool is_end_keyword(std::string_view lower) { return lower == "end"; } + +bool is_free_keyword(std::string_view lower) { return lower == "free"; } + +bool is_infinity_text(std::string_view lower) { return lower == "inf" || lower == "infinity"; } + +// Builds the symmetric Q in CSR from LP-format raw upper-triangular triples. +// Each input triple (i, j, c) with i <= j represents `c * x_i * x_j` in the +// LP source. The output Q satisfies x^T Q x = sum of those terms. +// Diagonal (i == j): Q[i,i] = c (one entry). +// Off-diagonal (i != j): Q[i,j] = Q[j,i] = c/2 (two entries; symmetric split). +template +void build_symmetric_q_csr(const std::vector>& raw_triples, + i_t n_vars, + std::vector& out_values, + std::vector& out_indices, + std::vector& out_offsets) +{ + std::vector>> row_data(n_vars); + for (const auto& [i, j, c] : raw_triples) { + if (i == j) { + row_data[i].emplace_back(i, c); + } else { + row_data[i].emplace_back(j, c / f_t(2)); + row_data[j].emplace_back(i, c / f_t(2)); + } + } + for (auto& row : row_data) { + std::sort(row.begin(), row.end()); + } + out_offsets.clear(); + out_indices.clear(); + out_values.clear(); + out_offsets.reserve(static_cast(n_vars) + 1); + out_offsets.push_back(0); + for (i_t r = 0; r < n_vars; ++r) { + for (const auto& [col, val] : row_data[r]) { + out_values.push_back(val); + out_indices.push_back(col); + } + out_offsets.push_back(static_cast(out_values.size())); + } +} + +// =========================================================================== +// Token stream +// =========================================================================== + +// Kinds of tokens produced by the LP tokenizer. The grammar is small enough +// that a hand-written scanner is easier to follow than a regex engine. +enum class LpTokenKind { + Number, // 12, -3.5, 1e-6 + Name, // variable names and section keywords (also the literal "inf") + Plus, // + + Minus, // - + Star, // * + Caret, // ^ + Slash, // / + LessEq, // <= (and < treated as <=) + GreaterEq, // >= (and > treated as >=) + Equal, // = + LBracket, // [ + RBracket, // ] + Colon, // : + Eof, +}; + +struct LpToken { + LpTokenKind kind; + // Owned copy of the token text so the token stream is independent of the + // backing file buffer. + std::string text; + int line; + // True when this is the first non-whitespace/non-comment token on its line. + // Used to detect section headers without emitting newline tokens. + bool is_line_start; +}; + +// =========================================================================== +// Parsing engine — holds all transient parsing state and writes directly +// into the lp_parser_t's public fields. Strictly internal to this TU. +// =========================================================================== + +template +class LpParseEngine { + public: + LpParseEngine(lp_parser_t& out, const std::string& file); + // Parses `text` directly (used by parse_lp_from_string()). + LpParseEngine(lp_parser_t& out, std::string_view text); + + private: + lp_parser_t& out_; + std::vector tokens_; + size_t tok_pos_{0}; + + std::unordered_map var_names_map_{}; + std::unordered_map row_names_map_{}; + std::unordered_set bounds_defined_for_var_id_{}; + // Variables for which a lower bound was set explicitly in the Bounds + // section (via 'x >= lb', 'x = v', 'x free', or 'lb <= x ...'). Used to + // reject 'x <= -1' forms with no paired lower bound: the default lower of + // 0 would collide with the negative upper and silently make the variable + // infeasible. + std::unordered_set lower_explicitly_set_{}; + // Counter used to generate row names for unlabeled constraints (R0, R1, ...). + i_t anon_row_counter_{0}; + + // File → token stream. + void read_and_tokenize(const std::string& file); + void tokenize(const std::string& text); + + // Token stream helpers. + const LpToken& peek(size_t lookahead = 0) const; + const LpToken& advance(); + bool at_eof() const; + bool match(LpTokenKind kind); + void expect(LpTokenKind kind, const char* context); + static bool name_equals_ci(const LpToken& tok, std::string_view lower); + bool is_infinity_keyword(const LpToken& tok) const; + f_t number_from_text(const std::string& text) const; + + // Variable bookkeeping. + i_t get_or_add_var(std::string_view name); + + // Top-level dispatch. + void parse_all(); + + // Section parsers. + void parse_objective_section(); + void parse_constraints_section(); + void parse_bounds_section(); + void parse_integer_list_section(bool is_binary); + void parse_semi_continuous_section(); + + // Expression parsers. + struct LinearTerm { + i_t var_id; + f_t coeff; + }; + void parse_linear_expression(std::vector& out_terms, f_t& out_constant); + + // Where a quadratic '[ ... ]' bracket appears. The two roles differ in + // post-processing: + // Objective: must be followed by '/ 2'; the inner-coefficient convention + // is QUADOBJ-style 0.5 x^T Q x, so off-diagonals are halved + // and linear-inside-bracket terms also get /2. + // Constraint: must NOT be followed by '/ 2'; coefficients are taken at + // face value (x^T Q x); bracket must contain at least one + // quadratic term. + enum class BracketRole { Objective, Constraint }; + void parse_quadratic_bracket(std::vector& out_linear, + int outer_sign, + BracketRole role, + std::vector>& out_quad_entries); + + // Atomic readers. + f_t parse_signed_number(); + + // Section header classification. + enum class SectionKind { + None, + Objective, + Constraints, + Bounds, + Generals, + Binaries, + SemiContinuous, + End, + }; + SectionKind try_consume_section_header(); + void reject_unsupported_section(); + bool at_section_boundary() const; +}; + +// ---- Constructor ---------------------------------------------------------- + +template +LpParseEngine::LpParseEngine(lp_parser_t& out, const std::string& file) + : out_(out) +{ + read_and_tokenize(file); + parse_all(); +} + +template +LpParseEngine::LpParseEngine(lp_parser_t& out, std::string_view text) + : out_(out) +{ + // Skip read_and_tokenize: the caller already supplied the LP text. + // Make a contiguous null-terminated string for tokenize(). + std::string buffered(text); + tokenize(buffered); + parse_all(); +} + +// ---- File I/O + tokenizer ------------------------------------------------- + +template +void LpParseEngine::read_and_tokenize(const std::string& file) +{ + // Delegates to the shared helper so .lp.gz / .lp.bz2 are handled the same + // way as .mps.gz / .mps.bz2 (dlopen-loaded libz / libbz2). The returned + // buffer is null-terminated; strip it before constructing the string view + // since `tokenize` walks the entire string range. + auto buf = detail::file_to_string(file); + std::string text(buf.data(), buf.size() > 0 ? buf.size() - 1 : 0); + tokenize(text); +} + +template +void LpParseEngine::tokenize(const std::string& text) +{ + size_t i = 0; + int line = 1; + bool at_start = true; // next non-whitespace token starts a new line + const size_t n = text.size(); + + auto push = [&](LpTokenKind kind, std::string s) { + tokens_.push_back(LpToken{kind, std::move(s), line, at_start}); + at_start = false; + }; + + while (i < n) { + char c = text[i]; + + if (c == '\n') { + ++line; + at_start = true; + ++i; + continue; + } + if (c == '\r') { + ++i; + continue; + } + if (c == '\\') { // LP comment: '\' through end of line + while (i < n && text[i] != '\n') + ++i; + continue; + } + if (c == ' ' || c == '\t') { + ++i; + continue; + } + + // Single-character punctuation. + switch (c) { + case '+': + push(LpTokenKind::Plus, "+"); + ++i; + continue; + case '-': + push(LpTokenKind::Minus, "-"); + ++i; + continue; + case '*': + push(LpTokenKind::Star, "*"); + ++i; + continue; + case '^': + push(LpTokenKind::Caret, "^"); + ++i; + continue; + case '/': + push(LpTokenKind::Slash, "/"); + ++i; + continue; + case '[': + push(LpTokenKind::LBracket, "["); + ++i; + continue; + case ']': + push(LpTokenKind::RBracket, "]"); + ++i; + continue; + case ':': + push(LpTokenKind::Colon, ":"); + ++i; + continue; + case '=': + // Accept the swapped spellings: '=<' ≡ '<=' and '=>' ≡ '>='. + if (i + 1 < n && text[i + 1] == '<') { + push(LpTokenKind::LessEq, "=<"); + i += 2; + } else if (i + 1 < n && text[i + 1] == '>') { + push(LpTokenKind::GreaterEq, "=>"); + i += 2; + } else { + push(LpTokenKind::Equal, "="); + ++i; + } + continue; + default: break; + } + + // Relation operators. Our LP dialect treats bare '<' as '<=' and bare + // '>' as '>='; we do the same for robustness. + if (c == '<') { + if (i + 1 < n && text[i + 1] == '=') { + push(LpTokenKind::LessEq, "<="); + i += 2; + } else { + push(LpTokenKind::LessEq, "<"); + ++i; + } + continue; + } + if (c == '>') { + if (i + 1 < n && text[i + 1] == '=') { + push(LpTokenKind::GreaterEq, ">="); + i += 2; + } else { + push(LpTokenKind::GreaterEq, ">"); + ++i; + } + continue; + } + + // Numbers: [0-9]+ ('.' [0-9]*)? ([eE] [+-]? [0-9]+)? | '.' [0-9]+ ... + if ((c >= '0' && c <= '9') || + (c == '.' && i + 1 < n && text[i + 1] >= '0' && text[i + 1] <= '9')) { + size_t start = i; + while (i < n && text[i] >= '0' && text[i] <= '9') + ++i; + if (i < n && text[i] == '.') { + ++i; + while (i < n && text[i] >= '0' && text[i] <= '9') + ++i; + } + if (i < n && (text[i] == 'e' || text[i] == 'E')) { + ++i; + if (i < n && (text[i] == '+' || text[i] == '-')) ++i; + mps_parser_expects(i < n && text[i] >= '0' && text[i] <= '9', + error_type_t::ValidationError, + "Malformed number (missing exponent digits) at line %d", + line); + while (i < n && text[i] >= '0' && text[i] <= '9') + ++i; + } + push(LpTokenKind::Number, text.substr(start, i - start)); + continue; + } + + // Names: [A-Za-z_] [A-Za-z0-9_.]* + if (is_name_start_char(c)) { + size_t start = i; + while (i < n && is_name_char(text[i])) + ++i; + push(LpTokenKind::Name, text.substr(start, i - start)); + continue; + } + + mps_parser_expects(false, + error_type_t::ValidationError, + "Unexpected character '%c' (0x%02x) at line %d in LP file", + c, + static_cast(static_cast(c)), + line); + } + + tokens_.push_back(LpToken{LpTokenKind::Eof, "", line, true}); +} + +// ---- Token stream helpers -------------------------------------------------- + +template +const LpToken& LpParseEngine::peek(size_t lookahead) const +{ + size_t idx = tok_pos_ + lookahead; + if (idx >= tokens_.size()) return tokens_.back(); // guaranteed Eof token + return tokens_[idx]; +} + +template +const LpToken& LpParseEngine::advance() +{ + const LpToken& t = tokens_[tok_pos_]; + if (tok_pos_ + 1 < tokens_.size()) ++tok_pos_; + return t; +} + +template +bool LpParseEngine::at_eof() const +{ + return peek().kind == LpTokenKind::Eof; +} + +template +bool LpParseEngine::match(LpTokenKind kind) +{ + if (peek().kind == kind) { + advance(); + return true; + } + return false; +} + +template +void LpParseEngine::expect(LpTokenKind kind, const char* context) +{ + mps_parser_expects(peek().kind == kind, + error_type_t::ValidationError, + "LP parse error at line %d: expected %s, got '%s'", + peek().line, + context, + peek().text.c_str()); + advance(); +} + +template +bool LpParseEngine::name_equals_ci(const LpToken& tok, std::string_view lower) +{ + if (tok.kind != LpTokenKind::Name) return false; + if (tok.text.size() != lower.size()) return false; + for (size_t i = 0; i < tok.text.size(); ++i) { + if (to_lower(tok.text[i]) != lower[i]) return false; + } + return true; +} + +template +bool LpParseEngine::is_infinity_keyword(const LpToken& tok) const +{ + return tok.kind == LpTokenKind::Name && is_infinity_text(lowercase(tok.text)); +} + +template +f_t LpParseEngine::number_from_text(const std::string& text) const +{ + try { + if constexpr (std::is_same_v) { + return std::stof(text); + } else { + return std::stod(text); + } + } catch (...) { + mps_parser_expects(false, + error_type_t::ValidationError, + "LP parse error: could not parse number '%s'", + text.c_str()); + } + return f_t(0); // unreachable; mps_parser_expects throws +} + +// ---- Variable bookkeeping -------------------------------------------------- + +template +i_t LpParseEngine::get_or_add_var(std::string_view name) +{ + std::string key(name); + auto it = var_names_map_.find(key); + if (it != var_names_map_.end()) return it->second; + i_t id = static_cast(out_.var_names.size()); + out_.var_names.push_back(key); + var_names_map_.emplace(std::move(key), id); + out_.var_types.push_back('C'); + out_.c_values.push_back(f_t(0)); + out_.variable_lower_bounds.push_back(f_t(0)); + out_.variable_upper_bounds.push_back(std::numeric_limits::infinity()); + return id; +} + +// ---- Section header detection --------------------------------------------- + +template +bool LpParseEngine::at_section_boundary() const +{ + if (at_eof()) return true; + const LpToken& t = peek(); + if (!t.is_line_start || t.kind != LpTokenKind::Name) return false; + std::string lower = lowercase(t.text); + + if (is_objective_min_keyword(lower) || is_objective_max_keyword(lower)) return true; + if (is_bounds_keyword(lower)) return true; + if (is_generals_keyword(lower)) return true; + if (is_binaries_keyword(lower)) return true; + if (is_end_keyword(lower)) return true; + + // Multi-word section headers: "Subject To" / "Such That" are supported; + // "Lazy Constraints", "User Cuts", and "General Constraints" are + // recognized as boundaries so the prior section ends cleanly, but + // reject_unsupported_section() throws once dispatch reaches them. + const LpToken& t2 = peek(1); + if (lower == "subject" && name_equals_ci(t2, "to")) return true; + if (lower == "such" && name_equals_ci(t2, "that")) return true; + if (lower == "st" || lower == "st." || lower == "s.t.") return true; + if (lower == "lazy" && name_equals_ci(t2, "constraints")) return true; + if (lower == "user" && name_equals_ci(t2, "cuts")) return true; + if (lower == "general" && name_equals_ci(t2, "constraints")) return true; + + // Semi-Continuous section header (supported); plus other section headers + // that we recognize as boundaries (some supported, some unsupported — + // dispatch decides). + if (lower == "semi" && peek(1).kind == LpTokenKind::Minus && + name_equals_ci(peek(2), "continuous")) + return true; + if (lower == "sos") return true; + if (lower == "pwlobj") return true; + if (lower == "scenarios" || lower == "scenario") return true; + + return false; +} + +template +void LpParseEngine::reject_unsupported_section() +{ + std::string name = peek().text; + std::string lower = lowercase(name); + // Compose a useful display name for multi-word headers. + if (lower == "user" && name_equals_ci(peek(1), "cuts")) { + name = "User Cuts"; + } else if (lower == "lazy" && name_equals_ci(peek(1), "constraints")) { + name = "Lazy Constraints"; + } else if (lower == "general" && name_equals_ci(peek(1), "constraints")) { + name = "General Constraints"; + } + mps_parser_expects(false, + error_type_t::ValidationError, + "LP section '%s' is not supported (scope is LP/MIP/QP only)", + name.c_str()); +} + +template +typename LpParseEngine::SectionKind LpParseEngine::try_consume_section_header() +{ + if (at_eof()) return SectionKind::None; + const LpToken& t = peek(); + mps_parser_expects(t.is_line_start && t.kind == LpTokenKind::Name, + error_type_t::ValidationError, + "LP parse error at line %d: expected section header, got '%s'", + t.line, + t.text.c_str()); + std::string lower = lowercase(t.text); + + if (is_objective_min_keyword(lower)) { + out_.maximize = false; + advance(); + return SectionKind::Objective; + } + if (is_objective_max_keyword(lower)) { + out_.maximize = true; + advance(); + return SectionKind::Objective; + } + if (lower == "subject" && name_equals_ci(peek(1), "to")) { + advance(); + advance(); + return SectionKind::Constraints; + } + if (lower == "such" && name_equals_ci(peek(1), "that")) { + advance(); + advance(); + return SectionKind::Constraints; + } + if (lower == "st" || lower == "st." || lower == "s.t.") { + advance(); + return SectionKind::Constraints; + } + if (is_bounds_keyword(lower)) { + advance(); + return SectionKind::Bounds; + } + if (is_generals_keyword(lower)) { + // "General" alone means Generals; "General Constraints" is unsupported. + if (lower == "general" && name_equals_ci(peek(1), "constraints")) { + reject_unsupported_section(); + } + advance(); + return SectionKind::Generals; + } + if (is_binaries_keyword(lower)) { + advance(); + return SectionKind::Binaries; + } + // "Semi-Continuous" (3 tokens: semi - continuous). + if (lower == "semi" && peek(1).kind == LpTokenKind::Minus && + name_equals_ci(peek(2), "continuous")) { + advance(); + advance(); + advance(); + return SectionKind::SemiContinuous; + } + if (is_end_keyword(lower)) { + advance(); + return SectionKind::End; + } + + // Known unsupported sections → throw with a clear message. + reject_unsupported_section(); + return SectionKind::None; // unreachable +} + +// ---- Expression parsing --------------------------------------------------- + +template +f_t LpParseEngine::parse_signed_number() +{ + int sign = 1; + if (match(LpTokenKind::Minus)) { + sign = -1; + } else { + match(LpTokenKind::Plus); // optional leading '+' + } + if (is_infinity_keyword(peek())) { + advance(); + return sign > 0 ? std::numeric_limits::infinity() : -std::numeric_limits::infinity(); + } + mps_parser_expects(peek().kind == LpTokenKind::Number, + error_type_t::ValidationError, + "LP parse error at line %d: expected a number, got '%s'", + peek().line, + peek().text.c_str()); + f_t val = number_from_text(peek().text); + advance(); + return sign * val; +} + +template +void LpParseEngine::parse_linear_expression(std::vector& out_terms, + f_t& out_constant) +{ + out_constant = f_t(0); + int sign = 1; + bool first = true; + + while (true) { + // A quadratic bracket ends the linear expression. If the bracket is + // preceded by a sign, leave the sign unconsumed so the caller can + // attribute it to the bracket. + if (peek().kind == LpTokenKind::LBracket) break; + if ((peek().kind == LpTokenKind::Plus || peek().kind == LpTokenKind::Minus) && + peek(1).kind == LpTokenKind::LBracket) { + break; + } + + if (peek().kind == LpTokenKind::Plus) { + advance(); + sign = 1; + } else if (peek().kind == LpTokenKind::Minus) { + advance(); + sign = -1; + } else if (!first) { + // No sign between terms → expression ends here. (Relation tokens, + // ']', section headers, EOF all terminate.) + break; + } + + // A term is: (number ('*')?)? varname | number (constant) | varname. + // 'inf' is a bounds-only keyword and never appears here. + f_t coeff = f_t(1); + bool had_coeff = false; + if (peek().kind == LpTokenKind::Number) { + coeff = number_from_text(peek().text); + had_coeff = true; + advance(); + match(LpTokenKind::Star); // optional '*' + } + + if (peek().kind == LpTokenKind::Name && !at_section_boundary() && + !is_free_keyword(lowercase(peek().text)) && !is_infinity_keyword(peek())) { + std::string var_name = peek().text; + advance(); + i_t id = get_or_add_var(var_name); + out_terms.push_back({id, sign * coeff}); + } else if (had_coeff) { + // It was a pure number → contributes to the constant. + out_constant += sign * coeff; + } else { + // Nothing consumed this iteration → not a term, stop. + if (!first) { + // We consumed a sign without a term: malformed. + mps_parser_expects(false, + error_type_t::ValidationError, + "LP parse error at line %d: expected a term after '+' or '-'", + peek().line); + } + break; + } + + first = false; + sign = 1; + } +} + +template +void LpParseEngine::parse_quadratic_bracket( + std::vector& out_linear, + int outer_sign, + BracketRole role, + std::vector>& out_quad_entries) +{ + expect(LpTokenKind::LBracket, "'[' at start of quadratic section"); + + // Accumulate raw LP-format entries first (diagonal vs off-diagonal), then + // apply the role-specific convention and outer sign after we see the + // closing bracket. + std::vector> raw_quad; + + int sign = 1; + bool first = true; + while (peek().kind != LpTokenKind::RBracket) { + mps_parser_expects(!at_eof(), + error_type_t::ValidationError, + "LP parse error: unterminated quadratic '[' section"); + + if (peek().kind == LpTokenKind::Plus) { + advance(); + sign = 1; + } else if (peek().kind == LpTokenKind::Minus) { + advance(); + sign = -1; + } else if (!first) { + mps_parser_expects(false, + error_type_t::ValidationError, + "LP parse error at line %d: expected '+' or '-' between " + "quadratic terms, got '%s'", + peek().line, + peek().text.c_str()); + } + + f_t coeff = f_t(1); + if (peek().kind == LpTokenKind::Number) { + coeff = number_from_text(peek().text); + advance(); + match(LpTokenKind::Star); // optional + } + + mps_parser_expects(peek().kind == LpTokenKind::Name, + error_type_t::ValidationError, + "LP parse error at line %d: expected variable name in quadratic term", + peek().line); + std::string var1 = peek().text; + advance(); + i_t i1 = get_or_add_var(var1); + + if (match(LpTokenKind::Caret)) { + // Must be "^ 2". + mps_parser_expects(peek().kind == LpTokenKind::Number && peek().text == "2", + error_type_t::ValidationError, + "LP parse error at line %d: only 'x ^ 2' is supported in quadratic " + "terms (got '%s')", + peek().line, + peek().text.c_str()); + advance(); + raw_quad.emplace_back(i1, i1, sign * coeff); + } else if (match(LpTokenKind::Star)) { + mps_parser_expects(peek().kind == LpTokenKind::Name, + error_type_t::ValidationError, + "LP parse error at line %d: expected variable name after '*' in " + "quadratic cross term", + peek().line); + std::string var2 = peek().text; + advance(); + i_t i2 = get_or_add_var(var2); + // Store in upper-triangular form (i <= j) to match QUADOBJ convention. + i_t a = std::min(i1, i2); + i_t b = std::max(i1, i2); + raw_quad.emplace_back(a, b, sign * coeff); + } else { + // Purely linear term inside the brackets — permitted as long as the + // surrounding /2 convention is respected (the linear term is scaled + // the same way as the quadratic ones). + out_linear.push_back({i1, sign * coeff}); + } + + first = false; + sign = 1; + } + expect(LpTokenKind::RBracket, "closing ']' of quadratic section"); + + const f_t sign_scale = static_cast(outer_sign); + + if (role == BracketRole::Objective) { + // Require the "/ 2" suffix after a quadratic objective expression. + // Without it there is no ambiguity-free way to tell whether the user + // meant /2 and forgot vs. intended bare coefficients, so we enforce the + // stricter form. + mps_parser_expects(peek().kind == LpTokenKind::Slash && peek(1).kind == LpTokenKind::Number && + peek(1).text == "2", + error_type_t::ValidationError, + "LP parse error at line %d: quadratic expression '[ ... ]' in the " + "objective must be followed by '/ 2'", + peek().line); + advance(); // '/' + advance(); // '2' + + // Apply the /2 convention and the QUADOBJ-convention scaling so that + // finalize_problem()'s expansion to full symmetric and *0.5 factor yield + // the right Q for cuOpt's 'x^T Q x' form. + // + // LP term ([...]/2): → quadobj entry + // diagonal c x^2 (actual = c/2) → c + // off-diag c x*y (actual = c/2) → c/2 + for (auto& [a, b, v] : raw_quad) { + if (a != b) { + // off-diagonal: /2 to recover Q[i,j] = Q[j,i] after the later x^T Q x expansion. + v /= f_t(2); + } + out_quad_entries.emplace_back(a, b, sign_scale * v); + } + // Linear terms inside the brackets pick up the /2 scaling and the outer sign. + for (auto& lt : out_linear) + lt.coeff /= f_t(2); + if (outer_sign < 0) { + for (auto& lt : out_linear) + lt.coeff = -lt.coeff; + } + } else { + // Constraint: '/ 2' is forbidden — the LP convention is that constraint + // quadratic brackets carry bare face-value coefficients of x^T Q x. + mps_parser_expects(!(peek().kind == LpTokenKind::Slash && peek(1).kind == LpTokenKind::Number && + peek(1).text == "2"), + error_type_t::ValidationError, + "LP parse error at line %d: quadratic expression '[ ... ]' in a " + "constraint must NOT be followed by '/ 2' (the '/ 2' suffix is " + "reserved for the objective)", + peek().line); + // A bracket containing only linear terms is meaningless in a constraint + // — the user can write the same constraint without the brackets. + mps_parser_expects(!raw_quad.empty(), + error_type_t::ValidationError, + "LP parse error at line %d: quadratic bracket '[ ... ]' in a " + "constraint must contain at least one quadratic term", + peek().line); + + // Coefficients are at face value — the post-pass that flushes the + // quadratic_constraint_block_t to the data model handles the symmetric + // expansion and the /2 split for off-diagonals. + for (auto& [a, b, v] : raw_quad) { + out_quad_entries.emplace_back(a, b, sign_scale * v); + } + if (outer_sign < 0) { + for (auto& lt : out_linear) + lt.coeff = -lt.coeff; + } + } +} + +// ---- Section bodies ------------------------------------------------------- + +template +void LpParseEngine::parse_objective_section() +{ + // Optional "name:" label. + if (peek().kind == LpTokenKind::Name && peek(1).kind == LpTokenKind::Colon && + !at_section_boundary()) { + out_.objective_name = peek().text; + advance(); + advance(); + } + + std::vector linear; + f_t constant = 0; + parse_linear_expression(linear, constant); + + // Optional quadratic bracket, possibly preceded by a sign. In this LP + // dialect the bracket sits inside the objective expression and can + // appear before or after linear terms; we support one bracket followed + // by (possibly) more linear terms. + int quad_sign = 1; + if (peek().kind == LpTokenKind::Plus && peek(1).kind == LpTokenKind::LBracket) { + advance(); + } else if (peek().kind == LpTokenKind::Minus && peek(1).kind == LpTokenKind::LBracket) { + advance(); + quad_sign = -1; + } + if (peek().kind == LpTokenKind::LBracket) { + std::vector in_bracket_linear; + parse_quadratic_bracket( + in_bracket_linear, quad_sign, BracketRole::Objective, out_.quadobj_entries); + for (const auto& lt : in_bracket_linear) + linear.push_back(lt); + + // More linear terms may follow the bracket. + std::vector more; + f_t more_constant = 0; + parse_linear_expression(more, more_constant); + for (const auto& lt : more) + linear.push_back(lt); + constant += more_constant; + } + + // Apply linear terms to the objective vector. Coefficients accumulate in + // case the same variable appears twice. + for (const auto& lt : linear) { + if (static_cast(lt.var_id) >= out_.c_values.size()) { + out_.c_values.resize(lt.var_id + 1, f_t(0)); + } + out_.c_values[lt.var_id] += lt.coeff; + } + // A constant term in the objective becomes the objective offset. + out_.objective_offset_value += constant; +} + +template +void LpParseEngine::parse_constraints_section() +{ + while (!at_section_boundary()) { + // Optional "name:" label — present iff the first two tokens are Name + ':'. + std::string row_name; + if (peek().kind == LpTokenKind::Name && peek(1).kind == LpTokenKind::Colon) { + row_name = peek().text; + advance(); + advance(); + } else { + row_name = "R" + std::to_string(anon_row_counter_++); + } + + std::vector linear; + f_t lhs_constant = 0; + parse_linear_expression(linear, lhs_constant); + + // Optional '+ [ ... ]' or '- [ ... ]' quadratic block in the LHS. + // Mirrors the objective handling; if present, this row becomes a + // quadratic constraint and is stored on quadratic_constraint_blocks + // instead of the linear arrays. + std::vector> qc_triples; + bool is_quadratic_row = false; + int quad_sign = 1; + if (peek().kind == LpTokenKind::Plus && peek(1).kind == LpTokenKind::LBracket) { + advance(); + } else if (peek().kind == LpTokenKind::Minus && peek(1).kind == LpTokenKind::LBracket) { + advance(); + quad_sign = -1; + } + if (peek().kind == LpTokenKind::LBracket) { + is_quadratic_row = true; + std::vector in_bracket_linear; + parse_quadratic_bracket(in_bracket_linear, quad_sign, BracketRole::Constraint, qc_triples); + for (const auto& lt : in_bracket_linear) + linear.push_back(lt); + + // More linear terms may follow the bracket. parse_linear_expression + // does not produce a constant unless the user wrote one in the LHS; + // a constant gets moved to RHS just like the pre-bracket constant. + std::vector more; + f_t more_constant = 0; + parse_linear_expression(more, more_constant); + for (const auto& lt : more) + linear.push_back(lt); + lhs_constant += more_constant; + } + + RowType row_type{}; + if (peek().kind == LpTokenKind::LessEq) { + row_type = LesserThanOrEqual; + advance(); + } else if (peek().kind == LpTokenKind::GreaterEq) { + row_type = GreaterThanOrEqual; + advance(); + } else if (peek().kind == LpTokenKind::Equal) { + row_type = Equality; + advance(); + } else { + mps_parser_expects(false, + error_type_t::ValidationError, + "LP parse error at line %d: expected a relation operator " + "(<=, >=, =) in constraint, got '%s'", + peek().line, + peek().text.c_str()); + } + + // Quadratic constraints currently only support '≤' in the data model + // (see mps_data_model_t::quadratic_constraint_t docs). + if (is_quadratic_row) { + mps_parser_expects(row_type == LesserThanOrEqual, + error_type_t::ValidationError, + "LP parse error at line %d: quadratic constraint '%s' must use " + "'<=' (only convex '≤' quadratic constraints are supported)", + peek().line, + row_name.c_str()); + } + + f_t rhs_value = parse_signed_number(); + // Any constant that appeared on the LHS is moved to the RHS with a sign flip. + rhs_value -= lhs_constant; + + // Register the row (track name uniqueness regardless of linear/quadratic). + mps_parser_expects(row_names_map_.find(row_name) == row_names_map_.end(), + error_type_t::ValidationError, + "Duplicate constraint name '%s'", + row_name.c_str()); + + // Collect the linear part. Coefficients accumulate for repeated variables; + // sort by var_id for deterministic CSR output. + std::unordered_map row_coeffs; + for (const auto& lt : linear) + row_coeffs[lt.var_id] += lt.coeff; + std::vector> ordered(row_coeffs.begin(), row_coeffs.end()); + std::sort(ordered.begin(), ordered.end()); + std::vector indices; + std::vector values; + indices.reserve(ordered.size()); + values.reserve(ordered.size()); + for (const auto& [vid, val] : ordered) { + if (val == f_t(0)) continue; + indices.push_back(vid); + values.push_back(val); + } + + if (is_quadratic_row) { + // Stash for the post-pass; quadratic rows are *not* added to the + // linear arrays (row_names/row_types/A_indices/A_values/b_values). + // We still record the name in row_names_map_ for uniqueness checks + // — but using a sentinel id below the linear count would be wrong, + // so use a separate sentinel and a placeholder name reservation. + typename lp_parser_t::quadratic_constraint_block_t block; + block.row_name = row_name; + block.row_type = row_type; + block.linear_indices = std::move(indices); + block.linear_values = std::move(values); + block.rhs_value = rhs_value; + block.quad_triples = std::move(qc_triples); + out_.quadratic_constraint_blocks.push_back(std::move(block)); + // Use std::numeric_limits::max() as a sentinel; the map is only + // used for uniqueness, never for index lookup. + row_names_map_.emplace(row_name, std::numeric_limits::max()); + } else { + i_t row_id = static_cast(out_.row_names.size()); + out_.row_names.push_back(row_name); + row_names_map_.emplace(row_name, row_id); + out_.row_types.push_back(row_type); + out_.b_values.push_back(rhs_value); + out_.A_indices.push_back(std::move(indices)); + out_.A_values.push_back(std::move(values)); + } + } +} + +template +void LpParseEngine::parse_bounds_section() +{ + while (!at_section_boundary()) { + // Either starts with a variable name or with a signed number. 'inf' / + // 'infinity' tokens are Names but only valid in the lb-first form. + if (peek().kind == LpTokenKind::Name && !is_infinity_keyword(peek())) { + std::string var_name = peek().text; + advance(); + i_t vid = get_or_add_var(var_name); + bounds_defined_for_var_id_.insert(vid); + + // Suffix after the name. + if (peek().kind == LpTokenKind::Name && is_free_keyword(lowercase(peek().text))) { + advance(); + out_.variable_lower_bounds[vid] = -std::numeric_limits::infinity(); + out_.variable_upper_bounds[vid] = std::numeric_limits::infinity(); + lower_explicitly_set_.insert(vid); + } else if (match(LpTokenKind::LessEq)) { + // x <= ub + out_.variable_upper_bounds[vid] = parse_signed_number(); + } else if (match(LpTokenKind::GreaterEq)) { + // x >= lb + out_.variable_lower_bounds[vid] = parse_signed_number(); + lower_explicitly_set_.insert(vid); + } else if (match(LpTokenKind::Equal)) { + // x = value (fixed) + f_t v = parse_signed_number(); + out_.variable_lower_bounds[vid] = v; + out_.variable_upper_bounds[vid] = v; + lower_explicitly_set_.insert(vid); + } else { + mps_parser_expects(false, + error_type_t::ValidationError, + "LP parse error at line %d: expected 'free', '<=', '>=' or '=' " + "after variable name in Bounds section, got '%s'", + peek().line, + peek().text.c_str()); + } + } else { + // lb <= x [<= ub] + f_t lb = parse_signed_number(); + expect(LpTokenKind::LessEq, "'<=' in 'lb <= var' bound"); + mps_parser_expects(peek().kind == LpTokenKind::Name && !is_infinity_keyword(peek()), + error_type_t::ValidationError, + "LP parse error at line %d: expected variable name after 'lb <='", + peek().line); + std::string var_name = peek().text; + advance(); + i_t vid = get_or_add_var(var_name); + bounds_defined_for_var_id_.insert(vid); + out_.variable_lower_bounds[vid] = lb; + lower_explicitly_set_.insert(vid); + if (match(LpTokenKind::LessEq)) { out_.variable_upper_bounds[vid] = parse_signed_number(); } + } + } + + // A negative upper bound requires an explicitly stated lower bound, + // otherwise the default lower of 0 would collide with the upper and make + // the variable silently infeasible. Flag this at parse time. + for (i_t vid : bounds_defined_for_var_id_) { + if (out_.variable_upper_bounds[vid] < f_t(0) && !lower_explicitly_set_.count(vid)) { + mps_parser_expects(false, + error_type_t::ValidationError, + "LP parse error: variable '%s' has a negative upper bound (%g) " + "without an explicit lower bound. Write '-inf <= %s <= %g' or give " + "an explicit lower bound alongside the upper bound.", + out_.var_names[vid].c_str(), + static_cast(out_.variable_upper_bounds[vid]), + out_.var_names[vid].c_str(), + static_cast(out_.variable_upper_bounds[vid])); + } + } +} + +template +void LpParseEngine::parse_integer_list_section(bool is_binary) +{ + while (!at_section_boundary()) { + mps_parser_expects(peek().kind == LpTokenKind::Name, + error_type_t::ValidationError, + "LP parse error at line %d: expected variable name in %s section, got '%s'", + peek().line, + is_binary ? "Binaries" : "Generals", + peek().text.c_str()); + std::string var_name = peek().text; + advance(); + i_t vid = get_or_add_var(var_name); + // Reject if this variable was previously declared semi-continuous; the + // combination is ambiguous (integer vs. continuous-or-zero). + mps_parser_expects(out_.var_types[vid] != 'S', + error_type_t::ValidationError, + "Variable '%s' appears in both Semi-Continuous and %s sections", + var_name.c_str(), + is_binary ? "Binaries" : "Generals"); + out_.var_types[vid] = 'I'; + if (is_binary) { + out_.variable_lower_bounds[vid] = f_t(0); + out_.variable_upper_bounds[vid] = f_t(1); + bounds_defined_for_var_id_.insert(vid); + } + } +} + +template +void LpParseEngine::parse_semi_continuous_section() +{ + while (!at_section_boundary()) { + mps_parser_expects( + peek().kind == LpTokenKind::Name, + error_type_t::ValidationError, + "LP parse error at line %d: expected variable name in Semi-Continuous section, got '%s'", + peek().line, + peek().text.c_str()); + std::string var_name = peek().text; + advance(); + i_t vid = get_or_add_var(var_name); + // Reject if the variable was previously declared integer/binary; the + // combination is ambiguous (integer vs. continuous-or-zero). + mps_parser_expects(out_.var_types[vid] != 'I', + error_type_t::ValidationError, + "Variable '%s' appears in both Generals/Binaries and Semi-Continuous " + "sections", + var_name.c_str()); + out_.var_types[vid] = 'S'; + } +} + +// ---- Top-level dispatch ---------------------------------------------------- + +template +void LpParseEngine::parse_all() +{ + bool saw_objective = false; + bool saw_end = false; + + while (!at_eof()) { + SectionKind kind = try_consume_section_header(); + switch (kind) { + case SectionKind::Objective: + mps_parser_expects(!saw_objective, + error_type_t::ValidationError, + "LP parse error: multiple objective sections"); + parse_objective_section(); + saw_objective = true; + break; + case SectionKind::Constraints: parse_constraints_section(); break; + case SectionKind::Bounds: parse_bounds_section(); break; + case SectionKind::Generals: parse_integer_list_section(false); break; + case SectionKind::Binaries: parse_integer_list_section(true); break; + case SectionKind::SemiContinuous: parse_semi_continuous_section(); break; + case SectionKind::End: + saw_end = true; + break; // Break out of the switch; the check below ends parsing. + case SectionKind::None: break; + } + if (saw_end) break; // Anything after 'End' is ignored. + } + if (!saw_end) { printf("LP parser: 'End' section is missing\n"); } + mps_parser_expects(saw_objective, + error_type_t::ValidationError, + "LP parse error: no objective (Minimize/Maximize) section found"); +} + +} // namespace + +// =========================================================================== +// lp_parser_t — thin public wrapper. All parsing state/types live in the +// anonymous namespace above. +// =========================================================================== + +namespace { + +// Emits one quadratic_constraint_block_t to `problem` via +// append_quadratic_constraint(). Row indices are assigned +// linear_row_count..linear_row_count + nqc - 1, mirroring MPS's QCMATRIX +// handling in mps_parser_t::fill_problem. +template +void flush_quadratic_constraints(mps_data_model_t& problem, + const lp_parser_t& parser) +{ + const i_t n_vars = static_cast(parser.var_names.size()); + const i_t linear_row_count = static_cast(parser.row_names.size()); + i_t k = 0; + for (const auto& block : parser.quadratic_constraint_blocks) { + std::vector q_values; + std::vector q_indices; + std::vector q_offsets; + build_symmetric_q_csr(block.quad_triples, n_vars, q_values, q_indices, q_offsets); + problem.append_quadratic_constraint(linear_row_count + k, + block.row_name, + static_cast(block.row_type), + block.linear_values, + block.linear_indices, + block.rhs_value, + q_values, + q_indices, + q_offsets); + ++k; + } +} + +} // end namespace + +template +lp_parser_t::lp_parser_t(mps_data_model_t& problem, const std::string& file) +{ + LpParseEngine engine(*this, file); + detail::finalize_problem(problem, *this); + flush_quadratic_constraints(problem, *this); +} + +template +lp_parser_t::lp_parser_t(mps_data_model_t& problem, std::string_view input) +{ + LpParseEngine engine(*this, input); + detail::finalize_problem(problem, *this); + flush_quadratic_constraints(problem, *this); +} + +template class lp_parser_t; +template class lp_parser_t; + +// =========================================================================== +// Public parse_lp() / parse_lp_from_string() +// =========================================================================== + +template +mps_data_model_t parse_lp(const std::string& lp_file_path) +{ + mps_data_model_t problem; + lp_parser_t parser(problem, lp_file_path); + return problem; +} + +template +mps_data_model_t parse_lp_from_string(std::string_view lp_contents) +{ + mps_data_model_t problem; + lp_parser_t parser(problem, lp_contents); + return problem; +} + +template mps_data_model_t parse_lp(const std::string&); +template mps_data_model_t parse_lp(const std::string&); +template mps_data_model_t parse_lp_from_string(std::string_view); +template mps_data_model_t parse_lp_from_string(std::string_view); + +} // namespace cuopt::linear_programming::io diff --git a/cpp/src/io/lp_parser.hpp b/cpp/src/io/lp_parser.hpp new file mode 100644 index 0000000000..b068f7535a --- /dev/null +++ b/cpp/src/io/lp_parser.hpp @@ -0,0 +1,84 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace cuopt::linear_programming::io { + +/** + * @brief Parser for the LP format. + * + * The class is a thin holder for the parsed problem data. All parsing + * machinery (tokenizer, expression/section parsers, token types) lives in + * src/lp_parser.cpp and is never exposed. + * + * The public fields mirror mps_parser_t so the two parsers share a single + * finalization path (see src/parser_finalize.hpp) and so tests and tools + * can introspect the same shape of intermediate data from either parser. + */ +template +class lp_parser_t { + public: + // Parses `file` and populates `problem`. + lp_parser_t(mps_data_model_t& problem, const std::string& file); + + // Parses `input` (LP format text already loaded in memory) and populates + // `problem`. Used by parse_lp_from_string() — compressed inputs are only + // supported via the file-path constructor since compression is detected + // from the path suffix. + lp_parser_t(mps_data_model_t& problem, std::string_view input); + + // Intermediate parsed problem data (mirrors mps_parser_t's public fields). + std::string problem_name{}; + std::vector row_names{}; + std::vector row_types{}; + std::string objective_name{"OBJ"}; + std::vector var_names{}; + std::vector var_types{}; + std::vector> A_indices{}; + std::vector> A_values{}; + std::vector b_values{}; + std::vector c_values{}; + f_t objective_offset_value{0}; + std::vector variable_upper_bounds{}; + std::vector variable_lower_bounds{}; + bool maximize{false}; + // Quadratic objective entries (row, col, value) in upper-triangular + // QUADOBJ convention; finalize_problem() mirrors to the full symmetric + // matrix and applies the *0.5 factor required by cuOpt's x^T Q x form. + std::vector> quadobj_entries{}; + + // Per-row data for constraints whose LHS contains a quadratic bracket. + // These rows do NOT appear in row_names/row_types/A_indices/A_values/ + // b_values — those vectors carry only the linear constraints — and they + // are emitted to the data model after finalize_problem via + // mps_data_model_t::append_quadratic_constraint(), with row indices + // assigned linear_row_count..linear_row_count+nqc (mirroring MPS's + // QCMATRIX handling). + struct quadratic_constraint_block_t { + std::string row_name{}; + RowType row_type{}; + std::vector linear_indices{}; + std::vector linear_values{}; + f_t rhs_value{}; + // Upper-triangular (i <= j) raw triples directly from the LP source + // (face value, no /2). The post-pass mirrors and halves off-diagonals + // to build the symmetric Q in CSR. + std::vector> quad_triples{}; + }; + std::vector quadratic_constraint_blocks{}; +}; + +} // namespace cuopt::linear_programming::io diff --git a/cpp/src/io/mps_parser.cpp b/cpp/src/io/mps_parser.cpp index 61cb1fa314..51527f3dab 100644 --- a/cpp/src/io/mps_parser.cpp +++ b/cpp/src/io/mps_parser.cpp @@ -7,6 +7,7 @@ #include +#include #include #include @@ -20,30 +21,9 @@ #include #include -#ifdef MPS_PARSER_WITH_BZIP2 -#include -#endif // MPS_PARSER_WITH_BZIP2 - -#ifdef MPS_PARSER_WITH_ZLIB -#include -#endif // MPS_PARSER_WITH_ZLIB - -#if defined(MPS_PARSER_WITH_BZIP2) || defined(MPS_PARSER_WITH_ZLIB) -#include -#endif // MPS_PARSER_WITH_BZIP2 || MPS_PARSER_WITH_ZLIB - namespace { using cuopt::linear_programming::io::error_type_t; using cuopt::linear_programming::io::mps_parser_expects; -using cuopt::linear_programming::io::mps_parser_expects_fatal; - -struct FcloseDeleter { - void operator()(FILE* fp) - { - mps_parser_expects_fatal( - fclose(fp) == 0, error_type_t::ValidationError, "Error closing MPS file!"); - } -}; std::vector string_to_buffer(std::string_view input) { @@ -53,163 +33,6 @@ std::vector string_to_buffer(std::string_view input) } } // end namespace -#ifdef MPS_PARSER_WITH_BZIP2 -namespace { -using BZ2_bzReadOpen_t = decltype(&BZ2_bzReadOpen); -using BZ2_bzReadClose_t = decltype(&BZ2_bzReadClose); -using BZ2_bzRead_t = decltype(&BZ2_bzRead); - -std::vector bz2_file_to_string(const std::string& file) -{ - struct DlCloseDeleter { - void operator()(void* fp) - { - mps_parser_expects_fatal( - dlclose(fp) == 0, error_type_t::ValidationError, "Error closing libbz2.so!"); - } - }; - struct BzReadCloseDeleter { - void operator()(void* f) - { - int bzerror; - if (f != nullptr) fptr(&bzerror, f); - mps_parser_expects_fatal( - bzerror == BZ_OK, error_type_t::ValidationError, "Error closing bzip2 file!"); - } - BZ2_bzReadClose_t fptr = nullptr; - }; - - std::unique_ptr lbz2handle{dlopen("libbz2.so", RTLD_LAZY)}; - mps_parser_expects( - lbz2handle != nullptr, - error_type_t::ValidationError, - "Could not open .mps.bz2 file since libbz2.so was not found. In order to open .mps.bz2 files " - "directly, please ensure libbzip2 is installed. Alternatively, decompress the .mps.bz2 file " - "manually and open the uncompressed .mps file. Given path: %s", - file.c_str()); - - BZ2_bzReadOpen_t BZ2_bzReadOpen = - reinterpret_cast(dlsym(lbz2handle.get(), "BZ2_bzReadOpen")); - BZ2_bzReadClose_t BZ2_bzReadClose = - reinterpret_cast(dlsym(lbz2handle.get(), "BZ2_bzReadClose")); - BZ2_bzRead_t BZ2_bzRead = reinterpret_cast(dlsym(lbz2handle.get(), "BZ2_bzRead")); - mps_parser_expects( - BZ2_bzReadOpen != nullptr && BZ2_bzReadClose != nullptr && BZ2_bzRead != nullptr, - error_type_t::ValidationError, - "Error loading libbzip2! Library version might be incompatible. Please decompress the .mps.bz2 " - "file manually and open the uncompressed .mps file. Given path: %s", - file.c_str()); - - std::unique_ptr fp{fopen(file.c_str(), "rb")}; - mps_parser_expects(fp != nullptr, - error_type_t::ValidationError, - "Error opening MPS file! Given path: %s", - file.c_str()); - int bzerror = BZ_OK; - std::unique_ptr bzfile{ - BZ2_bzReadOpen(&bzerror, fp.get(), 0, 0, nullptr, 0), {BZ2_bzReadClose}}; - mps_parser_expects(bzerror == BZ_OK, - error_type_t::ValidationError, - "Could not open bzip2 compressed file! Given path: %s", - file.c_str()); - - std::vector buf; - const size_t readbufsize = 1ull << 24; // 16MiB - just a guess. - std::vector readbuf(readbufsize); - while (bzerror == BZ_OK) { - const size_t bytes_read = BZ2_bzRead(&bzerror, bzfile.get(), readbuf.data(), readbuf.size()); - if (bzerror == BZ_OK || bzerror == BZ_STREAM_END) { - buf.insert(buf.end(), begin(readbuf), begin(readbuf) + bytes_read); - } - } - buf.push_back('\0'); - mps_parser_expects(bzerror == BZ_STREAM_END, - error_type_t::ValidationError, - "Error in bzip2 decompression of MPS file! Given path: %s", - file.c_str()); - return buf; -} -} // end namespace -#endif // MPS_PARSER_WITH_BZIP2 - -#ifdef MPS_PARSER_WITH_ZLIB -namespace { -using gzopen_t = decltype(&gzopen); -using gzclose_r_t = decltype(&gzclose_r); -using gzbuffer_t = decltype(&gzbuffer); -using gzread_t = decltype(&gzread); -using gzerror_t = decltype(&gzerror); -std::vector zlib_file_to_string(const std::string& file) -{ - struct DlCloseDeleter { - void operator()(void* fp) - { - mps_parser_expects_fatal( - dlclose(fp) == 0, error_type_t::ValidationError, "Error closing libbz2.so!"); - } - }; - struct GzCloseDeleter { - void operator()(gzFile_s* f) - { - int err = fptr(f); - mps_parser_expects_fatal( - err == Z_OK, error_type_t::ValidationError, "Error closing gz file!"); - } - gzclose_r_t fptr = nullptr; - }; - - std::unique_ptr lzhandle{dlopen("libz.so.1", RTLD_LAZY)}; - mps_parser_expects( - lzhandle != nullptr, - error_type_t::ValidationError, - "Could not open .mps.gz file since libz.so was not found. In order to open .mps.gz files " - "directly, please ensure zlib is installed. Alternatively, decompress the .mps.gz file " - "manually and open the uncompressed .mps file. Given path: %s", - file.c_str()); - gzopen_t gzopen = reinterpret_cast(dlsym(lzhandle.get(), "gzopen")); - gzclose_r_t gzclose_r = reinterpret_cast(dlsym(lzhandle.get(), "gzclose_r")); - gzbuffer_t gzbuffer = reinterpret_cast(dlsym(lzhandle.get(), "gzbuffer")); - gzread_t gzread = reinterpret_cast(dlsym(lzhandle.get(), "gzread")); - gzerror_t gzerror = reinterpret_cast(dlsym(lzhandle.get(), "gzerror")); - mps_parser_expects( - gzopen != nullptr && gzclose_r != nullptr && gzbuffer != nullptr && gzread != nullptr && - gzerror != nullptr, - error_type_t::ValidationError, - "Error loading zlib! Library version might be incompatible. Please decompress the .mps.gz file " - "manually and open the uncompressed .mps file. Given path: %s", - file.c_str()); - std::unique_ptr gzfp{gzopen(file.c_str(), "rb"), {gzclose_r}}; - mps_parser_expects(gzfp != nullptr, - error_type_t::ValidationError, - "Error opening compressed MPS file! Given path: %s", - file.c_str()); - int zlib_status = gzbuffer(gzfp.get(), 1 << 20); // 1 MiB - mps_parser_expects(zlib_status == Z_OK, - error_type_t::ValidationError, - "Could not set zlib internal buffer size for decompression! Given path: %s", - file.c_str()); - std::vector buf; - const size_t readbufsize = 1ull << 24; // 16MiB - std::vector readbuf(readbufsize); - int bytes_read = -1; - while (bytes_read != 0) { - bytes_read = gzread(gzfp.get(), readbuf.data(), readbuf.size()); - if (bytes_read > 0) { buf.insert(buf.end(), begin(readbuf), begin(readbuf) + bytes_read); } - if (bytes_read < 0) { - gzerror(gzfp.get(), &zlib_status); - break; - } - } - buf.push_back('\0'); - mps_parser_expects(zlib_status == Z_OK, - error_type_t::ValidationError, - "Error in zlib decompression of MPS file! Given path: %s", - file.c_str()); - return buf; -} -} // end namespace -#endif // MPS_PARSER_WITH_ZLIB - namespace cuopt::linear_programming::io { template @@ -598,51 +421,6 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) } } -template -std::vector mps_parser_t::file_to_string(const std::string& file) -{ - // raft::common::nvtx::range fun_scope("file to string"); - -#ifdef MPS_PARSER_WITH_BZIP2 - if (file.size() > 4 && file.substr(file.size() - 4, 4) == ".bz2") { - return bz2_file_to_string(file); - } -#endif // MPS_PARSER_WITH_BZIP2 - -#ifdef MPS_PARSER_WITH_ZLIB - if (file.size() > 3 && file.substr(file.size() - 3, 3) == ".gz") { - return zlib_file_to_string(file); - } -#endif // MPS_PARSER_WITH_ZLIB - - // Faster than using C++ I/O - std::unique_ptr fp{fopen(file.c_str(), "r")}; - mps_parser_expects(fp != nullptr, - error_type_t::ValidationError, - "Error opening MPS file! Given path: %s", - mps_file.c_str()); - - mps_parser_expects(fseek(fp.get(), 0L, SEEK_END) == 0, - error_type_t::ValidationError, - "File browsing MPS file! Given path: %s", - mps_file.c_str()); - const long bufsize = ftell(fp.get()); - mps_parser_expects(bufsize != -1L, - error_type_t::ValidationError, - "File browsing MPS file! Given path: %s", - mps_file.c_str()); - std::vector buf(bufsize + 1); - rewind(fp.get()); - - mps_parser_expects(fread(buf.data(), sizeof(char), bufsize, fp.get()) == bufsize, - error_type_t::ValidationError, - "Error reading MPS file! Given path: %s", - mps_file.c_str()); - buf[bufsize] = '\0'; - - return buf; -} - template void mps_parser_t::parse_string(char* buf) { @@ -914,7 +692,7 @@ mps_parser_t::mps_parser_t(mps_data_model_t& problem, { // raft::common::nvtx::range fun_scope("mps parser"); - std::vector buf = file_to_string(file); + std::vector buf = detail::file_to_string(file); parse_string(buf.data()); diff --git a/cpp/src/io/mps_parser_internal.hpp b/cpp/src/io/mps_parser_internal.hpp index f0cc1d6c05..af27083fcb 100644 --- a/cpp/src/io/mps_parser_internal.hpp +++ b/cpp/src/io/mps_parser_internal.hpp @@ -163,12 +163,6 @@ class mps_parser_t { std::unordered_set lower_bounds_defined_for_var_id{}; static constexpr f_t unset_range_value = std::numeric_limits::infinity(); - /* Reads an MPS input file into a buffer. - * - * If the file has a .gz or .bz2 suffix and zlib or libbzip2 are installed, respectively, - * the function directly reads and decompresses the compressed MPS file. - */ - std::vector file_to_string(const std::string& file); void fill_problem(mps_data_model_t& problem); void parse_string(char* buf); void parse_rows(std::string_view line); diff --git a/cpp/src/io/parser_finalize.hpp b/cpp/src/io/parser_finalize.hpp new file mode 100644 index 0000000000..3c9a2ffbe1 --- /dev/null +++ b/cpp/src/io/parser_finalize.hpp @@ -0,0 +1,255 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace cuopt::linear_programming::io::detail { + +// Consumes the LP parser's intermediate parsed data and populates `problem`. +// +// CSR flatten, row-type → constraint-bound conversion, quadratic objective +// matrix construction, metadata setters. MPS uses its own fill_problem +// because it handles QCMATRIX quadratic constraints; the LP format does not +// support quadratic constraints, so the two finalization paths intentionally +// diverge. +// +// Required fields on `parser`: +// problem_name, objective_name, row_names, row_types, var_names, +// var_types, A_indices, A_values, b_values, c_values, +// variable_lower_bounds, variable_upper_bounds, objective_offset_value, +// maximize, quadobj_entries. +// +// The requires-expression branches for objective_scaling_factor_value, +// ranges_values, and qmatrix_entries are dormant for LP but kept so the +// template would remain reusable if a non-LP caller ever wants them. +template +void finalize_problem(mps_data_model_t& problem, Parser& parser) +{ + const i_t n_vars = static_cast(parser.var_names.size()); + const i_t n_rows = static_cast(parser.row_names.size()); + + // Pad per-variable vectors that may have grown after their initial size + // (e.g., a variable first appeared after c_values was already initialized). + if (static_cast(parser.c_values.size()) < n_vars) parser.c_values.resize(n_vars, f_t(0)); + if (static_cast(parser.variable_lower_bounds.size()) < n_vars) { + parser.variable_lower_bounds.resize(n_vars, f_t(0)); + } + if (static_cast(parser.variable_upper_bounds.size()) < n_vars) { + parser.variable_upper_bounds.resize(n_vars, std::numeric_limits::infinity()); + } + if (static_cast(parser.var_types.size()) < n_vars) parser.var_types.resize(n_vars, 'C'); + + // Flatten the ragged A_indices / A_values into a single CSR. + std::vector offsets; + std::vector indices; + std::vector values; + offsets.reserve(n_rows + 1); + offsets.push_back(0); + for (i_t i = 0; i < n_rows; ++i) { + for (i_t idx : parser.A_indices[i]) + indices.push_back(idx); + for (f_t v : parser.A_values[i]) + values.push_back(v); + offsets.push_back(static_cast(values.size())); + } + problem.set_csr_constraint_matrix(values, indices, offsets); + + mps_parser_expects(indices.size() == values.size(), + error_type_t::ValidationError, + "Constraint matrix nonzero vector (%zu) and column-index vector (%zu) " + "must have the same size.", + indices.size(), + values.size()); + mps_parser_expects(!offsets.empty() && offsets.back() == static_cast(values.size()), + error_type_t::ValidationError, + "CSR offset tail (%d) must equal the nonzero count (%zu).", + offsets.empty() ? 0 : offsets.back(), + values.size()); + + problem.set_constraint_bounds(parser.b_values); + problem.set_objective_coefficients(parser.c_values); + + f_t scaling = f_t(1); + if constexpr (requires { parser.objective_scaling_factor_value; }) { + scaling = parser.objective_scaling_factor_value; + } + problem.set_objective_scaling_factor(scaling); + problem.set_objective_offset(parser.objective_offset_value); + + problem.set_variable_lower_bounds(parser.variable_lower_bounds); + problem.set_variable_upper_bounds(parser.variable_upper_bounds); + + mps_parser_expects( + (problem.get_variable_lower_bounds().size() == problem.get_variable_upper_bounds().size()) && + (problem.get_variable_upper_bounds().size() == problem.get_objective_coefficients().size()), + error_type_t::ValidationError, + "Per-variable vectors are inconsistently sized. objective=%zu, lb=%zu, ub=%zu.", + problem.get_objective_coefficients().size(), + problem.get_variable_lower_bounds().size(), + problem.get_variable_upper_bounds().size()); + + // Semi-continuous variables must have a finite upper bound; otherwise the + // "x = 0 or lb <= x <= ub" semantics collapse to a regular continuous + // variable. Matches the MPS parser's rule. + for (i_t i = 0; i < n_vars; ++i) { + if (parser.var_types[i] == 'S') { + mps_parser_expects(!std::isinf(parser.variable_upper_bounds[i]), + error_type_t::ValidationError, + "Semi-continuous variable '%s' must have a finite upper bound", + parser.var_names[i].c_str()); + } + } + + // Row types + RHS (+ MPS ranges) → explicit constraint lower/upper bounds. + const f_t inf = std::numeric_limits::infinity(); + std::vector clb; + std::vector cub; + clb.reserve(n_rows); + cub.reserve(n_rows); + constexpr bool has_ranges = requires { parser.ranges_values; }; + for (i_t i = 0; i < n_rows; ++i) { + switch (parser.row_types[i]) { + case Equality: + clb.push_back(parser.b_values[i]); + cub.push_back(parser.b_values[i]); + if constexpr (has_ranges) { + if (!parser.ranges_values.empty() && parser.ranges_values[i] != inf) { + mps_parser_expects(!std::isnan(parser.ranges_values[i]), + error_type_t::ValidationError, + "Equality range value %d is NaN", + i); + if (parser.ranges_values[i] < f_t(0)) { + clb.back() += parser.ranges_values[i]; + } else { + cub.back() += parser.ranges_values[i]; + } + } + } + break; + case GreaterThanOrEqual: + clb.push_back(parser.b_values[i]); + cub.push_back(inf); + if constexpr (has_ranges) { + if (!parser.ranges_values.empty() && parser.ranges_values[i] != inf) { + mps_parser_expects(!std::isnan(parser.ranges_values[i]), + error_type_t::ValidationError, + "Greater range value %d is NaN", + i); + cub.back() = clb.back() + std::abs(parser.ranges_values[i]); + } + } + break; + case LesserThanOrEqual: + clb.push_back(-inf); + cub.push_back(parser.b_values[i]); + if constexpr (has_ranges) { + if (!parser.ranges_values.empty() && parser.ranges_values[i] != inf) { + mps_parser_expects(!std::isnan(parser.ranges_values[i]), + error_type_t::ValidationError, + "Lesser range value %d is NaN", + i); + clb.back() = cub.back() - std::abs(parser.ranges_values[i]); + } + } + break; + default: + mps_parser_expects(false, + error_type_t::ValidationError, + "Unsupported row type for row '%s'", + parser.row_names[i].c_str()); + } + mps_parser_expects(!std::isnan(clb.back()) && !std::isnan(cub.back()), + error_type_t::ValidationError, + "Constraint bound for row '%s' is NaN", + parser.row_names[i].c_str()); + } + problem.set_constraint_lower_bounds(clb); + problem.set_constraint_upper_bounds(cub); + + mps_parser_expects( + (problem.get_constraint_lower_bounds().size() == + problem.get_constraint_upper_bounds().size()) && + (problem.get_constraint_upper_bounds().size() == problem.get_constraint_bounds().size()), + error_type_t::ValidationError, + "Per-constraint vectors are inconsistently sized. rhs=%zu, lb=%zu, ub=%zu.", + problem.get_constraint_bounds().size(), + problem.get_constraint_lower_bounds().size(), + problem.get_constraint_upper_bounds().size()); + + problem.set_problem_name(parser.problem_name); + problem.set_objective_name(parser.objective_name); + // Setters take const refs — pass the fields directly to avoid an extra + // temporary copy. + problem.set_variable_names(parser.var_names); + problem.set_variable_types(parser.var_types); + problem.set_row_names(parser.row_names); + std::vector row_types_chars(parser.row_types.size()); + for (size_t i = 0; i < parser.row_types.size(); ++i) { + row_types_chars[i] = static_cast(parser.row_types[i]); + } + problem.set_row_types(row_types_chars); + problem.set_maximize(parser.maximize); + + // Quadratic objective: build a full symmetric Q via double-transpose. + // - QUADOBJ entries are upper-triangular; each off-diagonal entry is + // mirrored to its transpose when assembling. + // - QMATRIX entries are already the full symmetric matrix. + // Every stored value is multiplied by 0.5 to convert from the file's + // '0.5 x^T Q x' convention to cuOpt's 'x^T Q x'. See mps_parser.cpp for + // the original derivation. + auto build_q_csr = [&](const std::vector>& entries, + bool mirror_off_diagonal) { + std::vector>> csc(n_vars); + for (const auto& [row, col, val] : entries) { + csc[col].emplace_back(row, val); + if (mirror_off_diagonal && row != col) { csc[row].emplace_back(col, val); } + } + std::vector>> csr(n_vars); + for (i_t col = 0; col < n_vars; ++col) { + for (const auto& [row, val] : csc[col]) { + csr[row].emplace_back(col, val); + } + } + // Within each row the entries are naturally ordered by column because + // the outer loop above walks columns in ascending order — no sort needed. + std::vector q_values; + std::vector q_indices; + std::vector q_offsets; + q_offsets.reserve(n_vars + 1); + q_offsets.push_back(0); + for (i_t row = 0; row < n_vars; ++row) { + for (const auto& [col, val] : csr[row]) { + q_values.push_back(val * f_t(0.5)); + q_indices.push_back(col); + } + q_offsets.push_back(static_cast(q_values.size())); + } + problem.set_quadratic_objective_matrix(q_values, q_indices, q_offsets); + }; + + if (!parser.quadobj_entries.empty()) { + build_q_csr(parser.quadobj_entries, /*mirror_off_diagonal=*/true); + } else if constexpr (requires { parser.qmatrix_entries; }) { + if (!parser.qmatrix_entries.empty()) { + build_q_csr(parser.qmatrix_entries, /*mirror_off_diagonal=*/false); + } + } +} + +} // namespace cuopt::linear_programming::io::detail diff --git a/cpp/src/io/utilities/cython_mps_parser.cpp b/cpp/src/io/utilities/cython_parser.cpp similarity index 64% rename from cpp/src/io/utilities/cython_mps_parser.cpp rename to cpp/src/io/utilities/cython_parser.cpp index 1c4ae20a27..86bd0bb77d 100644 --- a/cpp/src/io/utilities/cython_mps_parser.cpp +++ b/cpp/src/io/utilities/cython_parser.cpp @@ -6,7 +6,7 @@ /* clang-format on */ #include -#include +#include namespace cuopt { namespace cython { @@ -18,5 +18,12 @@ std::unique_ptr> ca cuopt::linear_programming::io::parse_mps(mps_file_path, fixed_mps_format))); } +std::unique_ptr> call_parse_lp( + const std::string& lp_file_path) +{ + return std::make_unique>( + std::move(cuopt::linear_programming::io::parse_lp(lp_file_path))); +} + } // namespace cython } // namespace cuopt diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index 993a2c039f..42c083fdbf 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -104,16 +104,17 @@ cuopt_int_t cuOptReadProblem(const char* filename, cuOptOptimizationProblem* pro problem_and_stream_view_t* problem_and_stream = new problem_and_stream_view_t(get_memory_backend_type()); std::string filename_str(filename); - bool input_mps_strict = false; std::unique_ptr> mps_data_model_ptr; try { + // Dispatches on file extension; see parse_problem for the enumerated rules. mps_data_model_ptr = std::make_unique>( - parse_mps(filename_str, input_mps_strict)); + parse_problem(filename_str)); } catch (const std::exception& e) { - CUOPT_LOG_INFO("Error parsing MPS file: %s", e.what()); + CUOPT_LOG_INFO("Error parsing input file: %s", e.what()); delete problem_and_stream; - *problem_ptr = nullptr; - if (std::string(e.what()).find("Error opening MPS file") != std::string::npos) { + *problem_ptr = nullptr; + std::string err_msg = e.what(); + if (err_msg.find("Error opening input file") != std::string::npos) { return CUOPT_MPS_FILE_ERROR; } else { return CUOPT_MPS_PARSE_ERROR; diff --git a/cpp/tests/linear_programming/CMakeLists.txt b/cpp/tests/linear_programming/CMakeLists.txt index a4bdbfbb2e..bc057db1e2 100644 --- a/cpp/tests/linear_programming/CMakeLists.txt +++ b/cpp/tests/linear_programming/CMakeLists.txt @@ -16,9 +16,9 @@ ConfigureTest(PDLP_TEST LABELS numopt) # ################################################################################################## -# - MPS parser tests ------------------------------------------------------------------------------- +# - MPS / LP parser tests -------------------------------------------------------------------------- ConfigureTest(MPS_PARSER_TEST - ${CMAKE_CURRENT_SOURCE_DIR}/mps_parser_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/parser_test.cpp LABELS numopt) # ################################################################################################## diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp index 1912b15cb5..2fc9bdbbb2 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -114,6 +115,44 @@ TEST(c_api, burglar) { EXPECT_EQ(burglar_problem(), CUOPT_SUCCESS); } TEST(c_api, test_missing_file) { EXPECT_EQ(test_missing_file(), CUOPT_MPS_FILE_ERROR); } +// Verifies that cuOptReadProblem dispatches to the LP parser when given a +// path with a .lp extension. The input is a minimal LP (1 variable, 1 +// constraint); we just check the round-trip read produces the expected shape. +TEST(c_api, read_lp_file_by_extension) +{ + constexpr const char* lp_text = R"LP( +Minimize + x +Subject To + c1: x >= 2.5 +Bounds + x <= 10 +End +)LP"; + std::filesystem::path lp_path = + std::filesystem::temp_directory_path() / + (std::string{"c_api_read_lp_"} + std::to_string(::getpid()) + ".lp"); + { + std::ofstream out(lp_path); + out << lp_text; + } + + cuOptOptimizationProblem handle = nullptr; + cuopt_int_t status = cuOptReadProblem(lp_path.string().c_str(), &handle); + EXPECT_EQ(status, CUOPT_SUCCESS); + ASSERT_NE(handle, nullptr); + + cuopt_int_t n_vars = 0; + cuopt_int_t n_constrs = 0; + EXPECT_EQ(cuOptGetNumVariables(handle, &n_vars), CUOPT_SUCCESS); + EXPECT_EQ(cuOptGetNumConstraints(handle, &n_constrs), CUOPT_SUCCESS); + EXPECT_EQ(n_vars, 1); + EXPECT_EQ(n_constrs, 1); + + cuOptDestroyProblem(&handle); + std::filesystem::remove(lp_path); +} + TEST(c_api, test_infeasible_problem) { EXPECT_EQ(test_infeasible_problem(), CUOPT_SUCCESS); } TEST(c_api, test_ranged_problem) diff --git a/cpp/tests/linear_programming/mps_parser_test.cpp b/cpp/tests/linear_programming/mps_parser_test.cpp deleted file mode 100644 index 607a22fd1d..0000000000 --- a/cpp/tests/linear_programming/mps_parser_test.cpp +++ /dev/null @@ -1,1401 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#include -#include - -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cuopt::linear_programming::io { - -constexpr double tolerance = 1e-6; - -mps_parser_t read_from_mps(const std::string& file, bool fixed_format = true) -{ - std::string rel_file{}; - // assume relative paths are relative to RAPIDS_DATASET_ROOT_DIR - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - rel_file = rapidsDatasetRootDir + "/" + file; - // Empty problem not used in the test - mps_data_model_t problem; - mps_parser_t mps{problem, rel_file, fixed_format}; - return mps; -} - -bool file_exists(const std::string& file) -{ - std::string rel_file{}; - // assume relative paths are relative to RAPIDS_DATASET_ROOT_DIR - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - rel_file = rapidsDatasetRootDir + "/" + file; - return std::filesystem::exists(rel_file); -} - -TEST(mps_parser, bad_mps_files) -{ - std::stringstream ss; - static constexpr int NumMpsFiles = 15; - for (int i = 1; i <= NumMpsFiles; ++i) { - ss << "linear_programming/bad-mps-" << i << ".mps"; - // Check if file exists - if (file_exists(ss.str())) ASSERT_THROW(read_from_mps(ss.str()), std::logic_error); - ss.str(std::string{}); - ss.clear(); - } -} - -TEST(mps_parser, good_mps_file_1) -{ - auto mps = read_from_mps("linear_programming/good-mps-1.mps"); - EXPECT_EQ("good-1", mps.problem_name); - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(3., mps.A_values[0][0]); - EXPECT_EQ(4., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(2.7, mps.A_values[1][0]); - EXPECT_EQ(10.1, mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(5.4, mps.b_values[0]); - EXPECT_EQ(4.9, mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(0.2, mps.c_values[0]); - EXPECT_EQ(0.1, mps.c_values[1]); -} - -TEST(mps_parser, good_mps_file_clrf) -{ - auto mps = read_from_mps("linear_programming/good-mps-1-clrf.mps"); - EXPECT_EQ("good-1", mps.problem_name); - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(3., mps.A_values[0][0]); - EXPECT_EQ(4., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(2.7, mps.A_values[1][0]); - EXPECT_EQ(10.1, mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(5.4, mps.b_values[0]); - EXPECT_EQ(4.9, mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(0.2, mps.c_values[0]); - EXPECT_EQ(0.1, mps.c_values[1]); -} - -TEST(mps_parser, good_mps_free_file_clrf) -{ - auto mps = read_from_mps("linear_programming/good-mps-1-clrf.mps", false); - EXPECT_EQ("good-1", mps.problem_name); - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(3., mps.A_values[0][0]); - EXPECT_EQ(4., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(2.7, mps.A_values[1][0]); - EXPECT_EQ(10.1, mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(5.4, mps.b_values[0]); - EXPECT_EQ(4.9, mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(0.2, mps.c_values[0]); - EXPECT_EQ(0.1, mps.c_values[1]); -} - -TEST(mps_parser, good_mps_file_comments) -{ - auto mps = read_from_mps("linear_programming/good-mps-1-comments.mps", false); - EXPECT_EQ("good-1", mps.problem_name); - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(1), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(3., mps.A_values[0][0]); - EXPECT_EQ(4., mps.A_values[0][1]); - ASSERT_EQ(int(1), mps.A_values[1].size()); - EXPECT_EQ(2.7, mps.A_values[1][0]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(5.4, mps.b_values[0]); - EXPECT_EQ(4.9, mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(0.2, mps.c_values[0]); - EXPECT_EQ(0.1, mps.c_values[1]); -} - -TEST(mps_parser, good_mps_file_no_name) -{ - // Should not throw an error - read_from_mps("linear_programming/good-mps-fixed-no-name.mps"); -} - -TEST(mps_parser, good_mps_file_empty_name) -{ - // Should not throw an error - read_from_mps("linear_programming/good-mps-fixed-empty-name.mps"); -} - -TEST(mps_parser, good_mps_file_2) -{ - auto mps = read_from_mps("linear_programming/good-fixed-mps-2.mps"); - EXPECT_EQ("good-1", mps.problem_name); - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("RO W1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VA R1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(3., mps.A_values[0][0]); - EXPECT_EQ(4., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(2.7, mps.A_values[1][0]); - EXPECT_EQ(10.1, mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(5.4, mps.b_values[0]); - EXPECT_EQ(4.9, mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(0.2, mps.c_values[0]); - EXPECT_EQ(0.1, mps.c_values[1]); -} - -TEST(mps_parser_free_format, free_format_mps_file_1) -{ // tests for arbitrary spacing in rows, column, rhs - auto mps = read_from_mps("linear_programming/free-format-mps-1.mps", false); - EXPECT_EQ("good-1", mps.problem_name); - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(3., mps.A_values[0][0]); - EXPECT_EQ(4., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(2.7, mps.A_values[1][0]); - EXPECT_EQ(10.1, mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(5.4, mps.b_values[0]); - EXPECT_EQ(4.9, mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(0.2, mps.c_values[0]); - EXPECT_EQ(0.1, mps.c_values[1]); - EXPECT_EQ(false, mps.maximize); -} - -TEST(mps_parser_free_format, bad_free_format_mps_with_spaces_in_names) -{ - ASSERT_THROW(read_from_mps("linear_programming/good-fixed-mps-2.mps", false), std::logic_error); -} - -TEST(mps_parser_free_format, bad_mps_files_free_format) -{ - std::stringstream ss; - static constexpr int NumMpsFiles = 13; - for (int i = 1; i <= NumMpsFiles; ++i) { - ss << "linear_programming/bad-mps-" << i << ".mps"; - if (file_exists(ss.str())) ASSERT_THROW(read_from_mps(ss.str(), false), std::logic_error); - ss.str(std::string{}); - ss.clear(); - } -} - -TEST(mps_bounds, up_low_bounds) -{ - auto mps = read_from_mps("linear_programming/lp_model_with_var_bounds.mps", false); - EXPECT_EQ("lp_model_with_var_bounds", mps.problem_name); - - ASSERT_EQ(int(1), mps.row_names.size()); - EXPECT_EQ("con", mps.row_names[0]); - ASSERT_EQ(int(1), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ("OBJ", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("x", mps.var_names[0]); - EXPECT_EQ("y", mps.var_names[1]); - ASSERT_EQ(int(1), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(1), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(1., mps.A_values[0][0]); - EXPECT_EQ(1., mps.A_values[0][1]); - ASSERT_EQ(int(1), mps.b_values.size()); - EXPECT_EQ(3., mps.b_values[0]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(2., mps.c_values[0]); - EXPECT_EQ(-1., mps.c_values[1]); - EXPECT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(0., mps.variable_lower_bounds[0]); - EXPECT_EQ(1., mps.variable_lower_bounds[1]); - EXPECT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(1., mps.variable_upper_bounds[0]); - EXPECT_EQ(2., mps.variable_upper_bounds[1]); -} - -TEST(mps_bounds, standard_var_bounds_0_inf) -{ - auto mps = read_from_mps("linear_programming/free-format-mps-1.mps", false); - - // standard bounds are 0,inf when no var bounds are specified - EXPECT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(0., mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - EXPECT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[0]); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[1]); -} - -TEST(mps_bounds, only_some_UP_LO_var_bounds) -{ - auto mps = read_from_mps("linear_programming/good-mps-some-var-bounds.mps"); - - // standard bounds are 0,inf when no var bounds are specified - EXPECT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(-1., mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - EXPECT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[0]); - EXPECT_EQ(2., mps.variable_upper_bounds[1]); -} - -TEST(mps_bounds, fixed_var_bound) -{ - auto mps = read_from_mps("linear_programming/good-mps-fixed-var.mps"); - - // standard bounds are 0,inf when no var bounds are specified - EXPECT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(2., mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - EXPECT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(2., mps.variable_upper_bounds[0]); - EXPECT_EQ(std ::numeric_limits::infinity(), mps.variable_upper_bounds[1]); -} - -TEST(mps_bounds, free_var_bound) -{ - auto mps = read_from_mps("linear_programming/good-mps-free-var.mps"); - - // standard bounds are 0,inf when no var bounds are specified - EXPECT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(-std::numeric_limits::infinity(), mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - EXPECT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[0]); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[1]); -} - -TEST(mps_bounds, lower_inf_var_bound) -{ - auto mps = read_from_mps("linear_programming/good-mps-lower-bound-inf-var.mps"); - - // standard bounds are 0,inf when no var bounds are specified - EXPECT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(-std::numeric_limits::infinity(), mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - EXPECT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[0]); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[1]); -} - -TEST(mps_bounds, rhs_cost) -{ - auto mps = read_from_mps("linear_programming/good-mps-rhs-cost.mps"); - - // objective value offset should be set to -5 - EXPECT_EQ(int(-5), mps.objective_offset_value); -} - -TEST(mps_bounds, upper_inf_var_bound) -{ - auto mps = read_from_mps("linear_programming/good-mps-upper-bound-inf-var.mps"); - - // standard bounds are 0,inf when no var bounds are specified - EXPECT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(0., mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - EXPECT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[0]); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[1]); -} - -TEST(mps_bounds, semi_continuous_var_bounds_from_dataset) -{ - struct Case { - const char* name; - const char* mps; - int n_vars; - double lower; - double upper; - }; - const std::vector cases = { - {"sc_standard", cuopt::test::inline_mps::sc_standard_mps, 2, 2.0, 10.0}, - {"sc_lb_zero", cuopt::test::inline_mps::sc_lb_zero_mps, 2, 0.0, 10.0}, - {"sc_no_ub", cuopt::test::inline_mps::sc_no_ub_mps, 2, 2.0, 1e30}, - }; - - for (const auto& c : cases) { - SCOPED_TRACE(c.name); - auto mps = cuopt::test::inline_mps::parse_inline_mps(c.mps); - const auto& var_types = mps.get_variable_types(); - const auto& lower = mps.get_variable_lower_bounds(); - const auto& upper = mps.get_variable_upper_bounds(); - - ASSERT_EQ(c.n_vars, static_cast(var_types.size())); - EXPECT_EQ('S', var_types[0]); - ASSERT_EQ(c.n_vars, static_cast(lower.size())); - ASSERT_EQ(c.n_vars, static_cast(upper.size())); - EXPECT_DOUBLE_EQ(c.lower, lower[0]); - EXPECT_DOUBLE_EQ(c.upper, upper[0]); - } -} - -TEST(mps_bounds, semi_continuous_missing_lower_defaults_to_zero) -{ - auto mps = cuopt::test::inline_mps::parse_inline_mps(cuopt::test::inline_mps::sc_lb_zero_mps); - const auto& var_types = mps.get_variable_types(); - const auto& lower = mps.get_variable_lower_bounds(); - const auto& upper = mps.get_variable_upper_bounds(); - - ASSERT_EQ(2, static_cast(var_types.size())); - EXPECT_EQ('S', var_types[0]); - ASSERT_EQ(2, static_cast(lower.size())); - ASSERT_EQ(2, static_cast(upper.size())); - EXPECT_DOUBLE_EQ(0.0, lower[0]); - EXPECT_DOUBLE_EQ(10.0, upper[0]); -} - -TEST(mps_bounds, semi_continuous_missing_upper_rejected) -{ - EXPECT_THROW( - cuopt::test::inline_mps::parse_inline_mps(cuopt::test::inline_mps::sc_missing_upper_mps), - std::logic_error); -} - -TEST(mps_ranges, fixed_ranges) -{ - std::string file = "linear_programming/good-mps-fixed-ranges.mps"; - auto mps = read_from_mps(file); - - EXPECT_NEAR(4.2, mps.ranges_values[0], tolerance); // ROW1 range value - EXPECT_NEAR(3.4, mps.ranges_values[1], tolerance); // ROW2 range value - EXPECT_NEAR(-1.6, mps.ranges_values[2], tolerance); // ROW3 range value - EXPECT_NEAR(3.4, mps.ranges_values[3], tolerance); // ROW3 range value - - std::string rel_file{}; - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - rel_file = rapidsDatasetRootDir + "/" + file; - auto data_model = parse_mps(rel_file, true); - - EXPECT_NEAR(1.2, data_model.get_constraint_lower_bounds()[0], tolerance); // ROW1 lower bound - EXPECT_NEAR(5.4, data_model.get_constraint_upper_bounds()[0], tolerance); // ROW1 upper bound - EXPECT_NEAR(1.5, data_model.get_constraint_lower_bounds()[1], tolerance); // ROW2 lower bound - EXPECT_NEAR(4.9, data_model.get_constraint_upper_bounds()[1], tolerance); // ROW2 upper bound - EXPECT_NEAR( - 7.9, data_model.get_constraint_lower_bounds()[2], tolerance); // ROW3, equal constraint - EXPECT_NEAR( - 9.5, data_model.get_constraint_upper_bounds()[2], tolerance); // ROW3, equal constraint - EXPECT_NEAR( - 3.5, data_model.get_constraint_lower_bounds()[3], tolerance); // ROW4, equal constraint - EXPECT_NEAR( - 6.9, data_model.get_constraint_upper_bounds()[3], tolerance); // ROW4, equal constraint - EXPECT_NEAR(3.9, - data_model.get_constraint_lower_bounds()[4], - tolerance); // ROW5, lower turned into equal constraint - EXPECT_NEAR(3.9, - data_model.get_constraint_upper_bounds()[4], - tolerance); // ROW5, lower turned into equal constraint - EXPECT_NEAR(4.9, - data_model.get_constraint_lower_bounds()[5], - tolerance); // ROW6, greater turned into equal constraint - EXPECT_NEAR(4.9, - data_model.get_constraint_upper_bounds()[5], - tolerance); // ROW6, greater turned into equal constraint -} - -TEST(mps_ranges, free_ranges) -{ - std::string file = "linear_programming/good-mps-free-ranges.mps"; - auto mps = read_from_mps(file, false); - - EXPECT_NEAR(4.2, mps.ranges_values[0], tolerance); // ROW1 range value - EXPECT_NEAR(3.4, mps.ranges_values[1], tolerance); // ROW2 range value - EXPECT_NEAR(-1.6, mps.ranges_values[2], tolerance); // ROW3 range value - EXPECT_NEAR(3.4, mps.ranges_values[3], tolerance); // ROW3 range value - - std::string rel_file{}; - const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); - rel_file = rapidsDatasetRootDir + "/" + file; - auto data_model = parse_mps(rel_file, false); - - EXPECT_NEAR(1.2, data_model.get_constraint_lower_bounds()[0], tolerance); // ROW1 lower bound - EXPECT_NEAR(5.4, data_model.get_constraint_upper_bounds()[0], tolerance); // ROW1 upper bound - EXPECT_NEAR(1.5, data_model.get_constraint_lower_bounds()[1], tolerance); // ROW2 lower bound - EXPECT_NEAR(4.9, data_model.get_constraint_upper_bounds()[1], tolerance); // ROW2 upper bound - EXPECT_NEAR( - 7.9, data_model.get_constraint_lower_bounds()[2], tolerance); // ROW3, equal constraint - EXPECT_NEAR( - 9.5, data_model.get_constraint_upper_bounds()[2], tolerance); // ROW3, equal constraint - EXPECT_NEAR( - 3.5, data_model.get_constraint_lower_bounds()[3], tolerance); // ROW4, equal constraint - EXPECT_NEAR( - 6.9, data_model.get_constraint_upper_bounds()[3], tolerance); // ROW4, equal constraint - EXPECT_NEAR(3.9, - data_model.get_constraint_lower_bounds()[4], - tolerance); // ROW5, lower turned into equal constraint - EXPECT_NEAR(3.9, - data_model.get_constraint_upper_bounds()[4], - tolerance); // ROW5, lower turned into equal constraint - EXPECT_NEAR(4.9, - data_model.get_constraint_lower_bounds()[5], - tolerance); // ROW6, greater turned into equal constraint - EXPECT_NEAR(4.9, - data_model.get_constraint_upper_bounds()[5], - tolerance); // ROW6, greater turned into equal constraint -} - -TEST(mps_name, two_objectives) -{ - std::string file = "linear_programming/good-mps-fixed-two-objectives.mps"; - auto mps = read_from_mps(file, false); - - // Objective name should be first one found and not trigger an error - EXPECT_EQ(mps.objective_name, "COST"); -} - -TEST(mps_objname, two_objectives) -{ - std::string file = "linear_programming/good-mps-fixed-two-objectives-objname.mps"; - auto mps = read_from_mps(file, false); - - // Objective name is the second one found since it's specified as objname - EXPECT_EQ(mps.objective_name, "COST6679327"); -} - -TEST(mps_objname, two_objectives_next_line) -{ - std::string file = "linear_programming/good-mps-fixed-two-objectives-objname-next-line.mps"; - auto mps = read_from_mps(file, false); - - // Objective name is the second one found since it's specified as objname - EXPECT_EQ(mps.objective_name, "COST6679327"); -} - -TEST(mps_objname, bad_after) -{ - std::string file = "linear_programming/bad-mps-fixed-objname-after-rows.mps"; - ASSERT_THROW(read_from_mps(file, false), std::logic_error); -} - -TEST(mps_objname, bad_no_fixed) -{ - std::string file = "linear_programming/bad-mps-fixed-objname-after-rows.mps"; - ASSERT_THROW(read_from_mps(file, true), std::logic_error); -} - -TEST(mps_ranges, bad_name) -{ - ASSERT_THROW(read_from_mps("linear_programming/bad-mps-fixed-ranges-name.mps", false), - std::logic_error); -} - -TEST(mps_ranges, bad_value) -{ - ASSERT_THROW(read_from_mps("linear_programming/bad-mps-fixed-ranges-value.mps", false), - std::logic_error); -} - -TEST(mps_bounds, semi_continuous_bound_type) -{ - auto mps = read_from_mps("linear_programming/good-mps-semi-continuous-bound.mps", false); - - ASSERT_EQ(int(2), mps.var_names.size()); - ASSERT_EQ(int(2), mps.var_types.size()); - EXPECT_EQ('S', mps.var_types[0]); - ASSERT_EQ(int(2), mps.variable_lower_bounds.size()); - ASSERT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_DOUBLE_EQ(0.0, mps.variable_lower_bounds[0]); - EXPECT_DOUBLE_EQ(2.0, mps.variable_upper_bounds[0]); -} - -TEST(mps_bounds, invalid_bound_type) -{ - ASSERT_THROW(read_from_mps("linear_programming/bad-mps-bound-1.mps", false), std::logic_error); -} - -TEST(mps_parser, good_mps_file_mip_1) -{ - auto mps = read_from_mps("mixed_integer_programming/good-mip-mps-1.mps", false); - - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(8000., mps.A_values[0][0]); - EXPECT_EQ(4000., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(15., mps.A_values[1][0]); - EXPECT_EQ(30., mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(40000., mps.b_values[0]); - EXPECT_EQ(200., mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(100., mps.c_values[0]); - EXPECT_EQ(150., mps.c_values[1]); - ASSERT_EQ(int(2), mps.var_types.size()); - EXPECT_EQ('I', mps.var_types[0]); - EXPECT_EQ('I', mps.var_types[1]); - ASSERT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(0., mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - ASSERT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(10., mps.variable_upper_bounds[0]); - EXPECT_EQ(10., mps.variable_upper_bounds[1]); -} - -TEST(mps_parser, good_mps_file_mip_no_marker) -{ - auto mps = read_from_mps("mixed_integer_programming/good-mip-mps-1-no-mark.mps", false); - - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(8000., mps.A_values[0][0]); - EXPECT_EQ(4000., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(15., mps.A_values[1][0]); - EXPECT_EQ(30., mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(40000., mps.b_values[0]); - EXPECT_EQ(200., mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(100., mps.c_values[0]); - EXPECT_EQ(150., mps.c_values[1]); - ASSERT_EQ(int(2), mps.var_types.size()); - EXPECT_EQ('I', mps.var_types[0]); - EXPECT_EQ('I', mps.var_types[1]); - ASSERT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(0., mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - ASSERT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(10., mps.variable_upper_bounds[0]); - EXPECT_EQ(10., mps.variable_upper_bounds[1]); -} - -TEST(mps_parser, good_mps_file_no_bounds) -{ - auto mps = read_from_mps("mixed_integer_programming/good-mip-mps-no-bounds.mps", false); - - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(8000., mps.A_values[0][0]); - EXPECT_EQ(4000., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(15., mps.A_values[1][0]); - EXPECT_EQ(30., mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(40000., mps.b_values[0]); - EXPECT_EQ(200., mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(100., mps.c_values[0]); - EXPECT_EQ(150., mps.c_values[1]); - ASSERT_EQ(int(2), mps.var_types.size()); - EXPECT_EQ('I', mps.var_types[0]); - EXPECT_EQ('C', mps.var_types[1]); - - ASSERT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(0., mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - ASSERT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(1.0, mps.variable_upper_bounds[0]); - EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[1]); -} - -TEST(mps_parser, good_mps_file_partial_bounds) -{ - auto mps = read_from_mps("mixed_integer_programming/good-mip-mps-partial-bounds.mps", false); - - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(8000., mps.A_values[0][0]); - EXPECT_EQ(4000., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(15., mps.A_values[1][0]); - EXPECT_EQ(30., mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(40000., mps.b_values[0]); - EXPECT_EQ(200., mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(100., mps.c_values[0]); - EXPECT_EQ(150., mps.c_values[1]); - ASSERT_EQ(int(2), mps.var_types.size()); - EXPECT_EQ('I', mps.var_types[0]); - EXPECT_EQ('C', mps.var_types[1]); - - ASSERT_EQ(int(2), mps.variable_lower_bounds.size()); - EXPECT_EQ(0., mps.variable_lower_bounds[0]); - EXPECT_EQ(0., mps.variable_lower_bounds[1]); - ASSERT_EQ(int(2), mps.variable_upper_bounds.size()); - EXPECT_EQ(1.0, mps.variable_upper_bounds[0]); - EXPECT_EQ(10.0, mps.variable_upper_bounds[1]); -} - -#ifdef MPS_PARSER_WITH_BZIP2 -TEST(mps_parser, good_mps_file_bzip2_compressed) -{ - auto mps = read_from_mps("linear_programming/good-mps-1.mps.bz2"); - EXPECT_EQ("good-1", mps.problem_name); - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(3., mps.A_values[0][0]); - EXPECT_EQ(4., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(2.7, mps.A_values[1][0]); - EXPECT_EQ(10.1, mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(5.4, mps.b_values[0]); - EXPECT_EQ(4.9, mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(0.2, mps.c_values[0]); - EXPECT_EQ(0.1, mps.c_values[1]); -} -#endif // MPS_PARSER_WITH_BZIP2 - -#ifdef MPS_PARSER_WITH_ZLIB -TEST(mps_parser, good_mps_file_zlib_compressed) -{ - auto mps = read_from_mps("linear_programming/good-mps-1.mps.gz"); - EXPECT_EQ("good-1", mps.problem_name); - ASSERT_EQ(int(2), mps.row_names.size()); - EXPECT_EQ("ROW1", mps.row_names[0]); - EXPECT_EQ("ROW2", mps.row_names[1]); - ASSERT_EQ(int(2), mps.row_types.size()); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); - EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); - EXPECT_EQ("COST", mps.objective_name); - ASSERT_EQ(int(2), mps.var_names.size()); - EXPECT_EQ("VAR1", mps.var_names[0]); - EXPECT_EQ("VAR2", mps.var_names[1]); - ASSERT_EQ(int(2), mps.A_indices.size()); - ASSERT_EQ(int(2), mps.A_indices[0].size()); - EXPECT_EQ(int(0), mps.A_indices[0][0]); - EXPECT_EQ(int(1), mps.A_indices[0][1]); - ASSERT_EQ(int(2), mps.A_indices[1].size()); - EXPECT_EQ(int(0), mps.A_indices[1][0]); - EXPECT_EQ(int(1), mps.A_indices[1][1]); - ASSERT_EQ(int(2), mps.A_values.size()); - ASSERT_EQ(int(2), mps.A_values[0].size()); - EXPECT_EQ(3., mps.A_values[0][0]); - EXPECT_EQ(4., mps.A_values[0][1]); - ASSERT_EQ(int(2), mps.A_values[1].size()); - EXPECT_EQ(2.7, mps.A_values[1][0]); - EXPECT_EQ(10.1, mps.A_values[1][1]); - ASSERT_EQ(int(2), mps.b_values.size()); - EXPECT_EQ(5.4, mps.b_values[0]); - EXPECT_EQ(4.9, mps.b_values[1]); - ASSERT_EQ(int(2), mps.c_values.size()); - EXPECT_EQ(0.2, mps.c_values[0]); - EXPECT_EQ(0.1, mps.c_values[1]); -} -#endif // MPS_PARSER_WITH_ZLIB - -// ================================================================================================ -// QPS (Quadratic Programming) Support Tests -// ================================================================================================ - -// QPS-specific tests for quadratic programming support -TEST(qps_parser, quadratic_objective_basic) -{ - // Create a simple QPS test to verify quadratic objective parsing - // This would require actual QPS test files - for now, test the API - mps_data_model_t model; - - // Test setting quadratic objective matrix - std::vector Q_values = {2.0, 1.0, 1.0, 2.0}; // 2x2 matrix - std::vector Q_indices = {0, 1, 0, 1}; - std::vector Q_offsets = {0, 2, 4}; // CSR offsets - - model.set_quadratic_objective_matrix(Q_values, Q_indices, Q_offsets); - - // Verify the data was stored correctly - EXPECT_TRUE(model.has_quadratic_objective()); - EXPECT_EQ(4, model.get_quadratic_objective_values().size()); - EXPECT_EQ(2.0, model.get_quadratic_objective_values()[0]); - EXPECT_EQ(1.0, model.get_quadratic_objective_values()[1]); -} - -// ================================================================================================ -// QCMATRIX Support Tests -// ================================================================================================ - -TEST(qps_parser, qcmatrix_append_api) -{ - using model_t = mps_data_model_t; - model_t model; - - // Validate default-constructed struct shape. - model_t::quadratic_constraint_t default_qcm; - EXPECT_EQ(0, default_qcm.constraint_row_index); - EXPECT_TRUE(default_qcm.quadratic_values.empty()); - EXPECT_TRUE(default_qcm.quadratic_indices.empty()); - EXPECT_TRUE(default_qcm.quadratic_offsets.empty()); - EXPECT_TRUE(default_qcm.linear_values.empty()); - EXPECT_TRUE(default_qcm.linear_indices.empty()); - EXPECT_EQ(0.0, default_qcm.rhs_value); - - // QC0: [[10, 2], [2, 2]] - const std::vector qc0_values = {10.0, 2.0, 2.0, 2.0}; - const std::vector qc0_indices = {0, 1, 0, 1}; - const std::vector qc0_offsets = {0, 2, 4}; - const std::vector qc0_linear_values = {1.0, 1.0}; - const std::vector qc0_linear_indices = {0, 1}; - model.append_quadratic_constraint(0, - "QC0", - 'L', - qc0_linear_values, - qc0_linear_indices, - 5.0, - qc0_values, - qc0_indices, - qc0_offsets); - - // QC1: [[4, 1], [1, 6]] - const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; - const std::vector qc1_indices = {0, 1, 0, 1}; - const std::vector qc1_offsets = {0, 2, 4}; - const std::vector qc1_linear_values = {3.0, 1.0}; - const std::vector qc1_linear_indices = {0, 1}; - model.append_quadratic_constraint(1, - "QC1", - 'L', - qc1_linear_values, - qc1_linear_indices, - 10.0, - qc1_values, - qc1_indices, - qc1_offsets); - - ASSERT_TRUE(model.has_quadratic_constraints()); - const auto& qcs = model.get_quadratic_constraints(); - ASSERT_EQ(2u, qcs.size()); - - EXPECT_EQ(0, qcs[0].constraint_row_index); - EXPECT_EQ("QC0", qcs[0].constraint_row_name); - EXPECT_EQ('L', qcs[0].constraint_row_type); - EXPECT_EQ(qc0_linear_values, qcs[0].linear_values); - EXPECT_EQ(qc0_linear_indices, qcs[0].linear_indices); - EXPECT_EQ(5.0, qcs[0].rhs_value); - EXPECT_EQ(qc0_values, qcs[0].quadratic_values); - EXPECT_EQ(qc0_indices, qcs[0].quadratic_indices); - EXPECT_EQ(qc0_offsets, qcs[0].quadratic_offsets); - - EXPECT_EQ(1, qcs[1].constraint_row_index); - EXPECT_EQ("QC1", qcs[1].constraint_row_name); - EXPECT_EQ('L', qcs[1].constraint_row_type); - EXPECT_EQ(qc1_linear_values, qcs[1].linear_values); - EXPECT_EQ(qc1_linear_indices, qcs[1].linear_indices); - EXPECT_EQ(10.0, qcs[1].rhs_value); - EXPECT_EQ(qc1_values, qcs[1].quadratic_values); - EXPECT_EQ(qc1_indices, qcs[1].quadratic_indices); - EXPECT_EQ(qc1_offsets, qcs[1].quadratic_offsets); -} - -// QCQP MPS: each quadratic constraint bundles row + linear + rhs + quadratic. -TEST(qps_parser, qcmatrix_mps_linear_rhs_and_bounds) -{ - if (!file_exists("qcqp/QC_Test_1.mps")) { - GTEST_SKIP() << "qcqp/QC_Test_1.mps not in dataset root"; - } - const auto model = parse_mps( - cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/QC_Test_1.mps", false); - - ASSERT_TRUE(model.has_quadratic_constraints()); - const auto& qcs = model.get_quadratic_constraints(); - ASSERT_EQ(2u, qcs.size()); - - ASSERT_EQ(1, model.get_n_constraints()); - ASSERT_EQ(1u, model.get_row_names().size()); - EXPECT_EQ("LIN0", model.get_row_names()[0]); - EXPECT_EQ('L', model.get_row_types()[0]); - - // LIN0: 2*x1 + x2 ≤ 15 (linear row only; not duplicated in quadratic_constraints) - EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), - model.get_constraint_lower_bounds()[0]); - EXPECT_DOUBLE_EQ(15.0, model.get_constraint_upper_bounds()[0]); - const auto& A_off = model.get_constraint_matrix_offsets(); - const auto& A_val = model.get_constraint_matrix_values(); - const auto& A_idx = model.get_constraint_matrix_indices(); - ASSERT_EQ(2, A_off[1] - A_off[0]); - EXPECT_EQ(2.0, A_val[A_off[0] + 0]); - EXPECT_EQ(1.0, A_val[A_off[0] + 1]); - EXPECT_EQ(0, A_idx[A_off[0] + 0]); - EXPECT_EQ(1, A_idx[A_off[0] + 1]); - - // QC0: x1 + x2 + xᵀQ₀x ≤ 5 (MPS ROWS declaration index 1; OBJ 'N' rows are not counted) - EXPECT_EQ(1, qcs[0].constraint_row_index); - EXPECT_EQ("QC0", qcs[0].constraint_row_name); - EXPECT_EQ('L', qcs[0].constraint_row_type); - ASSERT_EQ(2u, qcs[0].linear_values.size()); - EXPECT_EQ(1.0, qcs[0].linear_values[0]); - EXPECT_EQ(1.0, qcs[0].linear_values[1]); - EXPECT_EQ(0, qcs[0].linear_indices[0]); - EXPECT_EQ(1, qcs[0].linear_indices[1]); - EXPECT_DOUBLE_EQ(5.0, qcs[0].rhs_value); - EXPECT_FALSE(qcs[0].quadratic_values.empty()); - - // QC1: 3*x1 + x2 + xᵀQ₁x ≤ 10 - EXPECT_EQ(2, qcs[1].constraint_row_index); - EXPECT_EQ("QC1", qcs[1].constraint_row_name); - EXPECT_EQ('L', qcs[1].constraint_row_type); - ASSERT_EQ(2u, qcs[1].linear_values.size()); - EXPECT_EQ(3.0, qcs[1].linear_values[0]); - EXPECT_EQ(1.0, qcs[1].linear_values[1]); - EXPECT_DOUBLE_EQ(10.0, qcs[1].rhs_value); -} - -TEST(qps_parser, qcqp_p0033_mps_sections) -{ - if (!file_exists("qcqp/p0033_qc1.mps")) { - GTEST_SKIP() << "qcqp/p0033_qc1.mps not in dataset root"; - } - const auto model = parse_mps( - cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps", false); - - EXPECT_EQ(12, model.get_n_constraints()); - EXPECT_EQ(33, model.get_n_variables()); - ASSERT_EQ(12u, model.get_row_types().size()); - ASSERT_EQ(12u, model.get_row_names().size()); - - const auto& qcs = model.get_quadratic_constraints(); - ASSERT_EQ(4u, qcs.size()); - EXPECT_EQ(12, qcs[0].constraint_row_index); - ASSERT_EQ(1u, qcs[0].linear_values.size()); - EXPECT_DOUBLE_EQ(1.0, qcs[0].linear_values[0]); - - const auto& vnames = model.get_variable_names(); - auto c159_it = std::find(vnames.begin(), vnames.end(), std::string("C159")); - ASSERT_NE(c159_it, vnames.end()); - EXPECT_EQ(static_cast(c159_it - vnames.begin()), qcs[0].linear_indices[0]); - - EXPECT_DOUBLE_EQ(1.0, qcs[0].rhs_value); - EXPECT_FALSE(qcs[0].quadratic_values.empty()); -} - -// Test actual QPS files from the dataset -TEST(qps_parser, test_qps_files) -{ - // Test QP_Test_1.qps if it exists - if (file_exists("quadratic_programming/QP_Test_1.qps")) { - auto parsed_data = parse_mps( - cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_1.qps", false); - - EXPECT_EQ("QP_Test_1", parsed_data.get_problem_name()); - EXPECT_EQ(2, parsed_data.get_n_variables()); // C------1 and C------2 - EXPECT_EQ(1, parsed_data.get_n_constraints()); // R------1 - EXPECT_TRUE(parsed_data.has_quadratic_objective()); - - // Check variable bounds - const auto& lower_bounds = parsed_data.get_variable_lower_bounds(); - const auto& upper_bounds = parsed_data.get_variable_upper_bounds(); - - EXPECT_NEAR(2.0, lower_bounds[0], tolerance); // C------1 lower bound - EXPECT_NEAR(50.0, upper_bounds[0], tolerance); // C------1 upper bound - EXPECT_NEAR(-50.0, lower_bounds[1], tolerance); // C------2 lower bound - EXPECT_NEAR(50.0, upper_bounds[1], tolerance); // C------2 upper bound - } - - // Test QP_Test_2.qps if it exists - if (file_exists("quadratic_programming/QP_Test_2.qps")) { - auto parsed_data = parse_mps( - cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_2.qps", false); - - EXPECT_EQ("QP_Test_2", parsed_data.get_problem_name()); - EXPECT_EQ(3, parsed_data.get_n_variables()); // C------1, C------2, C------3 - EXPECT_EQ(1, parsed_data.get_n_constraints()); // R------1 - EXPECT_TRUE(parsed_data.has_quadratic_objective()); - - // Check that quadratic objective matrix has values - const auto& Q_values = parsed_data.get_quadratic_objective_values(); - EXPECT_GT(Q_values.size(), 0) << "Quadratic objective should have non-zero elements"; - } -} - -// ================================================================================================ -// MPS Round-Trip Tests (Read -> Write -> Read -> Compare) -// ================================================================================================ - -// Helper function to compare two data models -template -void compare_data_models(const mps_data_model_t& original, - const mps_data_model_t& reloaded, - f_t tol = 1e-9) -{ - // Compare basic dimensions - EXPECT_EQ(original.get_n_variables(), reloaded.get_n_variables()); - EXPECT_EQ(original.get_n_constraints(), reloaded.get_n_constraints()); - - // Compare objective coefficients - auto orig_c = original.get_objective_coefficients(); - auto reload_c = reloaded.get_objective_coefficients(); - ASSERT_EQ(orig_c.size(), reload_c.size()); - for (size_t i = 0; i < orig_c.size(); ++i) { - EXPECT_NEAR(orig_c[i], reload_c[i], tol) << "Objective coefficient mismatch at index " << i; - } - - // Compare constraint matrix values - auto orig_A = original.get_constraint_matrix_values(); - auto reload_A = reloaded.get_constraint_matrix_values(); - ASSERT_EQ(orig_A.size(), reload_A.size()); - for (size_t i = 0; i < orig_A.size(); ++i) { - EXPECT_NEAR(orig_A[i], reload_A[i], tol) << "Constraint matrix value mismatch at index " << i; - } - - // Compare constraint matrix indices - auto orig_A_idx = original.get_constraint_matrix_indices(); - auto reload_A_idx = reloaded.get_constraint_matrix_indices(); - ASSERT_EQ(orig_A_idx.size(), reload_A_idx.size()); - for (size_t i = 0; i < orig_A_idx.size(); ++i) { - EXPECT_EQ(orig_A_idx[i], reload_A_idx[i]) << "Constraint matrix index mismatch at index " << i; - } - - // Compare constraint matrix offsets - auto orig_A_off = original.get_constraint_matrix_offsets(); - auto reload_A_off = reloaded.get_constraint_matrix_offsets(); - ASSERT_EQ(orig_A_off.size(), reload_A_off.size()); - for (size_t i = 0; i < orig_A_off.size(); ++i) { - EXPECT_EQ(orig_A_off[i], reload_A_off[i]) << "Constraint matrix offset mismatch at index " << i; - } - - // Compare variable bounds - auto orig_lb = original.get_variable_lower_bounds(); - auto reload_lb = reloaded.get_variable_lower_bounds(); - ASSERT_EQ(orig_lb.size(), reload_lb.size()); - for (size_t i = 0; i < orig_lb.size(); ++i) { - if (std::isinf(orig_lb[i]) && std::isinf(reload_lb[i])) { - EXPECT_EQ(std::signbit(orig_lb[i]), std::signbit(reload_lb[i])) - << "Variable lower bound infinity sign mismatch at index " << i; - } else { - EXPECT_NEAR(orig_lb[i], reload_lb[i], tol) << "Variable lower bound mismatch at index " << i; - } - } - - auto orig_ub = original.get_variable_upper_bounds(); - auto reload_ub = reloaded.get_variable_upper_bounds(); - ASSERT_EQ(orig_ub.size(), reload_ub.size()); - for (size_t i = 0; i < orig_ub.size(); ++i) { - if (std::isinf(orig_ub[i]) && std::isinf(reload_ub[i])) { - EXPECT_EQ(std::signbit(orig_ub[i]), std::signbit(reload_ub[i])) - << "Variable upper bound infinity sign mismatch at index " << i; - } else { - EXPECT_NEAR(orig_ub[i], reload_ub[i], tol) << "Variable upper bound mismatch at index " << i; - } - } - - // Compare constraint bounds - auto orig_cl = original.get_constraint_lower_bounds(); - auto reload_cl = reloaded.get_constraint_lower_bounds(); - ASSERT_EQ(orig_cl.size(), reload_cl.size()); - for (size_t i = 0; i < orig_cl.size(); ++i) { - if (std::isinf(orig_cl[i]) && std::isinf(reload_cl[i])) { - EXPECT_EQ(std::signbit(orig_cl[i]), std::signbit(reload_cl[i])) - << "Constraint lower bound infinity sign mismatch at index " << i; - } else { - EXPECT_NEAR(orig_cl[i], reload_cl[i], tol) - << "Constraint lower bound mismatch at index " << i; - } - } - - auto orig_cu = original.get_constraint_upper_bounds(); - auto reload_cu = reloaded.get_constraint_upper_bounds(); - ASSERT_EQ(orig_cu.size(), reload_cu.size()); - for (size_t i = 0; i < orig_cu.size(); ++i) { - if (std::isinf(orig_cu[i]) && std::isinf(reload_cu[i])) { - EXPECT_EQ(std::signbit(orig_cu[i]), std::signbit(reload_cu[i])) - << "Constraint upper bound infinity sign mismatch at index " << i; - } else { - EXPECT_NEAR(orig_cu[i], reload_cu[i], tol) - << "Constraint upper bound mismatch at index " << i; - } - } - - // Compare quadratic objective if present - EXPECT_EQ(original.has_quadratic_objective(), reloaded.has_quadratic_objective()); - if (original.has_quadratic_objective() && reloaded.has_quadratic_objective()) { - auto orig_Q = original.get_quadratic_objective_values(); - auto orig_Q_idx = original.get_quadratic_objective_indices(); - auto orig_Q_off = original.get_quadratic_objective_offsets(); - auto reload_Q = reloaded.get_quadratic_objective_values(); - auto reload_Q_idx = reloaded.get_quadratic_objective_indices(); - auto reload_Q_off = reloaded.get_quadratic_objective_offsets(); - - // Compare Q matrix structure and values - ASSERT_EQ(orig_Q.size(), reload_Q.size()) << "Q values size mismatch"; - ASSERT_EQ(orig_Q_idx.size(), reload_Q_idx.size()) << "Q indices size mismatch"; - ASSERT_EQ(orig_Q_off.size(), reload_Q_off.size()) << "Q offsets size mismatch"; - - for (size_t i = 0; i < orig_Q.size(); ++i) { - EXPECT_NEAR(orig_Q[i], reload_Q[i], tol) << "Q value mismatch at index " << i; - } - for (size_t i = 0; i < orig_Q_idx.size(); ++i) { - EXPECT_EQ(orig_Q_idx[i], reload_Q_idx[i]) << "Q index mismatch at index " << i; - } - for (size_t i = 0; i < orig_Q_off.size(); ++i) { - EXPECT_EQ(orig_Q_off[i], reload_Q_off[i]) << "Q offset mismatch at index " << i; - } - } - - EXPECT_EQ(original.has_quadratic_constraints(), reloaded.has_quadratic_constraints()); - if (original.has_quadratic_constraints() && reloaded.has_quadratic_constraints()) { - const auto& oqc = original.get_quadratic_constraints(); - const auto& rq = reloaded.get_quadratic_constraints(); - ASSERT_EQ(oqc.size(), rq.size()) << "Quadratic constraint count mismatch"; - for (size_t k = 0; k < oqc.size(); ++k) { - EXPECT_EQ(oqc[k].constraint_row_index, rq[k].constraint_row_index); - EXPECT_EQ(oqc[k].constraint_row_name, rq[k].constraint_row_name); - EXPECT_EQ(oqc[k].constraint_row_type, rq[k].constraint_row_type); - EXPECT_NEAR(oqc[k].rhs_value, rq[k].rhs_value, tol); - ASSERT_EQ(oqc[k].linear_values.size(), rq[k].linear_values.size()); - ASSERT_EQ(oqc[k].linear_indices.size(), rq[k].linear_indices.size()); - for (size_t i = 0; i < oqc[k].linear_values.size(); ++i) { - EXPECT_NEAR(oqc[k].linear_values[i], rq[k].linear_values[i], tol); - EXPECT_EQ(oqc[k].linear_indices[i], rq[k].linear_indices[i]); - } - ASSERT_EQ(oqc[k].quadratic_values.size(), rq[k].quadratic_values.size()); - ASSERT_EQ(oqc[k].quadratic_indices.size(), rq[k].quadratic_indices.size()); - ASSERT_EQ(oqc[k].quadratic_offsets.size(), rq[k].quadratic_offsets.size()); - for (size_t i = 0; i < oqc[k].quadratic_values.size(); ++i) { - EXPECT_NEAR(oqc[k].quadratic_values[i], rq[k].quadratic_values[i], tol); - } - for (size_t i = 0; i < oqc[k].quadratic_indices.size(); ++i) { - EXPECT_EQ(oqc[k].quadratic_indices[i], rq[k].quadratic_indices[i]); - } - for (size_t i = 0; i < oqc[k].quadratic_offsets.size(); ++i) { - EXPECT_EQ(oqc[k].quadratic_offsets[i], rq[k].quadratic_offsets[i]); - } - } - } -} - -TEST(mps_roundtrip, linear_programming_basic) -{ - std::string input_file = - cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/good-mps-1.mps"; - std::string temp_file = "/tmp/mps_roundtrip_lp_test.mps"; - - // Read original - auto original = parse_mps(input_file, true); - - // Write to temp file - mps_writer_t writer(original); - writer.write(temp_file); - - // Read back - auto reloaded = parse_mps(temp_file, false); - - // Compare - compare_data_models(original, reloaded); - - // Cleanup - std::filesystem::remove(temp_file); -} - -TEST(mps_roundtrip, linear_programming_with_bounds) -{ - if (!file_exists("linear_programming/lp_model_with_var_bounds.mps")) { - GTEST_SKIP() << "Test file not found"; - } - - std::string input_file = - cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/lp_model_with_var_bounds.mps"; - std::string temp_file = "/tmp/mps_roundtrip_lp_bounds_test.mps"; - - // Read original - auto original = parse_mps(input_file, false); - - // Write to temp file - mps_writer_t writer(original); - writer.write(temp_file); - - // Read back - auto reloaded = parse_mps(temp_file, false); - - // Compare - compare_data_models(original, reloaded); - - // Cleanup - std::filesystem::remove(temp_file); -} - -TEST(mps_roundtrip, quadratic_programming_qp_test_1) -{ - if (!file_exists("quadratic_programming/QP_Test_1.qps")) { - GTEST_SKIP() << "Test file not found"; - } - - std::string input_file = - cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_1.qps"; - std::string temp_file = "/tmp/mps_roundtrip_qp_test_1.mps"; - - // Read original - auto original = parse_mps(input_file, false); - ASSERT_TRUE(original.has_quadratic_objective()) << "Original should have quadratic objective"; - - // Write to temp file - mps_writer_t writer(original); - writer.write(temp_file); - - // Read back - auto reloaded = parse_mps(temp_file, false); - ASSERT_TRUE(reloaded.has_quadratic_objective()) << "Reloaded should have quadratic objective"; - - // Compare - compare_data_models(original, reloaded); - - // Cleanup - std::filesystem::remove(temp_file); -} - -TEST(mps_roundtrip, quadratic_programming_qp_test_2) -{ - if (!file_exists("quadratic_programming/QP_Test_2.qps")) { - GTEST_SKIP() << "Test file not found"; - } - - std::string input_file = - cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_2.qps"; - std::string temp_file = "/tmp/mps_roundtrip_qp_test_2.mps"; - - // Read original - auto original = parse_mps(input_file, false); - ASSERT_TRUE(original.has_quadratic_objective()) << "Original should have quadratic objective"; - - // Write to temp file - mps_writer_t writer(original); - writer.write(temp_file); - - // Read back - auto reloaded = parse_mps(temp_file, false); - ASSERT_TRUE(reloaded.has_quadratic_objective()) << "Reloaded should have quadratic objective"; - - // Compare - compare_data_models(original, reloaded); - - // Cleanup - std::filesystem::remove(temp_file); -} - -TEST(mps_roundtrip, qcqp_p0033_qc1) -{ - if (!file_exists("qcqp/p0033_qc1.mps")) { GTEST_SKIP() << "Test file not found"; } - - std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps"; - std::string temp_file = "/tmp/mps_roundtrip_p0033_qc1.mps"; - std::string temp_file_2 = "/tmp/mps_roundtrip_p0033_qc1_r2.mps"; - - auto original = parse_mps(input_file, false); - ASSERT_TRUE(original.has_quadratic_objective()); - ASSERT_TRUE(original.has_quadratic_constraints()); - - mps_writer_t writer(original); - writer.write(temp_file); - - auto reloaded = parse_mps(temp_file, false); - mps_writer_t writer_r2(reloaded); - writer_r2.write(temp_file_2); - auto reloaded_2 = parse_mps(temp_file_2, false); - compare_data_models(reloaded, reloaded_2); - - std::filesystem::remove(temp_file); - std::filesystem::remove(temp_file_2); -} - -} // namespace cuopt::linear_programming::io diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp new file mode 100644 index 0000000000..0b65c83c38 --- /dev/null +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -0,0 +1,2751 @@ +/* clang-format off */ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* clang-format on */ + +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cuopt::linear_programming::io { + +constexpr double tolerance = 1e-6; + +mps_parser_t read_from_mps(const std::string& file, bool fixed_format = true) +{ + std::string rel_file{}; + // assume relative paths are relative to RAPIDS_DATASET_ROOT_DIR + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + rel_file = rapidsDatasetRootDir + "/" + file; + // Empty problem not used in the test + mps_data_model_t problem; + mps_parser_t mps{problem, rel_file, fixed_format}; + return mps; +} + +bool file_exists(const std::string& file) +{ + std::string rel_file{}; + // assume relative paths are relative to RAPIDS_DATASET_ROOT_DIR + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + rel_file = rapidsDatasetRootDir + "/" + file; + return std::filesystem::exists(rel_file); +} + +namespace { + +// Non-template forwarding wrapper around parse_lp_from_string. +// Exists only so EXPECT_THROW(parse_lp_string(R"LP(...)LP"), exc) is parsed +// correctly — gtest's macro splits its args on top-level commas, and the +// comma inside would otherwise be treated as a macro-arg +// separator. +mps_data_model_t parse_lp_string(std::string_view content) +{ + return parse_lp_from_string(content); +} + +// Returns the index of `name` in the variable list, or -1 if absent. +int find_var(const mps_data_model_t& m, const std::string& name) +{ + const auto& names = m.get_variable_names(); + for (size_t i = 0; i < names.size(); ++i) { + if (names[i] == name) return static_cast(i); + } + return -1; +} + +int find_row(const mps_data_model_t& m, const std::string& name) +{ + const auto& names = m.get_row_names(); + for (size_t i = 0; i < names.size(); ++i) { + if (names[i] == name) return static_cast(i); + } + return -1; +} + +// Returns A[row, col] by scanning the CSR row. Zero if the entry is missing. +double a_entry(const mps_data_model_t& m, int row, int col) +{ + const auto& offsets = m.get_constraint_matrix_offsets(); + const auto& indices = m.get_constraint_matrix_indices(); + const auto& values = m.get_constraint_matrix_values(); + for (int k = offsets[row]; k < offsets[row + 1]; ++k) { + if (indices[k] == col) return values[k]; + } + return 0.0; +} + +// Returns Q[row, col] by scanning the CSR row of the quadratic matrix. +double q_entry(const mps_data_model_t& m, int row, int col) +{ + const auto& offsets = m.get_quadratic_objective_offsets(); + const auto& indices = m.get_quadratic_objective_indices(); + const auto& values = m.get_quadratic_objective_values(); + if (offsets.empty()) return 0.0; + for (int k = offsets[row]; k < offsets[row + 1]; ++k) { + if (indices[k] == col) return values[k]; + } + return 0.0; +} + +} // namespace + +// =========================================================================== +// Per-fixture test classes. Each class describes one named problem fixture +// and owns the checker for that problem's expected parsed data model. The +// MPS and LP TEST_F cases within a fixture share the same `check_model` +// method, so the expected values live in exactly one place per fixture. +// +// All fixtures inherit a common base that supplies parse_mps_file and +// parse_lp_file helpers. +// =========================================================================== + +class parser_fixture_base : public ::testing::Test { + protected: + static mps_data_model_t parse_mps_file(const std::string& file, + bool fixed_format = true) + { + const std::string& root = cuopt::test::get_rapids_dataset_root_dir(); + return parse_mps(root + "/" + file, fixed_format); + } + + static mps_data_model_t parse_lp_file(const std::string& file) + { + const std::string& root = cuopt::test::get_rapids_dataset_root_dir(); + return parse_lp(root + "/" + file); + } +}; + +// 2 vars (continuous, default [0,inf) bounds), 2 <= constraints. +// min 0.2*VAR1 + 0.1*VAR2 +// ROW1: 3*VAR1 + 4*VAR2 <= 5.4 +// ROW2: 2.7*VAR1 + 10.1*VAR2 <= 4.9 +class good_mps_1_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + EXPECT_FALSE(m.get_sense()); + ASSERT_EQ(2, m.get_n_variables()); + ASSERT_EQ(2, m.get_n_constraints()); + EXPECT_EQ("VAR1", m.get_variable_names()[0]); + EXPECT_EQ("VAR2", m.get_variable_names()[1]); + EXPECT_EQ("ROW1", m.get_row_names()[0]); + EXPECT_EQ("ROW2", m.get_row_names()[1]); + EXPECT_EQ('C', m.get_variable_types()[0]); + EXPECT_EQ('C', m.get_variable_types()[1]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[0]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[0]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[1]); + EXPECT_NEAR(0.2, m.get_objective_coefficients()[0], tolerance); + EXPECT_NEAR(0.1, m.get_objective_coefficients()[1], tolerance); + EXPECT_EQ(-std::numeric_limits::infinity(), m.get_constraint_lower_bounds()[0]); + EXPECT_NEAR(5.4, m.get_constraint_upper_bounds()[0], tolerance); + EXPECT_EQ(-std::numeric_limits::infinity(), m.get_constraint_lower_bounds()[1]); + EXPECT_NEAR(4.9, m.get_constraint_upper_bounds()[1], tolerance); + const auto& off = m.get_constraint_matrix_offsets(); + const auto& idx = m.get_constraint_matrix_indices(); + const auto& val = m.get_constraint_matrix_values(); + ASSERT_EQ(3u, off.size()); + EXPECT_EQ(0, off[0]); + EXPECT_EQ(2, off[1]); + EXPECT_EQ(4, off[2]); + EXPECT_EQ(0, idx[0]); + EXPECT_NEAR(3.0, val[0], tolerance); + EXPECT_EQ(1, idx[1]); + EXPECT_NEAR(4.0, val[1], tolerance); + EXPECT_EQ(0, idx[2]); + EXPECT_NEAR(2.7, val[2], tolerance); + EXPECT_EQ(1, idx[3]); + EXPECT_NEAR(10.1, val[3], tolerance); + EXPECT_FALSE(m.has_quadratic_objective()); + } +}; + +// min 2x - y; x+y <= 3; 0<=x<=1, 1<=y<=2. +class up_low_bounds_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + EXPECT_FALSE(m.get_sense()); + ASSERT_EQ(2, m.get_n_variables()); + ASSERT_EQ(1, m.get_n_constraints()); + EXPECT_EQ("x", m.get_variable_names()[0]); + EXPECT_EQ("y", m.get_variable_names()[1]); + EXPECT_EQ("con", m.get_row_names()[0]); + EXPECT_NEAR(2.0, m.get_objective_coefficients()[0], tolerance); + EXPECT_NEAR(-1.0, m.get_objective_coefficients()[1], tolerance); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[0]); + EXPECT_EQ(1.0, m.get_variable_upper_bounds()[0]); + EXPECT_EQ(1.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(2.0, m.get_variable_upper_bounds()[1]); + EXPECT_EQ(-std::numeric_limits::infinity(), m.get_constraint_lower_bounds()[0]); + EXPECT_NEAR(3.0, m.get_constraint_upper_bounds()[0], tolerance); + const auto& val = m.get_constraint_matrix_values(); + ASSERT_EQ(2u, val.size()); + EXPECT_NEAR(1.0, val[0], tolerance); + EXPECT_NEAR(1.0, val[1], tolerance); + } +}; + +// good-mps-1 objective/matrix/rows; -1 <= VAR1 <= inf, 0 <= VAR2 <= 2. +class some_var_bounds_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + ASSERT_EQ(2, m.get_n_variables()); + EXPECT_EQ(-1.0, m.get_variable_lower_bounds()[0]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[0]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(2.0, m.get_variable_upper_bounds()[1]); + } +}; + +// VAR1 fixed at 2; VAR2 default [0, inf). +class fixed_var_bound_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + ASSERT_EQ(2, m.get_n_variables()); + EXPECT_EQ(2.0, m.get_variable_lower_bounds()[0]); + EXPECT_EQ(2.0, m.get_variable_upper_bounds()[0]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[1]); + } +}; + +// VAR1 free (-inf, +inf); VAR2 default [0, +inf). +class free_var_bound_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + ASSERT_EQ(2, m.get_n_variables()); + EXPECT_EQ(-std::numeric_limits::infinity(), m.get_variable_lower_bounds()[0]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[0]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[1]); + } +}; + +// VAR1 lower=-inf (MI in MPS / -inf in LP), upper default +inf; VAR2 default. +// Effective bounds match free_var_bound_test — the two fixtures differ only in +// how the lower -inf is spelled (free vs explicit -inf bound). +class lower_inf_var_bound_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + ASSERT_EQ(2, m.get_n_variables()); + EXPECT_EQ(-std::numeric_limits::infinity(), m.get_variable_lower_bounds()[0]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[0]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[1]); + } +}; + +// VAR1 upper=+inf (PL in MPS / inf in LP); both default lower 0. Effective +// bounds match two default [0, +inf) variables. +class upper_inf_var_bound_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + ASSERT_EQ(2, m.get_n_variables()); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[0]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[0]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[1]); + } +}; + +// 2 integer vars bounded [0, 10]; max 100 VAR1 + 150 VAR2; +// 8000 VAR1 + 4000 VAR2 <= 40000 ; 15 VAR1 + 30 VAR2 <= 200. +class mip_with_bounds_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + EXPECT_TRUE(m.get_sense()); + ASSERT_EQ(2, m.get_n_variables()); + ASSERT_EQ(2, m.get_n_constraints()); + EXPECT_EQ("VAR1", m.get_variable_names()[0]); + EXPECT_EQ("VAR2", m.get_variable_names()[1]); + EXPECT_EQ('I', m.get_variable_types()[0]); + EXPECT_EQ('I', m.get_variable_types()[1]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[0]); + EXPECT_EQ(10.0, m.get_variable_upper_bounds()[0]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(10.0, m.get_variable_upper_bounds()[1]); + EXPECT_NEAR(100.0, m.get_objective_coefficients()[0], tolerance); + EXPECT_NEAR(150.0, m.get_objective_coefficients()[1], tolerance); + EXPECT_EQ(-std::numeric_limits::infinity(), m.get_constraint_lower_bounds()[0]); + EXPECT_NEAR(40000.0, m.get_constraint_upper_bounds()[0], tolerance); + EXPECT_EQ(-std::numeric_limits::infinity(), m.get_constraint_lower_bounds()[1]); + EXPECT_NEAR(200.0, m.get_constraint_upper_bounds()[1], tolerance); + const auto& val = m.get_constraint_matrix_values(); + ASSERT_EQ(4u, val.size()); + EXPECT_NEAR(8000.0, val[0], tolerance); + EXPECT_NEAR(4000.0, val[1], tolerance); + EXPECT_NEAR(15.0, val[2], tolerance); + EXPECT_NEAR(30.0, val[3], tolerance); + } +}; + +// Like mip_with_bounds but VAR1 is binary ([0,1]) and VAR2 is continuous, +// default upper +inf. (MPS: no explicit bounds on integer => [0,1]. LP: VAR1 +// listed under Binaries.) +class mip_no_bounds_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + EXPECT_TRUE(m.get_sense()); + ASSERT_EQ(2, m.get_n_variables()); + EXPECT_EQ('I', m.get_variable_types()[0]); + EXPECT_EQ('C', m.get_variable_types()[1]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[0]); + EXPECT_EQ(1.0, m.get_variable_upper_bounds()[0]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(std::numeric_limits::infinity(), m.get_variable_upper_bounds()[1]); + } +}; + +// VAR1 binary ([0,1]); VAR2 continuous with explicit upper 10. +class mip_partial_bounds_test : public parser_fixture_base { + protected: + static void check_model(const mps_data_model_t& m) + { + EXPECT_TRUE(m.get_sense()); + ASSERT_EQ(2, m.get_n_variables()); + EXPECT_EQ('I', m.get_variable_types()[0]); + EXPECT_EQ('C', m.get_variable_types()[1]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[0]); + EXPECT_EQ(1.0, m.get_variable_upper_bounds()[0]); + EXPECT_EQ(0.0, m.get_variable_lower_bounds()[1]); + EXPECT_EQ(10.0, m.get_variable_upper_bounds()[1]); + } +}; + +TEST(mps_parser, bad_mps_files) +{ + std::stringstream ss; + static constexpr int NumMpsFiles = 15; + for (int i = 1; i <= NumMpsFiles; ++i) { + ss << "linear_programming/bad-mps-" << i << ".mps"; + // Check if file exists + if (file_exists(ss.str())) ASSERT_THROW(read_from_mps(ss.str()), std::logic_error); + ss.str(std::string{}); + ss.clear(); + } +} + +TEST_F(good_mps_1_test, mps) +{ + check_model(parse_mps_file("linear_programming/good-mps-1.mps")); + // Parser-struct fields that are MPS-only (not exposed via the data model). + auto mps = read_from_mps("linear_programming/good-mps-1.mps"); + EXPECT_EQ("good-1", mps.problem_name); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); +} + +TEST_F(good_mps_1_test, lp) { check_model(parse_lp_file("linear_programming/good-mps-1.lp")); } + +// Compressed-LP coverage: parse_lp() shares file_to_string() with parse_mps(), +// so the same dlopen-based decompression path that handles .mps.gz / .mps.bz2 +// must also work for .lp.gz / .lp.bz2. +TEST_F(good_mps_1_test, lp_zlib_compressed) +{ + check_model(parse_lp_file("linear_programming/good-mps-1.lp.gz")); +} + +TEST_F(good_mps_1_test, lp_bzip2_compressed) +{ + check_model(parse_lp_file("linear_programming/good-mps-1.lp.bz2")); +} + +TEST(mps_parser, good_mps_file_clrf) +{ + auto mps = read_from_mps("linear_programming/good-mps-1-clrf.mps"); + EXPECT_EQ("good-1", mps.problem_name); + ASSERT_EQ(int(2), mps.row_names.size()); + EXPECT_EQ("ROW1", mps.row_names[0]); + EXPECT_EQ("ROW2", mps.row_names[1]); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.var_names.size()); + EXPECT_EQ("VAR1", mps.var_names[0]); + EXPECT_EQ("VAR2", mps.var_names[1]); + ASSERT_EQ(int(2), mps.A_indices.size()); + ASSERT_EQ(int(2), mps.A_indices[0].size()); + EXPECT_EQ(int(0), mps.A_indices[0][0]); + EXPECT_EQ(int(1), mps.A_indices[0][1]); + ASSERT_EQ(int(2), mps.A_indices[1].size()); + EXPECT_EQ(int(0), mps.A_indices[1][0]); + EXPECT_EQ(int(1), mps.A_indices[1][1]); + ASSERT_EQ(int(2), mps.A_values.size()); + ASSERT_EQ(int(2), mps.A_values[0].size()); + EXPECT_EQ(3., mps.A_values[0][0]); + EXPECT_EQ(4., mps.A_values[0][1]); + ASSERT_EQ(int(2), mps.A_values[1].size()); + EXPECT_EQ(2.7, mps.A_values[1][0]); + EXPECT_EQ(10.1, mps.A_values[1][1]); + ASSERT_EQ(int(2), mps.b_values.size()); + EXPECT_EQ(5.4, mps.b_values[0]); + EXPECT_EQ(4.9, mps.b_values[1]); + ASSERT_EQ(int(2), mps.c_values.size()); + EXPECT_EQ(0.2, mps.c_values[0]); + EXPECT_EQ(0.1, mps.c_values[1]); +} + +TEST(mps_parser, good_mps_free_file_clrf) +{ + auto mps = read_from_mps("linear_programming/good-mps-1-clrf.mps", false); + EXPECT_EQ("good-1", mps.problem_name); + ASSERT_EQ(int(2), mps.row_names.size()); + EXPECT_EQ("ROW1", mps.row_names[0]); + EXPECT_EQ("ROW2", mps.row_names[1]); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.var_names.size()); + EXPECT_EQ("VAR1", mps.var_names[0]); + EXPECT_EQ("VAR2", mps.var_names[1]); + ASSERT_EQ(int(2), mps.A_indices.size()); + ASSERT_EQ(int(2), mps.A_indices[0].size()); + EXPECT_EQ(int(0), mps.A_indices[0][0]); + EXPECT_EQ(int(1), mps.A_indices[0][1]); + ASSERT_EQ(int(2), mps.A_indices[1].size()); + EXPECT_EQ(int(0), mps.A_indices[1][0]); + EXPECT_EQ(int(1), mps.A_indices[1][1]); + ASSERT_EQ(int(2), mps.A_values.size()); + ASSERT_EQ(int(2), mps.A_values[0].size()); + EXPECT_EQ(3., mps.A_values[0][0]); + EXPECT_EQ(4., mps.A_values[0][1]); + ASSERT_EQ(int(2), mps.A_values[1].size()); + EXPECT_EQ(2.7, mps.A_values[1][0]); + EXPECT_EQ(10.1, mps.A_values[1][1]); + ASSERT_EQ(int(2), mps.b_values.size()); + EXPECT_EQ(5.4, mps.b_values[0]); + EXPECT_EQ(4.9, mps.b_values[1]); + ASSERT_EQ(int(2), mps.c_values.size()); + EXPECT_EQ(0.2, mps.c_values[0]); + EXPECT_EQ(0.1, mps.c_values[1]); +} + +TEST(mps_parser, good_mps_file_comments) +{ + auto mps = read_from_mps("linear_programming/good-mps-1-comments.mps", false); + EXPECT_EQ("good-1", mps.problem_name); + ASSERT_EQ(int(2), mps.row_names.size()); + EXPECT_EQ("ROW1", mps.row_names[0]); + EXPECT_EQ("ROW2", mps.row_names[1]); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.var_names.size()); + EXPECT_EQ("VAR1", mps.var_names[0]); + EXPECT_EQ("VAR2", mps.var_names[1]); + ASSERT_EQ(int(2), mps.A_indices.size()); + ASSERT_EQ(int(2), mps.A_indices[0].size()); + EXPECT_EQ(int(0), mps.A_indices[0][0]); + EXPECT_EQ(int(1), mps.A_indices[0][1]); + ASSERT_EQ(int(1), mps.A_indices[1].size()); + EXPECT_EQ(int(0), mps.A_indices[1][0]); + ASSERT_EQ(int(2), mps.A_values.size()); + ASSERT_EQ(int(2), mps.A_values[0].size()); + EXPECT_EQ(3., mps.A_values[0][0]); + EXPECT_EQ(4., mps.A_values[0][1]); + ASSERT_EQ(int(1), mps.A_values[1].size()); + EXPECT_EQ(2.7, mps.A_values[1][0]); + ASSERT_EQ(int(2), mps.b_values.size()); + EXPECT_EQ(5.4, mps.b_values[0]); + EXPECT_EQ(4.9, mps.b_values[1]); + ASSERT_EQ(int(2), mps.c_values.size()); + EXPECT_EQ(0.2, mps.c_values[0]); + EXPECT_EQ(0.1, mps.c_values[1]); +} + +TEST(mps_parser, good_mps_file_no_name) +{ + // Should not throw an error + read_from_mps("linear_programming/good-mps-fixed-no-name.mps"); +} + +TEST(mps_parser, good_mps_file_empty_name) +{ + // Should not throw an error + read_from_mps("linear_programming/good-mps-fixed-empty-name.mps"); +} + +TEST(mps_parser, good_mps_file_2) +{ + auto mps = read_from_mps("linear_programming/good-fixed-mps-2.mps"); + EXPECT_EQ("good-1", mps.problem_name); + ASSERT_EQ(int(2), mps.row_names.size()); + EXPECT_EQ("RO W1", mps.row_names[0]); + EXPECT_EQ("ROW2", mps.row_names[1]); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.var_names.size()); + EXPECT_EQ("VA R1", mps.var_names[0]); + EXPECT_EQ("VAR2", mps.var_names[1]); + ASSERT_EQ(int(2), mps.A_indices.size()); + ASSERT_EQ(int(2), mps.A_indices[0].size()); + EXPECT_EQ(int(0), mps.A_indices[0][0]); + EXPECT_EQ(int(1), mps.A_indices[0][1]); + ASSERT_EQ(int(2), mps.A_indices[1].size()); + EXPECT_EQ(int(0), mps.A_indices[1][0]); + EXPECT_EQ(int(1), mps.A_indices[1][1]); + ASSERT_EQ(int(2), mps.A_values.size()); + ASSERT_EQ(int(2), mps.A_values[0].size()); + EXPECT_EQ(3., mps.A_values[0][0]); + EXPECT_EQ(4., mps.A_values[0][1]); + ASSERT_EQ(int(2), mps.A_values[1].size()); + EXPECT_EQ(2.7, mps.A_values[1][0]); + EXPECT_EQ(10.1, mps.A_values[1][1]); + ASSERT_EQ(int(2), mps.b_values.size()); + EXPECT_EQ(5.4, mps.b_values[0]); + EXPECT_EQ(4.9, mps.b_values[1]); + ASSERT_EQ(int(2), mps.c_values.size()); + EXPECT_EQ(0.2, mps.c_values[0]); + EXPECT_EQ(0.1, mps.c_values[1]); +} + +TEST(mps_parser_free_format, free_format_mps_file_1) +{ // tests for arbitrary spacing in rows, column, rhs + auto mps = read_from_mps("linear_programming/free-format-mps-1.mps", false); + EXPECT_EQ("good-1", mps.problem_name); + ASSERT_EQ(int(2), mps.row_names.size()); + EXPECT_EQ("ROW1", mps.row_names[0]); + EXPECT_EQ("ROW2", mps.row_names[1]); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.var_names.size()); + EXPECT_EQ("VAR1", mps.var_names[0]); + EXPECT_EQ("VAR2", mps.var_names[1]); + ASSERT_EQ(int(2), mps.A_indices.size()); + ASSERT_EQ(int(2), mps.A_indices[0].size()); + EXPECT_EQ(int(0), mps.A_indices[0][0]); + EXPECT_EQ(int(1), mps.A_indices[0][1]); + ASSERT_EQ(int(2), mps.A_indices[1].size()); + EXPECT_EQ(int(0), mps.A_indices[1][0]); + EXPECT_EQ(int(1), mps.A_indices[1][1]); + ASSERT_EQ(int(2), mps.A_values.size()); + ASSERT_EQ(int(2), mps.A_values[0].size()); + EXPECT_EQ(3., mps.A_values[0][0]); + EXPECT_EQ(4., mps.A_values[0][1]); + ASSERT_EQ(int(2), mps.A_values[1].size()); + EXPECT_EQ(2.7, mps.A_values[1][0]); + EXPECT_EQ(10.1, mps.A_values[1][1]); + ASSERT_EQ(int(2), mps.b_values.size()); + EXPECT_EQ(5.4, mps.b_values[0]); + EXPECT_EQ(4.9, mps.b_values[1]); + ASSERT_EQ(int(2), mps.c_values.size()); + EXPECT_EQ(0.2, mps.c_values[0]); + EXPECT_EQ(0.1, mps.c_values[1]); + EXPECT_EQ(false, mps.maximize); +} + +TEST(mps_parser_free_format, bad_free_format_mps_with_spaces_in_names) +{ + ASSERT_THROW(read_from_mps("linear_programming/good-fixed-mps-2.mps", false), std::logic_error); +} + +TEST(mps_parser_free_format, bad_mps_files_free_format) +{ + std::stringstream ss; + static constexpr int NumMpsFiles = 13; + for (int i = 1; i <= NumMpsFiles; ++i) { + ss << "linear_programming/bad-mps-" << i << ".mps"; + if (file_exists(ss.str())) ASSERT_THROW(read_from_mps(ss.str(), false), std::logic_error); + ss.str(std::string{}); + ss.clear(); + } +} + +TEST_F(up_low_bounds_test, mps) +{ + check_model(parse_mps_file("linear_programming/lp_model_with_var_bounds.mps", false)); + auto mps = read_from_mps("linear_programming/lp_model_with_var_bounds.mps", false); + EXPECT_EQ("lp_model_with_var_bounds", mps.problem_name); + EXPECT_EQ("OBJ", mps.objective_name); + ASSERT_EQ(int(1), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); +} + +TEST_F(up_low_bounds_test, lp) +{ + check_model(parse_lp_file("linear_programming/lp_model_with_var_bounds.lp")); +} + +TEST_F(good_mps_1_test, mps_free_format) +{ + // free-format-mps-1.mps encodes the same problem as good-mps-1 with default + // [0, +inf) bounds (no BOUNDS section), so it satisfies the same checker. + check_model(parse_mps_file("linear_programming/free-format-mps-1.mps", false)); +} + +TEST_F(some_var_bounds_test, mps) +{ + check_model(parse_mps_file("linear_programming/good-mps-some-var-bounds.mps")); +} + +TEST_F(some_var_bounds_test, lp) +{ + check_model(parse_lp_file("linear_programming/good-mps-some-var-bounds.lp")); +} + +TEST_F(fixed_var_bound_test, mps) +{ + check_model(parse_mps_file("linear_programming/good-mps-fixed-var.mps")); +} + +TEST_F(fixed_var_bound_test, lp) +{ + check_model(parse_lp_file("linear_programming/good-mps-fixed-var.lp")); +} + +TEST_F(free_var_bound_test, mps) +{ + check_model(parse_mps_file("linear_programming/good-mps-free-var.mps")); +} + +TEST_F(free_var_bound_test, lp) +{ + check_model(parse_lp_file("linear_programming/good-mps-free-var.lp")); +} + +TEST_F(lower_inf_var_bound_test, mps) +{ + check_model(parse_mps_file("linear_programming/good-mps-lower-bound-inf-var.mps")); +} + +TEST_F(lower_inf_var_bound_test, lp) +{ + check_model(parse_lp_file("linear_programming/good-mps-lower-bound-inf-var.lp")); +} + +TEST(mps_bounds, rhs_cost) +{ + auto mps = read_from_mps("linear_programming/good-mps-rhs-cost.mps"); + + // objective value offset should be set to -5 + EXPECT_EQ(int(-5), mps.objective_offset_value); +} + +TEST_F(upper_inf_var_bound_test, mps) +{ + check_model(parse_mps_file("linear_programming/good-mps-upper-bound-inf-var.mps")); +} + +TEST_F(upper_inf_var_bound_test, lp) +{ + check_model(parse_lp_file("linear_programming/good-mps-upper-bound-inf-var.lp")); +} + +TEST(mps_ranges, fixed_ranges) +{ + std::string file = "linear_programming/good-mps-fixed-ranges.mps"; + auto mps = read_from_mps(file); + + EXPECT_NEAR(4.2, mps.ranges_values[0], tolerance); // ROW1 range value + EXPECT_NEAR(3.4, mps.ranges_values[1], tolerance); // ROW2 range value + EXPECT_NEAR(-1.6, mps.ranges_values[2], tolerance); // ROW3 range value + EXPECT_NEAR(3.4, mps.ranges_values[3], tolerance); // ROW3 range value + + std::string rel_file{}; + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + rel_file = rapidsDatasetRootDir + "/" + file; + auto data_model = parse_mps(rel_file, true); + + EXPECT_NEAR(1.2, data_model.get_constraint_lower_bounds()[0], tolerance); // ROW1 lower bound + EXPECT_NEAR(5.4, data_model.get_constraint_upper_bounds()[0], tolerance); // ROW1 upper bound + EXPECT_NEAR(1.5, data_model.get_constraint_lower_bounds()[1], tolerance); // ROW2 lower bound + EXPECT_NEAR(4.9, data_model.get_constraint_upper_bounds()[1], tolerance); // ROW2 upper bound + EXPECT_NEAR( + 7.9, data_model.get_constraint_lower_bounds()[2], tolerance); // ROW3, equal constraint + EXPECT_NEAR( + 9.5, data_model.get_constraint_upper_bounds()[2], tolerance); // ROW3, equal constraint + EXPECT_NEAR( + 3.5, data_model.get_constraint_lower_bounds()[3], tolerance); // ROW4, equal constraint + EXPECT_NEAR( + 6.9, data_model.get_constraint_upper_bounds()[3], tolerance); // ROW4, equal constraint + EXPECT_NEAR(3.9, + data_model.get_constraint_lower_bounds()[4], + tolerance); // ROW5, lower turned into equal constraint + EXPECT_NEAR(3.9, + data_model.get_constraint_upper_bounds()[4], + tolerance); // ROW5, lower turned into equal constraint + EXPECT_NEAR(4.9, + data_model.get_constraint_lower_bounds()[5], + tolerance); // ROW6, greater turned into equal constraint + EXPECT_NEAR(4.9, + data_model.get_constraint_upper_bounds()[5], + tolerance); // ROW6, greater turned into equal constraint +} + +TEST(mps_ranges, free_ranges) +{ + std::string file = "linear_programming/good-mps-free-ranges.mps"; + auto mps = read_from_mps(file, false); + + EXPECT_NEAR(4.2, mps.ranges_values[0], tolerance); // ROW1 range value + EXPECT_NEAR(3.4, mps.ranges_values[1], tolerance); // ROW2 range value + EXPECT_NEAR(-1.6, mps.ranges_values[2], tolerance); // ROW3 range value + EXPECT_NEAR(3.4, mps.ranges_values[3], tolerance); // ROW3 range value + + std::string rel_file{}; + const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); + rel_file = rapidsDatasetRootDir + "/" + file; + auto data_model = parse_mps(rel_file, false); + + EXPECT_NEAR(1.2, data_model.get_constraint_lower_bounds()[0], tolerance); // ROW1 lower bound + EXPECT_NEAR(5.4, data_model.get_constraint_upper_bounds()[0], tolerance); // ROW1 upper bound + EXPECT_NEAR(1.5, data_model.get_constraint_lower_bounds()[1], tolerance); // ROW2 lower bound + EXPECT_NEAR(4.9, data_model.get_constraint_upper_bounds()[1], tolerance); // ROW2 upper bound + EXPECT_NEAR( + 7.9, data_model.get_constraint_lower_bounds()[2], tolerance); // ROW3, equal constraint + EXPECT_NEAR( + 9.5, data_model.get_constraint_upper_bounds()[2], tolerance); // ROW3, equal constraint + EXPECT_NEAR( + 3.5, data_model.get_constraint_lower_bounds()[3], tolerance); // ROW4, equal constraint + EXPECT_NEAR( + 6.9, data_model.get_constraint_upper_bounds()[3], tolerance); // ROW4, equal constraint + EXPECT_NEAR(3.9, + data_model.get_constraint_lower_bounds()[4], + tolerance); // ROW5, lower turned into equal constraint + EXPECT_NEAR(3.9, + data_model.get_constraint_upper_bounds()[4], + tolerance); // ROW5, lower turned into equal constraint + EXPECT_NEAR(4.9, + data_model.get_constraint_lower_bounds()[5], + tolerance); // ROW6, greater turned into equal constraint + EXPECT_NEAR(4.9, + data_model.get_constraint_upper_bounds()[5], + tolerance); // ROW6, greater turned into equal constraint +} + +TEST(mps_name, two_objectives) +{ + std::string file = "linear_programming/good-mps-fixed-two-objectives.mps"; + auto mps = read_from_mps(file, false); + + // Objective name should be first one found and not trigger an error + EXPECT_EQ(mps.objective_name, "COST"); +} + +TEST(mps_objname, two_objectives) +{ + std::string file = "linear_programming/good-mps-fixed-two-objectives-objname.mps"; + auto mps = read_from_mps(file, false); + + // Objective name is the second one found since it's specified as objname + EXPECT_EQ(mps.objective_name, "COST6679327"); +} + +TEST(mps_objname, two_objectives_next_line) +{ + std::string file = "linear_programming/good-mps-fixed-two-objectives-objname-next-line.mps"; + auto mps = read_from_mps(file, false); + + // Objective name is the second one found since it's specified as objname + EXPECT_EQ(mps.objective_name, "COST6679327"); +} + +TEST(mps_objname, bad_after) +{ + std::string file = "linear_programming/bad-mps-fixed-objname-after-rows.mps"; + ASSERT_THROW(read_from_mps(file, false), std::logic_error); +} + +TEST(mps_objname, bad_no_fixed) +{ + std::string file = "linear_programming/bad-mps-fixed-objname-after-rows.mps"; + ASSERT_THROW(read_from_mps(file, true), std::logic_error); +} + +TEST(mps_ranges, bad_name) +{ + ASSERT_THROW(read_from_mps("linear_programming/bad-mps-fixed-ranges-name.mps", false), + std::logic_error); +} + +TEST(mps_ranges, bad_value) +{ + ASSERT_THROW(read_from_mps("linear_programming/bad-mps-fixed-ranges-value.mps", false), + std::logic_error); +} + +TEST(mps_bounds, unsupported_or_invalid_mps_types) +{ + std::stringstream ss; + static constexpr int NumMpsFiles = 2; + for (int i = 1; i <= NumMpsFiles; ++i) { + ss << "linear_programming/bad-mps-bound-" << i << ".mps"; + ASSERT_THROW(read_from_mps(ss.str(), false), std::logic_error); + ss.str(std::string{}); + ss.clear(); + }; +} + +TEST_F(mip_with_bounds_test, mps) +{ + check_model(parse_mps_file("mixed_integer_programming/good-mip-mps-1.mps", false)); + auto mps = read_from_mps("mixed_integer_programming/good-mip-mps-1.mps", false); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); +} + +TEST_F(mip_with_bounds_test, lp) +{ + check_model(parse_lp_file("mixed_integer_programming/good-mip-mps-1.lp")); +} + +TEST(mps_parser, good_mps_file_mip_no_marker) +{ + auto mps = read_from_mps("mixed_integer_programming/good-mip-mps-1-no-mark.mps", false); + + ASSERT_EQ(int(2), mps.row_names.size()); + EXPECT_EQ("ROW1", mps.row_names[0]); + EXPECT_EQ("ROW2", mps.row_names[1]); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.var_names.size()); + EXPECT_EQ("VAR1", mps.var_names[0]); + EXPECT_EQ("VAR2", mps.var_names[1]); + ASSERT_EQ(int(2), mps.A_indices.size()); + ASSERT_EQ(int(2), mps.A_indices[0].size()); + EXPECT_EQ(int(0), mps.A_indices[0][0]); + EXPECT_EQ(int(1), mps.A_indices[0][1]); + ASSERT_EQ(int(2), mps.A_indices[1].size()); + EXPECT_EQ(int(0), mps.A_indices[1][0]); + EXPECT_EQ(int(1), mps.A_indices[1][1]); + ASSERT_EQ(int(2), mps.A_values.size()); + ASSERT_EQ(int(2), mps.A_values[0].size()); + EXPECT_EQ(8000., mps.A_values[0][0]); + EXPECT_EQ(4000., mps.A_values[0][1]); + ASSERT_EQ(int(2), mps.A_values[1].size()); + EXPECT_EQ(15., mps.A_values[1][0]); + EXPECT_EQ(30., mps.A_values[1][1]); + ASSERT_EQ(int(2), mps.b_values.size()); + EXPECT_EQ(40000., mps.b_values[0]); + EXPECT_EQ(200., mps.b_values[1]); + ASSERT_EQ(int(2), mps.c_values.size()); + EXPECT_EQ(100., mps.c_values[0]); + EXPECT_EQ(150., mps.c_values[1]); + ASSERT_EQ(int(2), mps.var_types.size()); + EXPECT_EQ('I', mps.var_types[0]); + EXPECT_EQ('I', mps.var_types[1]); + ASSERT_EQ(int(2), mps.variable_lower_bounds.size()); + EXPECT_EQ(0., mps.variable_lower_bounds[0]); + EXPECT_EQ(0., mps.variable_lower_bounds[1]); + ASSERT_EQ(int(2), mps.variable_upper_bounds.size()); + EXPECT_EQ(10., mps.variable_upper_bounds[0]); + EXPECT_EQ(10., mps.variable_upper_bounds[1]); +} + +TEST_F(mip_no_bounds_test, mps) +{ + check_model(parse_mps_file("mixed_integer_programming/good-mip-mps-no-bounds.mps", false)); +} + +TEST_F(mip_no_bounds_test, lp) +{ + check_model(parse_lp_file("mixed_integer_programming/good-mip-mps-no-bounds.lp")); +} + +TEST_F(mip_partial_bounds_test, mps) +{ + check_model(parse_mps_file("mixed_integer_programming/good-mip-mps-partial-bounds.mps", false)); +} + +TEST_F(mip_partial_bounds_test, lp) +{ + check_model(parse_lp_file("mixed_integer_programming/good-mip-mps-partial-bounds.lp")); +} + +#ifdef MPS_PARSER_WITH_BZIP2 +TEST(mps_parser, good_mps_file_bzip2_compressed) +{ + auto mps = read_from_mps("linear_programming/good-mps-1.mps.bz2"); + EXPECT_EQ("good-1", mps.problem_name); + ASSERT_EQ(int(2), mps.row_names.size()); + EXPECT_EQ("ROW1", mps.row_names[0]); + EXPECT_EQ("ROW2", mps.row_names[1]); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.var_names.size()); + EXPECT_EQ("VAR1", mps.var_names[0]); + EXPECT_EQ("VAR2", mps.var_names[1]); + ASSERT_EQ(int(2), mps.A_indices.size()); + ASSERT_EQ(int(2), mps.A_indices[0].size()); + EXPECT_EQ(int(0), mps.A_indices[0][0]); + EXPECT_EQ(int(1), mps.A_indices[0][1]); + ASSERT_EQ(int(2), mps.A_indices[1].size()); + EXPECT_EQ(int(0), mps.A_indices[1][0]); + EXPECT_EQ(int(1), mps.A_indices[1][1]); + ASSERT_EQ(int(2), mps.A_values.size()); + ASSERT_EQ(int(2), mps.A_values[0].size()); + EXPECT_EQ(3., mps.A_values[0][0]); + EXPECT_EQ(4., mps.A_values[0][1]); + ASSERT_EQ(int(2), mps.A_values[1].size()); + EXPECT_EQ(2.7, mps.A_values[1][0]); + EXPECT_EQ(10.1, mps.A_values[1][1]); + ASSERT_EQ(int(2), mps.b_values.size()); + EXPECT_EQ(5.4, mps.b_values[0]); + EXPECT_EQ(4.9, mps.b_values[1]); + ASSERT_EQ(int(2), mps.c_values.size()); + EXPECT_EQ(0.2, mps.c_values[0]); + EXPECT_EQ(0.1, mps.c_values[1]); +} +#endif // MPS_PARSER_WITH_BZIP2 + +#ifdef MPS_PARSER_WITH_ZLIB +TEST(mps_parser, good_mps_file_zlib_compressed) +{ + auto mps = read_from_mps("linear_programming/good-mps-1.mps.gz"); + EXPECT_EQ("good-1", mps.problem_name); + ASSERT_EQ(int(2), mps.row_names.size()); + EXPECT_EQ("ROW1", mps.row_names[0]); + EXPECT_EQ("ROW2", mps.row_names[1]); + ASSERT_EQ(int(2), mps.row_types.size()); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[0]); + EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); + EXPECT_EQ("COST", mps.objective_name); + ASSERT_EQ(int(2), mps.var_names.size()); + EXPECT_EQ("VAR1", mps.var_names[0]); + EXPECT_EQ("VAR2", mps.var_names[1]); + ASSERT_EQ(int(2), mps.A_indices.size()); + ASSERT_EQ(int(2), mps.A_indices[0].size()); + EXPECT_EQ(int(0), mps.A_indices[0][0]); + EXPECT_EQ(int(1), mps.A_indices[0][1]); + ASSERT_EQ(int(2), mps.A_indices[1].size()); + EXPECT_EQ(int(0), mps.A_indices[1][0]); + EXPECT_EQ(int(1), mps.A_indices[1][1]); + ASSERT_EQ(int(2), mps.A_values.size()); + ASSERT_EQ(int(2), mps.A_values[0].size()); + EXPECT_EQ(3., mps.A_values[0][0]); + EXPECT_EQ(4., mps.A_values[0][1]); + ASSERT_EQ(int(2), mps.A_values[1].size()); + EXPECT_EQ(2.7, mps.A_values[1][0]); + EXPECT_EQ(10.1, mps.A_values[1][1]); + ASSERT_EQ(int(2), mps.b_values.size()); + EXPECT_EQ(5.4, mps.b_values[0]); + EXPECT_EQ(4.9, mps.b_values[1]); + ASSERT_EQ(int(2), mps.c_values.size()); + EXPECT_EQ(0.2, mps.c_values[0]); + EXPECT_EQ(0.1, mps.c_values[1]); +} +#endif // MPS_PARSER_WITH_ZLIB + +// ================================================================================================ +// QPS (Quadratic Programming) Support Tests +// ================================================================================================ + +// QPS-specific tests for quadratic programming support +TEST(qps_parser, quadratic_objective_basic) +{ + // Create a simple QPS test to verify quadratic objective parsing + // This would require actual QPS test files - for now, test the API + mps_data_model_t model; + + // Test setting quadratic objective matrix + std::vector Q_values = {2.0, 1.0, 1.0, 2.0}; // 2x2 matrix + std::vector Q_indices = {0, 1, 0, 1}; + std::vector Q_offsets = {0, 2, 4}; // CSR offsets + + model.set_quadratic_objective_matrix(Q_values, Q_indices, Q_offsets); + + // Verify the data was stored correctly + EXPECT_TRUE(model.has_quadratic_objective()); + EXPECT_EQ(4, model.get_quadratic_objective_values().size()); + EXPECT_EQ(2.0, model.get_quadratic_objective_values()[0]); + EXPECT_EQ(1.0, model.get_quadratic_objective_values()[1]); +} + +// Test actual QPS files from the dataset +TEST(qps_parser, test_qps_files) +{ + // Test QP_Test_1.qps if it exists + if (file_exists("quadratic_programming/QP_Test_1.qps")) { + auto parsed_data = parse_mps( + cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_1.qps", false); + + EXPECT_EQ("QP_Test_1", parsed_data.get_problem_name()); + EXPECT_EQ(2, parsed_data.get_n_variables()); // C------1 and C------2 + EXPECT_EQ(1, parsed_data.get_n_constraints()); // R------1 + EXPECT_TRUE(parsed_data.has_quadratic_objective()); + + // Check variable bounds + const auto& lower_bounds = parsed_data.get_variable_lower_bounds(); + const auto& upper_bounds = parsed_data.get_variable_upper_bounds(); + + EXPECT_NEAR(2.0, lower_bounds[0], tolerance); // C------1 lower bound + EXPECT_NEAR(50.0, upper_bounds[0], tolerance); // C------1 upper bound + EXPECT_NEAR(-50.0, lower_bounds[1], tolerance); // C------2 lower bound + EXPECT_NEAR(50.0, upper_bounds[1], tolerance); // C------2 upper bound + } + + // Test QP_Test_2.qps if it exists + if (file_exists("quadratic_programming/QP_Test_2.qps")) { + auto parsed_data = parse_mps( + cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_2.qps", false); + + EXPECT_EQ("QP_Test_2", parsed_data.get_problem_name()); + EXPECT_EQ(3, parsed_data.get_n_variables()); // C------1, C------2, C------3 + EXPECT_EQ(1, parsed_data.get_n_constraints()); // R------1 + EXPECT_TRUE(parsed_data.has_quadratic_objective()); + + // Check that quadratic objective matrix has values + const auto& Q_values = parsed_data.get_quadratic_objective_values(); + EXPECT_GT(Q_values.size(), 0) << "Quadratic objective should have non-zero elements"; + } +} + +// ================================================================================================ +// MPS Round-Trip Tests (Read -> Write -> Read -> Compare) +// ================================================================================================ + +// Helper function to compare two data models +template +void compare_data_models(const mps_data_model_t& original, + const mps_data_model_t& reloaded, + f_t tol = 1e-9) +{ + // Compare basic dimensions + EXPECT_EQ(original.get_n_variables(), reloaded.get_n_variables()); + EXPECT_EQ(original.get_n_constraints(), reloaded.get_n_constraints()); + + // Compare objective coefficients + auto orig_c = original.get_objective_coefficients(); + auto reload_c = reloaded.get_objective_coefficients(); + ASSERT_EQ(orig_c.size(), reload_c.size()); + for (size_t i = 0; i < orig_c.size(); ++i) { + EXPECT_NEAR(orig_c[i], reload_c[i], tol) << "Objective coefficient mismatch at index " << i; + } + + // Compare constraint matrix values + auto orig_A = original.get_constraint_matrix_values(); + auto reload_A = reloaded.get_constraint_matrix_values(); + ASSERT_EQ(orig_A.size(), reload_A.size()); + for (size_t i = 0; i < orig_A.size(); ++i) { + EXPECT_NEAR(orig_A[i], reload_A[i], tol) << "Constraint matrix value mismatch at index " << i; + } + + // Compare constraint matrix indices + auto orig_A_idx = original.get_constraint_matrix_indices(); + auto reload_A_idx = reloaded.get_constraint_matrix_indices(); + ASSERT_EQ(orig_A_idx.size(), reload_A_idx.size()); + for (size_t i = 0; i < orig_A_idx.size(); ++i) { + EXPECT_EQ(orig_A_idx[i], reload_A_idx[i]) << "Constraint matrix index mismatch at index " << i; + } + + // Compare constraint matrix offsets + auto orig_A_off = original.get_constraint_matrix_offsets(); + auto reload_A_off = reloaded.get_constraint_matrix_offsets(); + ASSERT_EQ(orig_A_off.size(), reload_A_off.size()); + for (size_t i = 0; i < orig_A_off.size(); ++i) { + EXPECT_EQ(orig_A_off[i], reload_A_off[i]) << "Constraint matrix offset mismatch at index " << i; + } + + // Compare variable bounds + auto orig_lb = original.get_variable_lower_bounds(); + auto reload_lb = reloaded.get_variable_lower_bounds(); + ASSERT_EQ(orig_lb.size(), reload_lb.size()); + for (size_t i = 0; i < orig_lb.size(); ++i) { + if (std::isinf(orig_lb[i]) && std::isinf(reload_lb[i])) { + EXPECT_EQ(std::signbit(orig_lb[i]), std::signbit(reload_lb[i])) + << "Variable lower bound infinity sign mismatch at index " << i; + } else { + EXPECT_NEAR(orig_lb[i], reload_lb[i], tol) << "Variable lower bound mismatch at index " << i; + } + } + + auto orig_ub = original.get_variable_upper_bounds(); + auto reload_ub = reloaded.get_variable_upper_bounds(); + ASSERT_EQ(orig_ub.size(), reload_ub.size()); + for (size_t i = 0; i < orig_ub.size(); ++i) { + if (std::isinf(orig_ub[i]) && std::isinf(reload_ub[i])) { + EXPECT_EQ(std::signbit(orig_ub[i]), std::signbit(reload_ub[i])) + << "Variable upper bound infinity sign mismatch at index " << i; + } else { + EXPECT_NEAR(orig_ub[i], reload_ub[i], tol) << "Variable upper bound mismatch at index " << i; + } + } + + // Compare constraint bounds + auto orig_cl = original.get_constraint_lower_bounds(); + auto reload_cl = reloaded.get_constraint_lower_bounds(); + ASSERT_EQ(orig_cl.size(), reload_cl.size()); + for (size_t i = 0; i < orig_cl.size(); ++i) { + if (std::isinf(orig_cl[i]) && std::isinf(reload_cl[i])) { + EXPECT_EQ(std::signbit(orig_cl[i]), std::signbit(reload_cl[i])) + << "Constraint lower bound infinity sign mismatch at index " << i; + } else { + EXPECT_NEAR(orig_cl[i], reload_cl[i], tol) + << "Constraint lower bound mismatch at index " << i; + } + } + + auto orig_cu = original.get_constraint_upper_bounds(); + auto reload_cu = reloaded.get_constraint_upper_bounds(); + ASSERT_EQ(orig_cu.size(), reload_cu.size()); + for (size_t i = 0; i < orig_cu.size(); ++i) { + if (std::isinf(orig_cu[i]) && std::isinf(reload_cu[i])) { + EXPECT_EQ(std::signbit(orig_cu[i]), std::signbit(reload_cu[i])) + << "Constraint upper bound infinity sign mismatch at index " << i; + } else { + EXPECT_NEAR(orig_cu[i], reload_cu[i], tol) + << "Constraint upper bound mismatch at index " << i; + } + } + + // Compare quadratic objective if present + EXPECT_EQ(original.has_quadratic_objective(), reloaded.has_quadratic_objective()); + if (original.has_quadratic_objective() && reloaded.has_quadratic_objective()) { + auto orig_Q = original.get_quadratic_objective_values(); + auto orig_Q_idx = original.get_quadratic_objective_indices(); + auto orig_Q_off = original.get_quadratic_objective_offsets(); + auto reload_Q = reloaded.get_quadratic_objective_values(); + auto reload_Q_idx = reloaded.get_quadratic_objective_indices(); + auto reload_Q_off = reloaded.get_quadratic_objective_offsets(); + + // Compare Q matrix structure and values + ASSERT_EQ(orig_Q.size(), reload_Q.size()) << "Q values size mismatch"; + ASSERT_EQ(orig_Q_idx.size(), reload_Q_idx.size()) << "Q indices size mismatch"; + ASSERT_EQ(orig_Q_off.size(), reload_Q_off.size()) << "Q offsets size mismatch"; + + for (size_t i = 0; i < orig_Q.size(); ++i) { + EXPECT_NEAR(orig_Q[i], reload_Q[i], tol) << "Q value mismatch at index " << i; + } + for (size_t i = 0; i < orig_Q_idx.size(); ++i) { + EXPECT_EQ(orig_Q_idx[i], reload_Q_idx[i]) << "Q index mismatch at index " << i; + } + for (size_t i = 0; i < orig_Q_off.size(); ++i) { + EXPECT_EQ(orig_Q_off[i], reload_Q_off[i]) << "Q offset mismatch at index " << i; + } + } +} + +TEST(mps_roundtrip, linear_programming_basic) +{ + std::string input_file = + cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/good-mps-1.mps"; + std::string temp_file = "/tmp/mps_roundtrip_lp_test.mps"; + + // Read original + auto original = parse_mps(input_file, true); + + // Write to temp file + mps_writer_t writer(original); + writer.write(temp_file); + + // Read back + auto reloaded = parse_mps(temp_file, false); + + // Compare + compare_data_models(original, reloaded); + + // Cleanup + std::filesystem::remove(temp_file); +} + +TEST(mps_roundtrip, linear_programming_with_bounds) +{ + if (!file_exists("linear_programming/lp_model_with_var_bounds.mps")) { + GTEST_SKIP() << "Test file not found"; + } + + std::string input_file = + cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/lp_model_with_var_bounds.mps"; + std::string temp_file = "/tmp/mps_roundtrip_lp_bounds_test.mps"; + + // Read original + auto original = parse_mps(input_file, false); + + // Write to temp file + mps_writer_t writer(original); + writer.write(temp_file); + + // Read back + auto reloaded = parse_mps(temp_file, false); + + // Compare + compare_data_models(original, reloaded); + + // Cleanup + std::filesystem::remove(temp_file); +} + +TEST(mps_roundtrip, quadratic_programming_qp_test_1) +{ + if (!file_exists("quadratic_programming/QP_Test_1.qps")) { + GTEST_SKIP() << "Test file not found"; + } + + std::string input_file = + cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_1.qps"; + std::string temp_file = "/tmp/mps_roundtrip_qp_test_1.mps"; + + // Read original + auto original = parse_mps(input_file, false); + ASSERT_TRUE(original.has_quadratic_objective()) << "Original should have quadratic objective"; + + // Write to temp file + mps_writer_t writer(original); + writer.write(temp_file); + + // Read back + auto reloaded = parse_mps(temp_file, false); + ASSERT_TRUE(reloaded.has_quadratic_objective()) << "Reloaded should have quadratic objective"; + + // Compare + compare_data_models(original, reloaded); + + // Cleanup + std::filesystem::remove(temp_file); +} + +TEST(mps_roundtrip, quadratic_programming_qp_test_2) +{ + if (!file_exists("quadratic_programming/QP_Test_2.qps")) { + GTEST_SKIP() << "Test file not found"; + } + + std::string input_file = + cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_2.qps"; + std::string temp_file = "/tmp/mps_roundtrip_qp_test_2.mps"; + + // Read original + auto original = parse_mps(input_file, false); + ASSERT_TRUE(original.has_quadratic_objective()) << "Original should have quadratic objective"; + + // Write to temp file + mps_writer_t writer(original); + writer.write(temp_file); + + // Read back + auto reloaded = parse_mps(temp_file, false); + ASSERT_TRUE(reloaded.has_quadratic_objective()) << "Reloaded should have quadratic objective"; + + // Compare + compare_data_models(original, reloaded); + + // Cleanup + std::filesystem::remove(temp_file); +} + +// ================================================================================================ +// LP -> MPS Round-Trip Tests (Read LP -> Write MPS -> Read MPS -> Compare) +// ================================================================================================ +// Parses an LP file, writes the resulting data model out as MPS, reads it +// back, and checks that the reloaded data model matches the one produced by +// the LP parser. Exercises the LP reader + the writer + the MPS reader end +// to end, without trusting any direct LP<->MPS comparison. + +TEST_F(good_mps_1_test, lp_roundtrip) +{ + std::string temp_file = "/tmp/lp_roundtrip_lp_basic.mps"; + + auto original = parse_lp_file("linear_programming/good-mps-1.lp"); + + mps_writer_t writer(original); + writer.write(temp_file); + + auto reloaded = parse_mps(temp_file, false); + + compare_data_models(original, reloaded); + + std::filesystem::remove(temp_file); +} + +TEST_F(up_low_bounds_test, lp_roundtrip) +{ + std::string temp_file = "/tmp/lp_roundtrip_lp_bounds.mps"; + + auto original = parse_lp_file("linear_programming/lp_model_with_var_bounds.lp"); + + mps_writer_t writer(original); + writer.write(temp_file); + + auto reloaded = parse_mps(temp_file, false); + + compare_data_models(original, reloaded); + + std::filesystem::remove(temp_file); +} + +TEST_F(mip_with_bounds_test, lp_roundtrip) +{ + std::string temp_file = "/tmp/lp_roundtrip_mip_basic.mps"; + + auto original = parse_lp_file("mixed_integer_programming/good-mip-mps-1.lp"); + + mps_writer_t writer(original); + writer.write(temp_file); + + auto reloaded = parse_mps(temp_file, false); + + compare_data_models(original, reloaded); + + std::filesystem::remove(temp_file); +} + +// ================================================================================================ +// LP syntax / feature / error-path tests (parse_lp on inline LP content) +// ================================================================================================ + +TEST(lp_parser, trivial) +{ + auto m = parse_lp_string(R"LP( +Minimize + x +Subject To + lb_constr: x >= 2.5 +Bounds + x <= 10 +End +)LP"); + + EXPECT_FALSE(m.get_sense()); // minimize + ASSERT_EQ(m.get_variable_names().size(), 1u); + int x = find_var(m, "x"); + ASSERT_GE(x, 0); + EXPECT_EQ(m.get_variable_types()[x], 'C'); + EXPECT_NEAR(m.get_variable_lower_bounds()[x], 0.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[x], 10.0, tolerance); + EXPECT_NEAR(m.get_objective_coefficients()[x], 1.0, tolerance); + + ASSERT_EQ(m.get_row_names().size(), 1u); + int r = find_row(m, "lb_constr"); + ASSERT_GE(r, 0); + // 'G' relation ⇒ finite lower bound, +inf upper bound. + EXPECT_NEAR(m.get_constraint_lower_bounds()[r], 2.5, tolerance); + EXPECT_TRUE(std::isinf(m.get_constraint_upper_bounds()[r])); + EXPECT_NEAR(a_entry(m, r, x), 1.0, tolerance); +} + +TEST(lp_parser, basic_lp_with_float_coefficients) +{ + auto m = parse_lp_string(R"LP( +Minimize + x1 + x2 +Subject To + c1: 2.5 x1 + x2 <= 10 + c2: x1 + 1.5 x2 <= 8 + c3: x1 + x2 <= 6 +End +)LP"); + + EXPECT_EQ(m.get_variable_names().size(), 2u); + int x1 = find_var(m, "x1"); + int x2 = find_var(m, "x2"); + ASSERT_GE(x1, 0); + ASSERT_GE(x2, 0); + // Default bounds for continuous variables. + EXPECT_NEAR(m.get_variable_lower_bounds()[x1], 0.0, tolerance); + EXPECT_TRUE(std::isinf(m.get_variable_upper_bounds()[x1])); + + ASSERT_EQ(m.get_row_names().size(), 3u); + int c1 = find_row(m, "c1"); + int c2 = find_row(m, "c2"); + ASSERT_GE(c1, 0); + ASSERT_GE(c2, 0); + EXPECT_NEAR(a_entry(m, c1, x1), 2.5, tolerance); + EXPECT_NEAR(a_entry(m, c1, x2), 1.0, tolerance); + EXPECT_NEAR(a_entry(m, c2, x2), 1.5, tolerance); + EXPECT_NEAR(m.get_constraint_upper_bounds()[c1], 10.0, tolerance); +} + +TEST(lp_parser, maximize_flips_sense) +{ + auto m = parse_lp_string(R"LP( +Maximize + 3 x + 2 y +Subject To + c1: x + y <= 6 + c2: 2 x + y <= 8 +End +)LP"); + + EXPECT_TRUE(m.get_sense()); + int x = find_var(m, "x"); + int y = find_var(m, "y"); + EXPECT_NEAR(m.get_objective_coefficients()[x], 3.0, tolerance); + EXPECT_NEAR(m.get_objective_coefficients()[y], 2.0, tolerance); +} + +TEST(lp_parser, equality_constraints) +{ + auto m = parse_lp_string(R"LP( +Minimize + c1 + 2 c2 + 3 c3 + 4 c4 +Subject To + s1: c1 + c2 = 10 + s2: c3 + c4 = 12 + d1: c1 + c3 = 9 + d2: c2 + c4 = 13 +End +)LP"); + + ASSERT_EQ(m.get_row_names().size(), 4u); + // All four are equality constraints ⇒ lb == ub for every row. + const auto& clb = m.get_constraint_lower_bounds(); + const auto& cub = m.get_constraint_upper_bounds(); + for (size_t i = 0; i < clb.size(); ++i) { + EXPECT_NEAR(clb[i], cub[i], tolerance); + } + int s1 = find_row(m, "s1"); + EXPECT_NEAR(m.get_constraint_lower_bounds()[s1], 10.0, tolerance); + EXPECT_NEAR(m.get_constraint_upper_bounds()[s1], 10.0, tolerance); +} + +TEST(lp_parser, mixed_constraint_relations) +{ + auto m = parse_lp_string(R"LP( +Minimize + x + 2 y + 3 z +Subject To + eq1: x + y + z = 10 + geq1: x + 2 y >= 6 + leq1: y + z <= 8 +End +)LP"); + + int eq = find_row(m, "eq1"); + int geq = find_row(m, "geq1"); + int leq = find_row(m, "leq1"); + // Relation is recovered from the constraint lower/upper bounds: + // 'E' ⇒ lb == ub + // 'G' ⇒ ub = +inf + // 'L' ⇒ lb = -inf + EXPECT_NEAR(m.get_constraint_lower_bounds()[eq], m.get_constraint_upper_bounds()[eq], tolerance); + EXPECT_NEAR(m.get_constraint_lower_bounds()[geq], 6.0, tolerance); + EXPECT_TRUE(std::isinf(m.get_constraint_upper_bounds()[geq])); + EXPECT_NEAR(m.get_constraint_upper_bounds()[leq], 8.0, tolerance); + EXPECT_TRUE(std::isinf(-m.get_constraint_lower_bounds()[leq])); +} + +TEST(lp_parser, free_and_negative_lower_bound_variables) +{ + auto m = parse_lp_string(R"LP( +Minimize + xfree + xneg_lb + xstd +Subject To + sum_lb: xfree + xneg_lb + xstd >= 1 + diff_ub: xfree - xneg_lb <= 3 + xst_cap: xstd <= 5 +Bounds + xfree free + -3 <= xneg_lb <= 10 +End +)LP"); + + int xf = find_var(m, "xfree"); + int xn = find_var(m, "xneg_lb"); + int xs = find_var(m, "xstd"); + EXPECT_TRUE(std::isinf(-m.get_variable_lower_bounds()[xf])); + EXPECT_TRUE(std::isinf(m.get_variable_upper_bounds()[xf])); + EXPECT_NEAR(m.get_variable_lower_bounds()[xn], -3.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[xn], 10.0, tolerance); + EXPECT_NEAR(m.get_variable_lower_bounds()[xs], 0.0, tolerance); + EXPECT_TRUE(std::isinf(m.get_variable_upper_bounds()[xs])); + + // - xneg_lb → coefficient -1 in the diff_ub row + int dr = find_row(m, "diff_ub"); + EXPECT_NEAR(a_entry(m, dr, xn), -1.0, tolerance); +} + +TEST(lp_parser, bounds_variety) +{ + auto m = parse_lp_string(R"LP( +Minimize + xfixed + xub_only + xlb_pos +Subject To + c1: xfixed + xub_only + xlb_pos >= 1 +Bounds + xfixed = 3 + xub_only <= 7.5 + xlb_pos >= 2 +End +)LP"); + + int xfixed = find_var(m, "xfixed"); + int xub = find_var(m, "xub_only"); + int xlb = find_var(m, "xlb_pos"); + EXPECT_NEAR(m.get_variable_lower_bounds()[xfixed], 3.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[xfixed], 3.0, tolerance); + EXPECT_NEAR(m.get_variable_lower_bounds()[xub], 0.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[xub], 7.5, tolerance); + EXPECT_NEAR(m.get_variable_lower_bounds()[xlb], 2.0, tolerance); + EXPECT_TRUE(std::isinf(m.get_variable_upper_bounds()[xlb])); +} + +TEST(lp_parser, general_integers) +{ + auto m = parse_lp_string(R"LP( +Maximize + 3 x + 5 y +Subject To + c1: x + 2 y <= 12 + c2: 2 x + y <= 10 +Generals + x y +End +)LP"); + + int x = find_var(m, "x"); + int y = find_var(m, "y"); + EXPECT_EQ(m.get_variable_types()[x], 'I'); + EXPECT_EQ(m.get_variable_types()[y], 'I'); + // Generals alone does NOT force [0,1]; default bounds remain [0, +inf). + EXPECT_NEAR(m.get_variable_lower_bounds()[x], 0.0, tolerance); + EXPECT_TRUE(std::isinf(m.get_variable_upper_bounds()[x])); +} + +TEST(lp_parser, binaries_set_zero_one_bounds) +{ + auto m = parse_lp_string(R"LP( +Maximize + 3 x1 + 5 x2 + 4 x3 + 2 x4 +Subject To + knapsack: 2 x1 + 3 x2 + x3 + x4 <= 5 +Binaries + x1 x2 x3 x4 +End +)LP"); + + for (const std::string& n : {"x1", "x2", "x3", "x4"}) { + int v = find_var(m, n); + EXPECT_EQ(m.get_variable_types()[v], 'I'); + EXPECT_NEAR(m.get_variable_lower_bounds()[v], 0.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[v], 1.0, tolerance); + } +} + +TEST(lp_parser, mixed_continuous_integer_binary) +{ + auto m = parse_lp_string(R"LP( +Maximize + 3 xc + 4 xi + 7 xb +Subject To + c1: xc + xi + xb <= 10 +Generals + xi +Binaries + xb +End +)LP"); + + int xc = find_var(m, "xc"); + int xi = find_var(m, "xi"); + int xb = find_var(m, "xb"); + EXPECT_EQ(m.get_variable_types()[xc], 'C'); + EXPECT_EQ(m.get_variable_types()[xi], 'I'); + EXPECT_EQ(m.get_variable_types()[xb], 'I'); + EXPECT_NEAR(m.get_variable_upper_bounds()[xb], 1.0, tolerance); + EXPECT_TRUE(std::isinf(m.get_variable_upper_bounds()[xi])); +} + +TEST(lp_parser, quadratic_diagonal_only) +{ + auto m = parse_lp_string(R"LP( +Minimize + x + y + [ 2 x ^2 + 4 y ^2 ] / 2 +Subject To + c1: x + y >= 1 +Bounds + x free + y free +End +)LP"); + + ASSERT_TRUE(m.has_quadratic_objective()); + int x = find_var(m, "x"); + int y = find_var(m, "y"); + // LP [2 x^2]/2 = x^2 ⇒ Q[x,x] should be 1 in cuOpt's x^T Q x form. + EXPECT_NEAR(q_entry(m, x, x), 1.0, tolerance); + // LP [4 y^2]/2 = 2 y^2 ⇒ Q[y,y] = 2. + EXPECT_NEAR(q_entry(m, y, y), 2.0, tolerance); + EXPECT_NEAR(q_entry(m, x, y), 0.0, tolerance); + // Linear part is preserved. + EXPECT_NEAR(m.get_objective_coefficients()[x], 1.0, tolerance); + EXPECT_NEAR(m.get_objective_coefficients()[y], 1.0, tolerance); +} + +TEST(lp_parser, quadratic_with_cross_terms) +{ + auto m = parse_lp_string(R"LP( +Minimize + - 3 x - 4 y - 2 z + [ 2 x ^2 + 2 x * y + 2 y ^2 + 2 y * z + 2 z ^2 ] / 2 +Subject To + c1: x + y + z <= 10 + c2: x + y >= 1 +End +)LP"); + + ASSERT_TRUE(m.has_quadratic_objective()); + int x = find_var(m, "x"); + int y = find_var(m, "y"); + int z = find_var(m, "z"); + // Diagonal 2 x^2 / 2 = x^2 ⇒ Q[x,x] = 1, similarly for y, z. + EXPECT_NEAR(q_entry(m, x, x), 1.0, tolerance); + EXPECT_NEAR(q_entry(m, y, y), 1.0, tolerance); + EXPECT_NEAR(q_entry(m, z, z), 1.0, tolerance); + // Cross 2 x*y / 2 = x*y ⇒ full matrix Q[x,y] = Q[y,x] = 0.5 each + // (so that x^T Q x sums to x*y). + EXPECT_NEAR(q_entry(m, x, y), 0.5, tolerance); + EXPECT_NEAR(q_entry(m, y, x), 0.5, tolerance); + EXPECT_NEAR(q_entry(m, y, z), 0.5, tolerance); + EXPECT_NEAR(q_entry(m, z, y), 0.5, tolerance); + // x and z have no cross term. + EXPECT_NEAR(q_entry(m, x, z), 0.0, tolerance); + + EXPECT_NEAR(m.get_objective_coefficients()[x], -3.0, tolerance); + EXPECT_NEAR(m.get_objective_coefficients()[y], -4.0, tolerance); + EXPECT_NEAR(m.get_objective_coefficients()[z], -2.0, tolerance); +} + +TEST(lp_parser, miqp_integer_with_quadratic_objective) +{ + auto m = parse_lp_string(R"LP( +Minimize + - 4 xi - 2 xc + [ 2 xi ^2 + 2 xc ^2 ] / 2 +Subject To + c1: xi + xc <= 5 +Bounds + xi <= 4 +Generals + xi +End +)LP"); + + int xi = find_var(m, "xi"); + int xc = find_var(m, "xc"); + EXPECT_EQ(m.get_variable_types()[xi], 'I'); + EXPECT_EQ(m.get_variable_types()[xc], 'C'); + EXPECT_NEAR(m.get_variable_upper_bounds()[xi], 4.0, tolerance); + EXPECT_NEAR(q_entry(m, xi, xi), 1.0, tolerance); + EXPECT_NEAR(q_entry(m, xc, xc), 1.0, tolerance); +} + +TEST(lp_parser, infeasible_model_parses_faithfully) +{ + auto m = parse_lp_string(R"LP( +Minimize + x + y +Subject To + high: x + y >= 15 + low: x + y <= 8 +Bounds + x <= 5 + y <= 5 +End +)LP"); + + EXPECT_EQ(m.get_row_names().size(), 2u); + int high = find_row(m, "high"); + int low = find_row(m, "low"); + EXPECT_NEAR(m.get_constraint_lower_bounds()[high], 15.0, tolerance); + EXPECT_NEAR(m.get_constraint_upper_bounds()[low], 8.0, tolerance); +} + +TEST(lp_parser, unbounded_model_parses) +{ + auto m = parse_lp_string(R"LP( +Maximize + x + y +Subject To + c1: x - y <= 5 +End +)LP"); + + int x = find_var(m, "x"); + EXPECT_NEAR(m.get_variable_lower_bounds()[x], 0.0, tolerance); + EXPECT_TRUE(std::isinf(m.get_variable_upper_bounds()[x])); + EXPECT_TRUE(m.get_sense()); +} + +TEST(lp_parser, missing_objective_throws) +{ + EXPECT_THROW(parse_lp_string(R"LP( +Subject To + c1: x + y <= 5 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, unsupported_sos_section_throws) +{ + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 1 +SOS + s1: S1 :: x : 1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, semi_continuous_basic) +{ + auto m = parse_lp_string(R"LP( +Minimize + x + y +Subject To + c1: x + y >= 1 +Bounds + 2 <= x <= 10 + y <= 5 +Semi-Continuous + x +End +)LP"); + ASSERT_EQ(m.get_variable_names().size(), 2u); + int xi = find_var(m, "x"); + int yi = find_var(m, "y"); + ASSERT_GE(xi, 0); + ASSERT_GE(yi, 0); + EXPECT_EQ(m.get_variable_types()[xi], 'S'); + EXPECT_EQ(m.get_variable_types()[yi], 'C'); + EXPECT_NEAR(m.get_variable_lower_bounds()[xi], 2.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[xi], 10.0, tolerance); +} + +TEST(lp_parser, semi_continuous_default_lower_is_zero) +{ + auto m = parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 0 +Bounds + x <= 3 +Semi-Continuous + x +End +)LP"); + int xi = find_var(m, "x"); + ASSERT_GE(xi, 0); + EXPECT_EQ(m.get_variable_types()[xi], 'S'); + // No explicit lower in Bounds ⇒ default 0. + EXPECT_NEAR(m.get_variable_lower_bounds()[xi], 0.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[xi], 3.0, tolerance); +} + +TEST(lp_parser, semi_continuous_missing_upper_throws) +{ + // No upper bound specified ⇒ infinity ⇒ semantics degenerate, reject. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 0 +Semi-Continuous + x +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, semi_continuous_and_generals_conflict_throws) +{ + // Variable appearing in both Semi-Continuous and Generals is ambiguous + // (integer vs. continuous-or-zero) ⇒ reject. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 0 +Bounds + x <= 5 +Generals + x +Semi-Continuous + x +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, semi_continuous_and_binaries_conflict_throws) +{ + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 0 +Bounds + x <= 5 +Binaries + x +Semi-Continuous + x +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, semi_continuous_before_generals_conflict_throws) +{ + // Conflict must also be detected when Semi-Continuous is declared first. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 0 +Bounds + x <= 5 +Semi-Continuous + x +Generals + x +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, unsupported_pwlobj_section_throws) +{ + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 1 +PWLObj + x: 0 0 1 1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, unsupported_lazy_constraints_section_throws) +{ + // Lazy constraints and user cuts are scope-limited out: LP/MIP/QP only. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 1 +Lazy Constraints + lc: x <= 10 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, unsupported_user_cuts_section_throws) +{ + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 1 +User Cuts + uc: x <= 10 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, unknown_file_throws) +{ + auto call = [] { return parse_lp("/definitely/does/not/exist.lp"); }; + EXPECT_THROW(call(), std::logic_error); +} + +TEST(lp_parser, case_insensitive_section_keywords) +{ + auto m = parse_lp_string(R"LP( +MINIMIZE + x +SUBJECT TO + c1: x >= 1 +BOUNDS + x <= 5 +END +)LP"); + int x = find_var(m, "x"); + EXPECT_NEAR(m.get_variable_upper_bounds()[x], 5.0, tolerance); +} + +TEST(lp_parser, backslash_comments_are_ignored) +{ + auto m = parse_lp_string(R"LP( +\ This is a comment +Minimize + x \ trailing comment +Subject To \ another comment + c1: x >= 1 +End +)LP"); + int x = find_var(m, "x"); + EXPECT_NEAR(m.get_objective_coefficients()[x], 1.0, tolerance); +} + +TEST(lp_parser, missing_end_warns_but_succeeds) +{ + // No End — should still parse. (A warning is printed; see parse_all().) + auto m = parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 1 +)LP"); + EXPECT_EQ(m.get_variable_names().size(), 1u); +} + +TEST(lp_parser, auto_generates_names_for_unlabeled_constraints) +{ + auto m = parse_lp_string(R"LP( +Minimize + x + y +Subject To + x + y <= 10 + x - y >= 0 +End +)LP"); + ASSERT_EQ(m.get_row_names().size(), 2u); + // Default auto-generated names are R0, R1. + EXPECT_EQ(m.get_row_names()[0], "R0"); + EXPECT_EQ(m.get_row_names()[1], "R1"); +} + +TEST(lp_parser, infinity_keyword_in_bounds) +{ + auto m = parse_lp_string(R"LP( +Minimize + x + y +Subject To + c1: x + y >= 0 +Bounds + -inf <= x <= inf + -infinity <= y +End +)LP"); + int x = find_var(m, "x"); + int y = find_var(m, "y"); + EXPECT_TRUE(std::isinf(-m.get_variable_lower_bounds()[x])); + EXPECT_TRUE(std::isinf(m.get_variable_upper_bounds()[x])); + EXPECT_TRUE(std::isinf(-m.get_variable_lower_bounds()[y])); +} + +TEST(lp_parser, coefficient_one_implicit_with_leading_minus) +{ + auto m = parse_lp_string(R"LP( +Minimize + - x + y +Subject To + c1: - x + y <= 0 +End +)LP"); + int x = find_var(m, "x"); + int y = find_var(m, "y"); + EXPECT_NEAR(m.get_objective_coefficients()[x], -1.0, tolerance); + EXPECT_NEAR(m.get_objective_coefficients()[y], 1.0, tolerance); + int r = find_row(m, "c1"); + EXPECT_NEAR(a_entry(m, r, x), -1.0, tolerance); +} + +TEST(lp_parser, quadratic_without_slash_two_is_rejected) +{ + // The quadratic bracket in the objective must be followed by '/ 2'. + // Without it there's no unambiguous way to tell whether the user meant + // '/ 2' and forgot or intended the bare coefficients, so cuopt rejects. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + [ 1 x ^2 ] +Subject To + c1: x >= 1 +End +)LP"), + std::logic_error); +} + +// =========================================================================== +// Quadratic constraints (LHS contains [ ... ] without the /2 divisor). +// =========================================================================== + +// Returns the kth quadratic constraint of `m` (`k` is the 0-indexed position +// in the order quadratic constraints were declared in the LP file). +const auto& nth_qc(const mps_data_model_t& m, size_t k) +{ + const auto& qcs = m.get_quadratic_constraints(); + return qcs.at(k); +} + +TEST(lp_parser, qc_basic_diagonal_only) +{ + auto m = parse_lp_string(R"LP( +Minimize + x + y +Subject To + q1: [ x ^ 2 + y ^ 2 ] <= 10 +Bounds + x free + y free +End +)LP"); + ASSERT_EQ(m.get_quadratic_constraints().size(), 1u); + const auto& qc = nth_qc(m, 0); + EXPECT_EQ(qc.constraint_row_name, "q1"); + EXPECT_EQ(qc.constraint_row_type, static_cast(LesserThanOrEqual)); + EXPECT_NEAR(qc.rhs_value, 10.0, tolerance); + EXPECT_TRUE(qc.linear_indices.empty()); + // Q = diag(1, 1). CSR: offsets=[0, 1, 2], indices=[0, 1], values=[1, 1]. + EXPECT_EQ(qc.quadratic_offsets, (std::vector{0, 1, 2})); + ASSERT_EQ(qc.quadratic_values.size(), 2u); + EXPECT_NEAR(qc.quadratic_values[0], 1.0, tolerance); + EXPECT_NEAR(qc.quadratic_values[1], 1.0, tolerance); +} + +TEST(lp_parser, qc_cross_term_splits_symmetrically) +{ + // `4 x*y` in the LP source means coefficient on x_i * x_j = 4 in the + // symmetric x^T Q x. Split into Q[x,y] = Q[y,x] = 2 in the CSR. + auto m = parse_lp_string(R"LP( +Minimize + x + y +Subject To + q1: [ x ^ 2 + 4 x * y + y ^ 2 ] <= 5 +End +)LP"); + ASSERT_EQ(m.get_quadratic_constraints().size(), 1u); + const auto& qc = nth_qc(m, 0); + // Q has 4 entries (all of [[1,2],[2,1]]). + EXPECT_EQ(qc.quadratic_offsets, (std::vector{0, 2, 4})); + ASSERT_EQ(qc.quadratic_values.size(), 4u); + EXPECT_NEAR(qc.quadratic_values[0], 1.0, tolerance); // (0, 0) + EXPECT_NEAR(qc.quadratic_values[1], 2.0, tolerance); // (0, 1) + EXPECT_NEAR(qc.quadratic_values[2], 2.0, tolerance); // (1, 0) + EXPECT_NEAR(qc.quadratic_values[3], 1.0, tolerance); // (1, 1) +} + +TEST(lp_parser, qc_linear_and_quadratic_mixed) +{ + auto m = parse_lp_string(R"LP( +Minimize + x + y +Subject To + q1: 3 x + 2 y + [ x ^ 2 + y ^ 2 ] <= 7 +End +)LP"); + ASSERT_EQ(m.get_quadratic_constraints().size(), 1u); + const auto& qc = nth_qc(m, 0); + EXPECT_NEAR(qc.rhs_value, 7.0, tolerance); + // Linear part: 3 x + 2 y. + ASSERT_EQ(qc.linear_indices.size(), 2u); + ASSERT_EQ(qc.linear_values.size(), 2u); + // Indices may be in any order; check coefficients via lookup. + std::vector xi_yi = {find_var(m, "x"), find_var(m, "y")}; + std::vector expected_coefs; + for (size_t i = 0; i < qc.linear_indices.size(); ++i) { + if (qc.linear_indices[i] == xi_yi[0]) EXPECT_NEAR(qc.linear_values[i], 3.0, tolerance); + if (qc.linear_indices[i] == xi_yi[1]) EXPECT_NEAR(qc.linear_values[i], 2.0, tolerance); + } +} + +TEST(lp_parser, qc_multiple_constraints_indexing) +{ + // 2 linear constraints, then 2 quadratic constraints. Per the data-model + // convention, quadratic rows are indexed after all linear rows. + auto m = parse_lp_string(R"LP( +Minimize + x + y +Subject To + c1: x + y <= 100 + c2: x - y >= -50 + q1: [ x ^ 2 ] <= 1 + q2: [ y ^ 2 ] <= 4 +End +)LP"); + EXPECT_EQ(m.get_row_names().size(), 2u); // linear rows only + ASSERT_EQ(m.get_quadratic_constraints().size(), 2u); + EXPECT_EQ(nth_qc(m, 0).constraint_row_index, 2); + EXPECT_EQ(nth_qc(m, 0).constraint_row_name, "q1"); + EXPECT_EQ(nth_qc(m, 1).constraint_row_index, 3); + EXPECT_EQ(nth_qc(m, 1).constraint_row_name, "q2"); +} + +TEST(lp_parser, qc_outer_minus_sign_flips_quadratic_and_linear) +{ + // `- [ x^2 + 2 x ] + 5` on the LHS contributes -x^2 - 2 x + 5 to the LHS. + // After moving the constant to the RHS: -x^2 - 2 x <= rhs - 5. + // Here the RHS is 10, so the row becomes: -x^2 - 2 x <= 5 (in x^T Q x form + // Q[x,x] = -1). + auto m = parse_lp_string(R"LP( +Minimize + x +Subject To + q1: - [ x ^ 2 + 2 x ] + 5 <= 10 +Bounds + x free +End +)LP"); + ASSERT_EQ(m.get_quadratic_constraints().size(), 1u); + const auto& qc = nth_qc(m, 0); + EXPECT_NEAR(qc.rhs_value, 5.0, tolerance); + ASSERT_EQ(qc.quadratic_values.size(), 1u); + EXPECT_NEAR(qc.quadratic_values[0], -1.0, tolerance); + ASSERT_EQ(qc.linear_indices.size(), 1u); + EXPECT_NEAR(qc.linear_values[0], -2.0, tolerance); +} + +TEST(lp_parser, qc_named_constraint) +{ + auto m = parse_lp_string(R"LP( +Minimize + x +Subject To + my_quad: [ x ^ 2 ] <= 1 +End +)LP"); + ASSERT_EQ(m.get_quadratic_constraints().size(), 1u); + EXPECT_EQ(nth_qc(m, 0).constraint_row_name, "my_quad"); +} + +TEST(lp_parser, qc_ge_relation_throws) +{ + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + q1: [ x ^ 2 ] >= 1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, qc_eq_relation_throws) +{ + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + q1: [ x ^ 2 ] = 1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, qc_with_slash_two_is_rejected) +{ + // '/ 2' is reserved for the objective bracket; using it in a constraint + // bracket is rejected so the convention is unambiguous. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + q1: [ x ^ 2 ] / 2 <= 1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, qc_linear_only_bracket_is_rejected) +{ + // A bracket with no quadratic terms inside is meaningless in a constraint + // (the user could just write the linear terms directly). + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + q1: [ 2 x ] <= 5 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, qc_objective_quadratic_still_requires_slash_two) +{ + // Regression: the existing '/ 2' requirement on the objective bracket + // must not change after adding constraint-bracket support. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + [ x ^ 2 ] +Subject To + c1: x >= 1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, duplicate_coefficient_accumulates) +{ + // Repeated variable in the objective should sum coefficients. + auto m = parse_lp_string(R"LP( +Minimize + 2 x + 3 x + y +Subject To + c1: x + y >= 1 +End +)LP"); + int x = find_var(m, "x"); + int y = find_var(m, "y"); + EXPECT_NEAR(m.get_objective_coefficients()[x], 5.0, tolerance); + EXPECT_NEAR(m.get_objective_coefficients()[y], 1.0, tolerance); +} + +TEST(lp_parser, subject_to_variant_st_dot) +{ + // 'st.' with a trailing period is a Subject-To synonym in the LP-format + // convention. + auto m = parse_lp_string(R"LP( +Minimize + x +st. + c: x >= 1 +End +)LP"); + EXPECT_EQ(m.get_row_names().size(), 1u); + EXPECT_EQ(m.get_row_names()[0], "c"); +} + +TEST(lp_parser, swapped_relational_operators_eq_lt_and_eq_gt) +{ + // '=<' is an alias for '<=' and '=>' for '>=', in both constraints and + // bounds. Tokenizer must produce LessEq / GreaterEq tokens regardless of + // spelling. + auto m = parse_lp_string(R"LP( +Minimize + x + y +Subject To + c_le: x + y =< 10 + c_ge: x + y => 1 +Bounds + y =< 5 + x => 0 +End +)LP"); + int c_le = find_row(m, "c_le"); + int c_ge = find_row(m, "c_ge"); + EXPECT_TRUE(std::isinf(-m.get_constraint_lower_bounds()[c_le])); + EXPECT_NEAR(m.get_constraint_upper_bounds()[c_le], 10.0, tolerance); + EXPECT_NEAR(m.get_constraint_lower_bounds()[c_ge], 1.0, tolerance); + EXPECT_TRUE(std::isinf(m.get_constraint_upper_bounds()[c_ge])); + int x = find_var(m, "x"); + int y = find_var(m, "y"); + EXPECT_NEAR(m.get_variable_upper_bounds()[y], 5.0, tolerance); + EXPECT_NEAR(m.get_variable_lower_bounds()[x], 0.0, tolerance); +} + +TEST(lp_parser, variable_names_with_special_characters) +{ + // Per the LP-format convention, variable names may contain assorted + // punctuation beyond letters + underscore. The names are treated as + // opaque identifiers; cuopt just has to keep them distinct. + auto m = parse_lp_string(R"LP( +Minimize + x!a + x#b + x$c + x@d + x'e + x~f + x.g + x_h + x|i + x{j} + x(k) + a/b +Subject To + c1: x!a + x#b + x$c + x@d + x'e + x~f + x.g + x_h + x|i + x{j} + x(k) + a/b >= 1 +End +)LP"); + ASSERT_EQ(m.get_variable_names().size(), 12u); + for (const std::string& n : + {"x!a", "x#b", "x$c", "x@d", "x'e", "x~f", "x.g", "x_h", "x|i", "x{j}", "x(k)", "a/b"}) { + EXPECT_GE(find_var(m, n), 0) << "missing variable '" << n << "'"; + } +} + +TEST(lp_parser, negative_upper_without_explicit_lower_throws) +{ + // 'x <= -1' with no explicit lower makes the default lb=0 collide with the + // upper. cuopt rejects rather than accept a silently infeasible problem. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + c: x <= 10 +Bounds + x <= -1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, negative_upper_with_explicit_lower_ok) +{ + // Same test as above, but now the lower bound is explicit: no error. + auto m = parse_lp_string(R"LP( +Minimize + x +Subject To + c: x <= 10 +Bounds + x >= -5 + x <= -1 +End +)LP"); + int x = find_var(m, "x"); + EXPECT_NEAR(m.get_variable_lower_bounds()[x], -5.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[x], -1.0, tolerance); +} + +TEST(lp_parser, negative_upper_with_range_bound_ok) +{ + // -5 <= x <= -1 declares both bounds in a single line: no error. + auto m = parse_lp_string(R"LP( +Minimize + x +Subject To + c: x <= 10 +Bounds + -5 <= x <= -1 +End +)LP"); + int x = find_var(m, "x"); + EXPECT_NEAR(m.get_variable_lower_bounds()[x], -5.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[x], -1.0, tolerance); +} + +// ================================================================================================ +// parse_problem dispatch tests +// +// Verifies the extension-based dispatch used by cuopt_cli and the C API. +// ================================================================================================ + +namespace { + +// Writes `content` to a temp file with the given suffix, parses it via +// parse_problem, removes the file, and returns the resulting model. +mps_data_model_t dispatch_parse(const std::string& content, const std::string& suffix) +{ + std::filesystem::path tmp = std::filesystem::temp_directory_path() / + (std::string{"cuopt_dispatch_test_"} + std::to_string(::getpid()) + + "_" + std::to_string(std::rand()) + suffix); + { + std::ofstream out(tmp); + out << content; + } + auto model = parse_problem(tmp.string()); + std::filesystem::remove(tmp); + return model; +} + +constexpr const char* kTrivialLp = R"LP( +Minimize + x +Subject To + c1: x >= 2.5 +Bounds + x <= 10 +End +)LP"; + +constexpr const char* kTrivialMps = R"MPS(NAME trivial +ROWS + N OBJ + G c1 +COLUMNS + x OBJ 1 + x c1 1 +RHS + RHS1 c1 2.5 +BOUNDS + UP BND1 x 10 +ENDATA +)MPS"; + +} // namespace + +TEST(parse_problem, lp_extension_dispatches_to_lp_parser) +{ + auto m = dispatch_parse(kTrivialLp, ".lp"); + ASSERT_EQ(m.get_variable_names().size(), 1u); + EXPECT_EQ(m.get_variable_names()[0], "x"); + EXPECT_NEAR(m.get_variable_upper_bounds()[0], 10.0, tolerance); +} + +TEST(parse_problem, lp_gz_extension_dispatches_to_lp_parser) +{ + // Real compressed LP fixture; successful parse proves dispatch picked the + // LP path. (Routing a .lp.gz to parse_mps would either fail at + // decompression or fail to parse the LP content as MPS.) + auto m = parse_problem(cuopt::test::get_rapids_dataset_root_dir() + + "/linear_programming/good-mps-1.lp.gz"); + ASSERT_EQ(m.get_variable_names().size(), 2u); + EXPECT_EQ(m.get_variable_names()[0], "VAR1"); +} + +TEST(parse_problem, lp_bz2_extension_dispatches_to_lp_parser) +{ + auto m = parse_problem(cuopt::test::get_rapids_dataset_root_dir() + + "/linear_programming/good-mps-1.lp.bz2"); + ASSERT_EQ(m.get_variable_names().size(), 2u); + EXPECT_EQ(m.get_variable_names()[0], "VAR1"); +} + +TEST(parse_problem, mps_extension_dispatches_to_mps_parser) +{ + auto m = dispatch_parse(kTrivialMps, ".mps"); + ASSERT_EQ(m.get_variable_names().size(), 1u); + EXPECT_EQ(m.get_variable_names()[0], "x"); + EXPECT_NEAR(m.get_variable_upper_bounds()[0], 10.0, tolerance); +} + +TEST(parse_problem, qps_extension_dispatches_to_mps_parser) +{ + // QPS is a superset of MPS; the MPS parser handles both. We just need + // parse_problem to route ".qps" to it. + auto m = dispatch_parse(kTrivialMps, ".qps"); + ASSERT_EQ(m.get_variable_names().size(), 1u); + EXPECT_EQ(m.get_variable_names()[0], "x"); +} + +TEST(parse_problem, mps_gz_extension_dispatches_to_mps_parser) +{ + auto m = parse_problem(cuopt::test::get_rapids_dataset_root_dir() + + "/linear_programming/good-mps-1.mps.gz"); + EXPECT_EQ("good-1", m.get_problem_name()); +} + +TEST(parse_problem, mps_bz2_extension_dispatches_to_mps_parser) +{ + auto m = parse_problem(cuopt::test::get_rapids_dataset_root_dir() + + "/linear_programming/good-mps-1.mps.bz2"); + EXPECT_EQ("good-1", m.get_problem_name()); +} + +TEST(parse_problem, uppercase_lp_extension_dispatches_to_lp_parser) +{ + // Matching is case-insensitive: .LP must still route to parse_lp. + auto m = dispatch_parse(kTrivialLp, ".LP"); + ASSERT_EQ(m.get_variable_names().size(), 1u); + EXPECT_EQ(m.get_variable_names()[0], "x"); +} + +TEST(parse_problem, mixed_case_mps_extension_dispatches_to_mps_parser) +{ + auto m = dispatch_parse(kTrivialMps, ".MpS"); + ASSERT_EQ(m.get_variable_names().size(), 1u); + EXPECT_EQ(m.get_variable_names()[0], "x"); +} + +TEST(parse_problem, unrecognized_extension_throws) +{ + // Extensionless and unrelated suffixes are rejected; case doesn't matter + // (matching is case-insensitive, so ".lpgz" stays rejected too). + for (const char* suffix : {".txt", ".lpgz", ""}) { + SCOPED_TRACE(suffix); + EXPECT_THROW(dispatch_parse(kTrivialLp, suffix), std::logic_error); + } +} + +// =========================================================================== +// MPS-only tests preserved from cpp/tests/linear_programming/mps_parser_test.cpp +// (semi-continuous variables and quadratic-constraint coverage added in #1193; +// kept here because the LP parser does not support these constructs). +// =========================================================================== + +TEST(mps_bounds, standard_var_bounds_0_inf) +{ + auto mps = read_from_mps("linear_programming/free-format-mps-1.mps", false); + + // standard bounds are 0,inf when no var bounds are specified + EXPECT_EQ(int(2), mps.variable_lower_bounds.size()); + EXPECT_EQ(0., mps.variable_lower_bounds[0]); + EXPECT_EQ(0., mps.variable_lower_bounds[1]); + EXPECT_EQ(int(2), mps.variable_upper_bounds.size()); + EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[0]); + EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[1]); +} + +TEST(mps_bounds, only_some_UP_LO_var_bounds) +{ + auto mps = read_from_mps("linear_programming/good-mps-some-var-bounds.mps"); + + // standard bounds are 0,inf when no var bounds are specified + EXPECT_EQ(int(2), mps.variable_lower_bounds.size()); + EXPECT_EQ(-1., mps.variable_lower_bounds[0]); + EXPECT_EQ(0., mps.variable_lower_bounds[1]); + EXPECT_EQ(int(2), mps.variable_upper_bounds.size()); + EXPECT_EQ(std::numeric_limits::infinity(), mps.variable_upper_bounds[0]); + EXPECT_EQ(2., mps.variable_upper_bounds[1]); +} + +TEST(mps_bounds, semi_continuous_var_bounds_from_dataset) +{ + struct Case { + const char* name; + const char* mps; + int n_vars; + double lower; + double upper; + }; + const std::vector cases = { + {"sc_standard", cuopt::test::inline_mps::sc_standard_mps, 2, 2.0, 10.0}, + {"sc_lb_zero", cuopt::test::inline_mps::sc_lb_zero_mps, 2, 0.0, 10.0}, + {"sc_no_ub", cuopt::test::inline_mps::sc_no_ub_mps, 2, 2.0, 1e30}, + }; + + for (const auto& c : cases) { + SCOPED_TRACE(c.name); + auto mps = cuopt::test::inline_mps::parse_inline_mps(c.mps); + const auto& var_types = mps.get_variable_types(); + const auto& lower = mps.get_variable_lower_bounds(); + const auto& upper = mps.get_variable_upper_bounds(); + + ASSERT_EQ(c.n_vars, static_cast(var_types.size())); + EXPECT_EQ('S', var_types[0]); + ASSERT_EQ(c.n_vars, static_cast(lower.size())); + ASSERT_EQ(c.n_vars, static_cast(upper.size())); + EXPECT_DOUBLE_EQ(c.lower, lower[0]); + EXPECT_DOUBLE_EQ(c.upper, upper[0]); + } +} + +TEST(mps_bounds, semi_continuous_missing_lower_defaults_to_zero) +{ + auto mps = cuopt::test::inline_mps::parse_inline_mps(cuopt::test::inline_mps::sc_lb_zero_mps); + const auto& var_types = mps.get_variable_types(); + const auto& lower = mps.get_variable_lower_bounds(); + const auto& upper = mps.get_variable_upper_bounds(); + + ASSERT_EQ(2, static_cast(var_types.size())); + EXPECT_EQ('S', var_types[0]); + ASSERT_EQ(2, static_cast(lower.size())); + ASSERT_EQ(2, static_cast(upper.size())); + EXPECT_DOUBLE_EQ(0.0, lower[0]); + EXPECT_DOUBLE_EQ(10.0, upper[0]); +} + +TEST(mps_bounds, semi_continuous_missing_upper_rejected) +{ + EXPECT_THROW( + cuopt::test::inline_mps::parse_inline_mps(cuopt::test::inline_mps::sc_missing_upper_mps), + std::logic_error); +} + +TEST(mps_bounds, semi_continuous_bound_type) +{ + auto mps = read_from_mps("linear_programming/good-mps-semi-continuous-bound.mps", false); + + ASSERT_EQ(int(2), mps.var_names.size()); + ASSERT_EQ(int(2), mps.var_types.size()); + EXPECT_EQ('S', mps.var_types[0]); + ASSERT_EQ(int(2), mps.variable_lower_bounds.size()); + ASSERT_EQ(int(2), mps.variable_upper_bounds.size()); + EXPECT_DOUBLE_EQ(0.0, mps.variable_lower_bounds[0]); + EXPECT_DOUBLE_EQ(2.0, mps.variable_upper_bounds[0]); +} + +TEST(mps_bounds, invalid_bound_type) +{ + ASSERT_THROW(read_from_mps("linear_programming/bad-mps-bound-1.mps", false), std::logic_error); +} + +TEST(qps_parser, qcmatrix_append_api) +{ + using model_t = mps_data_model_t; + model_t model; + + // Validate default-constructed struct shape. + model_t::quadratic_constraint_t default_qcm; + EXPECT_EQ(0, default_qcm.constraint_row_index); + EXPECT_TRUE(default_qcm.quadratic_values.empty()); + EXPECT_TRUE(default_qcm.quadratic_indices.empty()); + EXPECT_TRUE(default_qcm.quadratic_offsets.empty()); + EXPECT_TRUE(default_qcm.linear_values.empty()); + EXPECT_TRUE(default_qcm.linear_indices.empty()); + EXPECT_EQ(0.0, default_qcm.rhs_value); + + // QC0: [[10, 2], [2, 2]] + const std::vector qc0_values = {10.0, 2.0, 2.0, 2.0}; + const std::vector qc0_indices = {0, 1, 0, 1}; + const std::vector qc0_offsets = {0, 2, 4}; + const std::vector qc0_linear_values = {1.0, 1.0}; + const std::vector qc0_linear_indices = {0, 1}; + model.append_quadratic_constraint(0, + "QC0", + 'L', + qc0_linear_values, + qc0_linear_indices, + 5.0, + qc0_values, + qc0_indices, + qc0_offsets); + + // QC1: [[4, 1], [1, 6]] + const std::vector qc1_values = {4.0, 1.0, 1.0, 6.0}; + const std::vector qc1_indices = {0, 1, 0, 1}; + const std::vector qc1_offsets = {0, 2, 4}; + const std::vector qc1_linear_values = {3.0, 1.0}; + const std::vector qc1_linear_indices = {0, 1}; + model.append_quadratic_constraint(1, + "QC1", + 'L', + qc1_linear_values, + qc1_linear_indices, + 10.0, + qc1_values, + qc1_indices, + qc1_offsets); + + ASSERT_TRUE(model.has_quadratic_constraints()); + const auto& qcs = model.get_quadratic_constraints(); + ASSERT_EQ(2u, qcs.size()); + + EXPECT_EQ(0, qcs[0].constraint_row_index); + EXPECT_EQ("QC0", qcs[0].constraint_row_name); + EXPECT_EQ('L', qcs[0].constraint_row_type); + EXPECT_EQ(qc0_linear_values, qcs[0].linear_values); + EXPECT_EQ(qc0_linear_indices, qcs[0].linear_indices); + EXPECT_EQ(5.0, qcs[0].rhs_value); + EXPECT_EQ(qc0_values, qcs[0].quadratic_values); + EXPECT_EQ(qc0_indices, qcs[0].quadratic_indices); + EXPECT_EQ(qc0_offsets, qcs[0].quadratic_offsets); + + EXPECT_EQ(1, qcs[1].constraint_row_index); + EXPECT_EQ("QC1", qcs[1].constraint_row_name); + EXPECT_EQ('L', qcs[1].constraint_row_type); + EXPECT_EQ(qc1_linear_values, qcs[1].linear_values); + EXPECT_EQ(qc1_linear_indices, qcs[1].linear_indices); + EXPECT_EQ(10.0, qcs[1].rhs_value); + EXPECT_EQ(qc1_values, qcs[1].quadratic_values); + EXPECT_EQ(qc1_indices, qcs[1].quadratic_indices); + EXPECT_EQ(qc1_offsets, qcs[1].quadratic_offsets); +} + +// QCQP MPS: each quadratic constraint bundles row + linear + rhs + quadratic. +TEST(qps_parser, qcmatrix_mps_linear_rhs_and_bounds) +{ + if (!file_exists("qcqp/QC_Test_1.mps")) { + GTEST_SKIP() << "qcqp/QC_Test_1.mps not in dataset root"; + } + const auto model = parse_mps( + cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/QC_Test_1.mps", false); + + ASSERT_TRUE(model.has_quadratic_constraints()); + const auto& qcs = model.get_quadratic_constraints(); + ASSERT_EQ(2u, qcs.size()); + + ASSERT_EQ(1, model.get_n_constraints()); + ASSERT_EQ(1u, model.get_row_names().size()); + EXPECT_EQ("LIN0", model.get_row_names()[0]); + EXPECT_EQ('L', model.get_row_types()[0]); + + // LIN0: 2*x1 + x2 ≤ 15 (linear row only; not duplicated in quadratic_constraints) + EXPECT_DOUBLE_EQ(-std::numeric_limits::infinity(), + model.get_constraint_lower_bounds()[0]); + EXPECT_DOUBLE_EQ(15.0, model.get_constraint_upper_bounds()[0]); + const auto& A_off = model.get_constraint_matrix_offsets(); + const auto& A_val = model.get_constraint_matrix_values(); + const auto& A_idx = model.get_constraint_matrix_indices(); + ASSERT_EQ(2, A_off[1] - A_off[0]); + EXPECT_EQ(2.0, A_val[A_off[0] + 0]); + EXPECT_EQ(1.0, A_val[A_off[0] + 1]); + EXPECT_EQ(0, A_idx[A_off[0] + 0]); + EXPECT_EQ(1, A_idx[A_off[0] + 1]); + + // QC0: x1 + x2 + xᵀQ₀x ≤ 5 (MPS ROWS declaration index 1; OBJ 'N' rows are not counted) + EXPECT_EQ(1, qcs[0].constraint_row_index); + EXPECT_EQ("QC0", qcs[0].constraint_row_name); + EXPECT_EQ('L', qcs[0].constraint_row_type); + ASSERT_EQ(2u, qcs[0].linear_values.size()); + EXPECT_EQ(1.0, qcs[0].linear_values[0]); + EXPECT_EQ(1.0, qcs[0].linear_values[1]); + EXPECT_EQ(0, qcs[0].linear_indices[0]); + EXPECT_EQ(1, qcs[0].linear_indices[1]); + EXPECT_DOUBLE_EQ(5.0, qcs[0].rhs_value); + EXPECT_FALSE(qcs[0].quadratic_values.empty()); + + // QC1: 3*x1 + x2 + xᵀQ₁x ≤ 10 + EXPECT_EQ(2, qcs[1].constraint_row_index); + EXPECT_EQ("QC1", qcs[1].constraint_row_name); + EXPECT_EQ('L', qcs[1].constraint_row_type); + ASSERT_EQ(2u, qcs[1].linear_values.size()); + EXPECT_EQ(3.0, qcs[1].linear_values[0]); + EXPECT_EQ(1.0, qcs[1].linear_values[1]); + EXPECT_DOUBLE_EQ(10.0, qcs[1].rhs_value); +} + +TEST(qps_parser, qcqp_p0033_mps_sections) +{ + if (!file_exists("qcqp/p0033_qc1.mps")) { + GTEST_SKIP() << "qcqp/p0033_qc1.mps not in dataset root"; + } + const auto model = parse_mps( + cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps", false); + + EXPECT_EQ(12, model.get_n_constraints()); + EXPECT_EQ(33, model.get_n_variables()); + ASSERT_EQ(12u, model.get_row_types().size()); + ASSERT_EQ(12u, model.get_row_names().size()); + + const auto& qcs = model.get_quadratic_constraints(); + ASSERT_EQ(4u, qcs.size()); + EXPECT_EQ(12, qcs[0].constraint_row_index); + ASSERT_EQ(1u, qcs[0].linear_values.size()); + EXPECT_DOUBLE_EQ(1.0, qcs[0].linear_values[0]); + + const auto& vnames = model.get_variable_names(); + auto c159_it = std::find(vnames.begin(), vnames.end(), std::string("C159")); + ASSERT_NE(c159_it, vnames.end()); + EXPECT_EQ(static_cast(c159_it - vnames.begin()), qcs[0].linear_indices[0]); + + EXPECT_DOUBLE_EQ(1.0, qcs[0].rhs_value); + EXPECT_FALSE(qcs[0].quadratic_values.empty()); +} + +TEST(mps_roundtrip, qcqp_p0033_qc1) +{ + if (!file_exists("qcqp/p0033_qc1.mps")) { GTEST_SKIP() << "Test file not found"; } + + std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps"; + std::string temp_file = "/tmp/mps_roundtrip_p0033_qc1.mps"; + std::string temp_file_2 = "/tmp/mps_roundtrip_p0033_qc1_r2.mps"; + + auto original = parse_mps(input_file, false); + ASSERT_TRUE(original.has_quadratic_objective()); + ASSERT_TRUE(original.has_quadratic_constraints()); + + mps_writer_t writer(original); + writer.write(temp_file); + + auto reloaded = parse_mps(temp_file, false); + mps_writer_t writer_r2(reloaded); + writer_r2.write(temp_file_2); + auto reloaded_2 = parse_mps(temp_file_2, false); + compare_data_models(reloaded, reloaded_2); + + std::filesystem::remove(temp_file); + std::filesystem::remove(temp_file_2); +} +} // namespace cuopt::linear_programming::io diff --git a/datasets/linear_programming/good-mps-1.lp b/datasets/linear_programming/good-mps-1.lp new file mode 100644 index 0000000000..3d5a98225e --- /dev/null +++ b/datasets/linear_programming/good-mps-1.lp @@ -0,0 +1,12 @@ +\ Equivalent of good-mps-1.mps +\ min 0.2*VAR1 + 0.1*VAR2 +\ s.t. 3*VAR1 + 4*VAR2 <= 5.4 +\ 2.7*VAR1 + 10.1*VAR2 <= 4.9 +\ VAR1, VAR2 >= 0 (default) + +Minimize + 0.2 VAR1 + 0.1 VAR2 +Subject To + ROW1: 3 VAR1 + 4 VAR2 <= 5.4 + ROW2: 2.7 VAR1 + 10.1 VAR2 <= 4.9 +End diff --git a/datasets/linear_programming/good-mps-1.lp.bz2 b/datasets/linear_programming/good-mps-1.lp.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..9c4ef6ec266d5bdae575d66374c410fd9ceee473 GIT binary patch literal 215 zcmV;|04V=LT4*^jL0KkKS^bZHmH+^3Uw{Y@K!1L>A_AR+Kez7?FaWHBQ$jT~^*u+F z&^<#zr>Fxo$qWDu8fXJ1fY4+HfRs&0-l?EHAPqFrL7)H*n0{?)Y7rE8sNIxeHk!en z;Z4U68xAM|W)&!4jqgp;Iyy+|E4#S%#Dw#_M4*^*SzLmun}moiwk%{x2BJs|D2f*) zV5*>nf+nae#bP52rD{Y(2#AQF5Me#jx23SgHeo`Ul3x~k??p1<$oCTB#Y-?$!e-Yp Rc)LLUF64@Ep&|Po|178jTD|}P literal 0 HcmV?d00001 diff --git a/datasets/linear_programming/good-mps-1.lp.gz b/datasets/linear_programming/good-mps-1.lp.gz new file mode 100644 index 0000000000000000000000000000000000000000..bf0bd1d76395bdb6f071ddb492cfb392744ea0d2 GIT binary patch literal 199 zcmV;&06702iwFqcatCSv17~kR#J9QQoOy;tP#UVX|&Y70~2VN4aZuP$PA=;R8tN+YIx zcHR{X^I>Fefu3%AY@bnP667Vyh$h8UAd)@9$=DIt5M zRja;|!2QSnzt_pJzdhyM;$#M)I9ynYBuE0|C?50FMZqWv#!+%zkspz&(E1Dk005>* BTp$1d literal 0 HcmV?d00001 diff --git a/datasets/linear_programming/good-mps-fixed-var.lp b/datasets/linear_programming/good-mps-fixed-var.lp new file mode 100644 index 0000000000..ab12d0eb5a --- /dev/null +++ b/datasets/linear_programming/good-mps-fixed-var.lp @@ -0,0 +1,11 @@ +\ Equivalent of good-mps-fixed-var.mps +\ VAR1 fixed at 2; VAR2 default [0, inf) + +Minimize + 0.2 VAR1 + 0.1 VAR2 +Subject To + ROW1: 3 VAR1 + 4 VAR2 <= 5.4 + ROW2: 2.7 VAR1 + 10.1 VAR2 <= 4.9 +Bounds + VAR1 = 2 +End diff --git a/datasets/linear_programming/good-mps-free-var.lp b/datasets/linear_programming/good-mps-free-var.lp new file mode 100644 index 0000000000..58077c94dd --- /dev/null +++ b/datasets/linear_programming/good-mps-free-var.lp @@ -0,0 +1,11 @@ +\ Equivalent of good-mps-free-var.mps +\ VAR1 free (-inf, +inf); VAR2 default [0, +inf) + +Minimize + 0.2 VAR1 + 0.1 VAR2 +Subject To + ROW1: 3 VAR1 + 4 VAR2 <= 5.4 + ROW2: 2.7 VAR1 + 10.1 VAR2 <= 4.9 +Bounds + VAR1 free +End diff --git a/datasets/linear_programming/good-mps-lower-bound-inf-var.lp b/datasets/linear_programming/good-mps-lower-bound-inf-var.lp new file mode 100644 index 0000000000..db83d77fd4 --- /dev/null +++ b/datasets/linear_programming/good-mps-lower-bound-inf-var.lp @@ -0,0 +1,11 @@ +\ Equivalent of good-mps-lower-bound-inf-var.mps +\ VAR1 has MI (lower -inf, upper default +inf); VAR2 default [0, +inf) + +Minimize + 0.2 VAR1 + 0.1 VAR2 +Subject To + ROW1: 3 VAR1 + 4 VAR2 <= 5.4 + ROW2: 2.7 VAR1 + 10.1 VAR2 <= 4.9 +Bounds + -inf <= VAR1 +End diff --git a/datasets/linear_programming/good-mps-some-var-bounds.lp b/datasets/linear_programming/good-mps-some-var-bounds.lp new file mode 100644 index 0000000000..3c76a1ae2c --- /dev/null +++ b/datasets/linear_programming/good-mps-some-var-bounds.lp @@ -0,0 +1,12 @@ +\ Equivalent of good-mps-some-var-bounds.mps +\ -1 <= VAR1 <= inf; 0 <= VAR2 <= 2 + +Minimize + 0.2 VAR1 + 0.1 VAR2 +Subject To + ROW1: 3 VAR1 + 4 VAR2 <= 5.4 + ROW2: 2.7 VAR1 + 10.1 VAR2 <= 4.9 +Bounds + -1 <= VAR1 + VAR2 <= 2 +End diff --git a/datasets/linear_programming/good-mps-upper-bound-inf-var.lp b/datasets/linear_programming/good-mps-upper-bound-inf-var.lp new file mode 100644 index 0000000000..014d671e5e --- /dev/null +++ b/datasets/linear_programming/good-mps-upper-bound-inf-var.lp @@ -0,0 +1,12 @@ +\ Equivalent of good-mps-upper-bound-inf-var.mps +\ VAR1 has PL (lower default 0, upper +inf); VAR2 default [0, +inf) +\ Semantically both at default [0, +inf). + +Minimize + 0.2 VAR1 + 0.1 VAR2 +Subject To + ROW1: 3 VAR1 + 4 VAR2 <= 5.4 + ROW2: 2.7 VAR1 + 10.1 VAR2 <= 4.9 +Bounds + VAR1 <= inf +End diff --git a/datasets/linear_programming/lp_model_with_var_bounds.lp b/datasets/linear_programming/lp_model_with_var_bounds.lp new file mode 100644 index 0000000000..246bdfe4ad --- /dev/null +++ b/datasets/linear_programming/lp_model_with_var_bounds.lp @@ -0,0 +1,14 @@ +\ Equivalent of lp_model_with_var_bounds.mps +\ min 2x - y +\ s.t. x + y <= 3 +\ 0 <= x <= 1 +\ 1 <= y <= 2 + +Minimize + 2 x - y +Subject To + con: x + y <= 3 +Bounds + 0 <= x <= 1 + 1 <= y <= 2 +End diff --git a/datasets/mixed_integer_programming/good-mip-mps-1.lp b/datasets/mixed_integer_programming/good-mip-mps-1.lp new file mode 100644 index 0000000000..4f480ed3bf --- /dev/null +++ b/datasets/mixed_integer_programming/good-mip-mps-1.lp @@ -0,0 +1,17 @@ +\ Equivalent of good-mip-mps-1.mps +\ maximize 100*VAR1 + 150*VAR2 +\ s.t. 8000*VAR1 + 4000*VAR2 <= 40000 +\ 15*VAR1 + 30*VAR2 <= 200 +\ 0 <= VAR1, VAR2 <= 10 and integer + +Maximize + 100 VAR1 + 150 VAR2 +Subject To + ROW1: 8000 VAR1 + 4000 VAR2 <= 40000 + ROW2: 15 VAR1 + 30 VAR2 <= 200 +Bounds + VAR1 <= 10 + VAR2 <= 10 +Generals + VAR1 VAR2 +End diff --git a/datasets/mixed_integer_programming/good-mip-mps-no-bounds.lp b/datasets/mixed_integer_programming/good-mip-mps-no-bounds.lp new file mode 100644 index 0000000000..00512d372b --- /dev/null +++ b/datasets/mixed_integer_programming/good-mip-mps-no-bounds.lp @@ -0,0 +1,12 @@ +\ Equivalent of good-mip-mps-no-bounds.mps +\ maximize 100*VAR1 + 150*VAR2; VAR1 integer, VAR2 continuous. +\ MPS defaults VAR1 (unbounded integer) to [0, 1]; LP "Binaries" matches. + +Maximize + 100 VAR1 + 150 VAR2 +Subject To + ROW1: 8000 VAR1 + 4000 VAR2 <= 40000 + ROW2: 15 VAR1 + 30 VAR2 <= 200 +Binaries + VAR1 +End diff --git a/datasets/mixed_integer_programming/good-mip-mps-partial-bounds.lp b/datasets/mixed_integer_programming/good-mip-mps-partial-bounds.lp new file mode 100644 index 0000000000..7b904c8f30 --- /dev/null +++ b/datasets/mixed_integer_programming/good-mip-mps-partial-bounds.lp @@ -0,0 +1,15 @@ +\ Equivalent of good-mip-mps-partial-bounds.mps +\ maximize 100*VAR1 + 150*VAR2 +\ VAR1 integer with no explicit bounds (MPS default [0,1] ⇒ Binaries in LP) +\ VAR2 continuous with [0, 10] from BOUNDS section + +Maximize + 100 VAR1 + 150 VAR2 +Subject To + ROW1: 8000 VAR1 + 4000 VAR2 <= 40000 + ROW2: 15 VAR1 + 30 VAR2 <= 200 +Bounds + VAR2 <= 10 +Binaries + VAR1 +End diff --git a/docs/cuopt/source/cuopt-c/lp-qp-milp/examples/lp_file_example.c b/docs/cuopt/source/cuopt-c/lp-qp-milp/examples/lp_file_example.c new file mode 100644 index 0000000000..43a8fd7502 --- /dev/null +++ b/docs/cuopt/source/cuopt-c/lp-qp-milp/examples/lp_file_example.c @@ -0,0 +1,179 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* + * LP File C API Example + * + * This example demonstrates how to solve an LP problem from an LP format + * file using the cuOpt C API. The same ``cuOptReadProblem`` call handles + * both MPS and LP inputs — the format is dispatched automatically by the + * file extension (case-insensitive): ``.lp`` / ``.lp.gz`` / ``.lp.bz2`` + * go to the LP parser; ``.mps`` / ``.qps`` and their ``.gz`` / ``.bz2`` + * variants go to the MPS parser. + * + * Problem (from LP file): + * Minimize: -0.2*VAR1 + 0.1*VAR2 + * Subject to: + * 3*VAR1 + 4*VAR2 <= 5.4 + * 2.7*VAR1 + 10.1*VAR2 <= 4.9 + * VAR1, VAR2 >= 0 + * + * Expected Output: + * Number of variables: 2 + * Termination status: Optimal (1) + * Objective value: -0.360000 + * x1 = 1.800000 + * x2 = 0.000000 + * + * Build: + * gcc -I $INCLUDE_PATH -L $LIBCUOPT_LIBRARY_PATH -o lp_file_example lp_file_example.c -lcuopt + * + * Run: + * ./lp_file_example sample.lp + */ + +#include +#include +#include + +const char* termination_status_to_string(cuopt_int_t termination_status) +{ + switch (termination_status) { + case CUOPT_TERMINATION_STATUS_OPTIMAL: + return "Optimal"; + case CUOPT_TERMINATION_STATUS_INFEASIBLE: + return "Infeasible"; + case CUOPT_TERMINATION_STATUS_UNBOUNDED: + return "Unbounded"; + case CUOPT_TERMINATION_STATUS_ITERATION_LIMIT: + return "Iteration limit"; + case CUOPT_TERMINATION_STATUS_TIME_LIMIT: + return "Time limit"; + case CUOPT_TERMINATION_STATUS_NUMERICAL_ERROR: + return "Numerical error"; + case CUOPT_TERMINATION_STATUS_PRIMAL_FEASIBLE: + return "Primal feasible"; + case CUOPT_TERMINATION_STATUS_FEASIBLE_FOUND: + return "Feasible found"; + default: + return "Unknown"; + } +} + +cuopt_int_t solve_lp_file(const char* filename) +{ + cuOptOptimizationProblem problem = NULL; + cuOptSolverSettings settings = NULL; + cuOptSolution solution = NULL; + cuopt_int_t status; + cuopt_float_t time; + cuopt_int_t termination_status; + cuopt_float_t objective_value; + cuopt_int_t num_variables; + cuopt_float_t* solution_values = NULL; + + printf("Reading and solving input file: %s\n", filename); + + // Create the problem from the input file. cuOptReadProblem dispatches on + // the file extension (case-insensitive): ``.lp`` / ``.lp.gz`` / ``.lp.bz2`` + // go to the LP parser; ``.mps`` / ``.qps`` and their ``.gz`` / ``.bz2`` + // variants go to the MPS parser. + status = cuOptReadProblem(filename, &problem); + if (status != CUOPT_SUCCESS) { + printf("Error creating problem from input file: %d\n", status); + goto DONE; + } + + status = cuOptGetNumVariables(problem, &num_variables); + if (status != CUOPT_SUCCESS) { + printf("Error getting number of variables: %d\n", status); + goto DONE; + } + + status = cuOptCreateSolverSettings(&settings); + if (status != CUOPT_SUCCESS) { + printf("Error creating solver settings: %d\n", status); + goto DONE; + } + + status = cuOptSetFloatParameter(settings, CUOPT_ABSOLUTE_PRIMAL_TOLERANCE, 0.0001); + if (status != CUOPT_SUCCESS) { + printf("Error setting optimality tolerance: %d\n", status); + goto DONE; + } + + status = cuOptSolve(problem, settings, &solution); + if (status != CUOPT_SUCCESS) { + printf("Error solving problem: %d\n", status); + goto DONE; + } + + status = cuOptGetSolveTime(solution, &time); + if (status != CUOPT_SUCCESS) { + printf("Error getting solve time: %d\n", status); + goto DONE; + } + + status = cuOptGetTerminationStatus(solution, &termination_status); + if (status != CUOPT_SUCCESS) { + printf("Error getting termination status: %d\n", status); + goto DONE; + } + + status = cuOptGetObjectiveValue(solution, &objective_value); + if (status != CUOPT_SUCCESS) { + printf("Error getting objective value: %d\n", status); + goto DONE; + } + + printf("\nResults:\n"); + printf("--------\n"); + printf("Number of variables: %d\n", num_variables); + printf("Termination status: %s (%d)\n", + termination_status_to_string(termination_status), + termination_status); + printf("Solve time: %f seconds\n", time); + printf("Objective value: %f\n", objective_value); + + solution_values = (cuopt_float_t*)malloc(num_variables * sizeof(cuopt_float_t)); + status = cuOptGetPrimalSolution(solution, solution_values); + if (status != CUOPT_SUCCESS) { + printf("Error getting solution values: %d\n", status); + goto DONE; + } + + printf("\nPrimal Solution: First 10 solution variables (or fewer if less exist):\n"); + for (cuopt_int_t i = 0; i < (num_variables < 10 ? num_variables : 10); i++) { + printf("x%d = %f\n", i + 1, solution_values[i]); + } + if (num_variables > 10) { + printf("... (showing only first 10 of %d variables)\n", num_variables); + } + +DONE: + free(solution_values); + cuOptDestroyProblem(&problem); + cuOptDestroySolverSettings(&settings); + cuOptDestroySolution(&solution); + + return status; +} + +int main(int argc, char* argv[]) +{ + if (argc != 2) { + printf("Usage: %s \n", argv[0]); + return 1; + } + + cuopt_int_t status = solve_lp_file(argv[1]); + + if (status == CUOPT_SUCCESS) { + printf("\nSolver completed successfully!\n"); + return 0; + } else { + printf("\nSolver failed with status: %d\n", status); + return 1; + } +} diff --git a/docs/cuopt/source/cuopt-c/lp-qp-milp/examples/sample.lp b/docs/cuopt/source/cuopt-c/lp-qp-milp/examples/sample.lp new file mode 100644 index 0000000000..40a53ab33d --- /dev/null +++ b/docs/cuopt/source/cuopt-c/lp-qp-milp/examples/sample.lp @@ -0,0 +1,9 @@ +\ Problem name: good-1 +\ Equivalent to sample.mps in this directory. + +Minimize + COST: - 0.2 VAR1 + 0.1 VAR2 +Subject To + ROW1: 3 VAR1 + 4 VAR2 <= 5.4 + ROW2: 2.7 VAR1 + 10.1 VAR2 <= 4.9 +End diff --git a/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-example.rst b/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-example.rst index 07fdc72d58..dc163009ed 100644 --- a/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-example.rst +++ b/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-example.rst @@ -73,6 +73,12 @@ Example With MPS File This example demonstrates how to use the cuOpt linear programming solver in C to solve an MPS file. +The same ``cuOptReadProblem`` call also accepts **LP** format files. The +format is dispatched from the filename extension (case-insensitive): +``.lp`` / ``.lp.gz`` / ``.lp.bz2`` → LP parser; ``.mps`` / ``.qps`` and +their ``.gz`` / ``.bz2`` variants → MPS parser. Unknown extensions are +rejected. See :ref:`lp-file-example-c` for an LP counterpart. + The example code is available at ``examples/cuopt-c/lp/mps_file_example.c`` (:download:`download `): .. literalinclude:: examples/mps_file_example.c @@ -138,6 +144,42 @@ You should see the following output: Solver completed successfully! +.. _lp-file-example-c: + +Example With LP File +-------------------- + +``cuOptReadProblem`` also accepts LP format files. The same function is +used — it dispatches on the file extension (case-insensitive): +``.lp`` / ``.lp.gz`` / ``.lp.bz2`` → LP parser; ``.mps`` / ``.qps`` and +their ``.gz`` / ``.bz2`` variants → MPS parser; unknown extensions are +rejected. See the ``parse_lp`` declaration in +``cuopt/linear_programming/io/parser.hpp`` for the supported subset of +the LP format. + +The example code is available at ``examples/cuopt-c/lp/lp_file_example.c`` (:download:`download `): + +.. literalinclude:: examples/lp_file_example.c + :language: c + :linenos: + +A sample LP file (:download:`download sample.lp `), +equivalent to the MPS sample above: + +.. literalinclude:: examples/sample.lp + :language: text + :linenos: + +Build and run the example + +.. code-block:: bash + + gcc -I $INCLUDE_PATH -L $LIBCUOPT_LIBRARY_PATH -o lp_file_example lp_file_example.c -lcuopt + ./lp_file_example sample.lp + +The output matches the MPS example above (same problem, same objective = -0.36). + + .. _simple-qp-example-c: Simple Quadratic Programming Example diff --git a/docs/cuopt/source/cuopt-cli/cli-examples.rst b/docs/cuopt/source/cuopt-cli/cli-examples.rst index 066f4088e9..68ae0adba1 100644 --- a/docs/cuopt/source/cuopt-cli/cli-examples.rst +++ b/docs/cuopt/source/cuopt-cli/cli-examples.rst @@ -1,6 +1,22 @@ Examples ======== +Input File Format +################# + +``cuopt_cli`` accepts both **MPS** and **LP** format input files. The +format is dispatched automatically from the file extension +(case-insensitive): + +- ``*.lp``, ``*.lp.gz``, ``*.lp.bz2`` → parsed as LP format +- ``*.mps``, ``*.mps.gz``, ``*.mps.bz2`` (and the equivalent ``*.qps`` + variants) → parsed as MPS / QPS + +Any other extension (including no extension) is rejected with an error +listing the supported suffixes. See the ``parse_lp`` / ``parse_mps`` +declarations in ``cuopt/linear_programming/io/parser.hpp`` for the +supported subset of each format. + Basic Usage ########### diff --git a/docs/cuopt/source/cuopt-cli/index.rst b/docs/cuopt/source/cuopt-cli/index.rst index 9322c62bb6..cae126133d 100644 --- a/docs/cuopt/source/cuopt-cli/index.rst +++ b/docs/cuopt/source/cuopt-cli/index.rst @@ -1,7 +1,7 @@ Command Line Interface ====================== -The cuopt_cli is a command-line interface for LP/MILP solvers that accepts MPS format files as input models. It provides command-line arguments to control all solver settings and parameters when solving linear and mixed-integer programming problems. +The cuopt_cli is a command-line interface for LP/MILP solvers that accepts MPS, QPS, or LP format files as input models. The format is dispatched automatically from the file extension (case-insensitive): ``.lp`` (with optional ``.gz`` / ``.bz2``) goes to the LP parser, ``.mps`` / ``.qps`` (with optional ``.gz`` / ``.bz2``) goes to the MPS parser, and unknown extensions are rejected. It provides command-line arguments to control all solver settings and parameters when solving linear and mixed-integer programming problems. .. toctree:: :maxdepth: 3 diff --git a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst index 7bba75d046..12121a4cf8 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst +++ b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst @@ -202,10 +202,13 @@ The response would be as follows: } -Using MPS file directly ------------------------ +Using MPS or LP file directly +----------------------------- -An example on using .mps files as input is shown below: +The self-hosted client accepts both MPS and LP format files — the client +dispatches on the file extension (``.lp`` ⇒ LP parser, otherwise MPS) and +sends the parsed data model to the server. An example on using .mps files +as input is shown below: :download:`mps_file_example.py ` diff --git a/docs/cuopt/source/hidden/mps-api.rst b/docs/cuopt/source/hidden/mps-api.rst index 664077b451..362a26a51a 100644 --- a/docs/cuopt/source/hidden/mps-api.rst +++ b/docs/cuopt/source/hidden/mps-api.rst @@ -1,8 +1,13 @@ =============================== -cuOpt MPS Parser API Reference +cuOpt MPS/LP Parser API Reference =============================== MPS Parser ---------- .. autofunction:: cuopt.linear_programming.mps_parser.ParseMps + +LP Parser +--------- + +.. autofunction:: cuopt.linear_programming.mps_parser.ParseLp diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index c88490f866..d171b12878 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -3,7 +3,7 @@ from cuopt.linear_programming import internals from cuopt.linear_programming.data_model import DataModel -from cuopt.linear_programming.mps_parser import ParseMps +from cuopt.linear_programming.mps_parser import ParseLp, ParseMps from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solution import Solution from cuopt.linear_programming.solver import BatchSolve, Solve diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/__init__.py b/python/cuopt/cuopt/linear_programming/mps_parser/__init__.py index c61013bf50..ded8d5a06c 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/__init__.py +++ b/python/cuopt/cuopt/linear_programming/mps_parser/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -from cuopt.linear_programming.mps_parser.parser import ParseMps, toDict +from cuopt.linear_programming.mps_parser.parser import ParseLp, ParseMps, toDict diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd b/python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd index b4875a0bca..402873f9ab 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd +++ b/python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd @@ -37,9 +37,13 @@ cdef extern from "cuopt/linear_programming/io/mps_data_model.hpp" namespace "cuo string objective_name_ string problem_name_ -cdef extern from "cuopt/linear_programming/io/utilities/cython_mps_parser.hpp" namespace "cuopt::cython": # noqa +cdef extern from "cuopt/linear_programming/io/utilities/cython_parser.hpp" namespace "cuopt::cython": # noqa cdef unique_ptr[mps_data_model_t[int, double]] call_parse_mps( const string& mps_file_path, bool fixed_mps_format ) except + + + cdef unique_ptr[mps_data_model_t[int, double]] call_parse_lp( + const string& lp_file_path + ) except + diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser.py b/python/cuopt/cuopt/linear_programming/mps_parser/parser.py index 5e83b27ddb..3107b106b2 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser.py +++ b/python/cuopt/cuopt/linear_programming/mps_parser/parser.py @@ -13,6 +13,10 @@ def ParseMps(mps_file_path, fixed_mps_format=False): """ Reads the equation from the input text file which is MPS formatted + See Also + -------- + ParseLp : parses LP format files (for users with .lp inputs). + Notes ----- Read this link http://lpsolve.sourceforge.net/5.5/mps-format.htm for more @@ -50,6 +54,50 @@ def ParseMps(mps_file_path, fixed_mps_format=False): return parser_wrapper.ParseMps(mps_file_path, fixed_mps_format) +@catch_mps_parser_exception +def ParseLp(lp_file_path): + """ + Reads an optimization problem from a file in LP format. + + The LP format is a human-readable alternative to MPS and supports LP, + MIP, and QP, plus semi-continuous variables (declared via a + Semi-Continuous section; finite upper bound required) and + quadratic constraints (QCQP; ``<=`` only). + + Quadratic terms live in ``[ ... ]`` blocks. The objective bracket must + be followed by ``/ 2`` (the file states coefficients in the + ``0.5 x^T Q x`` convention); a constraint bracket must NOT be followed + by ``/ 2`` (coefficients are at face value, ``x^T Q x``). + + This function parses the conventional LP dialect implemented by most + commercial optimization solvers (not the lpsolve variant, which has a + different syntax). + + Unsupported LP sections (SOS, PWL objective, user cuts, general + constraints) raise a ValueError. + + Parameters + ---------- + lp_file_path : str + Path to LP-formatted file. + + Returns + ------- + data_model: DataModel + A fully formed LP/MIP/QP problem representing the given file. + + Examples + -------- + >>> from cuopt import linear_programming + >>> + >>> data_model = linear_programming.ParseLp(lp_file_path) + >>> solver_settings = linear_programming.SolverSettings() + >>> solution = linear_programming.Solve(data_model, solver_settings) + """ + + return parser_wrapper.ParseLp(lp_file_path) + + def toDict(model, json=False): if not isinstance(model, parser_wrapper.DataModel): raise ValueError( diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx index ffd6ac43f3..9026750d47 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx @@ -16,7 +16,7 @@ from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move -from .parser cimport call_parse_mps +from .parser cimport call_parse_lp, call_parse_mps, mps_data_model_t import warnings @@ -31,98 +31,90 @@ def type_cast(np_obj, np_type, name): return np_obj -@catch_mps_parser_exception -def ParseMps(mps_file_path, fixed_mps_formats): - data_model = DataModel() - - dm_ret_ptr = move( - call_parse_mps( - mps_file_path.encode('utf-8'), - fixed_mps_formats - ) - ) - dm_ret = move(dm_ret_ptr.get()[0]) - - A_values_data = dm_ret.A_.data() - A_values_size = dm_ret.A_.size() +# Copies the C++ data model behind `dm` into the Python-side `data_model`. +# Extracted from ParseMps so ParseLp shares the same marshaling path — +# every field on mps_data_model_t is format-agnostic. +cdef _marshal_data_model(mps_data_model_t[int, double]* dm, data_model): + A_values_data = dm.A_.data() + A_values_size = dm.A_.size() cdef double[:] A_values_ = A_values_data A_values = np.asarray(A_values_).copy() - A_indices_data = dm_ret.A_indices_.data() - A_indices_size = dm_ret.A_indices_.size() + A_indices_data = dm.A_indices_.data() + A_indices_size = dm.A_indices_.size() cdef int[:] A_indices_ = A_indices_data A_indices = np.asarray(A_indices_).copy() - A_offsets_data = dm_ret.A_offsets_.data() - A_offsets_size = dm_ret.A_offsets_.size() + A_offsets_data = dm.A_offsets_.data() + A_offsets_size = dm.A_offsets_.size() cdef int[:] A_offsets_ = A_offsets_data A_offsets = np.asarray(A_offsets_).copy() - b_data = dm_ret.b_.data() - b_size = dm_ret.b_.size() + b_data = dm.b_.data() + b_size = dm.b_.size() cdef double[:] b_ = b_data b = np.asarray(b_).copy() - c_data = dm_ret.c_.data() - c_size = dm_ret.c_.size() + c_data = dm.c_.data() + c_size = dm.c_.size() cdef double[:] c_ = c_data c = np.asarray(c_).copy() - Q_values_size = dm_ret.Q_objective_values_.size() + Q_values_size = dm.Q_objective_values_.size() if Q_values_size > 0: - Q_values_data = dm_ret.Q_objective_values_.data() + Q_values_data = dm.Q_objective_values_.data() Q_values = np.asarray(Q_values_data).copy() else: Q_values = np.array([], dtype=np.float64) - Q_indices_size = dm_ret.Q_objective_indices_.size() + Q_indices_size = dm.Q_objective_indices_.size() if Q_indices_size > 0: - Q_indices_data = dm_ret.Q_objective_indices_.data() + Q_indices_data = dm.Q_objective_indices_.data() Q_indices = np.asarray(Q_indices_data).copy() else: Q_indices = np.array([], dtype=np.int32) - Q_offsets_size = dm_ret.Q_objective_offsets_.size() + Q_offsets_size = dm.Q_objective_offsets_.size() if Q_offsets_size > 0: - Q_offsets_data = dm_ret.Q_objective_offsets_.data() + Q_offsets_data = dm.Q_objective_offsets_.data() Q_offsets = np.asarray(Q_offsets_data).copy() else: Q_offsets = np.array([], dtype=np.int32) - variable_lower_bounds_data = dm_ret.variable_lower_bounds_.data() - variable_lower_bounds_size = dm_ret.variable_lower_bounds_.size() + variable_lower_bounds_data = dm.variable_lower_bounds_.data() + variable_lower_bounds_size = dm.variable_lower_bounds_.size() cdef double[:] variable_lower_bounds_ = variable_lower_bounds_data # noqa variable_lower_bounds = np.asarray(variable_lower_bounds_).copy() - variable_upper_bounds_data = dm_ret.variable_upper_bounds_.data() - variable_upper_bounds_size = dm_ret.variable_upper_bounds_.size() + variable_upper_bounds_data = dm.variable_upper_bounds_.data() + variable_upper_bounds_size = dm.variable_upper_bounds_.size() cdef double[:] variable_upper_bounds_ = variable_upper_bounds_data # noqa variable_upper_bounds = np.asarray(variable_upper_bounds_).copy() - constraint_lower_bounds_data = dm_ret.constraint_lower_bounds_.data() - constraint_lower_bounds_size = dm_ret.constraint_lower_bounds_.size() + constraint_lower_bounds_data = dm.constraint_lower_bounds_.data() + constraint_lower_bounds_size = dm.constraint_lower_bounds_.size() cdef double[:] constraint_lower_bounds_ = constraint_lower_bounds_data # noqa constraint_lower_bounds = np.asarray(constraint_lower_bounds_).copy() - constraint_upper_bounds_data = dm_ret.constraint_upper_bounds_.data() - constraint_upper_bounds_size = dm_ret.constraint_upper_bounds_.size() + constraint_upper_bounds_data = dm.constraint_upper_bounds_.data() + constraint_upper_bounds_size = dm.constraint_upper_bounds_.size() cdef double[:] constraint_upper_bounds_ = constraint_upper_bounds_data # noqa constraint_upper_bounds = np.asarray(constraint_upper_bounds_).copy() - var_types_data = dm_ret.var_types_.data() - var_types_size = dm_ret.var_types_.size() + var_types_data = dm.var_types_.data() + var_types_size = dm.var_types_.size() cdef char[:] var_types_ = var_types_data # noqa var_types = np.asarray(var_types_, dtype='str').copy() - row_types_data = dm_ret.row_types_.data() - row_types_size = dm_ret.row_types_.size() + row_types_data = dm.row_types_.data() + row_types_size = dm.row_types_.size() cdef char[:] row_types_ if row_types_size > 0: row_types_ = row_types_data # noqa row_types = np.asarray(row_types_, dtype='str').copy() else: row_types = None - var_names_ = np.asarray([i.decode() for i in dm_ret.var_names_]) - row_names_ = np.asarray([i.decode() for i in dm_ret.row_names_]) + var_names_ = np.asarray([i.decode() for i in dm.var_names_]) + row_names_ = np.asarray([i.decode() for i in dm.row_names_]) data_model.set_csr_constraint_matrix(A_values, A_indices, A_offsets) data_model.set_constraint_bounds(b) @@ -131,16 +123,37 @@ def ParseMps(mps_file_path, fixed_mps_formats): data_model.set_variable_upper_bounds(variable_upper_bounds) data_model.set_constraint_lower_bounds(constraint_lower_bounds) data_model.set_constraint_upper_bounds(constraint_upper_bounds) - data_model.set_maximize(dm_ret.maximize_) - data_model.set_objective_scaling_factor(dm_ret.objective_scaling_factor_) - data_model.set_objective_offset(dm_ret.objective_offset_) + data_model.set_maximize(dm.maximize_) + data_model.set_objective_scaling_factor(dm.objective_scaling_factor_) + data_model.set_objective_offset(dm.objective_offset_) data_model.set_quadratic_objective_matrix(Q_values, Q_indices, Q_offsets) data_model.set_variable_types(var_types) if row_types is not None: data_model.set_row_types(row_types) data_model.set_variable_names(var_names_) data_model.set_row_names(row_names_) - data_model.set_objective_name(dm_ret.objective_name_.decode()) - data_model.set_problem_name(dm_ret.problem_name_.decode()) + data_model.set_objective_name(dm.objective_name_.decode()) + data_model.set_problem_name(dm.problem_name_.decode()) return data_model + + +@catch_mps_parser_exception +def ParseMps(mps_file_path, fixed_mps_formats): + data_model = DataModel() + dm_ret_ptr = move( + call_parse_mps( + mps_file_path.encode('utf-8'), + fixed_mps_formats + ) + ) + return _marshal_data_model(dm_ret_ptr.get(), data_model) + + +@catch_mps_parser_exception +def ParseLp(lp_file_path): + data_model = DataModel() + dm_ret_ptr = move( + call_parse_lp(lp_file_path.encode('utf-8')) + ) + return _marshal_data_model(dm_ret_ptr.get(), data_model) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_parser.py b/python/cuopt/cuopt/tests/linear_programming/test_parser.py index 53757a3abf..f1b569a092 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_parser.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_parser.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import os +import tempfile from cuopt.linear_programming import mps_parser import numpy as np @@ -67,3 +68,119 @@ def test_good_mps_file(): assert 5.4 == data_model.get_constraint_upper_bounds()[0] assert 4.9 == data_model.get_constraint_upper_bounds()[1] + + +# Minimal LP content that should parse identically regardless of whether it's +# routed through ParseLp() or the server's extension-based dispatch path. +_MINIMAL_LP = """ +Minimize + x +Subject To + c1: x >= 2.5 +Bounds + x <= 10 +End +""" + + +def test_parse_lp_basic(): + with tempfile.NamedTemporaryFile( + suffix=".lp", mode="w", delete=False + ) as f: + f.write(_MINIMAL_LP) + path = f.name + try: + data_model = mps_parser.ParseLp(path) + finally: + os.unlink(path) + + # Minimize ⇒ sense is False. + assert not data_model.get_sense() + # Single variable with default lb=0, explicit ub=10. + assert data_model.get_variable_names().tolist() == ["x"] + assert data_model.get_variable_lower_bounds()[0] == 0.0 + assert data_model.get_variable_upper_bounds()[0] == 10.0 + # Objective is just "x" ⇒ c = [1.0]. + assert data_model.get_objective_coefficients()[0] == 1.0 + # Single >= constraint c1: x >= 2.5. + assert data_model.get_row_names().tolist() == ["c1"] + assert data_model.get_constraint_lower_bounds()[0] == 2.5 + assert np.isinf(data_model.get_constraint_upper_bounds()[0]) + assert data_model.get_constraint_matrix_values().tolist() == [1.0] + + +def test_parse_lp_rejects_unsupported_section(): + # SOS is explicitly out of scope; the parser should raise. + bad_lp = """ +Minimize + x +Subject To + c1: x >= 1 +SOS + s1: S1 :: x : 1 +End +""" + with tempfile.NamedTemporaryFile( + suffix=".lp", mode="w", delete=False + ) as f: + f.write(bad_lp) + path = f.name + try: + with pytest.raises(InputValidationError): + mps_parser.ParseLp(path) + finally: + os.unlink(path) + + +def test_parse_lp_and_parse_mps_agree_on_trivial_problem(): + # Same problem written in LP and MPS — both parsers should produce the + # same data model (modulo variable/constraint ordering, but this problem + # has exactly one of each). + mps_text = ( + "NAME trivial\n" + "ROWS\n" + " N OBJ\n" + " G c1\n" + "COLUMNS\n" + " x OBJ 1\n" + " x c1 1\n" + "RHS\n" + " RHS1 c1 2.5\n" + "BOUNDS\n" + " UP BND1 x 10\n" + "ENDATA\n" + ) + with tempfile.NamedTemporaryFile( + suffix=".mps", mode="w", delete=False + ) as f: + f.write(mps_text) + mps_path = f.name + with tempfile.NamedTemporaryFile( + suffix=".lp", mode="w", delete=False + ) as f: + f.write(_MINIMAL_LP) + lp_path = f.name + try: + lp_model = mps_parser.ParseLp(lp_path) + mps_model = mps_parser.ParseMps(mps_path) + finally: + os.unlink(mps_path) + os.unlink(lp_path) + + assert lp_model.get_sense() == mps_model.get_sense() + assert ( + lp_model.get_variable_names().tolist() + == mps_model.get_variable_names().tolist() + ) + assert ( + lp_model.get_objective_coefficients().tolist() + == mps_model.get_objective_coefficients().tolist() + ) + assert ( + lp_model.get_variable_upper_bounds().tolist() + == mps_model.get_variable_upper_bounds().tolist() + ) + assert ( + lp_model.get_constraint_lower_bounds().tolist() + == mps_model.get_constraint_lower_bounds().tolist() + ) diff --git a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py index d5c9f711e9..ae59b84f5a 100644 --- a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py +++ b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py @@ -134,25 +134,33 @@ def is_uuid(cuopt_problem_data): return False -def _mps_parse(LP_problem_data, solver_config): +def _parse_file_to_data_model(problem_input, solver_config): try: from cuopt.linear_programming import mps_parser except ImportError as e: raise ImportError( - "MPS parsing on the client requires the cuopt package. " + "MPS/LP parsing on the client requires the cuopt package. " "Install it with `pip install cuopt-sh-client[mps]` (or " "`pip install cuopt-cu13` / `cuopt-cu12` matching your CUDA), " - "or pass an already-parsed dict instead of an MPS file or " + "or pass an already-parsed dict instead of an MPS/LP file or " "DataModel." ) from e - if isinstance(LP_problem_data, mps_parser.parser_wrapper.DataModel): - model = LP_problem_data - log.debug("Received Mps parser DataModel object") + # problem_input is either a path (str) to an MPS/LP file, or an + # mps_parser DataModel already handed to us. + if isinstance(problem_input, mps_parser.parser_wrapper.DataModel): + model = problem_input + log.debug("Received mps_parser DataModel object") else: t0 = time.time() - model = mps_parser.ParseMps(LP_problem_data) + # Dispatch on file extension: ".lp" ⇒ LP parser, otherwise MPS. + if isinstance(problem_input, str) and problem_input.lower().endswith( + ".lp" + ): + model = mps_parser.ParseLp(problem_input) + else: + model = mps_parser.ParseMps(problem_input) parse_time = time.time() - t0 - log.debug(f"mps_parsing time was {parse_time}") + log.debug(f"file parsing time was {parse_time}") problem_data = mps_parser.toDict(model, json=use_zlib) if type(solver_config) is dict: @@ -732,14 +740,14 @@ def get_LP_solve( cuopt_data_models : Note - Batch mode is only supported in LP and not in MILP - File path to mps or json/dict/DataModel returned by - cuopt.linear_programming.mps_parser/list[mps file paths]/list[dict]/list[DataModel]. + File path to mps/lp or json/dict/DataModel returned by + cuopt.linear_programming.mps_parser/list[mps or lp file paths]/list[dict]/list[DataModel]. - For single problem, input should be either a path to mps/json file, + For single problem, input should be either a path to mps/lp/json file, /DataModel returned by cuopt.linear_programming.mps_parser/ path to json file/ dictionary. - For batch problem, input should be either a list of paths to mps + For batch problem, input should be either a list of paths to mps or lp files/ a list of DataModel returned by cuopt.linear_programming.mps_parser/ a list of dictionaries. @@ -798,22 +806,29 @@ def get_LP_solve( def read_cuopt_problem_data(cuopt_data_model, filepath): if isinstance(cuopt_data_model, dict): - mps = False + needs_parsing = False filepath = False else: - mps = ( - isinstance(cuopt_data_model, str) - and cuopt_data_model.endswith(".mps") - ) or not isinstance(cuopt_data_model, str) + # Needs parsing if it's either (a) a string path ending in + # .mps/.lp, or (b) a non-string (DataModel) to normalize. + if isinstance(cuopt_data_model, str): + lowered = cuopt_data_model.lower() + needs_parsing = lowered.endswith( + ".mps" + ) or lowered.endswith(".lp") + else: + needs_parsing = True - if mps: + if needs_parsing: if filepath: raise ValueError( - "Cannot use local file mode with MPS data. " + "Cannot use local file mode with MPS/LP data. " "Resubmit with filepath=False." ) - cuopt_data_model = _mps_parse(cuopt_data_model, solver_config) + cuopt_data_model = _parse_file_to_data_model( + cuopt_data_model, solver_config + ) elif filepath and cuopt_data_model.startswith("/"): log.warning( From 7e944b19658fd22209d54ef6f01476d76355808d Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 17:07:13 +0000 Subject: [PATCH 02/30] ci: update expected error substring for MPS/LP local-file rejection The error message in cuopt_self_host_client.py was updated to say "MPS/LP data" alongside the LP reader changes, but the matching assertion in test_self_hosted_service.sh was not, causing CLI test 7 to fail. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- ci/test_self_hosted_service.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/test_self_hosted_service.sh b/ci/test_self_hosted_service.sh index 6ecd1bcbd3..05db33b1ee 100755 --- a/ci/test_self_hosted_service.sh +++ b/ci/test_self_hosted_service.sh @@ -120,7 +120,7 @@ if [ "$doservertest" -eq 1 ]; then run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.mps ../../datasets/linear_programming/good-mps-1.mps # Error, local file mode is not allowed with mps - run_cli_test "Cannot use local file mode with MPS data" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP -f good-mps-1.mps + run_cli_test "Cannot use local file mode with MPS/LP data" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP -f good-mps-1.mps # Just run validator cp ../../datasets/cuopt_service_data/cuopt_problem_data.json "$CUOPT_DATA_DIR" From 16c1c3da31de77eb139d46cc6365afaa41828362 Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 18:23:58 +0000 Subject: [PATCH 03/30] Reject ambiguous ' [' and stray ' *' in LP parser parse_linear_expression silently accepted two malformed inputs: - '2 [ x^2 ] / 2' parsed as objective_offset=2 plus quadratic x^2, rather than rejecting the unsupported scalar-multiplication-of-bracket form. Same shape occurs in quadratic constraint rows. - ' *' followed by a relation, section header, or EOL silently dropped the '*' and turned the number into a bare constant. Both now raise a ValidationError pointing at the offending line. Added tests for both malformed forms plus the legitimate forms ('5 + [...]/2' and '3 * x') to pin the boundary. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/src/io/lp_parser.cpp | 28 +++++++- cpp/tests/linear_programming/parser_test.cpp | 72 ++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp index 7341c7fa9e..91a1eec2d3 100644 --- a/cpp/src/io/lp_parser.cpp +++ b/cpp/src/io/lp_parser.cpp @@ -764,11 +764,37 @@ void LpParseEngine::parse_linear_expression(std::vector& o // 'inf' is a bounds-only keyword and never appears here. f_t coeff = f_t(1); bool had_coeff = false; + bool had_star = false; if (peek().kind == LpTokenKind::Number) { coeff = number_from_text(peek().text); had_coeff = true; advance(); - match(LpTokenKind::Star); // optional '*' + had_star = match(LpTokenKind::Star); + } + + // ' *' must be followed by a variable name; a stray '*' before a + // relation, section header, or EOL would otherwise be silently dropped + // (and the number would be misinterpreted as a constant). + if (had_star) { + mps_parser_expects( + peek().kind == LpTokenKind::Name && !at_section_boundary() && !is_infinity_keyword(peek()), + error_type_t::ValidationError, + "LP parse error at line %d: expected variable name after '*', got '%s'", + peek().line, + peek().text.c_str()); + } + + // ' [' is ambiguous: did the user mean " times the quadratic + // bracket" or "constant followed by a separate bracket"? Neither + // interpretation is supported. The LP convention places the coefficient + // inside the brackets, so reject and tell the user how to rewrite. + if (had_coeff && peek().kind == LpTokenKind::LBracket) { + mps_parser_expects(false, + error_type_t::ValidationError, + "LP parse error at line %d: a numeric coefficient may not " + "directly precede a quadratic bracket '['; place the coefficient " + "inside the brackets", + peek().line); } if (peek().kind == LpTokenKind::Name && !at_section_boundary() && diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index 0b65c83c38..9e3443c2ce 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -1998,6 +1998,78 @@ End std::logic_error); } +TEST(lp_parser, leading_coefficient_before_objective_bracket_rejected) +{ + // '2 [ x^2 ] / 2' is ambiguous between "constant 2 plus 0.5 x^2" and + // "scalar 2 times 0.5 x^2"; the LP convention is to place coefficients + // inside the brackets, so reject. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + 2 [ x ^ 2 ] / 2 +Subject To + c1: x >= 1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, leading_coefficient_before_constraint_bracket_rejected) +{ + // Same ambiguity as the objective case, in a quadratic constraint. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + q1: 2 [ x ^ 2 ] <= 5 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, constant_then_signed_bracket_in_objective_is_accepted) +{ + // The positive form: a literal constant in the objective followed by a + // signed quadratic bracket still parses (constant becomes objective offset). + auto m = parse_lp_string(R"LP( +Minimize + 5 + [ x ^ 2 ] / 2 +Subject To + c1: x >= 1 +End +)LP"); + EXPECT_NEAR(m.get_objective_offset(), 5.0, tolerance); + EXPECT_TRUE(m.has_quadratic_objective()); +} + +TEST(lp_parser, stray_star_after_number_without_variable_rejected) +{ + // '3 *' followed by a relation, section header, or EOL must error rather + // than silently drop the '*' and treat the '3' as a bare constant. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + 3 * +Subject To + c1: x >= 1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, explicit_star_between_coefficient_and_variable_is_accepted) +{ + // The positive form: '3 * x' is the same as '3 x'. + auto m = parse_lp_string(R"LP( +Minimize + 3 * x +Subject To + c1: x >= 1 +End +)LP"); + int x = find_var(m, "x"); + ASSERT_GE(x, 0); + EXPECT_NEAR(m.get_objective_coefficients()[x], 3.0, tolerance); +} + // =========================================================================== // Quadratic constraints (LHS contains [ ... ] without the /2 divisor). // =========================================================================== From 7e3899c0679715b5621cebb9e48b0c92b02ded6f Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 18:38:34 +0000 Subject: [PATCH 04/30] Inline finalize_problem into lp_parser.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parser_finalize.hpp was designed as a shared finalization template but only the LP parser ever called it; MPS keeps its own fill_problem. Move the implementation into lp_parser.cpp's anonymous namespace alongside flush_quadratic_constraints, since both serve the same caller and run consecutively. While inlining, drop the dormant requires-expression branches for objective_scaling_factor_value, ranges_values, and qmatrix_entries — none of these fields exist on lp_parser_t. The Parser template parameter becomes a concrete lp_parser_t&. No behavior change for any caller. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/src/io/lp_parser.cpp | 177 ++++++++++++++++++++++- cpp/src/io/lp_parser.hpp | 7 +- cpp/src/io/parser_finalize.hpp | 255 --------------------------------- 3 files changed, 178 insertions(+), 261 deletions(-) delete mode 100644 cpp/src/io/parser_finalize.hpp diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp index 91a1eec2d3..3dd4e3b45d 100644 --- a/cpp/src/io/lp_parser.cpp +++ b/cpp/src/io/lp_parser.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -1341,6 +1340,178 @@ void LpParseEngine::parse_all() namespace { +// Consumes the LP parser's intermediate parsed data and populates `problem`. +// +// CSR flatten, row-type → constraint-bound conversion, quadratic objective +// matrix construction, metadata setters. MPS uses its own fill_problem +// (mps_parser.cpp) because its quadratic rows are interleaved with linear +// rows in the per-row vectors and need a compaction pass; the LP parser +// partitions quadratic rows into quadratic_constraint_blocks at parse time, +// so the per-row vectors here contain only linear rows and no compaction is +// needed. Quadratic LP constraints are emitted by flush_quadratic_constraints +// below. +template +void finalize_problem(mps_data_model_t& problem, lp_parser_t& parser) +{ + const i_t n_vars = static_cast(parser.var_names.size()); + const i_t n_rows = static_cast(parser.row_names.size()); + + // Pad per-variable vectors that may have grown after their initial size + // (e.g., a variable first appeared after c_values was already initialized). + if (static_cast(parser.c_values.size()) < n_vars) parser.c_values.resize(n_vars, f_t(0)); + if (static_cast(parser.variable_lower_bounds.size()) < n_vars) { + parser.variable_lower_bounds.resize(n_vars, f_t(0)); + } + if (static_cast(parser.variable_upper_bounds.size()) < n_vars) { + parser.variable_upper_bounds.resize(n_vars, std::numeric_limits::infinity()); + } + if (static_cast(parser.var_types.size()) < n_vars) parser.var_types.resize(n_vars, 'C'); + + // Flatten the ragged A_indices / A_values into a single CSR. + std::vector offsets; + std::vector indices; + std::vector values; + offsets.reserve(n_rows + 1); + offsets.push_back(0); + for (i_t i = 0; i < n_rows; ++i) { + for (i_t idx : parser.A_indices[i]) + indices.push_back(idx); + for (f_t v : parser.A_values[i]) + values.push_back(v); + offsets.push_back(static_cast(values.size())); + } + problem.set_csr_constraint_matrix(values, indices, offsets); + + mps_parser_expects(indices.size() == values.size(), + error_type_t::ValidationError, + "Constraint matrix nonzero vector (%zu) and column-index vector (%zu) " + "must have the same size.", + indices.size(), + values.size()); + mps_parser_expects(!offsets.empty() && offsets.back() == static_cast(values.size()), + error_type_t::ValidationError, + "CSR offset tail (%d) must equal the nonzero count (%zu).", + offsets.empty() ? 0 : offsets.back(), + values.size()); + + problem.set_constraint_bounds(parser.b_values); + problem.set_objective_coefficients(parser.c_values); + problem.set_objective_scaling_factor(f_t(1)); + problem.set_objective_offset(parser.objective_offset_value); + + problem.set_variable_lower_bounds(parser.variable_lower_bounds); + problem.set_variable_upper_bounds(parser.variable_upper_bounds); + + mps_parser_expects( + (problem.get_variable_lower_bounds().size() == problem.get_variable_upper_bounds().size()) && + (problem.get_variable_upper_bounds().size() == problem.get_objective_coefficients().size()), + error_type_t::ValidationError, + "Per-variable vectors are inconsistently sized. objective=%zu, lb=%zu, ub=%zu.", + problem.get_objective_coefficients().size(), + problem.get_variable_lower_bounds().size(), + problem.get_variable_upper_bounds().size()); + + // Semi-continuous variables must have a finite upper bound; otherwise the + // "x = 0 or lb <= x <= ub" semantics collapse to a regular continuous + // variable. Matches the MPS parser's rule. + for (i_t i = 0; i < n_vars; ++i) { + if (parser.var_types[i] == 'S') { + mps_parser_expects(!std::isinf(parser.variable_upper_bounds[i]), + error_type_t::ValidationError, + "Semi-continuous variable '%s' must have a finite upper bound", + parser.var_names[i].c_str()); + } + } + + // Row types + RHS → explicit constraint lower/upper bounds. + const f_t inf = std::numeric_limits::infinity(); + std::vector clb; + std::vector cub; + clb.reserve(n_rows); + cub.reserve(n_rows); + for (i_t i = 0; i < n_rows; ++i) { + switch (parser.row_types[i]) { + case Equality: + clb.push_back(parser.b_values[i]); + cub.push_back(parser.b_values[i]); + break; + case GreaterThanOrEqual: + clb.push_back(parser.b_values[i]); + cub.push_back(inf); + break; + case LesserThanOrEqual: + clb.push_back(-inf); + cub.push_back(parser.b_values[i]); + break; + default: + mps_parser_expects(false, + error_type_t::ValidationError, + "Unsupported row type for row '%s'", + parser.row_names[i].c_str()); + } + mps_parser_expects(!std::isnan(clb.back()) && !std::isnan(cub.back()), + error_type_t::ValidationError, + "Constraint bound for row '%s' is NaN", + parser.row_names[i].c_str()); + } + problem.set_constraint_lower_bounds(clb); + problem.set_constraint_upper_bounds(cub); + + mps_parser_expects( + (problem.get_constraint_lower_bounds().size() == + problem.get_constraint_upper_bounds().size()) && + (problem.get_constraint_upper_bounds().size() == problem.get_constraint_bounds().size()), + error_type_t::ValidationError, + "Per-constraint vectors are inconsistently sized. rhs=%zu, lb=%zu, ub=%zu.", + problem.get_constraint_bounds().size(), + problem.get_constraint_lower_bounds().size(), + problem.get_constraint_upper_bounds().size()); + + problem.set_problem_name(parser.problem_name); + problem.set_objective_name(parser.objective_name); + problem.set_variable_names(parser.var_names); + problem.set_variable_types(parser.var_types); + problem.set_row_names(parser.row_names); + std::vector row_types_chars(parser.row_types.size()); + for (size_t i = 0; i < parser.row_types.size(); ++i) { + row_types_chars[i] = static_cast(parser.row_types[i]); + } + problem.set_row_types(row_types_chars); + problem.set_maximize(parser.maximize); + + // Quadratic objective: build the full symmetric Q from upper-triangular + // QUADOBJ-convention entries; mirror off-diagonals and apply the file's + // '0.5 x^T Q x' → cuOpt's 'x^T Q x' conversion (×0.5 on every stored value). + if (!parser.quadobj_entries.empty()) { + std::vector>> csc(n_vars); + for (const auto& [row, col, val] : parser.quadobj_entries) { + csc[col].emplace_back(row, val); + if (row != col) { csc[row].emplace_back(col, val); } + } + std::vector>> csr(n_vars); + for (i_t col = 0; col < n_vars; ++col) { + for (const auto& [row, val] : csc[col]) { + csr[row].emplace_back(col, val); + } + } + // Within each row the entries are naturally ordered by column because + // the outer loop above walks columns in ascending order — no sort needed. + std::vector q_values; + std::vector q_indices; + std::vector q_offsets; + q_offsets.reserve(n_vars + 1); + q_offsets.push_back(0); + for (i_t row = 0; row < n_vars; ++row) { + for (const auto& [col, val] : csr[row]) { + q_values.push_back(val * f_t(0.5)); + q_indices.push_back(col); + } + q_offsets.push_back(static_cast(q_values.size())); + } + problem.set_quadratic_objective_matrix(q_values, q_indices, q_offsets); + } +} + // Emits one quadratic_constraint_block_t to `problem` via // append_quadratic_constraint(). Row indices are assigned // linear_row_count..linear_row_count + nqc - 1, mirroring MPS's QCMATRIX @@ -1376,7 +1547,7 @@ template lp_parser_t::lp_parser_t(mps_data_model_t& problem, const std::string& file) { LpParseEngine engine(*this, file); - detail::finalize_problem(problem, *this); + finalize_problem(problem, *this); flush_quadratic_constraints(problem, *this); } @@ -1384,7 +1555,7 @@ template lp_parser_t::lp_parser_t(mps_data_model_t& problem, std::string_view input) { LpParseEngine engine(*this, input); - detail::finalize_problem(problem, *this); + finalize_problem(problem, *this); flush_quadratic_constraints(problem, *this); } diff --git a/cpp/src/io/lp_parser.hpp b/cpp/src/io/lp_parser.hpp index b068f7535a..8314d6c97a 100644 --- a/cpp/src/io/lp_parser.hpp +++ b/cpp/src/io/lp_parser.hpp @@ -24,9 +24,10 @@ namespace cuopt::linear_programming::io { * machinery (tokenizer, expression/section parsers, token types) lives in * src/lp_parser.cpp and is never exposed. * - * The public fields mirror mps_parser_t so the two parsers share a single - * finalization path (see src/parser_finalize.hpp) and so tests and tools - * can introspect the same shape of intermediate data from either parser. + * The public fields mirror mps_parser_t so tests and tools can introspect + * the same shape of intermediate data from either parser. Finalization + * (CSR flatten, constraint-bound derivation, quadratic objective assembly) + * is performed by finalize_problem() inside src/io/lp_parser.cpp. */ template class lp_parser_t { diff --git a/cpp/src/io/parser_finalize.hpp b/cpp/src/io/parser_finalize.hpp deleted file mode 100644 index 3c9a2ffbe1..0000000000 --- a/cpp/src/io/parser_finalize.hpp +++ /dev/null @@ -1,255 +0,0 @@ -/* clang-format off */ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -/* clang-format on */ - -#pragma once - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace cuopt::linear_programming::io::detail { - -// Consumes the LP parser's intermediate parsed data and populates `problem`. -// -// CSR flatten, row-type → constraint-bound conversion, quadratic objective -// matrix construction, metadata setters. MPS uses its own fill_problem -// because it handles QCMATRIX quadratic constraints; the LP format does not -// support quadratic constraints, so the two finalization paths intentionally -// diverge. -// -// Required fields on `parser`: -// problem_name, objective_name, row_names, row_types, var_names, -// var_types, A_indices, A_values, b_values, c_values, -// variable_lower_bounds, variable_upper_bounds, objective_offset_value, -// maximize, quadobj_entries. -// -// The requires-expression branches for objective_scaling_factor_value, -// ranges_values, and qmatrix_entries are dormant for LP but kept so the -// template would remain reusable if a non-LP caller ever wants them. -template -void finalize_problem(mps_data_model_t& problem, Parser& parser) -{ - const i_t n_vars = static_cast(parser.var_names.size()); - const i_t n_rows = static_cast(parser.row_names.size()); - - // Pad per-variable vectors that may have grown after their initial size - // (e.g., a variable first appeared after c_values was already initialized). - if (static_cast(parser.c_values.size()) < n_vars) parser.c_values.resize(n_vars, f_t(0)); - if (static_cast(parser.variable_lower_bounds.size()) < n_vars) { - parser.variable_lower_bounds.resize(n_vars, f_t(0)); - } - if (static_cast(parser.variable_upper_bounds.size()) < n_vars) { - parser.variable_upper_bounds.resize(n_vars, std::numeric_limits::infinity()); - } - if (static_cast(parser.var_types.size()) < n_vars) parser.var_types.resize(n_vars, 'C'); - - // Flatten the ragged A_indices / A_values into a single CSR. - std::vector offsets; - std::vector indices; - std::vector values; - offsets.reserve(n_rows + 1); - offsets.push_back(0); - for (i_t i = 0; i < n_rows; ++i) { - for (i_t idx : parser.A_indices[i]) - indices.push_back(idx); - for (f_t v : parser.A_values[i]) - values.push_back(v); - offsets.push_back(static_cast(values.size())); - } - problem.set_csr_constraint_matrix(values, indices, offsets); - - mps_parser_expects(indices.size() == values.size(), - error_type_t::ValidationError, - "Constraint matrix nonzero vector (%zu) and column-index vector (%zu) " - "must have the same size.", - indices.size(), - values.size()); - mps_parser_expects(!offsets.empty() && offsets.back() == static_cast(values.size()), - error_type_t::ValidationError, - "CSR offset tail (%d) must equal the nonzero count (%zu).", - offsets.empty() ? 0 : offsets.back(), - values.size()); - - problem.set_constraint_bounds(parser.b_values); - problem.set_objective_coefficients(parser.c_values); - - f_t scaling = f_t(1); - if constexpr (requires { parser.objective_scaling_factor_value; }) { - scaling = parser.objective_scaling_factor_value; - } - problem.set_objective_scaling_factor(scaling); - problem.set_objective_offset(parser.objective_offset_value); - - problem.set_variable_lower_bounds(parser.variable_lower_bounds); - problem.set_variable_upper_bounds(parser.variable_upper_bounds); - - mps_parser_expects( - (problem.get_variable_lower_bounds().size() == problem.get_variable_upper_bounds().size()) && - (problem.get_variable_upper_bounds().size() == problem.get_objective_coefficients().size()), - error_type_t::ValidationError, - "Per-variable vectors are inconsistently sized. objective=%zu, lb=%zu, ub=%zu.", - problem.get_objective_coefficients().size(), - problem.get_variable_lower_bounds().size(), - problem.get_variable_upper_bounds().size()); - - // Semi-continuous variables must have a finite upper bound; otherwise the - // "x = 0 or lb <= x <= ub" semantics collapse to a regular continuous - // variable. Matches the MPS parser's rule. - for (i_t i = 0; i < n_vars; ++i) { - if (parser.var_types[i] == 'S') { - mps_parser_expects(!std::isinf(parser.variable_upper_bounds[i]), - error_type_t::ValidationError, - "Semi-continuous variable '%s' must have a finite upper bound", - parser.var_names[i].c_str()); - } - } - - // Row types + RHS (+ MPS ranges) → explicit constraint lower/upper bounds. - const f_t inf = std::numeric_limits::infinity(); - std::vector clb; - std::vector cub; - clb.reserve(n_rows); - cub.reserve(n_rows); - constexpr bool has_ranges = requires { parser.ranges_values; }; - for (i_t i = 0; i < n_rows; ++i) { - switch (parser.row_types[i]) { - case Equality: - clb.push_back(parser.b_values[i]); - cub.push_back(parser.b_values[i]); - if constexpr (has_ranges) { - if (!parser.ranges_values.empty() && parser.ranges_values[i] != inf) { - mps_parser_expects(!std::isnan(parser.ranges_values[i]), - error_type_t::ValidationError, - "Equality range value %d is NaN", - i); - if (parser.ranges_values[i] < f_t(0)) { - clb.back() += parser.ranges_values[i]; - } else { - cub.back() += parser.ranges_values[i]; - } - } - } - break; - case GreaterThanOrEqual: - clb.push_back(parser.b_values[i]); - cub.push_back(inf); - if constexpr (has_ranges) { - if (!parser.ranges_values.empty() && parser.ranges_values[i] != inf) { - mps_parser_expects(!std::isnan(parser.ranges_values[i]), - error_type_t::ValidationError, - "Greater range value %d is NaN", - i); - cub.back() = clb.back() + std::abs(parser.ranges_values[i]); - } - } - break; - case LesserThanOrEqual: - clb.push_back(-inf); - cub.push_back(parser.b_values[i]); - if constexpr (has_ranges) { - if (!parser.ranges_values.empty() && parser.ranges_values[i] != inf) { - mps_parser_expects(!std::isnan(parser.ranges_values[i]), - error_type_t::ValidationError, - "Lesser range value %d is NaN", - i); - clb.back() = cub.back() - std::abs(parser.ranges_values[i]); - } - } - break; - default: - mps_parser_expects(false, - error_type_t::ValidationError, - "Unsupported row type for row '%s'", - parser.row_names[i].c_str()); - } - mps_parser_expects(!std::isnan(clb.back()) && !std::isnan(cub.back()), - error_type_t::ValidationError, - "Constraint bound for row '%s' is NaN", - parser.row_names[i].c_str()); - } - problem.set_constraint_lower_bounds(clb); - problem.set_constraint_upper_bounds(cub); - - mps_parser_expects( - (problem.get_constraint_lower_bounds().size() == - problem.get_constraint_upper_bounds().size()) && - (problem.get_constraint_upper_bounds().size() == problem.get_constraint_bounds().size()), - error_type_t::ValidationError, - "Per-constraint vectors are inconsistently sized. rhs=%zu, lb=%zu, ub=%zu.", - problem.get_constraint_bounds().size(), - problem.get_constraint_lower_bounds().size(), - problem.get_constraint_upper_bounds().size()); - - problem.set_problem_name(parser.problem_name); - problem.set_objective_name(parser.objective_name); - // Setters take const refs — pass the fields directly to avoid an extra - // temporary copy. - problem.set_variable_names(parser.var_names); - problem.set_variable_types(parser.var_types); - problem.set_row_names(parser.row_names); - std::vector row_types_chars(parser.row_types.size()); - for (size_t i = 0; i < parser.row_types.size(); ++i) { - row_types_chars[i] = static_cast(parser.row_types[i]); - } - problem.set_row_types(row_types_chars); - problem.set_maximize(parser.maximize); - - // Quadratic objective: build a full symmetric Q via double-transpose. - // - QUADOBJ entries are upper-triangular; each off-diagonal entry is - // mirrored to its transpose when assembling. - // - QMATRIX entries are already the full symmetric matrix. - // Every stored value is multiplied by 0.5 to convert from the file's - // '0.5 x^T Q x' convention to cuOpt's 'x^T Q x'. See mps_parser.cpp for - // the original derivation. - auto build_q_csr = [&](const std::vector>& entries, - bool mirror_off_diagonal) { - std::vector>> csc(n_vars); - for (const auto& [row, col, val] : entries) { - csc[col].emplace_back(row, val); - if (mirror_off_diagonal && row != col) { csc[row].emplace_back(col, val); } - } - std::vector>> csr(n_vars); - for (i_t col = 0; col < n_vars; ++col) { - for (const auto& [row, val] : csc[col]) { - csr[row].emplace_back(col, val); - } - } - // Within each row the entries are naturally ordered by column because - // the outer loop above walks columns in ascending order — no sort needed. - std::vector q_values; - std::vector q_indices; - std::vector q_offsets; - q_offsets.reserve(n_vars + 1); - q_offsets.push_back(0); - for (i_t row = 0; row < n_vars; ++row) { - for (const auto& [col, val] : csr[row]) { - q_values.push_back(val * f_t(0.5)); - q_indices.push_back(col); - } - q_offsets.push_back(static_cast(q_values.size())); - } - problem.set_quadratic_objective_matrix(q_values, q_indices, q_offsets); - }; - - if (!parser.quadobj_entries.empty()) { - build_q_csr(parser.quadobj_entries, /*mirror_off_diagonal=*/true); - } else if constexpr (requires { parser.qmatrix_entries; }) { - if (!parser.qmatrix_entries.empty()) { - build_q_csr(parser.qmatrix_entries, /*mirror_off_diagonal=*/false); - } - } -} - -} // namespace cuopt::linear_programming::io::detail From f32d12e1730550019cbb37e0a6c164fef2e251be Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 18:47:13 +0000 Subject: [PATCH 05/30] Rewrite refactor-history comments to describe current state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two block-level comments were framed in terms of how this PR rearranged the code ("MPS-only tests preserved from mps_parser_test.cpp", "Extracted from ParseMps"). Replace with descriptions of what the code actually does today, independent of the refactor that produced it. The MPS-only test header also incorrectly claimed the LP parser doesn't support semi-continuous variables or quadratic constraints — it does; those tests are preserved because they exercise MPS-specific syntax (bound codes, QCMATRIX blocks), not because LP lacks the semantics. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/tests/linear_programming/parser_test.cpp | 5 ++--- .../cuopt/linear_programming/mps_parser/parser_wrapper.pyx | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index 9e3443c2ce..7a2c2d1203 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -2541,9 +2541,8 @@ TEST(parse_problem, unrecognized_extension_throws) } // =========================================================================== -// MPS-only tests preserved from cpp/tests/linear_programming/mps_parser_test.cpp -// (semi-continuous variables and quadratic-constraint coverage added in #1193; -// kept here because the LP parser does not support these constructs). +// MPS-syntax-specific tests: bound codes (UP/LO/MI/PL/BV/SC) and QCMATRIX +// blocks. LP-equivalent semantic coverage lives above. // =========================================================================== TEST(mps_bounds, standard_var_bounds_0_inf) diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx index 9026750d47..6c392ac4bd 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx @@ -32,8 +32,8 @@ def type_cast(np_obj, np_type, name): # Copies the C++ data model behind `dm` into the Python-side `data_model`. -# Extracted from ParseMps so ParseLp shares the same marshaling path — -# every field on mps_data_model_t is format-agnostic. +# Shared by ParseMps and ParseLp — every field on mps_data_model_t is +# format-agnostic. cdef _marshal_data_model(mps_data_model_t[int, double]* dm, data_model): A_values_data = dm.A_.data() A_values_size = dm.A_.size() From 2b2ff753a688c8060d076311e455f26b88f29b8d Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 18:58:06 +0000 Subject: [PATCH 06/30] Accept bare 'Semi' and 'Semis' as Semi-Continuous section keywords The 3-token 'Semi - Continuous' form was the only spelling recognized, but both Gurobi and CPLEX LP-format references explicitly list bare 'Semi' and 'Semis' as documented synonyms: Gurobi: "Valid keywords for variable type headers are: binary, binaries, bin, general, generals, gen, semi-continuous, semis, or semi." CPLEX: "The SEMI-CONTINUOUS section is preceded by the keyword SEMI-CONTINUOUS, SEMI, or SEMIS." Accepted spellings (case-insensitive) are now: 'Semi-Continuous', 'Semi', and 'Semis'. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/src/io/lp_parser.cpp | 22 ++++++---- cpp/tests/linear_programming/parser_test.cpp | 44 ++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp index 3dd4e3b45d..54b27c8ebd 100644 --- a/cpp/src/io/lp_parser.cpp +++ b/cpp/src/io/lp_parser.cpp @@ -602,12 +602,13 @@ bool LpParseEngine::at_section_boundary() const if (lower == "user" && name_equals_ci(t2, "cuts")) return true; if (lower == "general" && name_equals_ci(t2, "constraints")) return true; - // Semi-Continuous section header (supported); plus other section headers - // that we recognize as boundaries (some supported, some unsupported — - // dispatch decides). - if (lower == "semi" && peek(1).kind == LpTokenKind::Minus && - name_equals_ci(peek(2), "continuous")) - return true; + // Semi-Continuous section header (supported); three spellings: + // - 3-token "Semi - Continuous" + // - bare "Semi" + // - bare "Semis" + // Plus other section headers that we recognize as boundaries (some + // supported, some unsupported — dispatch decides). + if (lower == "semi" || lower == "semis") return true; if (lower == "sos") return true; if (lower == "pwlobj") return true; if (lower == "scenarios" || lower == "scenario") return true; @@ -686,7 +687,10 @@ typename LpParseEngine::SectionKind LpParseEngine::try_consu advance(); return SectionKind::Binaries; } - // "Semi-Continuous" (3 tokens: semi - continuous). + // Semi-Continuous section header — accepted spellings: + // - 3-token "Semi - Continuous" (CPLEX/Gurobi documented) + // - bare "Semi" / "Semis" (CPLEX/Gurobi documented) + // Check the 3-token form first so it consumes all three tokens. if (lower == "semi" && peek(1).kind == LpTokenKind::Minus && name_equals_ci(peek(2), "continuous")) { advance(); @@ -694,6 +698,10 @@ typename LpParseEngine::SectionKind LpParseEngine::try_consu advance(); return SectionKind::SemiContinuous; } + if (lower == "semi" || lower == "semis") { + advance(); + return SectionKind::SemiContinuous; + } if (is_end_keyword(lower)) { advance(); return SectionKind::End; diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index 7a2c2d1203..f25eadf628 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -1748,6 +1748,50 @@ End EXPECT_NEAR(m.get_variable_upper_bounds()[xi], 10.0, tolerance); } +TEST(lp_parser, semi_continuous_bare_semi_keyword) +{ + // Both Gurobi and CPLEX accept the bare "Semi" keyword as a synonym for + // the "Semi-Continuous" section header. + auto m = parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 0 +Bounds + 2 <= x <= 10 +Semi + x +End +)LP"); + int xi = find_var(m, "x"); + ASSERT_GE(xi, 0); + EXPECT_EQ(m.get_variable_types()[xi], 'S'); + EXPECT_NEAR(m.get_variable_lower_bounds()[xi], 2.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[xi], 10.0, tolerance); +} + +TEST(lp_parser, semi_continuous_bare_semis_keyword) +{ + // Both Gurobi and CPLEX accept the bare "Semis" keyword as a synonym for + // the "Semi-Continuous" section header. + auto m = parse_lp_string(R"LP( +Minimize + x +Subject To + c1: x >= 0 +Bounds + 2 <= x <= 10 +Semis + x +End +)LP"); + int xi = find_var(m, "x"); + ASSERT_GE(xi, 0); + EXPECT_EQ(m.get_variable_types()[xi], 'S'); + EXPECT_NEAR(m.get_variable_lower_bounds()[xi], 2.0, tolerance); + EXPECT_NEAR(m.get_variable_upper_bounds()[xi], 10.0, tolerance); +} + TEST(lp_parser, semi_continuous_default_lower_is_zero) { auto m = parse_lp_string(R"LP( From 8c6f25a731c339b879d9261698edbfc2d9cd1052 Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 23:02:18 +0000 Subject: [PATCH 07/30] Simplify LP parser's quadratic objective to upper-triangular pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cuOpt's set_quadratic_objective_matrix symmetrizes internally via H = Q + Q^T before solving (1/2) x^T H x, so the parser does not need to materialize the full symmetric Q. Switch the quadratic-objective path to: parse_quadratic_bracket: apply LP's '/ 2' uniformly to diagonal and off-diagonal entries (instead of only halving off-diagonals). finalize_problem: emit the upper-triangular quadobj_entries as CSR directly (drop the mirror pass and the post-scale by 0.5). End-to-end PDLP tests (cpp/tests/qp/unit_tests/lp_parser_solve_test.cu) parse three small QP files (diagonal, positive cross term, negative cross term) and verify objective values and primal solutions against hand-computed optima. Quadratic constraints are NOT changed: cuOpt's set_quadratic_constraints does not symmetrize, so per-constraint Q must remain fully symmetric. The MPS parser is also unchanged. LP and MPS now store the quadratic objective in different equivalent forms (upper-tri vs full-sym × 0.5) in mps_data_model_t; both produce the same H after cuOpt's symmetrize step. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/src/io/lp_parser.cpp | 46 +++---- cpp/tests/linear_programming/parser_test.cpp | 16 ++- cpp/tests/qp/CMakeLists.txt | 1 + .../qp/unit_tests/lp_parser_solve_test.cu | 121 ++++++++++++++++++ 4 files changed, 150 insertions(+), 34 deletions(-) create mode 100644 cpp/tests/qp/unit_tests/lp_parser_solve_test.cu diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp index 54b27c8ebd..cbafc6b45b 100644 --- a/cpp/src/io/lp_parser.cpp +++ b/cpp/src/io/lp_parser.cpp @@ -932,18 +932,14 @@ void LpParseEngine::parse_quadratic_bracket( advance(); // '/' advance(); // '2' - // Apply the /2 convention and the QUADOBJ-convention scaling so that - // finalize_problem()'s expansion to full symmetric and *0.5 factor yield - // the right Q for cuOpt's 'x^T Q x' form. - // - // LP term ([...]/2): → quadobj entry - // diagonal c x^2 (actual = c/2) → c - // off-diag c x*y (actual = c/2) → c/2 + // Apply the LP "/ 2" convention uniformly: a bracket coefficient c on + // either x_i^2 or x_i*x_j contributes c/2 to the corresponding objective + // term. The resulting upper-triangular quadobj entries are passed + // directly to cuOpt's set_quadratic_objective_matrix, which internally + // computes H = Q + Q^T; the solver then minimizes (1/2) x^T H x, which + // recovers the user's intended objective. for (auto& [a, b, v] : raw_quad) { - if (a != b) { - // off-diagonal: /2 to recover Q[i,j] = Q[j,i] after the later x^T Q x expansion. - v /= f_t(2); - } + v /= f_t(2); out_quad_entries.emplace_back(a, b, sign_scale * v); } // Linear terms inside the brackets pick up the /2 scaling and the outer sign. @@ -1487,31 +1483,27 @@ void finalize_problem(mps_data_model_t& problem, lp_parser_t problem.set_row_types(row_types_chars); problem.set_maximize(parser.maximize); - // Quadratic objective: build the full symmetric Q from upper-triangular - // QUADOBJ-convention entries; mirror off-diagonals and apply the file's - // '0.5 x^T Q x' → cuOpt's 'x^T Q x' conversion (×0.5 on every stored value). + // Quadratic objective: emit the upper-triangular quadobj entries as CSR. + // cuOpt's GPU-side set_quadratic_objective_matrix applies H = Q + Q^T + // internally, so no mirror step is needed here — the entries are already + // /2-scaled inside parse_quadratic_bracket so the solver's (1/2) x^T H x + // recovers the user's intended objective. if (!parser.quadobj_entries.empty()) { - std::vector>> csc(n_vars); + std::vector>> row_data(n_vars); for (const auto& [row, col, val] : parser.quadobj_entries) { - csc[col].emplace_back(row, val); - if (row != col) { csc[row].emplace_back(col, val); } + row_data[row].emplace_back(col, val); } - std::vector>> csr(n_vars); - for (i_t col = 0; col < n_vars; ++col) { - for (const auto& [row, val] : csc[col]) { - csr[row].emplace_back(col, val); - } + for (auto& row : row_data) { + std::sort(row.begin(), row.end()); } - // Within each row the entries are naturally ordered by column because - // the outer loop above walks columns in ascending order — no sort needed. std::vector q_values; std::vector q_indices; std::vector q_offsets; - q_offsets.reserve(n_vars + 1); + q_offsets.reserve(static_cast(n_vars) + 1); q_offsets.push_back(0); for (i_t row = 0; row < n_vars; ++row) { - for (const auto& [col, val] : csr[row]) { - q_values.push_back(val * f_t(0.5)); + for (const auto& [col, val] : row_data[row]) { + q_values.push_back(val); q_indices.push_back(col); } q_offsets.push_back(static_cast(q_values.size())); diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index f25eadf628..68fffcedfb 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -1621,16 +1621,18 @@ End int x = find_var(m, "x"); int y = find_var(m, "y"); int z = find_var(m, "z"); - // Diagonal 2 x^2 / 2 = x^2 ⇒ Q[x,x] = 1, similarly for y, z. + // The LP parser stores Q in upper-triangular form (i <= j). cuOpt's + // set_quadratic_objective_matrix symmetrizes via H = Q + Q^T, and the + // solver minimizes (1/2) x^T H x. + // Diagonal 2 x^2 / 2 → Q[x,x] = 1. EXPECT_NEAR(q_entry(m, x, x), 1.0, tolerance); EXPECT_NEAR(q_entry(m, y, y), 1.0, tolerance); EXPECT_NEAR(q_entry(m, z, z), 1.0, tolerance); - // Cross 2 x*y / 2 = x*y ⇒ full matrix Q[x,y] = Q[y,x] = 0.5 each - // (so that x^T Q x sums to x*y). - EXPECT_NEAR(q_entry(m, x, y), 0.5, tolerance); - EXPECT_NEAR(q_entry(m, y, x), 0.5, tolerance); - EXPECT_NEAR(q_entry(m, y, z), 0.5, tolerance); - EXPECT_NEAR(q_entry(m, z, y), 0.5, tolerance); + // Cross 2 x*y / 2 → stored as Q[x,y] = 1 only (no Q[y,x]). + EXPECT_NEAR(q_entry(m, x, y), 1.0, tolerance); + EXPECT_NEAR(q_entry(m, y, x), 0.0, tolerance); + EXPECT_NEAR(q_entry(m, y, z), 1.0, tolerance); + EXPECT_NEAR(q_entry(m, z, y), 0.0, tolerance); // x and z have no cross term. EXPECT_NEAR(q_entry(m, x, z), 0.0, tolerance); diff --git a/cpp/tests/qp/CMakeLists.txt b/cpp/tests/qp/CMakeLists.txt index e552987384..54cc3dc073 100644 --- a/cpp/tests/qp/CMakeLists.txt +++ b/cpp/tests/qp/CMakeLists.txt @@ -7,4 +7,5 @@ ConfigureTest(QP_UNIT_TEST ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/no_constraints.cu ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/two_variable_test.cu ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/mps_writer_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/unit_tests/lp_parser_solve_test.cu LABELS numopt) diff --git a/cpp/tests/qp/unit_tests/lp_parser_solve_test.cu b/cpp/tests/qp/unit_tests/lp_parser_solve_test.cu new file mode 100644 index 0000000000..3dc96acbdb --- /dev/null +++ b/cpp/tests/qp/unit_tests/lp_parser_solve_test.cu @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights + * reserved. SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +namespace cuopt::linear_programming { + +// End-to-end smoke tests that parse an LP file and solve via PDLP. +// Validates objective value and primal solution against hand-computed +// optima. The point is to verify the LP parser's quadratic-objective +// representation (upper-triangular CSR) round-trips correctly through +// cuOpt's solver (which applies H = Q + Q^T internally before solving +// (1/2) x^T H x). + +namespace { + +void expect_optimal_solution(const std::string& lp_text, + double expected_objective, + const std::vector& expected_x) +{ + raft::handle_t handle; + auto problem = io::parse_lp_from_string(lp_text); + auto settings = pdlp_solver_settings_t(); + auto solution = solve_lp(&handle, problem, settings); + + ASSERT_EQ(solution.get_termination_status(), pdlp_termination_status_t::Optimal); + EXPECT_NEAR(solution.get_objective_value(), expected_objective, 1e-4); + + auto sol = cuopt::host_copy(solution.get_primal_solution(), handle.get_stream()); + ASSERT_EQ(sol.size(), expected_x.size()); + for (size_t i = 0; i < expected_x.size(); ++i) { + EXPECT_NEAR(sol[i], expected_x[i], 1e-4) << "x[" << i << "]"; + } +} + +} // namespace + +// Diagonal-only quadratic objective. +// Minimize x1^2 + 4 x2^2 - 8 x1 - 16 x2 s.t. x1 + x2 >= 5, 0 <= x1, x2 <= 10. +// Unconstrained optimum (4, 2) satisfies the constraint with slack; obj = -32. +TEST(lp_parser_solve, qp_diagonal_only) +{ + expect_optimal_solution(R"LP( +Minimize + obj: -8 x1 - 16 x2 + [ 2 x1 ^ 2 + 8 x2 ^ 2 ] / 2 +Subject To + c1: x1 + x2 >= 5 +Bounds + 0 <= x1 <= 10 + 0 <= x2 <= 10 +End +)LP", + -32.0, + {4.0, 2.0}); +} + +// Quadratic objective with a cross term — exercises the upper-triangular +// off-diagonal storage path that this PR introduced. +// +// Minimize x1^2 + 2 x1 x2 + 2 x2^2 - 6 x1 - 8 x2 s.t. x1 + x2 <= 10. +// Hessian H = [[2, 2], [2, 4]] is positive definite. +// Unconstrained optimum from KKT: (2, 1); obj = 4 + 4 + 2 - 12 - 8 = -10. +TEST(lp_parser_solve, qp_with_cross_term) +{ + expect_optimal_solution(R"LP( +Minimize + obj: -6 x1 - 8 x2 + [ 2 x1 ^ 2 + 4 x1 * x2 + 4 x2 ^ 2 ] / 2 +Subject To + c1: x1 + x2 <= 10 +Bounds + -100 <= x1 <= 100 + -100 <= x2 <= 100 +End +)LP", + -10.0, + {2.0, 1.0}); +} + +// Quadratic objective with a negative cross-term coefficient. This +// exercises the same upper-triangular off-diagonal storage path with a +// sign that gets carried through parse_quadratic_bracket via the per-term +// sign of `- 4 x1 * x2`. +// +// Minimize x1^2 - 2 x1 x2 + 2 x2^2 - 4 x1 s.t. x1 + x2 <= 100. +// Hessian H = [[2, -2], [-2, 4]] is positive definite. +// Unconstrained optimum from KKT: (4, 2); obj = 16 - 16 + 8 - 16 = -8. +TEST(lp_parser_solve, qp_with_negative_cross_term) +{ + expect_optimal_solution(R"LP( +Minimize + obj: -4 x1 + [ 2 x1 ^ 2 - 4 x1 * x2 + 4 x2 ^ 2 ] / 2 +Subject To + c1: x1 + x2 <= 100 +Bounds + -100 <= x1 <= 100 + -100 <= x2 <= 100 +End +)LP", + -8.0, + {4.0, 2.0}); +} + +} // namespace cuopt::linear_programming From 4268886e95fc01d3939a3b9f8b5e7d3acc6546eb Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 23:08:14 +0000 Subject: [PATCH 08/30] Reject bare linear terms inside quadratic '[ ... ]' brackets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Gurobi's LP-format reference, the bracket reserved for the quadratic portion of an objective or constraint expression accepts only squared terms (e.g., '2 x ^ 2') and product terms (e.g., '3 x * y'). HiGHS's LP reader (highs/io/filereaderlp/reader.cpp) likewise rejects anything other than those four forms. The parser previously accepted bare linear terms like '2 x' inside the bracket and folded them into the row's linear part — an extension not supported by any LP reader I could find documentation or source for. Tighten the parser to reject this and prune the now-dead out_linear plumbing from parse_quadratic_bracket and its two call sites. One existing test (qc_outer_minus_sign_flips_quadratic_and_linear) used the rejected form to verify outer-sign propagation. Rewrite it to use the conventional spelling with the linear term outside the bracket, and add two negative tests that pin the new rejection (one in objective, one in constraint). Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/src/io/lp_parser.cpp | 44 +++++++------------- cpp/tests/linear_programming/parser_test.cpp | 33 +++++++++++++-- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp index cbafc6b45b..1f46cf937d 100644 --- a/cpp/src/io/lp_parser.cpp +++ b/cpp/src/io/lp_parser.cpp @@ -262,8 +262,7 @@ class LpParseEngine { // face value (x^T Q x); bracket must contain at least one // quadratic term. enum class BracketRole { Objective, Constraint }; - void parse_quadratic_bracket(std::vector& out_linear, - int outer_sign, + void parse_quadratic_bracket(int outer_sign, BracketRole role, std::vector>& out_quad_entries); @@ -832,10 +831,7 @@ void LpParseEngine::parse_linear_expression(std::vector& o template void LpParseEngine::parse_quadratic_bracket( - std::vector& out_linear, - int outer_sign, - BracketRole role, - std::vector>& out_quad_entries) + int outer_sign, BracketRole role, std::vector>& out_quad_entries) { expect(LpTokenKind::LBracket, "'[' at start of quadratic section"); @@ -905,10 +901,16 @@ void LpParseEngine::parse_quadratic_bracket( i_t b = std::max(i1, i2); raw_quad.emplace_back(a, b, sign * coeff); } else { - // Purely linear term inside the brackets — permitted as long as the - // surrounding /2 convention is respected (the linear term is scaled - // the same way as the quadratic ones). - out_linear.push_back({i1, sign * coeff}); + // Pure linear terms are not allowed inside a quadratic bracket — the + // LP convention reserves '[ ... ]' for squared and product terms only + // (matches Gurobi's documented LP format). Place linear terms outside + // the bracket. + mps_parser_expects(false, + error_type_t::ValidationError, + "LP parse error at line %d: bare linear term '%s' is not " + "allowed inside a quadratic bracket '[ ... ]'; move it outside", + peek().line, + var1.c_str()); } first = false; @@ -942,13 +944,6 @@ void LpParseEngine::parse_quadratic_bracket( v /= f_t(2); out_quad_entries.emplace_back(a, b, sign_scale * v); } - // Linear terms inside the brackets pick up the /2 scaling and the outer sign. - for (auto& lt : out_linear) - lt.coeff /= f_t(2); - if (outer_sign < 0) { - for (auto& lt : out_linear) - lt.coeff = -lt.coeff; - } } else { // Constraint: '/ 2' is forbidden — the LP convention is that constraint // quadratic brackets carry bare face-value coefficients of x^T Q x. @@ -973,10 +968,6 @@ void LpParseEngine::parse_quadratic_bracket( for (auto& [a, b, v] : raw_quad) { out_quad_entries.emplace_back(a, b, sign_scale * v); } - if (outer_sign < 0) { - for (auto& lt : out_linear) - lt.coeff = -lt.coeff; - } } } @@ -1009,11 +1000,7 @@ void LpParseEngine::parse_objective_section() quad_sign = -1; } if (peek().kind == LpTokenKind::LBracket) { - std::vector in_bracket_linear; - parse_quadratic_bracket( - in_bracket_linear, quad_sign, BracketRole::Objective, out_.quadobj_entries); - for (const auto& lt : in_bracket_linear) - linear.push_back(lt); + parse_quadratic_bracket(quad_sign, BracketRole::Objective, out_.quadobj_entries); // More linear terms may follow the bracket. std::vector more; @@ -1069,10 +1056,7 @@ void LpParseEngine::parse_constraints_section() } if (peek().kind == LpTokenKind::LBracket) { is_quadratic_row = true; - std::vector in_bracket_linear; - parse_quadratic_bracket(in_bracket_linear, quad_sign, BracketRole::Constraint, qc_triples); - for (const auto& lt : in_bracket_linear) - linear.push_back(lt); + parse_quadratic_bracket(quad_sign, BracketRole::Constraint, qc_triples); // More linear terms may follow the bracket. parse_linear_expression // does not produce a constant unless the user wrote one in the LHS; diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index 68fffcedfb..d7a5fc0197 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -2221,9 +2221,9 @@ End EXPECT_EQ(nth_qc(m, 1).constraint_row_name, "q2"); } -TEST(lp_parser, qc_outer_minus_sign_flips_quadratic_and_linear) +TEST(lp_parser, qc_outer_minus_sign_flips_quadratic) { - // `- [ x^2 + 2 x ] + 5` on the LHS contributes -x^2 - 2 x + 5 to the LHS. + // `- 2 x + 5 - [ x^2 ]` on the LHS contributes -x^2 - 2 x + 5 to the LHS. // After moving the constant to the RHS: -x^2 - 2 x <= rhs - 5. // Here the RHS is 10, so the row becomes: -x^2 - 2 x <= 5 (in x^T Q x form // Q[x,x] = -1). @@ -2231,7 +2231,7 @@ TEST(lp_parser, qc_outer_minus_sign_flips_quadratic_and_linear) Minimize x Subject To - q1: - [ x ^ 2 + 2 x ] + 5 <= 10 + q1: - 2 x + 5 - [ x ^ 2 ] <= 10 Bounds x free End @@ -2245,6 +2245,33 @@ End EXPECT_NEAR(qc.linear_values[0], -2.0, tolerance); } +TEST(lp_parser, bare_linear_inside_objective_bracket_rejected) +{ + // Gurobi's LP-format docs reserve `[ ... ]` for quadratic terms only + // (squared and product). A bare linear term like `2 x` inside the + // bracket is malformed; the user should write it outside. + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + obj: [ x ^ 2 + 2 x ] / 2 +Subject To + c1: x >= 1 +End +)LP"), + std::logic_error); +} + +TEST(lp_parser, bare_linear_inside_constraint_bracket_rejected) +{ + EXPECT_THROW(parse_lp_string(R"LP( +Minimize + x +Subject To + q1: [ x ^ 2 + 2 x ] <= 5 +End +)LP"), + std::logic_error); +} + TEST(lp_parser, qc_named_constraint) { auto m = parse_lp_string(R"LP( From 9cfd53a06cce7e8016e17e2a3ef0dd0a1e21a8ab Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 23:16:49 +0000 Subject: [PATCH 09/30] Remove references to named commercial solvers from comments and docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline source comments and public docstrings shouldn't single out specific solver vendors when describing LP-format conventions. Rephrase each spot to describe the rule itself instead — the conventions are the same whether or not a particular product is named. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/include/cuopt/linear_programming/io/parser.hpp | 6 +++--- cpp/src/io/lp_parser.cpp | 11 +++++------ cpp/tests/linear_programming/parser_test.cpp | 10 +++++----- .../cuopt/linear_programming/mps_parser/parser.py | 7 ++++--- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/io/parser.hpp b/cpp/include/cuopt/linear_programming/io/parser.hpp index 434efa5f97..a175e821cd 100644 --- a/cpp/include/cuopt/linear_programming/io/parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/parser.hpp @@ -63,9 +63,9 @@ mps_data_model_t parse_mps_from_string(std::string_view mps_contents, * a file in LP format. * * The LP format is a human-readable alternative to MPS format. This parser - * supports the conventional LP dialect implemented by most commercial - * optimization solvers (not the lpsolve variant, which has a different - * syntax). + * supports the dialect in which the objective and constraints are written + * as algebraic expressions over named variables (it does not implement the + * alternative tableau-style LP dialect used by some open-source readers). * * Scope: LP, MIP, and QP problems are supported, plus semi-continuous * variables (via a Semi-Continuous section; finite upper bound required) diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp index 1f46cf937d..84daa101b4 100644 --- a/cpp/src/io/lp_parser.cpp +++ b/cpp/src/io/lp_parser.cpp @@ -686,9 +686,9 @@ typename LpParseEngine::SectionKind LpParseEngine::try_consu advance(); return SectionKind::Binaries; } - // Semi-Continuous section header — accepted spellings: - // - 3-token "Semi - Continuous" (CPLEX/Gurobi documented) - // - bare "Semi" / "Semis" (CPLEX/Gurobi documented) + // Semi-Continuous section header. Documented spellings: + // - 3-token "Semi - Continuous" + // - bare "Semi" / "Semis" // Check the 3-token form first so it consumes all three tokens. if (lower == "semi" && peek(1).kind == LpTokenKind::Minus && name_equals_ci(peek(2), "continuous")) { @@ -902,9 +902,8 @@ void LpParseEngine::parse_quadratic_bracket( raw_quad.emplace_back(a, b, sign * coeff); } else { // Pure linear terms are not allowed inside a quadratic bracket — the - // LP convention reserves '[ ... ]' for squared and product terms only - // (matches Gurobi's documented LP format). Place linear terms outside - // the bracket. + // LP-format convention reserves '[ ... ]' for squared and product + // terms only. Place linear terms outside the bracket. mps_parser_expects(false, error_type_t::ValidationError, "LP parse error at line %d: bare linear term '%s' is not " diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index d7a5fc0197..541c71f23f 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -1752,8 +1752,8 @@ End TEST(lp_parser, semi_continuous_bare_semi_keyword) { - // Both Gurobi and CPLEX accept the bare "Semi" keyword as a synonym for - // the "Semi-Continuous" section header. + // The LP-format convention accepts the bare "Semi" keyword as a synonym + // for the "Semi-Continuous" section header. auto m = parse_lp_string(R"LP( Minimize x @@ -1774,8 +1774,8 @@ End TEST(lp_parser, semi_continuous_bare_semis_keyword) { - // Both Gurobi and CPLEX accept the bare "Semis" keyword as a synonym for - // the "Semi-Continuous" section header. + // The LP-format convention accepts the bare "Semis" keyword as a synonym + // for the "Semi-Continuous" section header. auto m = parse_lp_string(R"LP( Minimize x @@ -2247,7 +2247,7 @@ End TEST(lp_parser, bare_linear_inside_objective_bracket_rejected) { - // Gurobi's LP-format docs reserve `[ ... ]` for quadratic terms only + // The LP-format convention reserves `[ ... ]` for quadratic terms only // (squared and product). A bare linear term like `2 x` inside the // bracket is malformed; the user should write it outside. EXPECT_THROW(parse_lp_string(R"LP( diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser.py b/python/cuopt/cuopt/linear_programming/mps_parser/parser.py index 3107b106b2..8a56804955 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser.py +++ b/python/cuopt/cuopt/linear_programming/mps_parser/parser.py @@ -69,9 +69,10 @@ def ParseLp(lp_file_path): ``0.5 x^T Q x`` convention); a constraint bracket must NOT be followed by ``/ 2`` (coefficients are at face value, ``x^T Q x``). - This function parses the conventional LP dialect implemented by most - commercial optimization solvers (not the lpsolve variant, which has a - different syntax). + This function parses the dialect in which the objective and constraints + are written as algebraic expressions over named variables (it does not + implement the alternative tableau-style LP dialect used by some + open-source readers). Unsupported LP sections (SOS, PWL objective, user cuts, general constraints) raise a ValueError. From a7cdfb8ed8238efc60326e5f7031c2230a588f96 Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 23:35:39 +0000 Subject: [PATCH 10/30] cuopt_cli: expand filename --help to match runtime dispatch The argparse help string for the positional filename argument said only "input MPS or LP file (dispatched by .lp / .mps extension)", which both underspecified the supported set and omitted .qps and the compressed variants. Update it to enumerate every extension parse_problem() accepts. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/cuopt_cli.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 5a325f2a0f..39aab47170 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -282,7 +282,10 @@ int main(int argc, char* argv[]) // Define all arguments with appropriate defaults and help messages program.add_argument("filename") - .help("input MPS or LP file (dispatched by .lp / .mps extension)") + .help( + "input problem file; format dispatched by extension (case-insensitive). " + "Supported: .lp, .mps, .qps and their .gz / .bz2 compressed variants " + "(e.g. .lp.gz, .mps.bz2, .qps.gz)") .nargs(1) .required(); From 94e730e36f4f436cb6e137148c2edc3d632dd8c4 Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 23:41:59 +0000 Subject: [PATCH 11/30] c_api_tests: guarantee cleanup in read_lp_file_by_extension on failure The test wrote a temp .lp file and called cuOptReadProblem; cleanup (cuOptDestroyProblem and std::filesystem::remove) ran only at the end of the test body, so an assertion failure mid-test would leak both the problem handle and the temp file. Introduce a small RAII guard whose destructor unconditionally destroys the handle (if non-null) and removes the temp file, so cleanup runs on every exit path. std::filesystem::remove is called with std::error_code to avoid throwing during stack unwinding. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- .../c_api_tests/c_api_tests.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp index 2fc9bdbbb2..35d6c2dd1d 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp @@ -138,7 +138,20 @@ End } cuOptOptimizationProblem handle = nullptr; - cuopt_int_t status = cuOptReadProblem(lp_path.string().c_str(), &handle); + // Scope guard: tear the temp file and the problem handle down on every + // exit path (including assertion failure) so the test doesn't leak. + struct cleanup_t { + cuOptOptimizationProblem* handle_ptr; + const std::filesystem::path& lp_path; + ~cleanup_t() + { + if (*handle_ptr != nullptr) { cuOptDestroyProblem(handle_ptr); } + std::error_code ec; + std::filesystem::remove(lp_path, ec); + } + } cleanup{&handle, lp_path}; + + cuopt_int_t status = cuOptReadProblem(lp_path.string().c_str(), &handle); EXPECT_EQ(status, CUOPT_SUCCESS); ASSERT_NE(handle, nullptr); @@ -148,9 +161,6 @@ End EXPECT_EQ(cuOptGetNumConstraints(handle, &n_constrs), CUOPT_SUCCESS); EXPECT_EQ(n_vars, 1); EXPECT_EQ(n_constrs, 1); - - cuOptDestroyProblem(&handle); - std::filesystem::remove(lp_path); } TEST(c_api, test_infeasible_problem) { EXPECT_EQ(test_infeasible_problem(), CUOPT_SUCCESS); } From 079ed412612c8a432e989e56a633df20a9d55ab3 Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 23:43:27 +0000 Subject: [PATCH 12/30] parser_test: exception-safe temp-file cleanup in dispatch_parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dispatch_parse() writes content to a temp file, then calls parse_problem(tmp.string()), then removes the temp file. If parse_problem throws (the unrecognized-extension test exercises exactly this path), the std::filesystem::remove call is skipped and the file leaks. Replace the manual post-parse remove with an RAII guard whose destructor removes the file on every scope exit — success, return, or exception. std::error_code is passed to remove so the destructor never throws during stack unwinding while a parse exception is propagating. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/tests/linear_programming/parser_test.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index 541c71f23f..8e49439194 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -2499,9 +2499,18 @@ mps_data_model_t dispatch_parse(const std::string& content, const s std::ofstream out(tmp); out << content; } - auto model = parse_problem(tmp.string()); - std::filesystem::remove(tmp); - return model; + // Scope guard: remove the temp file even if parse_problem throws. + // std::error_code is passed so the destructor does not throw during stack + // unwinding when a parse exception is propagating. + struct cleanup_t { + const std::filesystem::path& path; + ~cleanup_t() + { + std::error_code ec; + std::filesystem::remove(path, ec); + } + } cleanup{tmp}; + return parse_problem(tmp.string()); } constexpr const char* kTrivialLp = R"LP( From bee7dfbe7d676b35b9cb84718db8c4c724330b39 Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 23:46:52 +0000 Subject: [PATCH 13/30] Self-hosted client: extend extension dispatch to QPS and compressed variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two extension-routing blocks in cuopt_self_host_client.py only recognized literal '.lp' and '.mps' suffixes — '.qps' and the compressed variants (.lp.gz, .lp.bz2, .mps.gz, .mps.bz2, .qps.gz, .qps.bz2) fell through to the unparsed-upload path even though the underlying mps_parser.ParseMps / ParseLp accept all of them via the same C++ file_to_string dispatch as the CLI. Factor the extension check into a small helper that lowercases the path and strips a single .gz / .bz2 compression suffix before matching, then use it in both _parse_file_to_data_model (which picks ParseLp vs ParseMps) and read_cuopt_problem_data (which decides whether to parse client-side or ship to the server). QPS routes to ParseMps; that matches the C++ parse_problem() dispatch table. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- .../cuopt_sh_client/cuopt_self_host_client.py | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py index ae59b84f5a..b13adeee74 100644 --- a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py +++ b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py @@ -134,6 +134,34 @@ def is_uuid(cuopt_problem_data): return False +# File extensions (case-insensitive, after stripping a compression suffix) that +# the cuopt mps_parser package can parse client-side. Matches the dispatch +# table in parse_problem() on the C++ side. +_PARSEABLE_LP_EXTS = (".lp",) +_PARSEABLE_MPS_EXTS = (".mps", ".qps") +_COMPRESSION_SUFFIXES = (".gz", ".bz2") + + +def _strip_compression_suffix(lowered_path): + for suffix in _COMPRESSION_SUFFIXES: + if lowered_path.endswith(suffix): + return lowered_path[: -len(suffix)] + return lowered_path + + +def _client_parseable_extension(path): + """Return 'lp', 'mps', or None for a path. + + Case-insensitive; recognizes .gz / .bz2 compressed variants. + """ + base = _strip_compression_suffix(path.lower()) + if base.endswith(_PARSEABLE_LP_EXTS): + return "lp" + if base.endswith(_PARSEABLE_MPS_EXTS): + return "mps" + return None + + def _parse_file_to_data_model(problem_input, solver_config): try: from cuopt.linear_programming import mps_parser @@ -145,19 +173,24 @@ def _parse_file_to_data_model(problem_input, solver_config): "or pass an already-parsed dict instead of an MPS/LP file or " "DataModel." ) from e - # problem_input is either a path (str) to an MPS/LP file, or an - # mps_parser DataModel already handed to us. + # problem_input is either a path (str) to an MPS/LP/QPS file (optionally + # .gz / .bz2 compressed), or an mps_parser DataModel already handed to us. if isinstance(problem_input, mps_parser.parser_wrapper.DataModel): model = problem_input log.debug("Received mps_parser DataModel object") else: t0 = time.time() - # Dispatch on file extension: ".lp" ⇒ LP parser, otherwise MPS. - if isinstance(problem_input, str) and problem_input.lower().endswith( - ".lp" - ): + kind = ( + _client_parseable_extension(problem_input) + if isinstance(problem_input, str) + else None + ) + if kind == "lp": model = mps_parser.ParseLp(problem_input) else: + # MPS, QPS, and any unrecognized extension fall through to the + # MPS parser, which accepts both .mps and .qps (and their .gz / + # .bz2 variants) via the underlying C++ parse_mps(). model = mps_parser.ParseMps(problem_input) parse_time = time.time() - t0 log.debug(f"file parsing time was {parse_time}") @@ -809,13 +842,15 @@ def read_cuopt_problem_data(cuopt_data_model, filepath): needs_parsing = False filepath = False else: - # Needs parsing if it's either (a) a string path ending in - # .mps/.lp, or (b) a non-string (DataModel) to normalize. + # Needs parsing if it's either (a) a string path with a + # client-parseable extension (.lp / .mps / .qps, optionally + # .gz / .bz2 compressed), or (b) a non-string (DataModel) + # to normalize. if isinstance(cuopt_data_model, str): - lowered = cuopt_data_model.lower() - needs_parsing = lowered.endswith( - ".mps" - ) or lowered.endswith(".lp") + needs_parsing = ( + _client_parseable_extension(cuopt_data_model) + is not None + ) else: needs_parsing = True From 9a915329c6d3a7b5843f0a033c8267232fec5281 Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Sun, 17 May 2026 23:59:30 +0000 Subject: [PATCH 14/30] Rename Python mps_parser package to io MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cuopt.linear_programming.mps_parser package now exposes both ParseMps and ParseLp, so 'mps_parser' is no longer accurate. Rename the directory to 'io' (mirroring the cpp/include/cuopt/linear_programming/io/ layout on the C++ side) and rename the decorator catch_mps_parser_exception → catch_io_exception. This package was added on this branch in 72ba0540 (post-26.04) and has not appeared in any release, so the rename is safe without a compatibility shim. Importing files that previously did 'from cuopt.linear_programming import mps_parser' now import 'io as mps_parser' to keep the local binding stable; user-facing docs/examples and the public top-level import in cuopt/linear_programming/__init__.py use the new module name directly. The C++ error tag MPS_PARSER_ERROR_TYPE that the decorator parses out of RuntimeError messages is intentionally unchanged — that tag is produced by the mps_parser_expects() macro, which is shared between both parsers on the C++ side and would have a much larger blast radius to rename. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- .../examples/lp/examples/mps_datamodel_example.py | 6 +++--- docs/cuopt/source/hidden/mps-api.rst | 4 ++-- docs/cuopt/source/hidden/mps-example.rst | 4 ++-- python/cuopt/cuopt/CMakeLists.txt | 2 +- python/cuopt/cuopt/linear_programming/__init__.py | 2 +- .../{mps_parser => io}/CMakeLists.txt | 0 .../{mps_parser => io}/__init__.py | 2 +- .../linear_programming/{mps_parser => io}/parser.pxd | 0 .../linear_programming/{mps_parser => io}/parser.py | 12 ++++++------ .../{mps_parser => io}/parser_wrapper.pyx | 8 ++++---- .../{mps_parser => io}/utilities/__init__.py | 4 ++-- .../utilities/exception_handler.py | 11 +++++++++-- python/cuopt/cuopt/linear_programming/problem.py | 2 +- .../cuopt/cuopt/linear_programming/solver/solver.py | 4 ++-- .../linear_programming/test_cpu_only_execution.py | 10 +++++----- .../linear_programming/test_incumbent_callbacks.py | 2 +- .../cuopt/tests/linear_programming/test_lp_solver.py | 2 +- .../cuopt/tests/linear_programming/test_parser.py | 4 ++-- .../cuopt_sh_client/cuopt_self_host_client.py | 12 ++++++------ .../cuopt_server/tests/test_pdlp_warmstart.py | 2 +- 20 files changed, 50 insertions(+), 43 deletions(-) rename python/cuopt/cuopt/linear_programming/{mps_parser => io}/CMakeLists.txt (100%) rename python/cuopt/cuopt/linear_programming/{mps_parser => io}/__init__.py (63%) rename python/cuopt/cuopt/linear_programming/{mps_parser => io}/parser.pxd (100%) rename python/cuopt/cuopt/linear_programming/{mps_parser => io}/parser.py (95%) rename python/cuopt/cuopt/linear_programming/{mps_parser => io}/parser_wrapper.pyx (97%) rename python/cuopt/cuopt/linear_programming/{mps_parser => io}/utilities/__init__.py (66%) rename python/cuopt/cuopt/linear_programming/{mps_parser => io}/utilities/exception_handler.py (68%) diff --git a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py index e6ff6add73..04be2e6987 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py +++ b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py @@ -4,7 +4,7 @@ LP DataModel from MPS Parser Example This example demonstrates how to: -- Parse an MPS file using cuopt.linear_programming.mps_parser +- Parse an MPS file using cuopt.linear_programming.io - Create a DataModel from the parsed MPS - Solve using the DataModel via the server - Extract detailed solution information @@ -32,7 +32,7 @@ ThinClientSolverSettings, PDLPSolverMode, ) -from cuopt.linear_programming import mps_parser +from cuopt.linear_programming import ParseMps import time @@ -65,7 +65,7 @@ def main(): # Parse the MPS file and measure the time spent print("\n=== Parsing MPS File ===") parse_start = time.time() - data_model = mps_parser.ParseMps(data) + data_model = ParseMps(data) parse_time = time.time() - parse_start print(f"Parse time: {parse_time:.3f} seconds") diff --git a/docs/cuopt/source/hidden/mps-api.rst b/docs/cuopt/source/hidden/mps-api.rst index 362a26a51a..637d8a03cc 100644 --- a/docs/cuopt/source/hidden/mps-api.rst +++ b/docs/cuopt/source/hidden/mps-api.rst @@ -5,9 +5,9 @@ cuOpt MPS/LP Parser API Reference MPS Parser ---------- -.. autofunction:: cuopt.linear_programming.mps_parser.ParseMps +.. autofunction:: cuopt.linear_programming.io.ParseMps LP Parser --------- -.. autofunction:: cuopt.linear_programming.mps_parser.ParseLp +.. autofunction:: cuopt.linear_programming.io.ParseLp diff --git a/docs/cuopt/source/hidden/mps-example.rst b/docs/cuopt/source/hidden/mps-example.rst index 7ceae8aa21..cc6495d2b4 100644 --- a/docs/cuopt/source/hidden/mps-example.rst +++ b/docs/cuopt/source/hidden/mps-example.rst @@ -9,5 +9,5 @@ Example .. code-block:: python :linenos: - from cuopt.linear_programming import mps_parser - x = mps_parser.ParseMps('good-mps-1.mps') + from cuopt.linear_programming import ParseMps + x = ParseMps('good-mps-1.mps') diff --git a/python/cuopt/cuopt/CMakeLists.txt b/python/cuopt/cuopt/CMakeLists.txt index 996f1b1953..ba5eb25ddf 100644 --- a/python/cuopt/cuopt/CMakeLists.txt +++ b/python/cuopt/cuopt/CMakeLists.txt @@ -5,7 +5,7 @@ add_subdirectory(distance_engine) add_subdirectory(linear_programming/data_model) -add_subdirectory(linear_programming/mps_parser) +add_subdirectory(linear_programming/io) add_subdirectory(linear_programming/solver) add_subdirectory(routing) diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index d171b12878..6950d72bc8 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -3,7 +3,7 @@ from cuopt.linear_programming import internals from cuopt.linear_programming.data_model import DataModel -from cuopt.linear_programming.mps_parser import ParseLp, ParseMps +from cuopt.linear_programming.io import ParseLp, ParseMps from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solution import Solution from cuopt.linear_programming.solver import BatchSolve, Solve diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/CMakeLists.txt b/python/cuopt/cuopt/linear_programming/io/CMakeLists.txt similarity index 100% rename from python/cuopt/cuopt/linear_programming/mps_parser/CMakeLists.txt rename to python/cuopt/cuopt/linear_programming/io/CMakeLists.txt diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/__init__.py b/python/cuopt/cuopt/linear_programming/io/__init__.py similarity index 63% rename from python/cuopt/cuopt/linear_programming/mps_parser/__init__.py rename to python/cuopt/cuopt/linear_programming/io/__init__.py index ded8d5a06c..f81e9369ec 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/__init__.py +++ b/python/cuopt/cuopt/linear_programming/io/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -from cuopt.linear_programming.mps_parser.parser import ParseLp, ParseMps, toDict +from cuopt.linear_programming.io.parser import ParseLp, ParseMps, toDict diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd b/python/cuopt/cuopt/linear_programming/io/parser.pxd similarity index 100% rename from python/cuopt/cuopt/linear_programming/mps_parser/parser.pxd rename to python/cuopt/cuopt/linear_programming/io/parser.pxd diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser.py b/python/cuopt/cuopt/linear_programming/io/parser.py similarity index 95% rename from python/cuopt/cuopt/linear_programming/mps_parser/parser.py rename to python/cuopt/cuopt/linear_programming/io/parser.py index 8a56804955..9cec1556f6 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser.py +++ b/python/cuopt/cuopt/linear_programming/io/parser.py @@ -2,13 +2,13 @@ # SPDX-License-Identifier: Apache-2.0 import numpy as np -from cuopt.linear_programming.mps_parser import parser_wrapper -from cuopt.linear_programming.mps_parser.utilities import ( - catch_mps_parser_exception, +from cuopt.linear_programming.io import parser_wrapper +from cuopt.linear_programming.io.utilities import ( + catch_io_exception, ) -@catch_mps_parser_exception +@catch_io_exception def ParseMps(mps_file_path, fixed_mps_format=False): """ Reads the equation from the input text file which is MPS formatted @@ -54,7 +54,7 @@ def ParseMps(mps_file_path, fixed_mps_format=False): return parser_wrapper.ParseMps(mps_file_path, fixed_mps_format) -@catch_mps_parser_exception +@catch_io_exception def ParseLp(lp_file_path): """ Reads an optimization problem from a file in LP format. @@ -102,7 +102,7 @@ def ParseLp(lp_file_path): def toDict(model, json=False): if not isinstance(model, parser_wrapper.DataModel): raise ValueError( - "model must be a cuopt.linear_programming.mps_parser.parser_wrapper.DataModel" + "model must be a cuopt.linear_programming.io.parser_wrapper.DataModel" ) # Replace numpy objects in generated data so that it is JSON serializable diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx similarity index 97% rename from python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx rename to python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx index 6c392ac4bd..61e10b1864 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx @@ -7,8 +7,8 @@ # cython: embedsignature = True # cython: language_level = 3 -from cuopt.linear_programming.mps_parser.utilities import ( - catch_mps_parser_exception, +from cuopt.linear_programming.io.utilities import ( + catch_io_exception, ) from libc.stdint cimport uintptr_t @@ -138,7 +138,7 @@ cdef _marshal_data_model(mps_data_model_t[int, double]* dm, data_model): return data_model -@catch_mps_parser_exception +@catch_io_exception def ParseMps(mps_file_path, fixed_mps_formats): data_model = DataModel() dm_ret_ptr = move( @@ -150,7 +150,7 @@ def ParseMps(mps_file_path, fixed_mps_formats): return _marshal_data_model(dm_ret_ptr.get(), data_model) -@catch_mps_parser_exception +@catch_io_exception def ParseLp(lp_file_path): data_model = DataModel() dm_ret_ptr = move( diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/utilities/__init__.py b/python/cuopt/cuopt/linear_programming/io/utilities/__init__.py similarity index 66% rename from python/cuopt/cuopt/linear_programming/mps_parser/utilities/__init__.py rename to python/cuopt/cuopt/linear_programming/io/utilities/__init__.py index e782831bac..23edf306ed 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/utilities/__init__.py +++ b/python/cuopt/cuopt/linear_programming/io/utilities/__init__.py @@ -1,9 +1,9 @@ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -from cuopt.linear_programming.mps_parser.utilities.exception_handler import ( +from cuopt.linear_programming.io.utilities.exception_handler import ( InputRuntimeError, InputValidationError, OutOfMemoryError, - catch_mps_parser_exception, + catch_io_exception, ) diff --git a/python/cuopt/cuopt/linear_programming/mps_parser/utilities/exception_handler.py b/python/cuopt/cuopt/linear_programming/io/utilities/exception_handler.py similarity index 68% rename from python/cuopt/cuopt/linear_programming/mps_parser/utilities/exception_handler.py rename to python/cuopt/cuopt/linear_programming/io/utilities/exception_handler.py index 55041e6cfa..c6e4c6990b 100644 --- a/python/cuopt/cuopt/linear_programming/mps_parser/utilities/exception_handler.py +++ b/python/cuopt/cuopt/linear_programming/io/utilities/exception_handler.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import functools @@ -17,7 +17,14 @@ class OutOfMemoryError(Exception): pass -def catch_mps_parser_exception(f): +def catch_io_exception(f): + """Translate the C++ parser's JSON-tagged RuntimeError to a typed Python + exception. The error tag string ("MPS_PARSER_ERROR_TYPE") is preserved + verbatim because it's produced by the C++ mps_parser_expects() macro, + which is shared between the MPS and LP parsers; renaming it would be a + C++-side change with a larger blast radius. + """ + @functools.wraps(f) def func(*args, **kwargs): try: diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index f7f874fafd..0f4c6c3846 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -9,7 +9,7 @@ from scipy.sparse import coo_matrix import cuopt.linear_programming.data_model as data_model -import cuopt.linear_programming.mps_parser as mps_parser +import cuopt.linear_programming.io as mps_parser import cuopt.linear_programming.solver as solver import cuopt.linear_programming.solver_settings as solver_settings import warnings diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.py b/python/cuopt/cuopt/linear_programming/solver/solver.py index 3dd5af35c9..3c72956742 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.py +++ b/python/cuopt/cuopt/linear_programming/solver/solver.py @@ -149,11 +149,11 @@ def BatchSolve(data_model_list, solver_settings=None): >>> from cuopt import linear_programming >>> from cuopt.linear_programming.solver_settings import PDLPSolverMode >>> from cuopt.linear_programming.solver.solver_parameters import * - >>> from cuopt.linear_programming import mps_parser + >>> from cuopt.linear_programming import ParseMps >>> >>> data_models = [] >>> for i in range(...): - >>> data_models.append(mps_parser.ParseMps(...)) + >>> data_models.append(ParseMps(...)) >>> >>> # Build a solver setting object >>> settings = linear_programming.SolverSettings() diff --git a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py index bb84599aa5..1c3a83b162 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py @@ -23,7 +23,7 @@ import sys import time -from cuopt.linear_programming import mps_parser +from cuopt.linear_programming import io as mps_parser import pytest from cuopt import linear_programming from cuopt.linear_programming.solver.solver_parameters import CUOPT_TIME_LIMIT @@ -301,7 +301,7 @@ def _run_in_subprocess(func, env=None, timeout=120): def _impl_lp_solve_cpu_only(): """LP solve returns correctly-sized solution vectors.""" from cuopt import linear_programming - from cuopt.linear_programming import mps_parser + from cuopt.linear_programming import io as mps_parser dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" @@ -331,7 +331,7 @@ def _impl_lp_solve_cpu_only(): def _impl_lp_dual_solution_cpu_only(): """Dual solution and reduced costs are correctly sized.""" from cuopt import linear_programming - from cuopt.linear_programming import mps_parser + from cuopt.linear_programming import io as mps_parser dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" @@ -364,7 +364,7 @@ def _impl_mip_solve_cpu_only(): from cuopt.linear_programming.solver.solver_parameters import ( CUOPT_TIME_LIMIT, ) - from cuopt.linear_programming import mps_parser + from cuopt.linear_programming import io as mps_parser dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/mip/bb_optimality.mps" @@ -400,7 +400,7 @@ def _impl_warmstart_cpu_only(): CUOPT_PRESOLVE, ) from cuopt.linear_programming.solver_settings import SolverMethod - from cuopt.linear_programming import mps_parser + from cuopt.linear_programming import io as mps_parser dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" diff --git a/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py b/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py index 55a34016bd..20b0b0cca6 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py @@ -3,7 +3,7 @@ import os -from cuopt.linear_programming import mps_parser +from cuopt.linear_programming import io as mps_parser import pytest from cuopt.linear_programming import solver, solver_settings diff --git a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py index feb5b4ad5e..a3eab8d98e 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py @@ -3,7 +3,7 @@ import os -from cuopt.linear_programming import mps_parser +from cuopt.linear_programming import io as mps_parser import numpy as np import pytest diff --git a/python/cuopt/cuopt/tests/linear_programming/test_parser.py b/python/cuopt/cuopt/tests/linear_programming/test_parser.py index f1b569a092..795f1fe298 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_parser.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_parser.py @@ -4,10 +4,10 @@ import os import tempfile -from cuopt.linear_programming import mps_parser +from cuopt.linear_programming import io as mps_parser import numpy as np import pytest -from cuopt.linear_programming.mps_parser.utilities import InputValidationError +from cuopt.linear_programming.io.utilities import InputValidationError RAPIDS_DATASET_ROOT_DIR = os.getenv("RAPIDS_DATASET_ROOT_DIR") if RAPIDS_DATASET_ROOT_DIR is None: diff --git a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py index b13adeee74..a5b76d57f3 100644 --- a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py +++ b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py @@ -135,8 +135,8 @@ def is_uuid(cuopt_problem_data): # File extensions (case-insensitive, after stripping a compression suffix) that -# the cuopt mps_parser package can parse client-side. Matches the dispatch -# table in parse_problem() on the C++ side. +# the cuopt.linear_programming.io package can parse client-side. Matches the +# dispatch table in parse_problem() on the C++ side. _PARSEABLE_LP_EXTS = (".lp",) _PARSEABLE_MPS_EXTS = (".mps", ".qps") _COMPRESSION_SUFFIXES = (".gz", ".bz2") @@ -164,7 +164,7 @@ def _client_parseable_extension(path): def _parse_file_to_data_model(problem_input, solver_config): try: - from cuopt.linear_programming import mps_parser + from cuopt.linear_programming import io as mps_parser except ImportError as e: raise ImportError( "MPS/LP parsing on the client requires the cuopt package. " @@ -774,14 +774,14 @@ def get_LP_solve( Note - Batch mode is only supported in LP and not in MILP File path to mps/lp or json/dict/DataModel returned by - cuopt.linear_programming.mps_parser/list[mps or lp file paths]/list[dict]/list[DataModel]. + cuopt.linear_programming.io/list[mps or lp file paths]/list[dict]/list[DataModel]. For single problem, input should be either a path to mps/lp/json file, - /DataModel returned by cuopt.linear_programming.mps_parser/ path to json file/ + /DataModel returned by cuopt.linear_programming.io/ path to json file/ dictionary. For batch problem, input should be either a list of paths to mps or lp - files/ a list of DataModel returned by cuopt.linear_programming.mps_parser/ a + files/ a list of DataModel returned by cuopt.linear_programming.io/ a list of dictionaries. To use a cached cuopt problem data, input should be a uuid diff --git a/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py b/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py index 06c5df843d..76dd22c054 100644 --- a/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py +++ b/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py @@ -3,7 +3,7 @@ import os -from cuopt.linear_programming import mps_parser +from cuopt.linear_programming import io as mps_parser import msgpack from cuopt.linear_programming import solver_settings From 1683bea8c6110841b12e4fd01048020378ad5d4e Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Mon, 18 May 2026 00:09:02 +0000 Subject: [PATCH 15/30] ParseLp: add type annotations and Raises docstring section Annotate ParseLp with `lp_file_path: str` and `-> DataModel`, importing DataModel from cuopt.linear_programming.data_model so the annotation resolves at runtime. The old docstring claimed unsupported-section input raises ValueError; in practice the catch_io_exception decorator translates the C++ parser's tagged RuntimeError into typed Python exceptions (InputValidationError / InputRuntimeError / OutOfMemoryError). Replace the misleading sentence with a proper Raises section listing all three typed exceptions and the conditions that trigger them. Also expand the lp_file_path parameter description to mention the compressed-input variants the parser accepts, and trim the Returns phrasing so it points at lp_file_path explicitly. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- .../cuopt/linear_programming/io/parser.py | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/io/parser.py b/python/cuopt/cuopt/linear_programming/io/parser.py index 9cec1556f6..385edfefe1 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.py +++ b/python/cuopt/cuopt/linear_programming/io/parser.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import numpy as np +from cuopt.linear_programming.data_model import DataModel from cuopt.linear_programming.io import parser_wrapper from cuopt.linear_programming.io.utilities import ( catch_io_exception, @@ -55,9 +56,8 @@ def ParseMps(mps_file_path, fixed_mps_format=False): @catch_io_exception -def ParseLp(lp_file_path): - """ - Reads an optimization problem from a file in LP format. +def ParseLp(lp_file_path: str) -> DataModel: + """Read an optimization problem from a file in LP format. The LP format is a human-readable alternative to MPS and supports LP, MIP, and QP, plus semi-continuous variables (declared via a @@ -67,25 +67,47 @@ def ParseLp(lp_file_path): Quadratic terms live in ``[ ... ]`` blocks. The objective bracket must be followed by ``/ 2`` (the file states coefficients in the ``0.5 x^T Q x`` convention); a constraint bracket must NOT be followed - by ``/ 2`` (coefficients are at face value, ``x^T Q x``). + by ``/ 2`` (coefficients are at face value, ``x^T Q x``). Only squared + (``x^2``) and product (``x * y``) terms are allowed inside the + bracket; bare linear terms must be written outside it. This function parses the dialect in which the objective and constraints are written as algebraic expressions over named variables (it does not implement the alternative tableau-style LP dialect used by some open-source readers). - Unsupported LP sections (SOS, PWL objective, user cuts, general - constraints) raise a ValueError. - Parameters ---------- lp_file_path : str - Path to LP-formatted file. + Path to LP-formatted file. May end in ``.lp``, ``.lp.gz``, or + ``.lp.bz2``; compressed inputs are decompressed at read time + via zlib / libbz2 when those libraries are available. Returns ------- - data_model: DataModel - A fully formed LP/MIP/QP problem representing the given file. + data_model : DataModel + A fully formed LP/MIP/QP problem representing the contents of + ``lp_file_path``. + + Raises + ------ + InputValidationError + Raised when ``lp_file_path`` is malformed or uses unsupported + syntax. Examples include unsupported sections (SOS, PWL + objective, user cuts, general constraints), bare linear terms + inside a quadratic ``[ ... ]`` bracket, an objective bracket + not followed by ``/ 2``, a constraint bracket followed by + ``/ 2``, a semi-continuous variable without a finite upper + bound, and similar input-level errors raised by the underlying + C++ parser. Exceptions propagated from + :func:`parser_wrapper.ParseLp` are translated to this type by + :func:`catch_io_exception`. + InputRuntimeError + Raised for non-validation runtime errors that the C++ parser + flags during file I/O or parsing. + OutOfMemoryError + Raised when the parser cannot allocate memory for the + resulting data model. Examples -------- @@ -95,7 +117,6 @@ def ParseLp(lp_file_path): >>> solver_settings = linear_programming.SolverSettings() >>> solution = linear_programming.Solve(data_model, solver_settings) """ - return parser_wrapper.ParseLp(lp_file_path) From c82cfccb0a17bafd6f9e5c1d31dc2f3146a5bc2e Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Mon, 18 May 2026 00:16:28 +0000 Subject: [PATCH 16/30] parser_test: replace shared /tmp paths with RAII temp_file_t The mps_roundtrip and lp_roundtrip tests each used a fixed shared /tmp path (e.g. /tmp/mps_roundtrip_lp_test.mps) and removed it manually at the end of the test. Two problems: 1. Parallel test runs writing to the same path race each other. 2. An assertion failure or exception between write and remove leaks the file. Add a small temp_file_t RAII helper that picks a unique path under std::filesystem::temp_directory_path() using pid + an atomic counter, and removes the file on every scope exit (std::error_code so the destructor never throws). Adopt it in every test that previously spelled a literal /tmp/*.mps path (the six mps/lp roundtrip tests and the QCQP-QC1 roundtrip), and use it inside dispatch_parse too, replacing the existing inline cleanup_t scope guard. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/tests/linear_programming/parser_test.cpp | 124 +++++++++---------- 1 file changed, 59 insertions(+), 65 deletions(-) diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index 8e49439194..4263a594ce 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include #include namespace cuopt::linear_programming::io { @@ -1039,6 +1041,31 @@ TEST(qps_parser, test_qps_files) // MPS Round-Trip Tests (Read -> Write -> Read -> Compare) // ================================================================================================ +// RAII temp file path: builds a unique path under temp_directory_path() and +// removes it on scope exit, so write/parse/compare can throw without leaking +// the file and parallel test runs don't collide on a shared name. The file +// is not created at construction; it appears when the writer writes to +// `path()`. +struct temp_file_t { + std::filesystem::path p; + explicit temp_file_t(const std::string& suffix) + { + static std::atomic counter{0}; + const auto pid = static_cast(::getpid()); + const auto n = counter.fetch_add(1, std::memory_order_relaxed); + p = std::filesystem::temp_directory_path() / + ("cuopt_test_" + std::to_string(pid) + "_" + std::to_string(n) + suffix); + } + ~temp_file_t() + { + std::error_code ec; + std::filesystem::remove(p, ec); + } + temp_file_t(const temp_file_t&) = delete; + temp_file_t& operator=(const temp_file_t&) = delete; + std::string string() const { return p.string(); } +}; + // Helper function to compare two data models template void compare_data_models(const mps_data_model_t& original, @@ -1164,23 +1191,20 @@ TEST(mps_roundtrip, linear_programming_basic) { std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/good-mps-1.mps"; - std::string temp_file = "/tmp/mps_roundtrip_lp_test.mps"; + temp_file_t temp_file(".mps"); // Read original auto original = parse_mps(input_file, true); // Write to temp file mps_writer_t writer(original); - writer.write(temp_file); + writer.write(temp_file.string()); // Read back - auto reloaded = parse_mps(temp_file, false); + auto reloaded = parse_mps(temp_file.string(), false); // Compare compare_data_models(original, reloaded); - - // Cleanup - std::filesystem::remove(temp_file); } TEST(mps_roundtrip, linear_programming_with_bounds) @@ -1191,23 +1215,20 @@ TEST(mps_roundtrip, linear_programming_with_bounds) std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/lp_model_with_var_bounds.mps"; - std::string temp_file = "/tmp/mps_roundtrip_lp_bounds_test.mps"; + temp_file_t temp_file(".mps"); // Read original auto original = parse_mps(input_file, false); // Write to temp file mps_writer_t writer(original); - writer.write(temp_file); + writer.write(temp_file.string()); // Read back - auto reloaded = parse_mps(temp_file, false); + auto reloaded = parse_mps(temp_file.string(), false); // Compare compare_data_models(original, reloaded); - - // Cleanup - std::filesystem::remove(temp_file); } TEST(mps_roundtrip, quadratic_programming_qp_test_1) @@ -1218,7 +1239,7 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_1) std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_1.qps"; - std::string temp_file = "/tmp/mps_roundtrip_qp_test_1.mps"; + temp_file_t temp_file(".mps"); // Read original auto original = parse_mps(input_file, false); @@ -1226,17 +1247,14 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_1) // Write to temp file mps_writer_t writer(original); - writer.write(temp_file); + writer.write(temp_file.string()); // Read back - auto reloaded = parse_mps(temp_file, false); + auto reloaded = parse_mps(temp_file.string(), false); ASSERT_TRUE(reloaded.has_quadratic_objective()) << "Reloaded should have quadratic objective"; // Compare compare_data_models(original, reloaded); - - // Cleanup - std::filesystem::remove(temp_file); } TEST(mps_roundtrip, quadratic_programming_qp_test_2) @@ -1247,7 +1265,7 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_2) std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_2.qps"; - std::string temp_file = "/tmp/mps_roundtrip_qp_test_2.mps"; + temp_file_t temp_file(".mps"); // Read original auto original = parse_mps(input_file, false); @@ -1255,17 +1273,14 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_2) // Write to temp file mps_writer_t writer(original); - writer.write(temp_file); + writer.write(temp_file.string()); // Read back - auto reloaded = parse_mps(temp_file, false); + auto reloaded = parse_mps(temp_file.string(), false); ASSERT_TRUE(reloaded.has_quadratic_objective()) << "Reloaded should have quadratic objective"; // Compare compare_data_models(original, reloaded); - - // Cleanup - std::filesystem::remove(temp_file); } // ================================================================================================ @@ -1278,50 +1293,44 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_2) TEST_F(good_mps_1_test, lp_roundtrip) { - std::string temp_file = "/tmp/lp_roundtrip_lp_basic.mps"; + temp_file_t temp_file(".mps"); auto original = parse_lp_file("linear_programming/good-mps-1.lp"); mps_writer_t writer(original); - writer.write(temp_file); + writer.write(temp_file.string()); - auto reloaded = parse_mps(temp_file, false); + auto reloaded = parse_mps(temp_file.string(), false); compare_data_models(original, reloaded); - - std::filesystem::remove(temp_file); } TEST_F(up_low_bounds_test, lp_roundtrip) { - std::string temp_file = "/tmp/lp_roundtrip_lp_bounds.mps"; + temp_file_t temp_file(".mps"); auto original = parse_lp_file("linear_programming/lp_model_with_var_bounds.lp"); mps_writer_t writer(original); - writer.write(temp_file); + writer.write(temp_file.string()); - auto reloaded = parse_mps(temp_file, false); + auto reloaded = parse_mps(temp_file.string(), false); compare_data_models(original, reloaded); - - std::filesystem::remove(temp_file); } TEST_F(mip_with_bounds_test, lp_roundtrip) { - std::string temp_file = "/tmp/lp_roundtrip_mip_basic.mps"; + temp_file_t temp_file(".mps"); auto original = parse_lp_file("mixed_integer_programming/good-mip-mps-1.lp"); mps_writer_t writer(original); - writer.write(temp_file); + writer.write(temp_file.string()); - auto reloaded = parse_mps(temp_file, false); + auto reloaded = parse_mps(temp_file.string(), false); compare_data_models(original, reloaded); - - std::filesystem::remove(temp_file); } // ================================================================================================ @@ -2489,27 +2498,15 @@ End namespace { // Writes `content` to a temp file with the given suffix, parses it via -// parse_problem, removes the file, and returns the resulting model. +// parse_problem, and returns the resulting model. temp_file_t removes the +// file on every scope exit (including when parse_problem throws). mps_data_model_t dispatch_parse(const std::string& content, const std::string& suffix) { - std::filesystem::path tmp = std::filesystem::temp_directory_path() / - (std::string{"cuopt_dispatch_test_"} + std::to_string(::getpid()) + - "_" + std::to_string(std::rand()) + suffix); + temp_file_t tmp(suffix); { - std::ofstream out(tmp); + std::ofstream out(tmp.string()); out << content; } - // Scope guard: remove the temp file even if parse_problem throws. - // std::error_code is passed so the destructor does not throw during stack - // unwinding when a parse exception is propagating. - struct cleanup_t { - const std::filesystem::path& path; - ~cleanup_t() - { - std::error_code ec; - std::filesystem::remove(path, ec); - } - } cleanup{tmp}; return parse_problem(tmp.string()); } @@ -2881,24 +2878,21 @@ TEST(mps_roundtrip, qcqp_p0033_qc1) { if (!file_exists("qcqp/p0033_qc1.mps")) { GTEST_SKIP() << "Test file not found"; } - std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps"; - std::string temp_file = "/tmp/mps_roundtrip_p0033_qc1.mps"; - std::string temp_file_2 = "/tmp/mps_roundtrip_p0033_qc1_r2.mps"; + std::string input_file = cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps"; + temp_file_t temp_file(".mps"); + temp_file_t temp_file_2(".mps"); auto original = parse_mps(input_file, false); ASSERT_TRUE(original.has_quadratic_objective()); ASSERT_TRUE(original.has_quadratic_constraints()); mps_writer_t writer(original); - writer.write(temp_file); + writer.write(temp_file.string()); - auto reloaded = parse_mps(temp_file, false); + auto reloaded = parse_mps(temp_file.string(), false); mps_writer_t writer_r2(reloaded); - writer_r2.write(temp_file_2); - auto reloaded_2 = parse_mps(temp_file_2, false); + writer_r2.write(temp_file_2.string()); + auto reloaded_2 = parse_mps(temp_file_2.string(), false); compare_data_models(reloaded, reloaded_2); - - std::filesystem::remove(temp_file); - std::filesystem::remove(temp_file_2); } } // namespace cuopt::linear_programming::io From 7047c39e5e54893dcaee36f809ccef2a2ddf85a0 Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Mon, 18 May 2026 00:17:18 +0000 Subject: [PATCH 17/30] docs: fix mps_datamodel_example docstring to name ParseMps The "This example demonstrates how to" bullet pointed at the package path cuopt.linear_programming.io after the recent rename, but the example code calls ParseMps directly (imported from the public top-level). Update the bullet to name the actual function so the prose matches the sample. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- .../cuopt-server/examples/lp/examples/mps_datamodel_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py index 04be2e6987..be372f4bd5 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py +++ b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py @@ -4,7 +4,7 @@ LP DataModel from MPS Parser Example This example demonstrates how to: -- Parse an MPS file using cuopt.linear_programming.io +- Parse an MPS file using cuopt.linear_programming.ParseMps - Create a DataModel from the parsed MPS - Solve using the DataModel via the server - Extract detailed solution information From 9567265d738fb44fc89573befb49b5dc7fdeae37 Mon Sep 17 00:00:00 2001 From: Miles Lubin Date: Mon, 18 May 2026 00:32:25 +0000 Subject: [PATCH 18/30] cuOptReadProblem: validate inputs and return CUOPT_INVALID_ARGUMENT The C API entry point previously dereferenced filename via std::string(filename) without a null check and stored into *problem_ptr without a null check on the out-pointer. A null filename would segfault inside libstdc++'s string constructor, and a null problem_ptr would segfault on assignment; an empty filename would advance into the generic file-not-found / parse-error paths. Validate filename != nullptr && filename[0] != '\0' && problem_ptr != nullptr up front, return CUOPT_INVALID_ARGUMENT immediately on failure, and (importantly) skip the `new problem_and_stream_view_t` allocation that would otherwise leak when the validation fails. The existing parser exception path already handles cleanup for non-null inputs. Document the new return code on the public header docstring and add a test (c_api.read_problem_null_or_empty_inputs_rejected) covering all three invalid combinations. Signed-off-by: Miles Lubin Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/include/cuopt/linear_programming/cuopt_c.h | 12 ++++++++---- cpp/src/pdlp/cuopt_c.cpp | 7 +++++++ .../linear_programming/c_api_tests/c_api_tests.cpp | 13 +++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/cuopt_c.h b/cpp/include/cuopt/linear_programming/cuopt_c.h index 6665ba0fac..74071a5416 100644 --- a/cpp/include/cuopt/linear_programming/cuopt_c.h +++ b/cpp/include/cuopt/linear_programming/cuopt_c.h @@ -108,12 +108,16 @@ cuopt_int_t cuOptGetVersion(cuopt_int_t* version_major, * - ".mps", ".mps.gz", ".mps.bz2", ".qps", ".qps.gz", ".qps.bz2" → MPS parser * - anything else (including no extension) is rejected. * - * @param[in] filename - The path to the MPS, QPS, or LP file. + * @param[in] filename - The path to the MPS, QPS, or LP file. Must be a + * non-null, non-empty C string. * - * @param[out] problem_ptr - A pointer to a cuOptOptimizationProblem. On output - * the problem will be created and initialized with the data from the input file. + * @param[out] problem_ptr - A non-null pointer to a cuOptOptimizationProblem. + * On output the problem will be created and initialized with the data from + * the input file. * - * @return A status code indicating success or failure. + * @return A status code indicating success or failure. Returns + * CUOPT_INVALID_ARGUMENT if filename is null or empty, or if problem_ptr is + * null. */ cuopt_int_t cuOptReadProblem(const char* filename, cuOptOptimizationProblem* problem_ptr); diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index 42c083fdbf..c1142afeef 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -101,6 +101,13 @@ cuopt_int_t cuOptGetVersion(cuopt_int_t* version_major, cuopt_int_t cuOptReadProblem(const char* filename, cuOptOptimizationProblem* problem_ptr) { + // Validate C-API inputs before any allocation. A null/empty filename or a + // null out-pointer cannot succeed and must not leave the user with a + // partially-constructed problem_and_stream_view_t. + if (filename == nullptr || filename[0] == '\0' || problem_ptr == nullptr) { + return CUOPT_INVALID_ARGUMENT; + } + problem_and_stream_view_t* problem_and_stream = new problem_and_stream_view_t(get_memory_backend_type()); std::string filename_str(filename); diff --git a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp index 35d6c2dd1d..6fb4d83c42 100644 --- a/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp +++ b/cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp @@ -115,6 +115,19 @@ TEST(c_api, burglar) { EXPECT_EQ(burglar_problem(), CUOPT_SUCCESS); } TEST(c_api, test_missing_file) { EXPECT_EQ(test_missing_file(), CUOPT_MPS_FILE_ERROR); } +TEST(c_api, read_problem_null_or_empty_inputs_rejected) +{ + cuOptOptimizationProblem handle = nullptr; + // Null filename pointer. + EXPECT_EQ(cuOptReadProblem(nullptr, &handle), CUOPT_INVALID_ARGUMENT); + EXPECT_EQ(handle, nullptr); + // Empty filename string. + EXPECT_EQ(cuOptReadProblem("", &handle), CUOPT_INVALID_ARGUMENT); + EXPECT_EQ(handle, nullptr); + // Null out-pointer. + EXPECT_EQ(cuOptReadProblem("any.lp", nullptr), CUOPT_INVALID_ARGUMENT); +} + // Verifies that cuOptReadProblem dispatches to the LP parser when given a // path with a .lp extension. The input is a minimal LP (1 variable, 1 // constraint); we just check the round-trip read produces the expected shape. From 7e843ac2991dd3328e0ec87bb04a486afd81321b Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Tue, 19 May 2026 17:15:17 -0700 Subject: [PATCH 19/30] Add ParseProblem API and unify file-format parsing in Python. Expose parse_problem via Cython with fixed_mps_format support, add Problem.read(), deprecate readMPS, and migrate callers/tests/docs from ParseMps/ParseLp. Extend self-hosted CI and server examples for LP and compressed inputs. Co-authored-by: Cursor --- ci/test_self_hosted_service.sh | 21 +++- .../cuopt/linear_programming/io/parser.hpp | 7 +- .../io/utilities/cython_parser.hpp | 7 +- cpp/src/io/utilities/cython_parser.cpp | 14 +-- docs/cuopt/source/cuopt-cli/cli-examples.rst | 10 +- .../cuopt-server/examples/lp-examples.rst | 26 +++- .../lp/examples/lp_datamodel_example.py | 97 +++++++++++++++ .../lp/examples/mps_datamodel_example.py | 9 +- docs/cuopt/source/hidden/mps-api.rst | 11 +- docs/cuopt/source/hidden/mps-example.rst | 13 -- docs/cuopt/source/hidden/parser_example.rst | 30 +++++ .../cuopt/linear_programming/__init__.py | 2 +- .../cuopt/linear_programming/io/__init__.py | 2 +- .../cuopt/linear_programming/io/parser.pxd | 8 +- .../cuopt/linear_programming/io/parser.py | 114 +++--------------- .../linear_programming/io/parser_wrapper.pyx | 22 +--- .../cuopt/cuopt/linear_programming/problem.py | 56 ++++++++- .../cuopt/linear_programming/solver/solver.py | 8 +- .../test_cpu_only_execution.py | 22 ++-- .../test_incumbent_callbacks.py | 4 +- .../linear_programming/test_lp_solver.py | 40 +++--- .../tests/linear_programming/test_parser.py | 86 +++++++++++-- .../linear_programming/test_python_API.py | 26 ++++ .../cuopt_sh_client/cuopt_self_host_client.py | 13 +- .../cuopt_server/tests/test_pdlp_warmstart.py | 7 +- python/libcuopt/libcuopt/tests/test_cli.sh | 6 + regression/benchmark_scripts/utils.py | 4 +- 27 files changed, 422 insertions(+), 243 deletions(-) create mode 100644 docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py delete mode 100644 docs/cuopt/source/hidden/mps-example.rst create mode 100644 docs/cuopt/source/hidden/parser_example.rst diff --git a/ci/test_self_hosted_service.sh b/ci/test_self_hosted_service.sh index 5547a7c452..e0ce049b57 100755 --- a/ci/test_self_hosted_service.sh +++ b/ci/test_self_hosted_service.sh @@ -113,14 +113,29 @@ if [ "$doservertest" -eq 1 ]; then # Success, small MILP problem with pure JSON which returns a solution with Optimal status run_cli_test "'status': 'Optimal'" cuopt_sh -s -c $CLIENT_CERT -p $CUOPT_SERVER_PORT -t LP ../../datasets/mixed_integer_programming/milp_data.json - # Succes, small LP problem with mps. Data will be transformed to JSON + # Success, small LP problem with MPS. Data will be transformed to JSON run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.mps - # Succes, small Batch LP problem with mps. Data will be transformed to JSON + # Success, small Batch LP problem with MPS. Data will be transformed to JSON run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.mps ../../datasets/linear_programming/good-mps-1.mps - # Error, local file mode is not allowed with mps + # Success, small LP problem with LP format. Data will be transformed to JSON + run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.lp + + # Success, small Batch LP problem with LP format. Data will be transformed to JSON + run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.lp ../../datasets/linear_programming/good-mps-1.lp + + # Success, compressed LP inputs (.lp.gz / .lp.bz2) via ParseProblem dispatch + run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.lp.gz + run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.lp.bz2 + + # Success, compressed MPS inputs (.mps.gz / .mps.bz2) for parity + run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.mps.gz + run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.mps.bz2 + + # Error, local file mode is not allowed with MPS/LP file inputs run_cli_test "Cannot use local file mode with MPS/LP data" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP -f good-mps-1.mps + run_cli_test "Cannot use local file mode with MPS/LP data" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP -f good-mps-1.lp # Just run validator cp ../../datasets/cuopt_service_data/cuopt_problem_data.json "$CUOPT_DATA_DIR" diff --git a/cpp/include/cuopt/linear_programming/io/parser.hpp b/cpp/include/cuopt/linear_programming/io/parser.hpp index a175e821cd..9e4ea3f8f0 100644 --- a/cpp/include/cuopt/linear_programming/io/parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/parser.hpp @@ -120,10 +120,13 @@ mps_data_model_t parse_lp_from_string(std::string_view lp_contents); * want both formats to "just work" without an explicit format flag. * * @param[in] path Path to the input file. + * @param[in] fixed_mps_format If the MPS/QPS reader should use fixed format; + * ignored for LP inputs. False by default. * @return mps_data_model_t The parsed problem. */ template -inline mps_data_model_t parse_problem(const std::string& path) +inline mps_data_model_t parse_problem(const std::string& path, + bool fixed_mps_format = false) { std::string lower(path); std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { @@ -131,7 +134,7 @@ inline mps_data_model_t parse_problem(const std::string& path) }); if (lower.ends_with(".mps") || lower.ends_with(".mps.gz") || lower.ends_with(".mps.bz2") || lower.ends_with(".qps") || lower.ends_with(".qps.gz") || lower.ends_with(".qps.bz2")) { - return parse_mps(path); + return parse_mps(path, fixed_mps_format); } if (lower.ends_with(".lp") || lower.ends_with(".lp.gz") || lower.ends_with(".lp.bz2")) { return parse_lp(path); diff --git a/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp index eb4044d1d0..2c98462614 100644 --- a/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp @@ -14,11 +14,8 @@ namespace cuopt { namespace cython { -std::unique_ptr> call_parse_mps( - const std::string& mps_file_path, bool fixed_mps_format); - -std::unique_ptr> call_parse_lp( - const std::string& lp_file_path); +std::unique_ptr> call_parse_problem( + const std::string& file_path, bool fixed_mps_format); } // namespace cython } // namespace cuopt diff --git a/cpp/src/io/utilities/cython_parser.cpp b/cpp/src/io/utilities/cython_parser.cpp index 86bd0bb77d..d0e0902bc1 100644 --- a/cpp/src/io/utilities/cython_parser.cpp +++ b/cpp/src/io/utilities/cython_parser.cpp @@ -11,18 +11,12 @@ namespace cuopt { namespace cython { -std::unique_ptr> call_parse_mps( - const std::string& mps_file_path, bool fixed_mps_format) -{ - return std::make_unique>(std::move( - cuopt::linear_programming::io::parse_mps(mps_file_path, fixed_mps_format))); -} - -std::unique_ptr> call_parse_lp( - const std::string& lp_file_path) +std::unique_ptr> call_parse_problem( + const std::string& file_path, bool fixed_mps_format) { return std::make_unique>( - std::move(cuopt::linear_programming::io::parse_lp(lp_file_path))); + std::move( + cuopt::linear_programming::io::parse_problem(file_path, fixed_mps_format))); } } // namespace cython diff --git a/docs/cuopt/source/cuopt-cli/cli-examples.rst b/docs/cuopt/source/cuopt-cli/cli-examples.rst index 68ae0adba1..4a996dc106 100644 --- a/docs/cuopt/source/cuopt-cli/cli-examples.rst +++ b/docs/cuopt/source/cuopt-cli/cli-examples.rst @@ -13,9 +13,13 @@ format is dispatched automatically from the file extension variants) → parsed as MPS / QPS Any other extension (including no extension) is rejected with an error -listing the supported suffixes. See the ``parse_lp`` / ``parse_mps`` -declarations in ``cuopt/linear_programming/io/parser.hpp`` for the -supported subset of each format. +listing the supported suffixes. See ``parse_problem`` in +``cuopt/linear_programming/io/parser.hpp`` (and the Python +:func:`~cuopt.linear_programming.ParseProblem` wrapper). + +The ``good-mps-1`` fixtures under ``datasets/linear_programming/`` include +plain and compressed ``.mps`` / ``.lp`` files used in parser and CLI tests; +see ``good-mps-1-README.md`` in that directory. Basic Usage ########### diff --git a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst index 12121a4cf8..a47a768d76 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst +++ b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst @@ -261,10 +261,17 @@ The response is: } -Generate Datamodel from MPS Parser ----------------------------------- +Generate Datamodel using Problem File Parser +------------------------------------------- -Use a datamodel generated from mps file as input; this yields a solution object in response. For more details please refer to :doc:`LP/QP/MILP parameters <../../lp-qp-milp-settings>`. +Use a :class:`~cuopt.linear_programming.data_model.DataModel` built with +:func:`~cuopt.linear_programming.ParseProblem` as input to ``get_LP_solve``; +the client dispatches on the file extension (``.mps`` / ``.qps`` vs ``.lp``, +including ``.gz`` / ``.bz2`` compressed variants). For solver settings see +:doc:`LP/QP/MILP parameters <../../lp-qp-milp-settings>`. + +MPS format +~~~~~~~~~~ :download:`mps_datamodel_example.py ` @@ -272,8 +279,17 @@ Use a datamodel generated from mps file as input; this yields a solution object :language: python :linenos: +LP format +~~~~~~~~~ -The response would be as follows: +:download:`lp_datamodel_example.py ` + +.. literalinclude:: lp/examples/lp_datamodel_example.py + :language: python + :linenos: + +Expected output (either example, same problem instance) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: text :linenos: @@ -282,7 +298,7 @@ The response would be as follows: 1 Objective Value: -0.36000000000000004 - Mps Parse time: 0.000 sec + MPS Parse time: 0.000 sec Network time: 1.062 sec Engine Solve time: 0.004 sec Total end to end time: 1.066 sec diff --git a/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py b/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py new file mode 100644 index 0000000000..45ee9052e5 --- /dev/null +++ b/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +LP DataModel from LP file parser example + +This example demonstrates how to: +- Parse an LP-format file using cuopt.linear_programming.ParseProblem +- Create a DataModel from the parsed problem +- Solve using the DataModel via the server +- Extract detailed solution information + +Requirements: + - cuOpt server running (default: localhost:5000) + - cuopt_sh_client package installed + - cuopt package installed + +Problem (in LP format; same instance as the MPS datamodel example): + Minimize: -0.2*VAR1 + 0.1*VAR2 + Subject to: + 3*VAR1 + 4*VAR2 <= 5.4 + 2.7*VAR1 + 10.1*VAR2 <= 4.9 + VAR1, VAR2 >= 0 + +Expected Output: + Termination Reason: 1 (Optimal) + Objective Value: -0.36 + Variables Values: {'VAR1': 1.8, 'VAR2': 0.0} +""" + +from cuopt_sh_client import ( + CuOptServiceSelfHostClient, + ThinClientSolverSettings, + PDLPSolverMode, +) +from cuopt.linear_programming import ParseProblem +import time + + +def main(): + """Run the LP file DataModel example.""" + data = "sample.lp" + + lp_data = r"""\ Same problem as mps_datamodel_example.py (good-1) +Minimize + -0.2 VAR1 + 0.1 VAR2 +Subject To + ROW1: 3 VAR1 + 4 VAR2 <= 5.4 + ROW2: 2.7 VAR1 + 10.1 VAR2 <= 4.9 +End +""" + + with open(data, "w") as file: + file.write(lp_data) + + print(f"Created LP file: {data}") + + print("\n=== Parsing LP File ===") + parse_start = time.time() + data_model = ParseProblem(data) + parse_time = time.time() - parse_start + print(f"Parse time: {parse_time:.3f} seconds") + + cuopt_service_client = CuOptServiceSelfHostClient( + ip="localhost", port=5000, timeout_exception=False + ) + + ss = ThinClientSolverSettings() + ss.set_parameter("pdlp_solver_mode", PDLPSolverMode.Fast1) + ss.set_optimality_tolerance(1e-4) + ss.set_parameter("time_limit", 5) + + print("\n=== Solving with Server ===") + network_time = time.time() + solution = cuopt_service_client.get_LP_solve(data_model, ss) + network_time = time.time() - network_time + + solution_status = solution["response"]["solver_response"]["status"] + solution_obj = solution["response"]["solver_response"]["solution"] + + print("\n=== Results ===") + print(f"Termination Reason: {solution_status}") + print(f"Objective Value: {solution_obj.get_primal_objective()}") + print(f"LP Parse time: {parse_time:.3f} sec") + + network_time = network_time - (solution_obj.get_solve_time()) + print(f"Network time: {network_time:.3f} sec") + + solve_time = solution_obj.get_solve_time() + print(f"Engine Solve time: {solve_time:.3f} sec") + + end_to_end_time = parse_time + network_time + solve_time + print(f"Total end to end time: {end_to_end_time:.3f} sec") + print(f"Variables Values: {solution_obj.get_vars()}") + + +if __name__ == "__main__": + main() diff --git a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py index be372f4bd5..552feab86a 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py +++ b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py @@ -1,10 +1,10 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ -LP DataModel from MPS Parser Example +LP DataModel from MPS file parser example This example demonstrates how to: -- Parse an MPS file using cuopt.linear_programming.ParseMps +- Parse an MPS file using cuopt.linear_programming.ParseProblem - Create a DataModel from the parsed MPS - Solve using the DataModel via the server - Extract detailed solution information @@ -32,7 +32,7 @@ ThinClientSolverSettings, PDLPSolverMode, ) -from cuopt.linear_programming import ParseMps +from cuopt.linear_programming import ParseProblem import time @@ -65,7 +65,7 @@ def main(): # Parse the MPS file and measure the time spent print("\n=== Parsing MPS File ===") parse_start = time.time() - data_model = ParseMps(data) + data_model = ParseProblem(data) parse_time = time.time() - parse_start print(f"Parse time: {parse_time:.3f} seconds") @@ -110,7 +110,6 @@ def main(): # Check found objective value print(f"Objective Value: {solution_obj.get_primal_objective()}") - # Check the MPS parse time print(f"MPS Parse time: {parse_time:.3f} sec") # Check network time (client call - solve time) diff --git a/docs/cuopt/source/hidden/mps-api.rst b/docs/cuopt/source/hidden/mps-api.rst index 637d8a03cc..a7534376f4 100644 --- a/docs/cuopt/source/hidden/mps-api.rst +++ b/docs/cuopt/source/hidden/mps-api.rst @@ -2,12 +2,7 @@ cuOpt MPS/LP Parser API Reference =============================== -MPS Parser ----------- +MPS/QPS/LP parser +------------------- -.. autofunction:: cuopt.linear_programming.io.ParseMps - -LP Parser ---------- - -.. autofunction:: cuopt.linear_programming.io.ParseLp +.. autofunction:: cuopt.linear_programming.io.ParseProblem diff --git a/docs/cuopt/source/hidden/mps-example.rst b/docs/cuopt/source/hidden/mps-example.rst deleted file mode 100644 index cc6495d2b4..0000000000 --- a/docs/cuopt/source/hidden/mps-example.rst +++ /dev/null @@ -1,13 +0,0 @@ -~~~~~~~~~~~~~~~~~~~~~~~~ -cuOpt MPS Parser Example -~~~~~~~~~~~~~~~~~~~~~~~~ - - -Example -------- - -.. code-block:: python - :linenos: - - from cuopt.linear_programming import ParseMps - x = ParseMps('good-mps-1.mps') diff --git a/docs/cuopt/source/hidden/parser_example.rst b/docs/cuopt/source/hidden/parser_example.rst new file mode 100644 index 0000000000..f19c1511a5 --- /dev/null +++ b/docs/cuopt/source/hidden/parser_example.rst @@ -0,0 +1,30 @@ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +cuOpt problem file parser example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Example +------- + +Read MPS, QPS, or LP files (including ``.gz`` / ``.bz2`` compressed variants) +with :func:`~cuopt.linear_programming.ParseProblem`: + +.. code-block:: python + :linenos: + + from cuopt.linear_programming import ParseProblem + from cuopt.linear_programming.problem import Problem + + # MPS / QPS + mps_model = ParseProblem("good-mps-1.mps") + + # LP (plain or compressed) + lp_model = ParseProblem("good-mps-1.lp") + lp_gz = ParseProblem("good-mps-1.lp.gz") + + # High-level API + problem = Problem.read("good-mps-1.lp") + +See ``datasets/linear_programming/good-mps-1-README.md`` for the full set of +fixture files and why compressed ``.lp.gz`` / ``.lp.bz2`` variants are kept in +the tree. diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index 6950d72bc8..f2d0aca1ce 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -3,7 +3,7 @@ from cuopt.linear_programming import internals from cuopt.linear_programming.data_model import DataModel -from cuopt.linear_programming.io import ParseLp, ParseMps +from cuopt.linear_programming.io import ParseProblem from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solution import Solution from cuopt.linear_programming.solver import BatchSolve, Solve diff --git a/python/cuopt/cuopt/linear_programming/io/__init__.py b/python/cuopt/cuopt/linear_programming/io/__init__.py index f81e9369ec..1eb1e56f10 100644 --- a/python/cuopt/cuopt/linear_programming/io/__init__.py +++ b/python/cuopt/cuopt/linear_programming/io/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -from cuopt.linear_programming.io.parser import ParseLp, ParseMps, toDict +from cuopt.linear_programming.io.parser import ParseProblem, toDict diff --git a/python/cuopt/cuopt/linear_programming/io/parser.pxd b/python/cuopt/cuopt/linear_programming/io/parser.pxd index 402873f9ab..04c430ffbb 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.pxd +++ b/python/cuopt/cuopt/linear_programming/io/parser.pxd @@ -39,11 +39,7 @@ cdef extern from "cuopt/linear_programming/io/mps_data_model.hpp" namespace "cuo cdef extern from "cuopt/linear_programming/io/utilities/cython_parser.hpp" namespace "cuopt::cython": # noqa - cdef unique_ptr[mps_data_model_t[int, double]] call_parse_mps( - const string& mps_file_path, + cdef unique_ptr[mps_data_model_t[int, double]] call_parse_problem( + const string& file_path, bool fixed_mps_format ) except + - - cdef unique_ptr[mps_data_model_t[int, double]] call_parse_lp( - const string& lp_file_path - ) except + diff --git a/python/cuopt/cuopt/linear_programming/io/parser.py b/python/cuopt/cuopt/linear_programming/io/parser.py index 385edfefe1..d5e200043b 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.py +++ b/python/cuopt/cuopt/linear_programming/io/parser.py @@ -10,114 +10,40 @@ @catch_io_exception -def ParseMps(mps_file_path, fixed_mps_format=False): - """ - Reads the equation from the input text file which is MPS formatted +def ParseProblem(file_path: str, fixed_mps_format: bool = False) -> DataModel: + """Read an optimization problem from a file, dispatching on extension. - See Also - -------- - ParseLp : parses LP format files (for users with .lp inputs). + Dispatches to the MPS/QPS or LP parser based on the filename suffix + (case-insensitive), matching the C++ ``parse_problem`` entry point: - Notes - ----- - Read this link http://lpsolve.sourceforge.net/5.5/mps-format.htm for more - details on both free and fixed MPS format. + - ``.mps``, ``.mps.gz``, ``.mps.bz2``, ``.qps``, ``.qps.gz``, ``.qps.bz2`` + → MPS/QPS reader + - ``.lp``, ``.lp.gz``, ``.lp.bz2`` → LP reader Parameters ---------- - mps_file_path : str - Path to MPS formatted file + file_path : str + Path to an MPS, QPS, or LP file (optionally ``.gz`` / ``.bz2`` + compressed). fixed_mps_format : bool - If MPS file should be parsed as fixed, false by default - - Returns - ------- - data_model: DataModel - A fully formed LP problem which represents the given MPS file - - Examples - -------- - >>> from cuopt import linear_programming - >>> - >>> data_model = linear_programming.ParseMps(mps_file_path) - >>> - >>> # Build a solver setting object & lower the accuracy from 1e-4 to 1e-2 - >>> solver_settings = linear_programming.SolverSettings() - >>> solver_settings.set_optimality_tolerance(1e-2) - >>> - >>> # Call solve - >>> solution = linear_programming.Solve(data_model, solver_settings) - >>> - >>> # Print solution - >>> print(solution.get_primal_solution()) - """ - - return parser_wrapper.ParseMps(mps_file_path, fixed_mps_format) - - -@catch_io_exception -def ParseLp(lp_file_path: str) -> DataModel: - """Read an optimization problem from a file in LP format. - - The LP format is a human-readable alternative to MPS and supports LP, - MIP, and QP, plus semi-continuous variables (declared via a - Semi-Continuous section; finite upper bound required) and - quadratic constraints (QCQP; ``<=`` only). - - Quadratic terms live in ``[ ... ]`` blocks. The objective bracket must - be followed by ``/ 2`` (the file states coefficients in the - ``0.5 x^T Q x`` convention); a constraint bracket must NOT be followed - by ``/ 2`` (coefficients are at face value, ``x^T Q x``). Only squared - (``x^2``) and product (``x * y``) terms are allowed inside the - bracket; bare linear terms must be written outside it. - - This function parses the dialect in which the objective and constraints - are written as algebraic expressions over named variables (it does not - implement the alternative tableau-style LP dialect used by some - open-source readers). - - Parameters - ---------- - lp_file_path : str - Path to LP-formatted file. May end in ``.lp``, ``.lp.gz``, or - ``.lp.bz2``; compressed inputs are decompressed at read time - via zlib / libbz2 when those libraries are available. + If the MPS/QPS reader should parse as fixed MPS format. Ignored for + LP inputs. False by default. Returns ------- data_model : DataModel - A fully formed LP/MIP/QP problem representing the contents of - ``lp_file_path``. + A fully formed LP/MILP/QP problem. Raises ------ - InputValidationError - Raised when ``lp_file_path`` is malformed or uses unsupported - syntax. Examples include unsupported sections (SOS, PWL - objective, user cuts, general constraints), bare linear terms - inside a quadratic ``[ ... ]`` bracket, an objective bracket - not followed by ``/ 2``, a constraint bracket followed by - ``/ 2``, a semi-continuous variable without a finite upper - bound, and similar input-level errors raised by the underlying - C++ parser. Exceptions propagated from - :func:`parser_wrapper.ParseLp` are translated to this type by - :func:`catch_io_exception`. - InputRuntimeError - Raised for non-validation runtime errors that the C++ parser - flags during file I/O or parsing. - OutOfMemoryError - Raised when the parser cannot allocate memory for the - resulting data model. - - Examples - -------- - >>> from cuopt import linear_programming - >>> - >>> data_model = linear_programming.ParseLp(lp_file_path) - >>> solver_settings = linear_programming.SolverSettings() - >>> solution = linear_programming.Solve(data_model, solver_settings) + InputValidationError, InputRuntimeError, OutOfMemoryError + Parser errors from the underlying C++ readers (via + :func:`catch_io_exception`). + RuntimeError + If the file extension is not one of the supported suffixes (raised by + the C++ ``parse_problem`` dispatch). """ - return parser_wrapper.ParseLp(lp_file_path) + return parser_wrapper.ParseProblem(file_path, fixed_mps_format) def toDict(model, json=False): diff --git a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx index 61e10b1864..fd7ea052cf 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx @@ -16,7 +16,7 @@ from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move -from .parser cimport call_parse_lp, call_parse_mps, mps_data_model_t +from .parser cimport call_parse_problem, mps_data_model_t import warnings @@ -32,8 +32,7 @@ def type_cast(np_obj, np_type, name): # Copies the C++ data model behind `dm` into the Python-side `data_model`. -# Shared by ParseMps and ParseLp — every field on mps_data_model_t is -# format-agnostic. +# Copies every field on mps_data_model_t into the Python DataModel. cdef _marshal_data_model(mps_data_model_t[int, double]* dm, data_model): A_values_data = dm.A_.data() A_values_size = dm.A_.size() @@ -139,21 +138,12 @@ cdef _marshal_data_model(mps_data_model_t[int, double]* dm, data_model): @catch_io_exception -def ParseMps(mps_file_path, fixed_mps_formats): +def ParseProblem(file_path, fixed_mps_format=False): data_model = DataModel() dm_ret_ptr = move( - call_parse_mps( - mps_file_path.encode('utf-8'), - fixed_mps_formats + call_parse_problem( + file_path.encode('utf-8'), + fixed_mps_format, ) ) return _marshal_data_model(dm_ret_ptr.get(), data_model) - - -@catch_io_exception -def ParseLp(lp_file_path): - data_model = DataModel() - dm_ret_ptr = move( - call_parse_lp(lp_file_path.encode('utf-8')) - ) - return _marshal_data_model(dm_ret_ptr.get(), data_model) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 0f4c6c3846..d129bc548c 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -3,13 +3,14 @@ import copy +import os from enum import Enum import numpy as np from scipy.sparse import coo_matrix import cuopt.linear_programming.data_model as data_model -import cuopt.linear_programming.io as mps_parser +from cuopt.linear_programming import ParseProblem import cuopt.linear_programming.solver as solver import cuopt.linear_programming.solver_settings as solver_settings import warnings @@ -1789,19 +1790,64 @@ def getConstraint(self, identifier): return c @classmethod - def readMPS(cls, mps_file): + def read(cls, file_path, fixed_mps_format=False): """ - Initiliaze a problem from an `MPS `__ file. # noqa + Initialize a problem from an MPS, QPS, or LP file. + + Dispatches on the file extension via the C++ ``parse_problem`` entry + point (case-insensitive): ``.mps`` / ``.qps`` (and ``.gz`` / ``.bz2`` + variants) use the MPS/QPS reader; ``.lp`` (and compressed variants) + use the LP reader. + + Parameters + ---------- + file_path : str + Path to an MPS, QPS, or LP file. + fixed_mps_format : bool + If the MPS/QPS reader should parse as fixed MPS format. Ignored + for LP inputs. False by default. + + Returns + ------- + Problem + A problem populated from the file. + Examples -------- - >>> problem = problem.Problem.readMPS("model.mps") + >>> problem = problem.Problem.read("model.mps") + >>> lp_problem = problem.Problem.read("model.lp") """ + if not isinstance(file_path, str) or not file_path: + raise ValueError("file_path must be a non-empty string") + if not os.path.isfile(file_path): + raise FileNotFoundError(f"No such file: {file_path}") + problem = cls() - data_model = mps_parser.ParseMps(mps_file) + data_model = ParseProblem(file_path, fixed_mps_format) problem._from_data_model(data_model) problem.model = data_model return problem + @classmethod + def readMPS(cls, mps_file): + """ + Initialize a problem from an `MPS `__ file. # noqa + + .. deprecated:: + Use :meth:`read` instead. + + Examples + -------- + >>> problem = problem.Problem.readMPS("model.mps") + """ + warnings.warn( + "Problem.readMPS is deprecated and will be removed in a future " + "release. Use Problem.read instead.", + DeprecationWarning, + stacklevel=2, + ) + return cls.read(mps_file) + def writeMPS(self, mps_file): """ Write the problem into an `MPS `__ file. # noqa diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.py b/python/cuopt/cuopt/linear_programming/solver/solver.py index 3c72956742..949147fcd1 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.py +++ b/python/cuopt/cuopt/linear_programming/solver/solver.py @@ -16,7 +16,7 @@ def Solve(data_model, solver_settings=None): Data Model object can be construed through setters (see linear_programming.DataModel class) or through a MPS file - (see cuopt.linear_programming.ParseMps function) + (see cuopt.linear_programming.ParseProblem function) Notes @@ -113,7 +113,7 @@ def BatchSolve(data_model_list, solver_settings=None): Data Model objects can be construed through setters (see linear_programming.DataModel class) or through a MPS file - (see cuopt.linear_programming.ParseMps function) + (see cuopt.linear_programming.ParseProblem function) Notes @@ -149,11 +149,11 @@ def BatchSolve(data_model_list, solver_settings=None): >>> from cuopt import linear_programming >>> from cuopt.linear_programming.solver_settings import PDLPSolverMode >>> from cuopt.linear_programming.solver.solver_parameters import * - >>> from cuopt.linear_programming import ParseMps + >>> from cuopt.linear_programming import ParseProblem >>> >>> data_models = [] >>> for i in range(...): - >>> data_models.append(ParseMps(...)) + >>> data_models.append(ParseProblem(...)) >>> >>> # Build a solver setting object >>> settings = linear_programming.SolverSettings() diff --git a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py index 1c3a83b162..30e74c0f92 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py @@ -23,7 +23,7 @@ import sys import time -from cuopt.linear_programming import io as mps_parser +from cuopt.linear_programming import ParseProblem import pytest from cuopt import linear_programming from cuopt.linear_programming.solver.solver_parameters import CUOPT_TIME_LIMIT @@ -301,11 +301,9 @@ def _run_in_subprocess(func, env=None, timeout=120): def _impl_lp_solve_cpu_only(): """LP solve returns correctly-sized solution vectors.""" from cuopt import linear_programming - from cuopt.linear_programming import io as mps_parser - dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" - dm = mps_parser.ParseMps(mps_file) + dm = ParseProblem(mps_file) n_vars = len(dm.get_objective_coefficients()) solution = linear_programming.Solve( @@ -331,11 +329,9 @@ def _impl_lp_solve_cpu_only(): def _impl_lp_dual_solution_cpu_only(): """Dual solution and reduced costs are correctly sized.""" from cuopt import linear_programming - from cuopt.linear_programming import io as mps_parser - dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" - dm = mps_parser.ParseMps(mps_file) + dm = ParseProblem(mps_file) n_vars = len(dm.get_objective_coefficients()) n_cons = len(dm.get_constraint_bounds()) @@ -364,11 +360,9 @@ def _impl_mip_solve_cpu_only(): from cuopt.linear_programming.solver.solver_parameters import ( CUOPT_TIME_LIMIT, ) - from cuopt.linear_programming import io as mps_parser - dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/mip/bb_optimality.mps" - dm = mps_parser.ParseMps(mps_file) + dm = ParseProblem(mps_file) n_vars = len(dm.get_objective_coefficients()) settings = linear_programming.SolverSettings() @@ -400,11 +394,9 @@ def _impl_warmstart_cpu_only(): CUOPT_PRESOLVE, ) from cuopt.linear_programming.solver_settings import SolverMethod - from cuopt.linear_programming import io as mps_parser - dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" - dm = mps_parser.ParseMps(mps_file) + dm = ParseProblem(mps_file) settings = linear_programming.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) @@ -658,7 +650,7 @@ def test_lp_solution_values(self): mps_file = ( f"{RAPIDS_DATASET_ROOT_DIR}/linear_programming/afiro_original.mps" ) - dm = mps_parser.ParseMps(mps_file) + dm = ParseProblem(mps_file) n_vars = len(dm.get_objective_coefficients()) n_cons = len(dm.get_constraint_bounds()) @@ -687,7 +679,7 @@ def test_lp_solution_values(self): def test_mip_solution_values(self): """MIP solve of bb_optimality.mps returns valid stats.""" mps_file = f"{RAPIDS_DATASET_ROOT_DIR}/mip/bb_optimality.mps" - dm = mps_parser.ParseMps(mps_file) + dm = ParseProblem(mps_file) n_vars = len(dm.get_objective_coefficients()) settings = linear_programming.SolverSettings() diff --git a/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py b/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py index 20b0b0cca6..1cdd86c067 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py @@ -3,7 +3,7 @@ import os -from cuopt.linear_programming import io as mps_parser +from cuopt.linear_programming import ParseProblem import pytest from cuopt.linear_programming import solver, solver_settings @@ -85,7 +85,7 @@ def set_solution( ) file_path = RAPIDS_DATASET_ROOT_DIR + file_name - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_TIME_LIMIT, 10) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py index a3eab8d98e..8f9a666b78 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py @@ -3,7 +3,7 @@ import os -from cuopt.linear_programming import io as mps_parser +from cuopt.linear_programming import ParseProblem import numpy as np import pytest @@ -92,7 +92,7 @@ def test_solver(): def test_parser_and_solver(): file_path = RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-1.mps" - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_optimality_tolerance(1e-2) @@ -104,7 +104,7 @@ def test_very_low_tolerance(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_optimality_tolerance(1e-12) @@ -127,7 +127,7 @@ def test_iteration_limit_solver(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/savsched1/savsched1.mps" ) - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_optimality_tolerance(1e-12) @@ -148,7 +148,7 @@ def test_time_limit_solver(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/savsched1/savsched1.mps" ) - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_optimality_tolerance(1e-12) @@ -309,7 +309,7 @@ def test_solver_settings(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-1.mps" ) - solver.Solve(mps_parser.ParseMps(file_path), settings) + solver.Solve(ParseProblem(file_path), settings) settings.set_parameter(CUOPT_PDLP_SOLVER_MODE, PDLPSolverMode.Methodical1) assert settings.get_parameter(CUOPT_PDLP_SOLVER_MODE) == int( @@ -383,7 +383,7 @@ def test_parse_var_names(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) expected_names = [ "X01", @@ -482,7 +482,7 @@ def test_parser_and_batch_solver(): nb_solves = 5 for i in range(nb_solves): - data_model_list.append(mps_parser.ParseMps(file_path)) + data_model_list.append(ParseProblem(file_path)) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) @@ -495,7 +495,7 @@ def test_parser_and_batch_solver(): individual_solutions = [] * nb_solves for i in range(nb_solves): individual_solution = solver.Solve( - mps_parser.ParseMps(file_path), settings + ParseProblem(file_path), settings ) individual_solutions.append(individual_solution) @@ -509,7 +509,7 @@ def test_parser_and_batch_solver(): def test_warm_start(): file_path = RAPIDS_DATASET_ROOT_DIR + "/linear_programming/a2864/a2864.mps" - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) @@ -542,7 +542,7 @@ def test_warm_start(): def test_warm_start_other_problem(): file_path = RAPIDS_DATASET_ROOT_DIR + "/linear_programming/a2864/a2864.mps" - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_PDLP_SOLVER_MODE, PDLPSolverMode.Stable2) @@ -554,7 +554,7 @@ def test_warm_start_other_problem(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj2 = mps_parser.ParseMps(file_path) + data_model_obj2 = ParseProblem(file_path) settings.set_pdlp_warm_start_data(solution.get_pdlp_warm_start_data()) # Should raise an exception as problems are different @@ -571,13 +571,13 @@ def test_batch_solver_warm_start(): nb_solves = 2 for i in range(nb_solves): - data_model_list.append(mps_parser.ParseMps(file_path)) + data_model_list.append(ParseProblem(file_path)) settings = solver_settings.SolverSettings() settings.set_optimality_tolerance(1e-3) # Solve a first time to get a warm start - solution = solver.Solve(mps_parser.ParseMps(file_path), settings) + solution = solver.Solve(ParseProblem(file_path), settings) settings.set_pdlp_warm_start_data(solution.get_pdlp_warm_start_data()) @@ -590,7 +590,7 @@ def test_dual_simplex(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.DualSimplex) @@ -637,7 +637,7 @@ def test_barrier(): def test_heuristics_only(): file_path = RAPIDS_DATASET_ROOT_DIR + "/mip/swath1.mps" - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_MIP_HEURISTICS_ONLY, True) @@ -704,7 +704,7 @@ def test_write_files(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.DualSimplex) @@ -714,7 +714,7 @@ def test_write_files(): assert os.path.isfile("afiro_out.mps") - afiro = mps_parser.ParseMps("afiro_out.mps") + afiro = ParseProblem("afiro_out.mps") os.remove("afiro_out.mps") settings.set_parameter(CUOPT_USER_PROBLEM_FILE, "") @@ -755,7 +755,7 @@ def test_pdlp_precision_single(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) @@ -775,7 +775,7 @@ def test_pdlp_precision_single_crossover(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj = mps_parser.ParseMps(file_path) + data_model_obj = ParseProblem(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_parser.py b/python/cuopt/cuopt/tests/linear_programming/test_parser.py index 795f1fe298..e92e7d25cc 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_parser.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_parser.py @@ -4,7 +4,7 @@ import os import tempfile -from cuopt.linear_programming import io as mps_parser +from cuopt.linear_programming import ParseProblem import numpy as np import pytest from cuopt.linear_programming.io.utilities import InputValidationError @@ -14,6 +14,47 @@ RAPIDS_DATASET_ROOT_DIR = os.getcwd() RAPIDS_DATASET_ROOT_DIR = os.path.join(RAPIDS_DATASET_ROOT_DIR, "datasets") +GOOD_MPS_1_DIR = os.path.join(RAPIDS_DATASET_ROOT_DIR, "linear_programming") + +# Plain and compressed encodings of the same tiny LP (see good-mps-1-README.md). +GOOD_MPS_1_VARIANTS = ( + "good-mps-1.mps", + "good-mps-1.lp", + "good-mps-1.mps.gz", + "good-mps-1.mps.bz2", + "good-mps-1.lp.gz", + "good-mps-1.lp.bz2", +) + + +def _assert_good_mps_1_model(data_model): + """Same checks as C++ good_mps_1_test::check_model.""" + assert not data_model.get_sense() + assert data_model.get_variable_names().tolist() == ["VAR1", "VAR2"] + assert data_model.get_row_names().tolist() == ["ROW1", "ROW2"] + assert data_model.get_objective_coefficients().tolist() == pytest.approx( + [0.2, 0.1] + ) + assert data_model.get_variable_lower_bounds().tolist() == pytest.approx( + [0.0, 0.0] + ) + assert np.isinf(data_model.get_variable_upper_bounds()[0]) + assert np.isinf(data_model.get_variable_upper_bounds()[1]) + assert data_model.get_constraint_upper_bounds().tolist() == pytest.approx( + [5.4, 4.9] + ) + assert data_model.get_constraint_matrix_values().tolist() == pytest.approx( + [3.0, 4.0, 2.7, 10.1] + ) + + +@pytest.mark.parametrize("filename", GOOD_MPS_1_VARIANTS) +def test_parse_problem_good_mps_1_variants(filename): + path = os.path.join(GOOD_MPS_1_DIR, filename) + if not os.path.isfile(path): + pytest.skip(f"missing dataset {path}") + _assert_good_mps_1_model(ParseProblem(path)) + def test_bad_mps_files(): NumMpsFiles = 13 @@ -23,14 +64,14 @@ def test_bad_mps_files(): ) if os.path.exists(file_path): with pytest.raises(InputValidationError): - mps_parser.ParseMps(file_path, True) + ParseProblem(file_path, fixed_mps_format=True) def test_good_mps_file(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.mps" ) - data_model = mps_parser.ParseMps(file_path) + data_model = ParseProblem(file_path) assert not data_model.get_sense() @@ -71,7 +112,7 @@ def test_good_mps_file(): # Minimal LP content that should parse identically regardless of whether it's -# routed through ParseLp() or the server's extension-based dispatch path. +# routed through ParseProblem() or the server's extension-based dispatch path. _MINIMAL_LP = """ Minimize x @@ -90,7 +131,7 @@ def test_parse_lp_basic(): f.write(_MINIMAL_LP) path = f.name try: - data_model = mps_parser.ParseLp(path) + data_model = ParseProblem(path) finally: os.unlink(path) @@ -127,7 +168,7 @@ def test_parse_lp_rejects_unsupported_section(): path = f.name try: with pytest.raises(InputValidationError): - mps_parser.ParseLp(path) + ParseProblem(path) finally: os.unlink(path) @@ -161,8 +202,8 @@ def test_parse_lp_and_parse_mps_agree_on_trivial_problem(): f.write(_MINIMAL_LP) lp_path = f.name try: - lp_model = mps_parser.ParseLp(lp_path) - mps_model = mps_parser.ParseMps(mps_path) + lp_model = ParseProblem(lp_path) + mps_model = ParseProblem(mps_path) finally: os.unlink(mps_path) os.unlink(lp_path) @@ -184,3 +225,32 @@ def test_parse_lp_and_parse_mps_agree_on_trivial_problem(): lp_model.get_constraint_lower_bounds().tolist() == mps_model.get_constraint_lower_bounds().tolist() ) + + +def test_parse_problem_dispatches_mps_and_lp(): + mps_path = ( + RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.mps" + ) + lp_path = ( + RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.lp" + ) + mps_model = ParseProblem(mps_path) + lp_model = ParseProblem(lp_path) + assert mps_model.get_sense() == lp_model.get_sense() + assert ( + mps_model.get_variable_names().tolist() + == lp_model.get_variable_names().tolist() + ) + + +def test_parse_problem_unrecognized_extension(): + with tempfile.NamedTemporaryFile(suffix=".xyz", delete=False) as f: + f.write(b"x\n") + path = f.name + try: + with pytest.raises( + RuntimeError, match="unrecognized input file extension" + ): + ParseProblem(path) + finally: + os.unlink(path) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 467d714fee..b5cc684e0c 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -326,6 +326,32 @@ def test_read_write_mps_and_relaxation(): assert v.getValue() == pytest.approx(expected_values_lp[i]) +def test_problem_read_mps_and_lp(): + mps_path = ( + RAPIDS_DATASET_ROOT_DIR + + "/linear_programming/good-mps-free-var.mps" + ) + lp_path = ( + RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.lp" + ) + mps_problem = Problem.read(mps_path) + lp_problem = Problem.read(lp_path) + assert mps_problem.NumVariables == lp_problem.NumVariables == 2 + mps_names = {v.VariableName for v in mps_problem.getVariables()} + lp_names = {v.VariableName for v in lp_problem.getVariables()} + assert mps_names == lp_names == {"VAR1", "VAR2"} + + +def test_problem_read_mps_deprecated(): + mps_path = ( + RAPIDS_DATASET_ROOT_DIR + + "/linear_programming/good-mps-free-var.mps" + ) + with pytest.warns(DeprecationWarning, match="readMPS is deprecated"): + problem = Problem.readMPS(mps_path) + assert problem.NumVariables == 2 + + def _run_incumbent_solutions(include_set_callback): # Callback for incumbent solution class CustomGetSolutionCallback(GetSolutionCallback): diff --git a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py index a5b76d57f3..e7b7a465ae 100644 --- a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py +++ b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py @@ -180,18 +180,7 @@ def _parse_file_to_data_model(problem_input, solver_config): log.debug("Received mps_parser DataModel object") else: t0 = time.time() - kind = ( - _client_parseable_extension(problem_input) - if isinstance(problem_input, str) - else None - ) - if kind == "lp": - model = mps_parser.ParseLp(problem_input) - else: - # MPS, QPS, and any unrecognized extension fall through to the - # MPS parser, which accepts both .mps and .qps (and their .gz / - # .bz2 variants) via the underlying C++ parse_mps(). - model = mps_parser.ParseMps(problem_input) + model = mps_parser.ParseProblem(problem_input) parse_time = time.time() - t0 log.debug(f"file parsing time was {parse_time}") problem_data = mps_parser.toDict(model, json=use_zlib) diff --git a/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py b/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py index 76dd22c054..096b47f1d0 100644 --- a/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py +++ b/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py @@ -3,7 +3,8 @@ import os -from cuopt.linear_programming import io as mps_parser +from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming.io import toDict import msgpack from cuopt.linear_programming import solver_settings @@ -32,8 +33,8 @@ def test_warmstart(cuoptproc): # noqa RAPIDS_DATASET_ROOT_DIR, "linear_programming/square41/square41.mps", ) - data_model_obj = mps_parser.ParseMps(file_path) - data = mps_parser.toDict(data_model_obj, json=True) + data_model_obj = ParseProblem(file_path) + data = toDict(data_model_obj, json=True) settings = solver_settings.SolverSettings() settings.set_optimality_tolerance(1e-4) settings.set_parameter(CUOPT_INFEASIBILITY_DETECTION, False) diff --git a/python/libcuopt/libcuopt/tests/test_cli.sh b/python/libcuopt/libcuopt/tests/test_cli.sh index 90732a78ee..d0bc9f2236 100644 --- a/python/libcuopt/libcuopt/tests/test_cli.sh +++ b/python/libcuopt/libcuopt/tests/test_cli.sh @@ -22,6 +22,12 @@ cuopt_cli --help | grep "Usage: cuopt_cli" > /dev/null || (echo "Expected usage cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/linear_programming/good-mps-1.mps | grep -q "Status: " || (echo "Expected solution not found" && exit 1) +cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/linear_programming/good-mps-1.lp | grep -q "Status: " || (echo "Expected solution not found for .lp" && exit 1) + +cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/linear_programming/good-mps-1.lp.gz | grep -q "Status: " || (echo "Expected solution not found for .lp.gz" && exit 1) + +cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/linear_programming/good-mps-1.lp.bz2 | grep -q "Status: " || (echo "Expected solution not found for .lp.bz2" && exit 1) + # Add a for mixed integer programming test with options cuopt_cli "${RAPIDS_DATASET_ROOT_DIR}"/mip/sample.mps --mip-absolute-gap 0.01 --time-limit 10 | grep -q "Solution objective" || (echo "Expected solution objective not found" && exit 1) diff --git a/regression/benchmark_scripts/utils.py b/regression/benchmark_scripts/utils.py index f720dd81b1..eeae11f175 100644 --- a/regression/benchmark_scripts/utils.py +++ b/regression/benchmark_scripts/utils.py @@ -3,7 +3,7 @@ from cuopt_server.utils.utils import build_routing_datamodel_from_json -from cuopt.linear_programming import mps_parser +from cuopt.linear_programming import ParseProblem from cuopt.linear_programming.solver_settings import SolverSettings import os import json @@ -16,7 +16,7 @@ def build_datamodel_from_mps(data): """ if os.path.isfile(data): - data_model = mps_parser.ParseMps(data) + data_model = ParseProblem(data) else: raise ValueError( f"Invalid type : {type(data)} has been provided as input, " From 9f994d655878fc1d30aba85ae92dcaffeee04d47 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 21 May 2026 20:01:23 +0000 Subject: [PATCH 20/30] cleanup unsed references --- .../linear_programming/io/utilities/cython_parser.hpp | 3 --- cpp/src/io/utilities/cython_parser.cpp | 7 ------- python/cuopt/cuopt/linear_programming/io/parser.pxd | 4 ---- 3 files changed, 14 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp index aa42cba9a8..2c98462614 100644 --- a/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp @@ -17,8 +17,5 @@ namespace cython { std::unique_ptr> call_parse_problem( const std::string& file_path, bool fixed_mps_format); -std::unique_ptr> call_parse_lp( - const std::string& lp_file_path); - } // namespace cython } // namespace cuopt diff --git a/cpp/src/io/utilities/cython_parser.cpp b/cpp/src/io/utilities/cython_parser.cpp index 769aab541f..d0e0902bc1 100644 --- a/cpp/src/io/utilities/cython_parser.cpp +++ b/cpp/src/io/utilities/cython_parser.cpp @@ -19,12 +19,5 @@ std::unique_ptr> ca cuopt::linear_programming::io::parse_problem(file_path, fixed_mps_format))); } -std::unique_ptr> call_parse_lp( - const std::string& lp_file_path) -{ - return std::make_unique>( - std::move(cuopt::linear_programming::io::parse_lp(lp_file_path))); -} - } // namespace cython } // namespace cuopt diff --git a/python/cuopt/cuopt/linear_programming/io/parser.pxd b/python/cuopt/cuopt/linear_programming/io/parser.pxd index d8ba687831..04c430ffbb 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.pxd +++ b/python/cuopt/cuopt/linear_programming/io/parser.pxd @@ -43,7 +43,3 @@ cdef extern from "cuopt/linear_programming/io/utilities/cython_parser.hpp" names const string& file_path, bool fixed_mps_format ) except + - - cdef unique_ptr[mps_data_model_t[int, double]] call_parse_lp( - const string& lp_file_path - ) except + From ce60a9051b84bc22cac5e823906a5cc2cd58fe72 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 21 May 2026 20:22:08 +0000 Subject: [PATCH 21/30] doc fixes --- cpp/src/io/utilities/cython_parser.cpp | 5 +- docs/cuopt/source/cuopt-cli/cli-examples.rst | 2 +- .../lp-qp-milp/lp-qp-milp-api.rst | 2 + .../cuopt-server/examples/lp-examples.rst | 4 +- .../lp/examples/mps_datamodel_example.py | 4 - .../cuopt/linear_programming/io/parser.py | 75 +++++++- .../test_cpu_only_execution.py | 16 -- .../linear_programming/test_lp_solver.py | 175 +----------------- .../linear_programming/test_python_API.py | 6 +- python/libcuopt/libcuopt/tests/test_cli.sh | 2 +- 10 files changed, 86 insertions(+), 205 deletions(-) diff --git a/cpp/src/io/utilities/cython_parser.cpp b/cpp/src/io/utilities/cython_parser.cpp index d0e0902bc1..664b449782 100644 --- a/cpp/src/io/utilities/cython_parser.cpp +++ b/cpp/src/io/utilities/cython_parser.cpp @@ -14,9 +14,8 @@ namespace cython { std::unique_ptr> call_parse_problem( const std::string& file_path, bool fixed_mps_format) { - return std::make_unique>( - std::move( - cuopt::linear_programming::io::parse_problem(file_path, fixed_mps_format))); + return std::make_unique>(std::move( + cuopt::linear_programming::io::parse_problem(file_path, fixed_mps_format))); } } // namespace cython diff --git a/docs/cuopt/source/cuopt-cli/cli-examples.rst b/docs/cuopt/source/cuopt-cli/cli-examples.rst index 4a996dc106..7596d1bf85 100644 --- a/docs/cuopt/source/cuopt-cli/cli-examples.rst +++ b/docs/cuopt/source/cuopt-cli/cli-examples.rst @@ -15,7 +15,7 @@ format is dispatched automatically from the file extension Any other extension (including no extension) is rejected with an error listing the supported suffixes. See ``parse_problem`` in ``cuopt/linear_programming/io/parser.hpp`` (and the Python -:func:`~cuopt.linear_programming.ParseProblem` wrapper). +:func:`~cuopt.linear_programming.io.ParseProblem` wrapper). The ``good-mps-1`` fixtures under ``datasets/linear_programming/`` include plain and compressed ``.mps`` / ``.lp`` files used in parser and CLI tests; diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst index e86c4a2920..3a08418865 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst @@ -47,3 +47,5 @@ LP, QP and MILP API Reference :member-order: bysource :undoc-members: :exclude-members: __new__, __init__, _generate_next_value_, as_integer_ratio, bit_count, bit_length, conjugate, denominator, from_bytes, imag, is_integer, numerator, real, to_bytes + +.. autofunction:: cuopt.linear_programming.io.ParseProblem diff --git a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst index 6510be815f..11a2df4d88 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst +++ b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst @@ -268,10 +268,10 @@ The response is: Generate Datamodel using Problem File Parser -------------------------------------------- +-------------------------------------------- Use a :class:`~cuopt.linear_programming.data_model.DataModel` built with -:func:`~cuopt.linear_programming.ParseProblem` as input to ``get_LP_solve``; +:func:`~cuopt.linear_programming.io.ParseProblem` as input to ``get_LP_solve``; the client dispatches on the file extension (``.mps`` / ``.qps`` vs ``.lp``, including ``.gz`` / ``.bz2`` compressed variants). For solver settings see :doc:`LP/QP/MILP parameters <../../lp-qp-milp-settings>`. diff --git a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py index 04b565b8d5..552feab86a 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py +++ b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py @@ -4,11 +4,7 @@ LP DataModel from MPS file parser example This example demonstrates how to: -<<<<<<< HEAD - Parse an MPS file using cuopt.linear_programming.ParseProblem -======= -- Parse an MPS file using cuopt.linear_programming.ParseMps ->>>>>>> origin/release/26.06 - Create a DataModel from the parsed MPS - Solve using the DataModel via the server - Extract detailed solution information diff --git a/python/cuopt/cuopt/linear_programming/io/parser.py b/python/cuopt/cuopt/linear_programming/io/parser.py index 1179ea21a5..016fa1ffc7 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.py +++ b/python/cuopt/cuopt/linear_programming/io/parser.py @@ -43,4 +43,77 @@ def ParseProblem(file_path: str, fixed_mps_format: bool = False) -> DataModel: If the file extension is not one of the supported suffixes (raised by the C++ ``parse_problem`` dispatch). """ - return parser_wrapper.ParseProblem(file_path, fixed_mps_format) \ No newline at end of file + return parser_wrapper.ParseProblem(file_path, fixed_mps_format) + + +def toDict(model, json=False): + if not isinstance(model, parser_wrapper.DataModel): + raise ValueError( + "model must be a cuopt.linear_programming.io.parser_wrapper.DataModel" + ) + + def transform(data): + for key, value in data.items(): + if isinstance(value, dict): + transform(value) + elif isinstance(value, list): + if np.inf in data[key] or -np.inf in data[key]: + data[key] = [ + "inf" if x == np.inf else "ninf" if x == -np.inf else x + for x in data[key] + ] + + if json is True: + problem_data = { + "csr_constraint_matrix": { + "offsets": model.A_offsets.tolist(), + "indices": model.A_indices.tolist(), + "values": model.A_values.tolist(), + }, + "constraint_bounds": { + "bounds": model.b.tolist(), + "upper_bounds": model.constraint_upper_bounds.tolist(), + "lower_bounds": model.constraint_lower_bounds.tolist(), + "types": model.host_row_types.tolist(), + }, + "objective_data": { + "coefficients": model.c.tolist(), + "scalability_factor": model.objective_scaling_factor, + "offset": model.objective_offset, + }, + "variable_bounds": { + "upper_bounds": model.variable_upper_bounds.tolist(), + "lower_bounds": model.variable_lower_bounds.tolist(), + }, + "maximize": model.maximize, + "variable_types": model.variable_types.tolist(), + "variable_names": model.variable_names.tolist(), + } + transform(problem_data) + else: + problem_data = { + "csr_constraint_matrix": { + "offsets": model.A_offsets, + "indices": model.A_indices, + "values": model.A_values, + }, + "constraint_bounds": { + "bounds": model.b, + "upper_bounds": model.constraint_upper_bounds, + "lower_bounds": model.constraint_lower_bounds, + "types": model.host_row_types, + }, + "objective_data": { + "coefficients": model.c, + "scalability_factor": model.objective_scaling_factor, + "offset": model.objective_offset, + }, + "variable_bounds": { + "upper_bounds": model.variable_upper_bounds, + "lower_bounds": model.variable_lower_bounds, + }, + "maximize": model.maximize, + "variable_types": model.variable_types, + "variable_names": model.variable_names, + } + return problem_data diff --git a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py index 64e3ca749e..a3787d4123 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py @@ -301,11 +301,7 @@ def _run_in_subprocess(func, env=None, timeout=120): def _impl_lp_solve_cpu_only(): """LP solve returns correctly-sized solution vectors.""" from cuopt import linear_programming -<<<<<<< HEAD -======= - from cuopt.linear_programming import io as mps_parser ->>>>>>> origin/release/26.06 dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" dm = ParseProblem(mps_file) @@ -334,11 +330,7 @@ def _impl_lp_solve_cpu_only(): def _impl_lp_dual_solution_cpu_only(): """Dual solution and reduced costs are correctly sized.""" from cuopt import linear_programming -<<<<<<< HEAD -======= - from cuopt.linear_programming import io as mps_parser ->>>>>>> origin/release/26.06 dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" dm = ParseProblem(mps_file) @@ -370,11 +362,7 @@ def _impl_mip_solve_cpu_only(): from cuopt.linear_programming.solver.solver_parameters import ( CUOPT_TIME_LIMIT, ) -<<<<<<< HEAD -======= - from cuopt.linear_programming import io as mps_parser ->>>>>>> origin/release/26.06 dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/mip/bb_optimality.mps" dm = ParseProblem(mps_file) @@ -409,11 +397,7 @@ def _impl_warmstart_cpu_only(): CUOPT_PRESOLVE, ) from cuopt.linear_programming.solver_settings import SolverMethod -<<<<<<< HEAD -======= - from cuopt.linear_programming import io as mps_parser ->>>>>>> origin/release/26.06 dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" dm = ParseProblem(mps_file) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py index 63bc0d7e68..dea2abfe9d 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py @@ -101,75 +101,6 @@ def test_parser_and_solver(): assert solution.get_termination_reason() == "Optimal" -<<<<<<< HEAD -def test_very_low_tolerance(): - file_path = ( - RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" - ) - data_model_obj = ParseProblem(file_path) - - settings = solver_settings.SolverSettings() - settings.set_optimality_tolerance(1e-12) - # Test with the former/legacy solver_mode - settings.set_parameter(CUOPT_PDLP_SOLVER_MODE, PDLPSolverMode.Methodical1) - settings.set_parameter(CUOPT_INFEASIBILITY_DETECTION, False) - - solution = solver.Solve(data_model_obj, settings) - - expected_time = 69 - - assert solution.get_termination_status() == LPTerminationStatus.Optimal - assert solution.get_primal_objective() == pytest.approx(-464.7531) - # Rougly up to 5 times slower on V100 - assert solution.get_solve_time() <= expected_time * 5 - - -# TODO: should test all LP solver modes? -def test_iteration_limit_solver(): - file_path = ( - RAPIDS_DATASET_ROOT_DIR + "/linear_programming/savsched1/savsched1.mps" - ) - data_model_obj = ParseProblem(file_path) - - settings = solver_settings.SolverSettings() - settings.set_optimality_tolerance(1e-12) - settings.set_parameter(CUOPT_ITERATION_LIMIT, 1) - # Setting both to make sure the lowest one is picked - settings.set_parameter(CUOPT_TIME_LIMIT, 99999999) - - solution = solver.Solve(data_model_obj, settings) - assert ( - solution.get_termination_status() == LPTerminationStatus.IterationLimit - ) - # Check we don't return empty (all 0) solution - assert solution.get_primal_objective() != 0.0 - assert np.any(solution.get_primal_solution()) - - -def test_time_limit_solver(): - file_path = ( - RAPIDS_DATASET_ROOT_DIR + "/linear_programming/savsched1/savsched1.mps" - ) - data_model_obj = ParseProblem(file_path) - - settings = solver_settings.SolverSettings() - settings.set_optimality_tolerance(1e-12) - time_limit_seconds = 0.2 - settings.set_parameter(CUOPT_TIME_LIMIT, time_limit_seconds) - # Solver mode isn't what's tested here. - # Set it to Stable2 as CI is more reliable with this mode - settings.set_parameter(CUOPT_PDLP_SOLVER_MODE, PDLPSolverMode.Stable2) - # Setting both to make sure the lowest one is picked - settings.set_parameter(CUOPT_ITERATION_LIMIT, 99999999) - - solution = solver.Solve(data_model_obj, settings) - assert solution.get_termination_status() == LPTerminationStatus.TimeLimit - # Check that around 200 ms has passed with some tolerance - assert solution.get_solve_time() <= (time_limit_seconds * 10) - - -======= ->>>>>>> origin/release/26.06 def test_set_get_fields(): data_model_obj = data_model.DataModel() @@ -664,9 +595,7 @@ def test_parser_and_batch_solver(): # Call Solve on each individual data model object individual_solutions = [] for i in range(nb_solves): - individual_solution = solver.Solve( - ParseProblem(file_path), settings - ) + individual_solution = solver.Solve(ParseProblem(file_path), settings) individual_solutions.append(individual_solution) # Verify that the results are the same @@ -709,50 +638,14 @@ def test_warm_start(): == iterations_first_solve ) -<<<<<<< HEAD - -def test_warm_start_other_problem(): - file_path = RAPIDS_DATASET_ROOT_DIR + "/linear_programming/a2864/a2864.mps" - data_model_obj = ParseProblem(file_path) - - settings = solver_settings.SolverSettings() - settings.set_parameter(CUOPT_PDLP_SOLVER_MODE, PDLPSolverMode.Stable2) - settings.set_optimality_tolerance(1e-1) - settings.set_parameter(CUOPT_INFEASIBILITY_DETECTION, False) - settings.set_parameter(CUOPT_PRESOLVE, 0) - solution = solver.Solve(data_model_obj, settings) - - file_path = ( - RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" - ) - data_model_obj2 = ParseProblem(file_path) - settings.set_pdlp_warm_start_data(solution.get_pdlp_warm_start_data()) - -======= ->>>>>>> origin/release/26.06 # Should raise an exception as problems are different file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) -<<<<<<< HEAD - - nb_solves = 2 - for i in range(nb_solves): - data_model_list.append(ParseProblem(file_path)) - - settings = solver_settings.SolverSettings() - settings.set_optimality_tolerance(1e-3) - - # Solve a first time to get a warm start - solution = solver.Solve(ParseProblem(file_path), settings) - - settings.set_pdlp_warm_start_data(solution.get_pdlp_warm_start_data()) -======= - data_model_obj_different = mps_parser.ParseMps(file_path) + data_model_obj_different = ParseProblem(file_path) with pytest.raises(Exception, match="Invalid PDLPWarmStart data"): solver.Solve(data_model_obj_different, settings) ->>>>>>> origin/release/26.06 # Should raise an exception data_model_list = [data_model_obj, data_model_obj] @@ -762,28 +655,7 @@ def test_warm_start_other_problem(): solver.BatchSolve(data_model_list, settings) -<<<<<<< HEAD -def test_dual_simplex(): - file_path = ( - RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" - ) - data_model_obj = ParseProblem(file_path) - - settings = solver_settings.SolverSettings() - settings.set_parameter(CUOPT_METHOD, SolverMethod.DualSimplex) - settings.set_parameter(CUOPT_DUAL_POSTSOLVE, False) - - solution = solver.Solve(data_model_obj, settings) - - assert solution.get_termination_status() == LPTerminationStatus.Optimal - assert solution.get_primal_objective() == pytest.approx(-464.7531) - assert solution.get_solved_by() == SolverMethod.DualSimplex - - -def test_barrier(): -======= def test_solved_by(): ->>>>>>> origin/release/26.06 # maximize 5*xs + 20*xl # subject to 1*xs + 3*xl <= 200 # 3*xs + 2*xl <= 160 @@ -931,46 +803,3 @@ def test_unbounded_problem(): problem.solve(settings) assert problem.Status.name == "UnboundedOrInfeasible" -<<<<<<< HEAD - - -def test_pdlp_precision_single(): - file_path = ( - RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" - ) - data_model_obj = ParseProblem(file_path) - - settings = solver_settings.SolverSettings() - settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) - settings.set_parameter(CUOPT_PDLP_PRECISION, 0) # Single - settings.set_optimality_tolerance(1e-4) - - solution = solver.Solve(data_model_obj, settings) - - assert solution.get_termination_status() == LPTerminationStatus.Optimal - assert solution.get_primal_objective() == pytest.approx( - -464.7531, rel=1e-1 - ) - assert solution.get_solved_by() == SolverMethod.PDLP - - -def test_pdlp_precision_single_crossover(): - file_path = ( - RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" - ) - data_model_obj = ParseProblem(file_path) - - settings = solver_settings.SolverSettings() - settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) - settings.set_parameter(CUOPT_PDLP_PRECISION, 1) # Single - settings.set_parameter("crossover", True) - settings.set_optimality_tolerance(1e-4) - - solution = solver.Solve(data_model_obj, settings) - - assert solution.get_termination_status() == LPTerminationStatus.Optimal - assert solution.get_primal_objective() == pytest.approx( - -464.7531, rel=1e-1 - ) -======= ->>>>>>> origin/release/26.06 diff --git a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py index 5df0c0cd2f..afc1ec6992 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_python_API.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_python_API.py @@ -341,8 +341,7 @@ def test_read_write_mps_and_relaxation(): def test_problem_read_mps_and_lp(): mps_path = ( - RAPIDS_DATASET_ROOT_DIR - + "/linear_programming/good-mps-free-var.mps" + RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.mps" ) lp_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.lp" @@ -357,8 +356,7 @@ def test_problem_read_mps_and_lp(): def test_problem_read_mps_deprecated(): mps_path = ( - RAPIDS_DATASET_ROOT_DIR - + "/linear_programming/good-mps-free-var.mps" + RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.mps" ) with pytest.warns(DeprecationWarning, match="readMPS is deprecated"): problem = Problem.readMPS(mps_path) diff --git a/python/libcuopt/libcuopt/tests/test_cli.sh b/python/libcuopt/libcuopt/tests/test_cli.sh index d0bc9f2236..85f73fa58f 100644 --- a/python/libcuopt/libcuopt/tests/test_cli.sh +++ b/python/libcuopt/libcuopt/tests/test_cli.sh @@ -1,6 +1,6 @@ #!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail From 7d802b1cf9d9d5791c251fbc964dc8d6b7fb160f Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Thu, 21 May 2026 20:23:56 +0000 Subject: [PATCH 22/30] doc fixes --- python/cuopt/cuopt/linear_programming/io/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cuopt/cuopt/linear_programming/io/parser.py b/python/cuopt/cuopt/linear_programming/io/parser.py index 016fa1ffc7..4310fa4673 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.py +++ b/python/cuopt/cuopt/linear_programming/io/parser.py @@ -38,7 +38,7 @@ def ParseProblem(file_path: str, fixed_mps_format: bool = False) -> DataModel: ------ InputValidationError, InputRuntimeError, OutOfMemoryError Parser errors from the underlying C++ readers (via - :func:`catch_io_exception`). + ``catch_io_exception``). RuntimeError If the file extension is not one of the supported suffixes (raised by the C++ ``parse_problem`` dispatch). From 4a82497d5b49aea8ae7e443da76a3414eadede1d Mon Sep 17 00:00:00 2001 From: Ishika Roy <41401566+Iroy30@users.noreply.github.com> Date: Thu, 21 May 2026 15:35:07 -0500 Subject: [PATCH 23/30] Update parser_example.rst --- docs/cuopt/source/hidden/parser_example.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/cuopt/source/hidden/parser_example.rst b/docs/cuopt/source/hidden/parser_example.rst index f19c1511a5..6638d803d1 100644 --- a/docs/cuopt/source/hidden/parser_example.rst +++ b/docs/cuopt/source/hidden/parser_example.rst @@ -24,7 +24,3 @@ with :func:`~cuopt.linear_programming.ParseProblem`: # High-level API problem = Problem.read("good-mps-1.lp") - -See ``datasets/linear_programming/good-mps-1-README.md`` for the full set of -fixture files and why compressed ``.lp.gz`` / ``.lp.bz2`` variants are kept in -the tree. From 1384e7bde3903700d7c8e32b2426f6d5ba4a9465 Mon Sep 17 00:00:00 2001 From: Ishika Roy <41401566+Iroy30@users.noreply.github.com> Date: Thu, 21 May 2026 15:38:20 -0500 Subject: [PATCH 24/30] Update cli-examples.rst --- docs/cuopt/source/cuopt-cli/cli-examples.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/cuopt/source/cuopt-cli/cli-examples.rst b/docs/cuopt/source/cuopt-cli/cli-examples.rst index 7596d1bf85..d35e925e13 100644 --- a/docs/cuopt/source/cuopt-cli/cli-examples.rst +++ b/docs/cuopt/source/cuopt-cli/cli-examples.rst @@ -17,9 +17,6 @@ listing the supported suffixes. See ``parse_problem`` in ``cuopt/linear_programming/io/parser.hpp`` (and the Python :func:`~cuopt.linear_programming.io.ParseProblem` wrapper). -The ``good-mps-1`` fixtures under ``datasets/linear_programming/`` include -plain and compressed ``.mps`` / ``.lp`` files used in parser and CLI tests; -see ``good-mps-1-README.md`` in that directory. Basic Usage ########### From bc7030363e2e98f297c4fc0218a838549b294fe1 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Fri, 22 May 2026 18:14:28 +0000 Subject: [PATCH 25/30] renaming --- .../cuopt/benchmark_helper.hpp | 2 +- .../linear_programming/cuopt/run_mip.cpp | 2 +- .../linear_programming/cuopt/run_pdlp.cu | 2 +- ci/test_self_hosted_service.sh | 2 +- cpp/cuopt_cli.cpp | 2 +- .../cuopt/linear_programming/io/parser.hpp | 32 +- .../io/utilities/cython_parser.hpp | 2 +- cpp/src/io/lp_parser.cpp | 16 +- cpp/src/io/lp_parser.hpp | 2 +- cpp/src/io/parser.cpp | 12 +- cpp/src/io/utilities/cython_parser.cpp | 4 +- cpp/src/pdlp/cuopt_c.cpp | 4 +- .../dual_simplex/unit_tests/solve_barrier.cu | 2 +- .../grpc/grpc_integration_test.cpp | 2 +- cpp/tests/linear_programming/parser_test.cpp | 280 +++++++++--------- cpp/tests/linear_programming/pdlp_test.cu | 164 +++++----- .../unit_tests/optimization_problem_test.cu | 2 +- .../unit_tests/presolve_test.cu | 12 +- .../unit_tests/solution_interface_test.cu | 2 +- cpp/tests/mip/bounds_standardization_test.cu | 2 +- cpp/tests/mip/cuts_test.cu | 2 +- cpp/tests/mip/determinism_test.cu | 10 +- cpp/tests/mip/doc_example_test.cu | 2 +- cpp/tests/mip/elim_var_remap_test.cu | 4 +- cpp/tests/mip/feasibility_jump_tests.cu | 2 +- cpp/tests/mip/incumbent_callback_test.cu | 4 +- cpp/tests/mip/load_balancing_test.cu | 2 +- cpp/tests/mip/mip_utils.cuh | 2 +- cpp/tests/mip/miplib_test.cu | 6 +- cpp/tests/mip/multi_probe_test.cu | 2 +- cpp/tests/mip/presolve_test.cu | 2 +- .../qp/unit_tests/lp_parser_solve_test.cu | 2 +- cpp/tests/routing/level0/l0_ges_test.cu | 2 +- cpp/tests/routing/level0/l0_scross_test.cu | 2 +- cpp/tests/routing/routing_test.cuh | 6 +- cpp/tests/utilities/inline_mps_test_utils.hpp | 2 +- .../cuopt-c/lp-qp-milp/lp-qp-example.rst | 2 +- docs/cuopt/source/cuopt-cli/cli-examples.rst | 4 +- .../lp-qp-milp/lp-qp-milp-api.rst | 2 +- .../cuopt-server/examples/lp-examples.rst | 2 +- .../lp/examples/lp_datamodel_example.py | 8 +- .../lp/examples/mps_datamodel_example.py | 6 +- docs/cuopt/source/hidden/mps-api.rst | 2 +- docs/cuopt/source/hidden/parser_example.rst | 10 +- .../cuopt/linear_programming/__init__.py | 2 +- .../cuopt/linear_programming/io/__init__.py | 2 +- .../cuopt/linear_programming/io/parser.pxd | 2 +- .../cuopt/linear_programming/io/parser.py | 10 +- .../linear_programming/io/parser_wrapper.pyx | 6 +- .../cuopt/cuopt/linear_programming/problem.py | 6 +- .../cuopt/linear_programming/solver/solver.py | 8 +- .../test_cpu_only_execution.py | 14 +- .../test_incumbent_callbacks.py | 4 +- .../linear_programming/test_lp_solver.py | 22 +- .../tests/linear_programming/test_parser.py | 30 +- .../cuopt_sh_client/cuopt_self_host_client.py | 4 +- .../cuopt_server/tests/test_pdlp_warmstart.py | 4 +- regression/benchmark_scripts/utils.py | 4 +- 58 files changed, 377 insertions(+), 377 deletions(-) diff --git a/benchmarks/linear_programming/cuopt/benchmark_helper.hpp b/benchmarks/linear_programming/cuopt/benchmark_helper.hpp index 6e2bbc29f1..2f6de22c6b 100644 --- a/benchmarks/linear_programming/cuopt/benchmark_helper.hpp +++ b/benchmarks/linear_programming/cuopt/benchmark_helper.hpp @@ -275,7 +275,7 @@ void mps_file_to_binary(const std::filesystem::path& filename) std::string p = std::string(filename); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(p); + cuopt::linear_programming::io::read_mps(p); auto filename_string = filename.filename().string(); diff --git a/benchmarks/linear_programming/cuopt/run_mip.cpp b/benchmarks/linear_programming/cuopt/run_mip.cpp index 3201aa137a..f35543696f 100644 --- a/benchmarks/linear_programming/cuopt/run_mip.cpp +++ b/benchmarks/linear_programming/cuopt/run_mip.cpp @@ -172,7 +172,7 @@ int run_single_file(std::string file_path, CUOPT_LOG_INFO("running file %s on gpu : %d", base_filename.c_str(), device); try { mps_data_model = - cuopt::linear_programming::io::parse_mps(file_path, input_mps_strict); + cuopt::linear_programming::io::read_mps(file_path, input_mps_strict); } catch (const std::logic_error& e) { CUOPT_LOG_ERROR("MPS parser execption: %s", e.what()); parsing_failed = true; diff --git a/benchmarks/linear_programming/cuopt/run_pdlp.cu b/benchmarks/linear_programming/cuopt/run_pdlp.cu index b86f61ba1f..c88e5657df 100644 --- a/benchmarks/linear_programming/cuopt/run_pdlp.cu +++ b/benchmarks/linear_programming/cuopt/run_pdlp.cu @@ -149,7 +149,7 @@ static int run_solver(const argparse::ArgumentParser& program, const raft::handl // Parse MPS file cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(program.get("--path")); + cuopt::linear_programming::io::read_mps(program.get("--path")); // Solve LP problem bool problem_checking = true; diff --git a/ci/test_self_hosted_service.sh b/ci/test_self_hosted_service.sh index a645e586e4..b9824a7422 100755 --- a/ci/test_self_hosted_service.sh +++ b/ci/test_self_hosted_service.sh @@ -125,7 +125,7 @@ if [ "$doservertest" -eq 1 ]; then # Success, small Batch LP problem with LP format. Data will be transformed to JSON run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.lp ../../datasets/linear_programming/good-mps-1.lp - # Success, compressed LP inputs (.lp.gz / .lp.bz2) via ParseProblem dispatch + # Success, compressed LP inputs (.lp.gz / .lp.bz2) via Read dispatch run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.lp.gz run_cli_test "'status': 'Optimal'" cuopt_sh -s -c "$CLIENT_CERT" -p $CUOPT_SERVER_PORT -t LP ../../datasets/linear_programming/good-mps-1.lp.bz2 diff --git a/cpp/cuopt_cli.cpp b/cpp/cuopt_cli.cpp index 39aab47170..1d26f26503 100644 --- a/cpp/cuopt_cli.cpp +++ b/cpp/cuopt_cli.cpp @@ -108,7 +108,7 @@ int run_single_file(const std::string& file_path, { CUOPT_LOG_INFO("Reading file %s", base_filename.c_str()); try { - mps_data_model = cuopt::linear_programming::io::parse_problem(file_path); + mps_data_model = cuopt::linear_programming::io::read(file_path); } catch (const std::logic_error& e) { CUOPT_LOG_ERROR("Parser exception: %s", e.what()); parsing_failed = true; diff --git a/cpp/include/cuopt/linear_programming/io/parser.hpp b/cpp/include/cuopt/linear_programming/io/parser.hpp index 9e4ea3f8f0..2a46b60f7d 100644 --- a/cpp/include/cuopt/linear_programming/io/parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/parser.hpp @@ -40,22 +40,22 @@ namespace cuopt::linear_programming::io { * @return mps_data_model_t A fully formed LP/QP problem which represents the given file */ template -mps_data_model_t parse_mps(const std::string& mps_file_path, +mps_data_model_t read_mps(const std::string& mps_file_path, bool fixed_mps_format = false); /** * @brief Reads an MPS problem from in-memory file contents. * - * This parses the same plain-text MPS format as parse_mps(), but the input is + * This parses the same plain-text MPS format as read_mps(), but the input is * already loaded in memory. Compressed .mps.gz/.mps.bz2 inputs are only supported - * by parse_mps() because compression is detected from the file path. + * by read_mps() because compression is detected from the file path. * * @param[in] mps_contents MPS file contents. * @param[in] fixed_mps_format If MPS content should be parsed as fixed, false by default. * @return mps_data_model_t A fully formed problem which represents the given content. */ template -mps_data_model_t parse_mps_from_string(std::string_view mps_contents, +mps_data_model_t read_mps_from_string(std::string_view mps_contents, bool fixed_mps_format = false); /** @@ -82,22 +82,22 @@ mps_data_model_t parse_mps_from_string(std::string_view mps_contents, * a ValidationError when encountered. * * Compressed inputs (.lp.gz, .lp.bz2) are supported when zlib / libbzip2 - * are installed (same dispatching as parse_mps). + * are installed (same dispatching as read_mps). * * @param[in] lp_file_path Path to the LP file. * @return mps_data_model_t A fully formed LP/MIP/QP problem representing the * given file. */ template -mps_data_model_t parse_lp(const std::string& lp_file_path); +mps_data_model_t read_lp(const std::string& lp_file_path); /** * @brief Reads an LP, MIP, or QP problem from in-memory file contents. * - * This parses the same plain-text LP format as parse_lp(), but the input is + * This parses the same plain-text LP format as read_lp(), but the input is * already loaded in memory. Compressed .lp.gz/.lp.bz2 inputs are only - * supported by parse_lp() because compression is detected from the file - * path. Supports the same scope as parse_lp() (LP, MIP, QP, plus + * supported by read_lp() because compression is detected from the file + * path. Supports the same scope as read_lp() (LP, MIP, QP, plus * semi-continuous variables). * * @param[in] lp_contents LP file contents. @@ -105,15 +105,15 @@ mps_data_model_t parse_lp(const std::string& lp_file_path); * given content. */ template -mps_data_model_t parse_lp_from_string(std::string_view lp_contents); +mps_data_model_t read_lp_from_string(std::string_view lp_contents); /** * @brief Reads an optimization problem from a file, dispatching on the file * extension. Extension matching is case-insensitive. * * Routing: - * - .mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2 → parse_mps() - * - .lp, .lp.gz, .lp.bz2 → parse_lp() + * - .mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2 → read_mps() + * - .lp, .lp.gz, .lp.bz2 → read_lp() * - anything else → std::logic_error * * This is the entry point of choice for user-facing tools (CLI, C API) that @@ -125,7 +125,7 @@ mps_data_model_t parse_lp_from_string(std::string_view lp_contents); * @return mps_data_model_t The parsed problem. */ template -inline mps_data_model_t parse_problem(const std::string& path, +inline mps_data_model_t read(const std::string& path, bool fixed_mps_format = false) { std::string lower(path); @@ -134,13 +134,13 @@ inline mps_data_model_t parse_problem(const std::string& path, }); if (lower.ends_with(".mps") || lower.ends_with(".mps.gz") || lower.ends_with(".mps.bz2") || lower.ends_with(".qps") || lower.ends_with(".qps.gz") || lower.ends_with(".qps.bz2")) { - return parse_mps(path, fixed_mps_format); + return read_mps(path, fixed_mps_format); } if (lower.ends_with(".lp") || lower.ends_with(".lp.gz") || lower.ends_with(".lp.bz2")) { - return parse_lp(path); + return read_lp(path); } throw std::logic_error( - "parse_problem: unrecognized input file extension. Supported (case-insensitive): " + "read: unrecognized input file extension. Supported (case-insensitive): " ".mps, .mps.gz, .mps.bz2, .qps, .qps.gz, .qps.bz2, .lp, .lp.gz, .lp.bz2. " "Given path: " + path); diff --git a/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp index 2c98462614..1f5d8940f0 100644 --- a/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp @@ -14,7 +14,7 @@ namespace cuopt { namespace cython { -std::unique_ptr> call_parse_problem( +std::unique_ptr> call_read( const std::string& file_path, bool fixed_mps_format); } // namespace cython diff --git a/cpp/src/io/lp_parser.cpp b/cpp/src/io/lp_parser.cpp index 84daa101b4..4f8c3f223f 100644 --- a/cpp/src/io/lp_parser.cpp +++ b/cpp/src/io/lp_parser.cpp @@ -199,7 +199,7 @@ template class LpParseEngine { public: LpParseEngine(lp_parser_t& out, const std::string& file); - // Parses `text` directly (used by parse_lp_from_string()). + // Parses `text` directly (used by read_lp_from_string()). LpParseEngine(lp_parser_t& out, std::string_view text); private: @@ -1546,11 +1546,11 @@ template class lp_parser_t; template class lp_parser_t; // =========================================================================== -// Public parse_lp() / parse_lp_from_string() +// Public read_lp() / read_lp_from_string() // =========================================================================== template -mps_data_model_t parse_lp(const std::string& lp_file_path) +mps_data_model_t read_lp(const std::string& lp_file_path) { mps_data_model_t problem; lp_parser_t parser(problem, lp_file_path); @@ -1558,16 +1558,16 @@ mps_data_model_t parse_lp(const std::string& lp_file_path) } template -mps_data_model_t parse_lp_from_string(std::string_view lp_contents) +mps_data_model_t read_lp_from_string(std::string_view lp_contents) { mps_data_model_t problem; lp_parser_t parser(problem, lp_contents); return problem; } -template mps_data_model_t parse_lp(const std::string&); -template mps_data_model_t parse_lp(const std::string&); -template mps_data_model_t parse_lp_from_string(std::string_view); -template mps_data_model_t parse_lp_from_string(std::string_view); +template mps_data_model_t read_lp(const std::string&); +template mps_data_model_t read_lp(const std::string&); +template mps_data_model_t read_lp_from_string(std::string_view); +template mps_data_model_t read_lp_from_string(std::string_view); } // namespace cuopt::linear_programming::io diff --git a/cpp/src/io/lp_parser.hpp b/cpp/src/io/lp_parser.hpp index 8314d6c97a..a607359657 100644 --- a/cpp/src/io/lp_parser.hpp +++ b/cpp/src/io/lp_parser.hpp @@ -36,7 +36,7 @@ class lp_parser_t { lp_parser_t(mps_data_model_t& problem, const std::string& file); // Parses `input` (LP format text already loaded in memory) and populates - // `problem`. Used by parse_lp_from_string() — compressed inputs are only + // `problem`. Used by read_lp_from_string() — compressed inputs are only // supported via the file-path constructor since compression is detected // from the path suffix. lp_parser_t(mps_data_model_t& problem, std::string_view input); diff --git a/cpp/src/io/parser.cpp b/cpp/src/io/parser.cpp index af76c41ff1..a981f5ad95 100644 --- a/cpp/src/io/parser.cpp +++ b/cpp/src/io/parser.cpp @@ -12,7 +12,7 @@ namespace cuopt::linear_programming::io { template -mps_data_model_t parse_mps(const std::string& mps_file, bool fixed_mps_format) +mps_data_model_t read_mps(const std::string& mps_file, bool fixed_mps_format) { mps_data_model_t problem; mps_parser_t parser(problem, mps_file, fixed_mps_format); @@ -20,7 +20,7 @@ mps_data_model_t parse_mps(const std::string& mps_file, bool fixed_mps } template -mps_data_model_t parse_mps_from_string(std::string_view mps_contents, +mps_data_model_t read_mps_from_string(std::string_view mps_contents, bool fixed_mps_format) { mps_data_model_t problem; @@ -28,12 +28,12 @@ mps_data_model_t parse_mps_from_string(std::string_view mps_contents, return problem; } -template mps_data_model_t parse_mps(const std::string& mps_file, bool fixed_mps_format); -template mps_data_model_t parse_mps(const std::string& mps_file, +template mps_data_model_t read_mps(const std::string& mps_file, bool fixed_mps_format); +template mps_data_model_t read_mps(const std::string& mps_file, bool fixed_mps_format); -template mps_data_model_t parse_mps_from_string(std::string_view mps_contents, +template mps_data_model_t read_mps_from_string(std::string_view mps_contents, bool fixed_mps_format); -template mps_data_model_t parse_mps_from_string(std::string_view mps_contents, +template mps_data_model_t read_mps_from_string(std::string_view mps_contents, bool fixed_mps_format); } // namespace cuopt::linear_programming::io diff --git a/cpp/src/io/utilities/cython_parser.cpp b/cpp/src/io/utilities/cython_parser.cpp index 664b449782..1cc4ea9b62 100644 --- a/cpp/src/io/utilities/cython_parser.cpp +++ b/cpp/src/io/utilities/cython_parser.cpp @@ -11,11 +11,11 @@ namespace cuopt { namespace cython { -std::unique_ptr> call_parse_problem( +std::unique_ptr> call_read( const std::string& file_path, bool fixed_mps_format) { return std::make_unique>(std::move( - cuopt::linear_programming::io::parse_problem(file_path, fixed_mps_format))); + cuopt::linear_programming::io::read(file_path, fixed_mps_format))); } } // namespace cython diff --git a/cpp/src/pdlp/cuopt_c.cpp b/cpp/src/pdlp/cuopt_c.cpp index d29e849411..2e5c744f15 100644 --- a/cpp/src/pdlp/cuopt_c.cpp +++ b/cpp/src/pdlp/cuopt_c.cpp @@ -113,9 +113,9 @@ cuopt_int_t cuOptReadProblem(const char* filename, cuOptOptimizationProblem* pro std::string filename_str(filename); std::unique_ptr> mps_data_model_ptr; try { - // Dispatches on file extension; see parse_problem for the enumerated rules. + // Dispatches on file extension; see read for the enumerated rules. mps_data_model_ptr = std::make_unique>( - parse_problem(filename_str)); + read(filename_str)); } catch (const std::exception& e) { CUOPT_LOG_INFO("Error parsing input file: %s", e.what()); delete problem_and_stream; diff --git a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu index b0cbe624dc..0c8c591e97 100644 --- a/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu +++ b/cpp/tests/dual_simplex/unit_tests/solve_barrier.cu @@ -196,7 +196,7 @@ TEST(barrier, min_x_squared_free_variable_dual_correction) auto path = cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/min_x_squared.mps"; - auto mps_data = cuopt::linear_programming::io::parse_mps(path); + auto mps_data = cuopt::linear_programming::io::read_mps(path); auto settings = cuopt::linear_programming::pdlp_solver_settings_t{}; diff --git a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp index 0523a3529c..beb2778400 100644 --- a/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp +++ b/cpp/tests/linear_programming/grpc/grpc_integration_test.cpp @@ -379,7 +379,7 @@ class GrpcIntegrationTestBase : public ::testing::Test { cpu_optimization_problem_t load_problem_from_mps(const std::string& mps_path) { - auto mps_data = cuopt::linear_programming::io::parse_mps(mps_path); + auto mps_data = cuopt::linear_programming::io::read_mps(mps_path); cpu_optimization_problem_t problem; populate_from_mps_data_model(&problem, mps_data); return problem; diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index 4263a594ce..db89b0c7f3 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -56,14 +56,14 @@ bool file_exists(const std::string& file) namespace { -// Non-template forwarding wrapper around parse_lp_from_string. -// Exists only so EXPECT_THROW(parse_lp_string(R"LP(...)LP"), exc) is parsed +// Non-template forwarding wrapper around read_lp_from_string. +// Exists only so EXPECT_THROW(read_lp_string(R"LP(...)LP"), exc) is parsed // correctly — gtest's macro splits its args on top-level commas, and the // comma inside would otherwise be treated as a macro-arg // separator. -mps_data_model_t parse_lp_string(std::string_view content) +mps_data_model_t read_lp_string(std::string_view content) { - return parse_lp_from_string(content); + return read_lp_from_string(content); } // Returns the index of `name` in the variable list, or -1 if absent. @@ -118,23 +118,23 @@ double q_entry(const mps_data_model_t& m, int row, int col) // MPS and LP TEST_F cases within a fixture share the same `check_model` // method, so the expected values live in exactly one place per fixture. // -// All fixtures inherit a common base that supplies parse_mps_file and -// parse_lp_file helpers. +// All fixtures inherit a common base that supplies read_mps_file and +// read_lp_file helpers. // =========================================================================== class parser_fixture_base : public ::testing::Test { protected: - static mps_data_model_t parse_mps_file(const std::string& file, + static mps_data_model_t read_mps_file(const std::string& file, bool fixed_format = true) { const std::string& root = cuopt::test::get_rapids_dataset_root_dir(); - return parse_mps(root + "/" + file, fixed_format); + return read_mps(root + "/" + file, fixed_format); } - static mps_data_model_t parse_lp_file(const std::string& file) + static mps_data_model_t read_lp_file(const std::string& file) { const std::string& root = cuopt::test::get_rapids_dataset_root_dir(); - return parse_lp(root + "/" + file); + return read_lp(root + "/" + file); } }; @@ -359,7 +359,7 @@ TEST(mps_parser, bad_mps_files) TEST_F(good_mps_1_test, mps) { - check_model(parse_mps_file("linear_programming/good-mps-1.mps")); + check_model(read_mps_file("linear_programming/good-mps-1.mps")); // Parser-struct fields that are MPS-only (not exposed via the data model). auto mps = read_from_mps("linear_programming/good-mps-1.mps"); EXPECT_EQ("good-1", mps.problem_name); @@ -369,19 +369,19 @@ TEST_F(good_mps_1_test, mps) EXPECT_EQ(LesserThanOrEqual, mps.row_types[1]); } -TEST_F(good_mps_1_test, lp) { check_model(parse_lp_file("linear_programming/good-mps-1.lp")); } +TEST_F(good_mps_1_test, lp) { check_model(read_lp_file("linear_programming/good-mps-1.lp")); } -// Compressed-LP coverage: parse_lp() shares file_to_string() with parse_mps(), +// Compressed-LP coverage: read_lp() shares file_to_string() with read_mps(), // so the same dlopen-based decompression path that handles .mps.gz / .mps.bz2 // must also work for .lp.gz / .lp.bz2. TEST_F(good_mps_1_test, lp_zlib_compressed) { - check_model(parse_lp_file("linear_programming/good-mps-1.lp.gz")); + check_model(read_lp_file("linear_programming/good-mps-1.lp.gz")); } TEST_F(good_mps_1_test, lp_bzip2_compressed) { - check_model(parse_lp_file("linear_programming/good-mps-1.lp.bz2")); + check_model(read_lp_file("linear_programming/good-mps-1.lp.bz2")); } TEST(mps_parser, good_mps_file_clrf) @@ -594,7 +594,7 @@ TEST(mps_parser_free_format, bad_mps_files_free_format) TEST_F(up_low_bounds_test, mps) { - check_model(parse_mps_file("linear_programming/lp_model_with_var_bounds.mps", false)); + check_model(read_mps_file("linear_programming/lp_model_with_var_bounds.mps", false)); auto mps = read_from_mps("linear_programming/lp_model_with_var_bounds.mps", false); EXPECT_EQ("lp_model_with_var_bounds", mps.problem_name); EXPECT_EQ("OBJ", mps.objective_name); @@ -604,54 +604,54 @@ TEST_F(up_low_bounds_test, mps) TEST_F(up_low_bounds_test, lp) { - check_model(parse_lp_file("linear_programming/lp_model_with_var_bounds.lp")); + check_model(read_lp_file("linear_programming/lp_model_with_var_bounds.lp")); } TEST_F(good_mps_1_test, mps_free_format) { // free-format-mps-1.mps encodes the same problem as good-mps-1 with default // [0, +inf) bounds (no BOUNDS section), so it satisfies the same checker. - check_model(parse_mps_file("linear_programming/free-format-mps-1.mps", false)); + check_model(read_mps_file("linear_programming/free-format-mps-1.mps", false)); } TEST_F(some_var_bounds_test, mps) { - check_model(parse_mps_file("linear_programming/good-mps-some-var-bounds.mps")); + check_model(read_mps_file("linear_programming/good-mps-some-var-bounds.mps")); } TEST_F(some_var_bounds_test, lp) { - check_model(parse_lp_file("linear_programming/good-mps-some-var-bounds.lp")); + check_model(read_lp_file("linear_programming/good-mps-some-var-bounds.lp")); } TEST_F(fixed_var_bound_test, mps) { - check_model(parse_mps_file("linear_programming/good-mps-fixed-var.mps")); + check_model(read_mps_file("linear_programming/good-mps-fixed-var.mps")); } TEST_F(fixed_var_bound_test, lp) { - check_model(parse_lp_file("linear_programming/good-mps-fixed-var.lp")); + check_model(read_lp_file("linear_programming/good-mps-fixed-var.lp")); } TEST_F(free_var_bound_test, mps) { - check_model(parse_mps_file("linear_programming/good-mps-free-var.mps")); + check_model(read_mps_file("linear_programming/good-mps-free-var.mps")); } TEST_F(free_var_bound_test, lp) { - check_model(parse_lp_file("linear_programming/good-mps-free-var.lp")); + check_model(read_lp_file("linear_programming/good-mps-free-var.lp")); } TEST_F(lower_inf_var_bound_test, mps) { - check_model(parse_mps_file("linear_programming/good-mps-lower-bound-inf-var.mps")); + check_model(read_mps_file("linear_programming/good-mps-lower-bound-inf-var.mps")); } TEST_F(lower_inf_var_bound_test, lp) { - check_model(parse_lp_file("linear_programming/good-mps-lower-bound-inf-var.lp")); + check_model(read_lp_file("linear_programming/good-mps-lower-bound-inf-var.lp")); } TEST(mps_bounds, rhs_cost) @@ -664,12 +664,12 @@ TEST(mps_bounds, rhs_cost) TEST_F(upper_inf_var_bound_test, mps) { - check_model(parse_mps_file("linear_programming/good-mps-upper-bound-inf-var.mps")); + check_model(read_mps_file("linear_programming/good-mps-upper-bound-inf-var.mps")); } TEST_F(upper_inf_var_bound_test, lp) { - check_model(parse_lp_file("linear_programming/good-mps-upper-bound-inf-var.lp")); + check_model(read_lp_file("linear_programming/good-mps-upper-bound-inf-var.lp")); } TEST(mps_ranges, fixed_ranges) @@ -685,7 +685,7 @@ TEST(mps_ranges, fixed_ranges) std::string rel_file{}; const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); rel_file = rapidsDatasetRootDir + "/" + file; - auto data_model = parse_mps(rel_file, true); + auto data_model = read_mps(rel_file, true); EXPECT_NEAR(1.2, data_model.get_constraint_lower_bounds()[0], tolerance); // ROW1 lower bound EXPECT_NEAR(5.4, data_model.get_constraint_upper_bounds()[0], tolerance); // ROW1 upper bound @@ -726,7 +726,7 @@ TEST(mps_ranges, free_ranges) std::string rel_file{}; const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); rel_file = rapidsDatasetRootDir + "/" + file; - auto data_model = parse_mps(rel_file, false); + auto data_model = read_mps(rel_file, false); EXPECT_NEAR(1.2, data_model.get_constraint_lower_bounds()[0], tolerance); // ROW1 lower bound EXPECT_NEAR(5.4, data_model.get_constraint_upper_bounds()[0], tolerance); // ROW1 upper bound @@ -819,7 +819,7 @@ TEST(mps_bounds, unsupported_or_invalid_mps_types) TEST_F(mip_with_bounds_test, mps) { - check_model(parse_mps_file("mixed_integer_programming/good-mip-mps-1.mps", false)); + check_model(read_mps_file("mixed_integer_programming/good-mip-mps-1.mps", false)); auto mps = read_from_mps("mixed_integer_programming/good-mip-mps-1.mps", false); EXPECT_EQ("COST", mps.objective_name); ASSERT_EQ(int(2), mps.row_types.size()); @@ -829,7 +829,7 @@ TEST_F(mip_with_bounds_test, mps) TEST_F(mip_with_bounds_test, lp) { - check_model(parse_lp_file("mixed_integer_programming/good-mip-mps-1.lp")); + check_model(read_lp_file("mixed_integer_programming/good-mip-mps-1.lp")); } TEST(mps_parser, good_mps_file_mip_no_marker) @@ -879,22 +879,22 @@ TEST(mps_parser, good_mps_file_mip_no_marker) TEST_F(mip_no_bounds_test, mps) { - check_model(parse_mps_file("mixed_integer_programming/good-mip-mps-no-bounds.mps", false)); + check_model(read_mps_file("mixed_integer_programming/good-mip-mps-no-bounds.mps", false)); } TEST_F(mip_no_bounds_test, lp) { - check_model(parse_lp_file("mixed_integer_programming/good-mip-mps-no-bounds.lp")); + check_model(read_lp_file("mixed_integer_programming/good-mip-mps-no-bounds.lp")); } TEST_F(mip_partial_bounds_test, mps) { - check_model(parse_mps_file("mixed_integer_programming/good-mip-mps-partial-bounds.mps", false)); + check_model(read_mps_file("mixed_integer_programming/good-mip-mps-partial-bounds.mps", false)); } TEST_F(mip_partial_bounds_test, lp) { - check_model(parse_lp_file("mixed_integer_programming/good-mip-mps-partial-bounds.lp")); + check_model(read_lp_file("mixed_integer_programming/good-mip-mps-partial-bounds.lp")); } #ifdef MPS_PARSER_WITH_BZIP2 @@ -1003,7 +1003,7 @@ TEST(qps_parser, test_qps_files) { // Test QP_Test_1.qps if it exists if (file_exists("quadratic_programming/QP_Test_1.qps")) { - auto parsed_data = parse_mps( + auto parsed_data = read_mps( cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_1.qps", false); EXPECT_EQ("QP_Test_1", parsed_data.get_problem_name()); @@ -1023,7 +1023,7 @@ TEST(qps_parser, test_qps_files) // Test QP_Test_2.qps if it exists if (file_exists("quadratic_programming/QP_Test_2.qps")) { - auto parsed_data = parse_mps( + auto parsed_data = read_mps( cuopt::test::get_rapids_dataset_root_dir() + "/quadratic_programming/QP_Test_2.qps", false); EXPECT_EQ("QP_Test_2", parsed_data.get_problem_name()); @@ -1194,14 +1194,14 @@ TEST(mps_roundtrip, linear_programming_basic) temp_file_t temp_file(".mps"); // Read original - auto original = parse_mps(input_file, true); + auto original = read_mps(input_file, true); // Write to temp file mps_writer_t writer(original); writer.write(temp_file.string()); // Read back - auto reloaded = parse_mps(temp_file.string(), false); + auto reloaded = read_mps(temp_file.string(), false); // Compare compare_data_models(original, reloaded); @@ -1218,14 +1218,14 @@ TEST(mps_roundtrip, linear_programming_with_bounds) temp_file_t temp_file(".mps"); // Read original - auto original = parse_mps(input_file, false); + auto original = read_mps(input_file, false); // Write to temp file mps_writer_t writer(original); writer.write(temp_file.string()); // Read back - auto reloaded = parse_mps(temp_file.string(), false); + auto reloaded = read_mps(temp_file.string(), false); // Compare compare_data_models(original, reloaded); @@ -1242,7 +1242,7 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_1) temp_file_t temp_file(".mps"); // Read original - auto original = parse_mps(input_file, false); + auto original = read_mps(input_file, false); ASSERT_TRUE(original.has_quadratic_objective()) << "Original should have quadratic objective"; // Write to temp file @@ -1250,7 +1250,7 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_1) writer.write(temp_file.string()); // Read back - auto reloaded = parse_mps(temp_file.string(), false); + auto reloaded = read_mps(temp_file.string(), false); ASSERT_TRUE(reloaded.has_quadratic_objective()) << "Reloaded should have quadratic objective"; // Compare @@ -1268,7 +1268,7 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_2) temp_file_t temp_file(".mps"); // Read original - auto original = parse_mps(input_file, false); + auto original = read_mps(input_file, false); ASSERT_TRUE(original.has_quadratic_objective()) << "Original should have quadratic objective"; // Write to temp file @@ -1276,7 +1276,7 @@ TEST(mps_roundtrip, quadratic_programming_qp_test_2) writer.write(temp_file.string()); // Read back - auto reloaded = parse_mps(temp_file.string(), false); + auto reloaded = read_mps(temp_file.string(), false); ASSERT_TRUE(reloaded.has_quadratic_objective()) << "Reloaded should have quadratic objective"; // Compare @@ -1295,12 +1295,12 @@ TEST_F(good_mps_1_test, lp_roundtrip) { temp_file_t temp_file(".mps"); - auto original = parse_lp_file("linear_programming/good-mps-1.lp"); + auto original = read_lp_file("linear_programming/good-mps-1.lp"); mps_writer_t writer(original); writer.write(temp_file.string()); - auto reloaded = parse_mps(temp_file.string(), false); + auto reloaded = read_mps(temp_file.string(), false); compare_data_models(original, reloaded); } @@ -1309,12 +1309,12 @@ TEST_F(up_low_bounds_test, lp_roundtrip) { temp_file_t temp_file(".mps"); - auto original = parse_lp_file("linear_programming/lp_model_with_var_bounds.lp"); + auto original = read_lp_file("linear_programming/lp_model_with_var_bounds.lp"); mps_writer_t writer(original); writer.write(temp_file.string()); - auto reloaded = parse_mps(temp_file.string(), false); + auto reloaded = read_mps(temp_file.string(), false); compare_data_models(original, reloaded); } @@ -1323,23 +1323,23 @@ TEST_F(mip_with_bounds_test, lp_roundtrip) { temp_file_t temp_file(".mps"); - auto original = parse_lp_file("mixed_integer_programming/good-mip-mps-1.lp"); + auto original = read_lp_file("mixed_integer_programming/good-mip-mps-1.lp"); mps_writer_t writer(original); writer.write(temp_file.string()); - auto reloaded = parse_mps(temp_file.string(), false); + auto reloaded = read_mps(temp_file.string(), false); compare_data_models(original, reloaded); } // ================================================================================================ -// LP syntax / feature / error-path tests (parse_lp on inline LP content) +// LP syntax / feature / error-path tests (read_lp on inline LP content) // ================================================================================================ TEST(lp_parser, trivial) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x Subject To @@ -1369,7 +1369,7 @@ End TEST(lp_parser, basic_lp_with_float_coefficients) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x1 + x2 Subject To @@ -1401,7 +1401,7 @@ End TEST(lp_parser, maximize_flips_sense) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Maximize 3 x + 2 y Subject To @@ -1419,7 +1419,7 @@ End TEST(lp_parser, equality_constraints) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize c1 + 2 c2 + 3 c3 + 4 c4 Subject To @@ -1444,7 +1444,7 @@ End TEST(lp_parser, mixed_constraint_relations) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + 2 y + 3 z Subject To @@ -1470,7 +1470,7 @@ End TEST(lp_parser, free_and_negative_lower_bound_variables) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize xfree + xneg_lb + xstd Subject To @@ -1500,7 +1500,7 @@ End TEST(lp_parser, bounds_variety) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize xfixed + xub_only + xlb_pos Subject To @@ -1525,7 +1525,7 @@ End TEST(lp_parser, general_integers) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Maximize 3 x + 5 y Subject To @@ -1547,7 +1547,7 @@ End TEST(lp_parser, binaries_set_zero_one_bounds) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Maximize 3 x1 + 5 x2 + 4 x3 + 2 x4 Subject To @@ -1567,7 +1567,7 @@ End TEST(lp_parser, mixed_continuous_integer_binary) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Maximize 3 xc + 4 xi + 7 xb Subject To @@ -1591,7 +1591,7 @@ End TEST(lp_parser, quadratic_diagonal_only) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y + [ 2 x ^2 + 4 y ^2 ] / 2 Subject To @@ -1617,7 +1617,7 @@ End TEST(lp_parser, quadratic_with_cross_terms) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize - 3 x - 4 y - 2 z + [ 2 x ^2 + 2 x * y + 2 y ^2 + 2 y * z + 2 z ^2 ] / 2 Subject To @@ -1652,7 +1652,7 @@ End TEST(lp_parser, miqp_integer_with_quadratic_objective) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize - 4 xi - 2 xc + [ 2 xi ^2 + 2 xc ^2 ] / 2 Subject To @@ -1675,7 +1675,7 @@ End TEST(lp_parser, infeasible_model_parses_faithfully) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y Subject To @@ -1696,7 +1696,7 @@ End TEST(lp_parser, unbounded_model_parses) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Maximize x + y Subject To @@ -1712,7 +1712,7 @@ End TEST(lp_parser, missing_objective_throws) { - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Subject To c1: x + y <= 5 End @@ -1722,7 +1722,7 @@ End TEST(lp_parser, unsupported_sos_section_throws) { - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -1736,7 +1736,7 @@ End TEST(lp_parser, semi_continuous_basic) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y Subject To @@ -1763,7 +1763,7 @@ TEST(lp_parser, semi_continuous_bare_semi_keyword) { // The LP-format convention accepts the bare "Semi" keyword as a synonym // for the "Semi-Continuous" section header. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x Subject To @@ -1785,7 +1785,7 @@ TEST(lp_parser, semi_continuous_bare_semis_keyword) { // The LP-format convention accepts the bare "Semis" keyword as a synonym // for the "Semi-Continuous" section header. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x Subject To @@ -1805,7 +1805,7 @@ End TEST(lp_parser, semi_continuous_default_lower_is_zero) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x Subject To @@ -1827,7 +1827,7 @@ End TEST(lp_parser, semi_continuous_missing_upper_throws) { // No upper bound specified ⇒ infinity ⇒ semantics degenerate, reject. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -1843,7 +1843,7 @@ TEST(lp_parser, semi_continuous_and_generals_conflict_throws) { // Variable appearing in both Semi-Continuous and Generals is ambiguous // (integer vs. continuous-or-zero) ⇒ reject. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -1861,7 +1861,7 @@ End TEST(lp_parser, semi_continuous_and_binaries_conflict_throws) { - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -1880,7 +1880,7 @@ End TEST(lp_parser, semi_continuous_before_generals_conflict_throws) { // Conflict must also be detected when Semi-Continuous is declared first. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -1898,7 +1898,7 @@ End TEST(lp_parser, unsupported_pwlobj_section_throws) { - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -1913,7 +1913,7 @@ End TEST(lp_parser, unsupported_lazy_constraints_section_throws) { // Lazy constraints and user cuts are scope-limited out: LP/MIP/QP only. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -1927,7 +1927,7 @@ End TEST(lp_parser, unsupported_user_cuts_section_throws) { - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -1941,13 +1941,13 @@ End TEST(lp_parser, unknown_file_throws) { - auto call = [] { return parse_lp("/definitely/does/not/exist.lp"); }; + auto call = [] { return read_lp("/definitely/does/not/exist.lp"); }; EXPECT_THROW(call(), std::logic_error); } TEST(lp_parser, case_insensitive_section_keywords) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( MINIMIZE x SUBJECT TO @@ -1962,7 +1962,7 @@ END TEST(lp_parser, backslash_comments_are_ignored) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( \ This is a comment Minimize x \ trailing comment @@ -1977,7 +1977,7 @@ End TEST(lp_parser, missing_end_warns_but_succeeds) { // No End — should still parse. (A warning is printed; see parse_all().) - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x Subject To @@ -1988,7 +1988,7 @@ Subject To TEST(lp_parser, auto_generates_names_for_unlabeled_constraints) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y Subject To @@ -2004,7 +2004,7 @@ End TEST(lp_parser, infinity_keyword_in_bounds) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y Subject To @@ -2023,7 +2023,7 @@ End TEST(lp_parser, coefficient_one_implicit_with_leading_minus) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize - x + y Subject To @@ -2043,7 +2043,7 @@ TEST(lp_parser, quadratic_without_slash_two_is_rejected) // The quadratic bracket in the objective must be followed by '/ 2'. // Without it there's no unambiguous way to tell whether the user meant // '/ 2' and forgot or intended the bare coefficients, so cuopt rejects. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize [ 1 x ^2 ] Subject To @@ -2058,7 +2058,7 @@ TEST(lp_parser, leading_coefficient_before_objective_bracket_rejected) // '2 [ x^2 ] / 2' is ambiguous between "constant 2 plus 0.5 x^2" and // "scalar 2 times 0.5 x^2"; the LP convention is to place coefficients // inside the brackets, so reject. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize 2 [ x ^ 2 ] / 2 Subject To @@ -2071,7 +2071,7 @@ End TEST(lp_parser, leading_coefficient_before_constraint_bracket_rejected) { // Same ambiguity as the objective case, in a quadratic constraint. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -2085,7 +2085,7 @@ TEST(lp_parser, constant_then_signed_bracket_in_objective_is_accepted) { // The positive form: a literal constant in the objective followed by a // signed quadratic bracket still parses (constant becomes objective offset). - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize 5 + [ x ^ 2 ] / 2 Subject To @@ -2100,7 +2100,7 @@ TEST(lp_parser, stray_star_after_number_without_variable_rejected) { // '3 *' followed by a relation, section header, or EOL must error rather // than silently drop the '*' and treat the '3' as a bare constant. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize 3 * Subject To @@ -2113,7 +2113,7 @@ End TEST(lp_parser, explicit_star_between_coefficient_and_variable_is_accepted) { // The positive form: '3 * x' is the same as '3 x'. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize 3 * x Subject To @@ -2139,7 +2139,7 @@ const auto& nth_qc(const mps_data_model_t& m, size_t k) TEST(lp_parser, qc_basic_diagonal_only) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y Subject To @@ -2166,7 +2166,7 @@ TEST(lp_parser, qc_cross_term_splits_symmetrically) { // `4 x*y` in the LP source means coefficient on x_i * x_j = 4 in the // symmetric x^T Q x. Split into Q[x,y] = Q[y,x] = 2 in the CSR. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y Subject To @@ -2186,7 +2186,7 @@ End TEST(lp_parser, qc_linear_and_quadratic_mixed) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y Subject To @@ -2212,7 +2212,7 @@ TEST(lp_parser, qc_multiple_constraints_indexing) { // 2 linear constraints, then 2 quadratic constraints. Per the data-model // convention, quadratic rows are indexed after all linear rows. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y Subject To @@ -2236,7 +2236,7 @@ TEST(lp_parser, qc_outer_minus_sign_flips_quadratic) // After moving the constant to the RHS: -x^2 - 2 x <= rhs - 5. // Here the RHS is 10, so the row becomes: -x^2 - 2 x <= 5 (in x^T Q x form // Q[x,x] = -1). - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x Subject To @@ -2259,7 +2259,7 @@ TEST(lp_parser, bare_linear_inside_objective_bracket_rejected) // The LP-format convention reserves `[ ... ]` for quadratic terms only // (squared and product). A bare linear term like `2 x` inside the // bracket is malformed; the user should write it outside. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize obj: [ x ^ 2 + 2 x ] / 2 Subject To @@ -2271,7 +2271,7 @@ End TEST(lp_parser, bare_linear_inside_constraint_bracket_rejected) { - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -2283,7 +2283,7 @@ End TEST(lp_parser, qc_named_constraint) { - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x Subject To @@ -2296,7 +2296,7 @@ End TEST(lp_parser, qc_ge_relation_throws) { - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -2308,7 +2308,7 @@ End TEST(lp_parser, qc_eq_relation_throws) { - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -2322,7 +2322,7 @@ TEST(lp_parser, qc_with_slash_two_is_rejected) { // '/ 2' is reserved for the objective bracket; using it in a constraint // bracket is rejected so the convention is unambiguous. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -2336,7 +2336,7 @@ TEST(lp_parser, qc_linear_only_bracket_is_rejected) { // A bracket with no quadratic terms inside is meaningless in a constraint // (the user could just write the linear terms directly). - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -2350,7 +2350,7 @@ TEST(lp_parser, qc_objective_quadratic_still_requires_slash_two) { // Regression: the existing '/ 2' requirement on the objective bracket // must not change after adding constraint-bracket support. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize [ x ^ 2 ] Subject To @@ -2363,7 +2363,7 @@ End TEST(lp_parser, duplicate_coefficient_accumulates) { // Repeated variable in the objective should sum coefficients. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize 2 x + 3 x + y Subject To @@ -2380,7 +2380,7 @@ TEST(lp_parser, subject_to_variant_st_dot) { // 'st.' with a trailing period is a Subject-To synonym in the LP-format // convention. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x st. @@ -2396,7 +2396,7 @@ TEST(lp_parser, swapped_relational_operators_eq_lt_and_eq_gt) // '=<' is an alias for '<=' and '=>' for '>=', in both constraints and // bounds. Tokenizer must produce LessEq / GreaterEq tokens regardless of // spelling. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x + y Subject To @@ -2424,7 +2424,7 @@ TEST(lp_parser, variable_names_with_special_characters) // Per the LP-format convention, variable names may contain assorted // punctuation beyond letters + underscore. The names are treated as // opaque identifiers; cuopt just has to keep them distinct. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x!a + x#b + x$c + x@d + x'e + x~f + x.g + x_h + x|i + x{j} + x(k) + a/b Subject To @@ -2442,7 +2442,7 @@ TEST(lp_parser, negative_upper_without_explicit_lower_throws) { // 'x <= -1' with no explicit lower makes the default lb=0 collide with the // upper. cuopt rejects rather than accept a silently infeasible problem. - EXPECT_THROW(parse_lp_string(R"LP( + EXPECT_THROW(read_lp_string(R"LP( Minimize x Subject To @@ -2457,7 +2457,7 @@ End TEST(lp_parser, negative_upper_with_explicit_lower_ok) { // Same test as above, but now the lower bound is explicit: no error. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x Subject To @@ -2475,7 +2475,7 @@ End TEST(lp_parser, negative_upper_with_range_bound_ok) { // -5 <= x <= -1 declares both bounds in a single line: no error. - auto m = parse_lp_string(R"LP( + auto m = read_lp_string(R"LP( Minimize x Subject To @@ -2490,7 +2490,7 @@ End } // ================================================================================================ -// parse_problem dispatch tests +// read dispatch tests // // Verifies the extension-based dispatch used by cuopt_cli and the C API. // ================================================================================================ @@ -2498,8 +2498,8 @@ End namespace { // Writes `content` to a temp file with the given suffix, parses it via -// parse_problem, and returns the resulting model. temp_file_t removes the -// file on every scope exit (including when parse_problem throws). +// read, and returns the resulting model. temp_file_t removes the +// file on every scope exit (including when read throws). mps_data_model_t dispatch_parse(const std::string& content, const std::string& suffix) { temp_file_t tmp(suffix); @@ -2507,7 +2507,7 @@ mps_data_model_t dispatch_parse(const std::string& content, const s std::ofstream out(tmp.string()); out << content; } - return parse_problem(tmp.string()); + return read(tmp.string()); } constexpr const char* kTrivialLp = R"LP( @@ -2536,7 +2536,7 @@ ENDATA } // namespace -TEST(parse_problem, lp_extension_dispatches_to_lp_parser) +TEST(read, lp_extension_dispatches_to_lp_parser) { auto m = dispatch_parse(kTrivialLp, ".lp"); ASSERT_EQ(m.get_variable_names().size(), 1u); @@ -2544,26 +2544,26 @@ TEST(parse_problem, lp_extension_dispatches_to_lp_parser) EXPECT_NEAR(m.get_variable_upper_bounds()[0], 10.0, tolerance); } -TEST(parse_problem, lp_gz_extension_dispatches_to_lp_parser) +TEST(read, lp_gz_extension_dispatches_to_lp_parser) { // Real compressed LP fixture; successful parse proves dispatch picked the - // LP path. (Routing a .lp.gz to parse_mps would either fail at + // LP path. (Routing a .lp.gz to read_mps would either fail at // decompression or fail to parse the LP content as MPS.) - auto m = parse_problem(cuopt::test::get_rapids_dataset_root_dir() + + auto m = read(cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/good-mps-1.lp.gz"); ASSERT_EQ(m.get_variable_names().size(), 2u); EXPECT_EQ(m.get_variable_names()[0], "VAR1"); } -TEST(parse_problem, lp_bz2_extension_dispatches_to_lp_parser) +TEST(read, lp_bz2_extension_dispatches_to_lp_parser) { - auto m = parse_problem(cuopt::test::get_rapids_dataset_root_dir() + + auto m = read(cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/good-mps-1.lp.bz2"); ASSERT_EQ(m.get_variable_names().size(), 2u); EXPECT_EQ(m.get_variable_names()[0], "VAR1"); } -TEST(parse_problem, mps_extension_dispatches_to_mps_parser) +TEST(read, mps_extension_dispatches_to_mps_parser) { auto m = dispatch_parse(kTrivialMps, ".mps"); ASSERT_EQ(m.get_variable_names().size(), 1u); @@ -2571,45 +2571,45 @@ TEST(parse_problem, mps_extension_dispatches_to_mps_parser) EXPECT_NEAR(m.get_variable_upper_bounds()[0], 10.0, tolerance); } -TEST(parse_problem, qps_extension_dispatches_to_mps_parser) +TEST(read, qps_extension_dispatches_to_mps_parser) { // QPS is a superset of MPS; the MPS parser handles both. We just need - // parse_problem to route ".qps" to it. + // read to route ".qps" to it. auto m = dispatch_parse(kTrivialMps, ".qps"); ASSERT_EQ(m.get_variable_names().size(), 1u); EXPECT_EQ(m.get_variable_names()[0], "x"); } -TEST(parse_problem, mps_gz_extension_dispatches_to_mps_parser) +TEST(read, mps_gz_extension_dispatches_to_mps_parser) { - auto m = parse_problem(cuopt::test::get_rapids_dataset_root_dir() + + auto m = read(cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/good-mps-1.mps.gz"); EXPECT_EQ("good-1", m.get_problem_name()); } -TEST(parse_problem, mps_bz2_extension_dispatches_to_mps_parser) +TEST(read, mps_bz2_extension_dispatches_to_mps_parser) { - auto m = parse_problem(cuopt::test::get_rapids_dataset_root_dir() + + auto m = read(cuopt::test::get_rapids_dataset_root_dir() + "/linear_programming/good-mps-1.mps.bz2"); EXPECT_EQ("good-1", m.get_problem_name()); } -TEST(parse_problem, uppercase_lp_extension_dispatches_to_lp_parser) +TEST(read, uppercase_lp_extension_dispatches_to_lp_parser) { - // Matching is case-insensitive: .LP must still route to parse_lp. + // Matching is case-insensitive: .LP must still route to read_lp. auto m = dispatch_parse(kTrivialLp, ".LP"); ASSERT_EQ(m.get_variable_names().size(), 1u); EXPECT_EQ(m.get_variable_names()[0], "x"); } -TEST(parse_problem, mixed_case_mps_extension_dispatches_to_mps_parser) +TEST(read, mixed_case_mps_extension_dispatches_to_mps_parser) { auto m = dispatch_parse(kTrivialMps, ".MpS"); ASSERT_EQ(m.get_variable_names().size(), 1u); EXPECT_EQ(m.get_variable_names()[0], "x"); } -TEST(parse_problem, unrecognized_extension_throws) +TEST(read, unrecognized_extension_throws) { // Extensionless and unrelated suffixes are rejected; case doesn't matter // (matching is case-insensitive, so ".lpgz" stays rejected too). @@ -2799,7 +2799,7 @@ TEST(qps_parser, qcmatrix_mps_linear_rhs_and_bounds) if (!file_exists("qcqp/QC_Test_1.mps")) { GTEST_SKIP() << "qcqp/QC_Test_1.mps not in dataset root"; } - const auto model = parse_mps( + const auto model = read_mps( cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/QC_Test_1.mps", false); ASSERT_TRUE(model.has_quadratic_constraints()); @@ -2851,7 +2851,7 @@ TEST(qps_parser, qcqp_p0033_mps_sections) if (!file_exists("qcqp/p0033_qc1.mps")) { GTEST_SKIP() << "qcqp/p0033_qc1.mps not in dataset root"; } - const auto model = parse_mps( + const auto model = read_mps( cuopt::test::get_rapids_dataset_root_dir() + "/qcqp/p0033_qc1.mps", false); EXPECT_EQ(12, model.get_n_constraints()); @@ -2882,17 +2882,17 @@ TEST(mps_roundtrip, qcqp_p0033_qc1) temp_file_t temp_file(".mps"); temp_file_t temp_file_2(".mps"); - auto original = parse_mps(input_file, false); + auto original = read_mps(input_file, false); ASSERT_TRUE(original.has_quadratic_objective()); ASSERT_TRUE(original.has_quadratic_constraints()); mps_writer_t writer(original); writer.write(temp_file.string()); - auto reloaded = parse_mps(temp_file.string(), false); + auto reloaded = read_mps(temp_file.string(), false); mps_writer_t writer_r2(reloaded); writer_r2.write(temp_file_2.string()); - auto reloaded_2 = parse_mps(temp_file_2.string(), false); + auto reloaded_2 = read_mps(temp_file_2.string(), false); compare_data_models(reloaded, reloaded_2); } } // namespace cuopt::linear_programming::io diff --git a/cpp/tests/linear_programming/pdlp_test.cu b/cpp/tests/linear_programming/pdlp_test.cu index d29995efc5..92e09cd155 100644 --- a/cpp/tests/linear_programming/pdlp_test.cu +++ b/cpp/tests/linear_programming/pdlp_test.cu @@ -79,7 +79,7 @@ TEST(pdlp_class, run_double) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -98,7 +98,7 @@ TEST(pdlp_class, precision_mixed) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto settings = pdlp_solver_settings_t{}; settings.method = cuopt::linear_programming::method_t::PDLP; @@ -114,7 +114,7 @@ TEST(pdlp_class, precision_mixed) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto settings_mixed = pdlp_solver_settings_t{}; settings_mixed.method = cuopt::linear_programming::method_t::PDLP; @@ -149,7 +149,7 @@ TEST(pdlp_class, concurrent_pdlp_exception_joins_worker_threads) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto settings = pdlp_solver_settings_t{}; settings.method = cuopt::linear_programming::method_t::Concurrent; @@ -173,7 +173,7 @@ TEST(pdlp_class, run_double_very_low_accuracy) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); cuopt::linear_programming::pdlp_solver_settings_t settings = cuopt::linear_programming::pdlp_solver_settings_t{}; @@ -199,7 +199,7 @@ TEST(pdlp_class, run_double_initial_solution) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); std::vector inital_primal_sol(op_problem.get_n_variables()); std::fill(inital_primal_sol.begin(), inital_primal_sol.end(), 1.0); @@ -221,7 +221,7 @@ TEST(pdlp_class, run_iteration_limit) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); cuopt::linear_programming::pdlp_solver_settings_t settings = cuopt::linear_programming::pdlp_solver_settings_t{}; @@ -246,7 +246,7 @@ TEST(pdlp_class, batch_iteration_limit_updates_additional_termination_stats) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto settings = pdlp_solver_settings_t{}; settings.iteration_limit = 10; @@ -279,7 +279,7 @@ TEST(pdlp_class, batch_settings_overrides_preserve_user_limits_and_tolerances) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); constexpr int batch_size = 2; constexpr double tighter_tolerance = 1e-6; @@ -363,7 +363,7 @@ TEST(pdlp_class, run_time_limit) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/savsched1/savsched1.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); cuopt::linear_programming::pdlp_solver_settings_t settings = cuopt::linear_programming::pdlp_solver_settings_t{}; @@ -408,7 +408,7 @@ TEST(pdlp_class, run_sub_mittleman) auto path = make_path_absolute("linear_programming/" + name + "/" + name + ".mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); // Testing for each solver_mode is ok as it's parsing that is the bottleneck here, not // solving @@ -466,7 +466,7 @@ TEST(pdlp_class, initial_solution_test) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t mps_data_model = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto op_problem = cuopt::linear_programming::mps_data_model_to_optimization_problem( &handle_, mps_data_model); @@ -744,7 +744,7 @@ TEST(pdlp_class, initial_primal_weight_step_size_test) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t mps_data_model = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto op_problem = cuopt::linear_programming::mps_data_model_to_optimization_problem( &handle_, mps_data_model); @@ -932,9 +932,9 @@ TEST(pdlp_class, best_primal_so_far_iteration) solver_settings.method = cuopt::linear_programming::method_t::PDLP; solver_settings.pdlp_solver_mode = cuopt::linear_programming::pdlp_solver_mode_t::Stable2; cuopt::linear_programming::io::mps_data_model_t op_problem1 = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); cuopt::linear_programming::io::mps_data_model_t op_problem2 = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); optimization_problem_solution_t solution1 = solve_lp(&handle1, op_problem1, solver_settings); @@ -962,9 +962,9 @@ TEST(pdlp_class, best_primal_so_far_time) solver_settings.method = cuopt::linear_programming::method_t::PDLP; cuopt::linear_programming::io::mps_data_model_t op_problem1 = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); cuopt::linear_programming::io::mps_data_model_t op_problem2 = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); optimization_problem_solution_t solution1 = solve_lp(&handle1, op_problem1, solver_settings); @@ -992,9 +992,9 @@ TEST(pdlp_class, first_primal_feasible) solver_settings.method = cuopt::linear_programming::method_t::PDLP; cuopt::linear_programming::io::mps_data_model_t op_problem1 = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); cuopt::linear_programming::io::mps_data_model_t op_problem2 = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); optimization_problem_solution_t solution1 = solve_lp(&handle1, op_problem1, solver_settings); @@ -1022,7 +1022,7 @@ TEST(pdlp_class, per_constraint_residual_stable3) solver_settings.method = cuopt::linear_programming::method_t::PDLP; cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto sol = solve_lp(&handle, op_problem, solver_settings); RAFT_CUDA_TRY(cudaDeviceSynchronize()); @@ -1046,7 +1046,7 @@ TEST(pdlp_class, batch_per_constraint_residual_stable3) solver_settings.method = cuopt::linear_programming::method_t::PDLP; cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); constexpr int batch_size = 2; @@ -1093,7 +1093,7 @@ TEST(pdlp_class, batch_per_constraint_residual_different_rhs_stable3) solver_settings.method = cuopt::linear_programming::method_t::PDLP; cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); // Build two climbers that share A and variable bounds but differ on the constraint // lower/upper bounds (RHS): climber 0 keeps the original, climber 1 finite bounds get set to 100 @@ -1138,8 +1138,8 @@ TEST(pdlp_class, batch_per_constraint_residual_different_rhs_stable3) // Reload the original (single-climber) problem and build per-climber views so the // per-row sanity check evaluates each solution against its own constraint bounds. - auto climber0_problem = cuopt::linear_programming::io::parse_mps(path); - auto climber1_problem = cuopt::linear_programming::io::parse_mps(path); + auto climber0_problem = cuopt::linear_programming::io::read_mps(path); + auto climber1_problem = cuopt::linear_programming::io::read_mps(path); climber1_problem.set_constraint_lower_bounds({climber1_lb.data(), climber1_lb.size()}); climber1_problem.set_constraint_upper_bounds({climber1_ub.data(), climber1_ub.size()}); @@ -1176,7 +1176,7 @@ TEST(pdlp_class, first_primal_feasible_stable3) solver_settings.presolver = presolver_t::None; cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); // Wihout first primal feasible we hit iteration limit auto sol_base = solve_lp(&handle, op_problem, solver_settings); @@ -1205,7 +1205,7 @@ TEST(pdlp_class, first_primal_feasible_batch_stable3) auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1252,7 +1252,7 @@ TEST(pdlp_class, first_primal_feasible_batch_different_rhs_stable3) auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1316,7 +1316,7 @@ TEST(pdlp_class, all_primal_feasible_batch_different_rhs_stable3) auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1390,7 +1390,7 @@ TEST(pdlp_class, first_primal_feasible_and_per_constraint_residual_stable3) solver_settings.method = cuopt::linear_programming::method_t::PDLP; cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto sol = solve_lp(&handle, op_problem, solver_settings); RAFT_CUDA_TRY(cudaDeviceSynchronize()); @@ -1413,7 +1413,7 @@ TEST(pdlp_class, first_primal_feasible_and_per_constraint_residual_batch_stable3 auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1460,7 +1460,7 @@ TEST(pdlp_class, first_primal_feasible_and_per_constraint_residual_batch_differe auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1527,7 +1527,7 @@ TEST(pdlp_class, all_primal_feasible_and_per_constraint_residual_batch_different auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1594,7 +1594,7 @@ TEST(pdlp_class, all_primal_feasible_and_per_constraint_residual_batch_many_diff auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1705,7 +1705,7 @@ TEST(pdlp_class, all_primal_feasible_and_per_constraint_residual_batch_many_diff auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1815,7 +1815,7 @@ TEST(pdlp_class, batch_primal_feasible_non_batch_rejected) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1832,7 +1832,7 @@ TEST(pdlp_class, first_primal_feasible_and_batch_primal_feasible_rejected) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/ns1687037/ns1687037.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1870,7 +1870,7 @@ TEST(pdlp_class, warm_start) solver_settings.presolver = presolver_t::None; cuopt::linear_programming::io::mps_data_model_t mps_data_model = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto op_problem1 = cuopt::linear_programming::mps_data_model_to_optimization_problem( &handle, mps_data_model); @@ -1912,7 +1912,7 @@ TEST(pdlp_class, warm_start_stable3_not_supported) solver_settings.presolver = presolver_t::None; cuopt::linear_programming::io::mps_data_model_t mps_data_model = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto op_problem = cuopt::linear_programming::mps_data_model_to_optimization_problem( &handle, mps_data_model); optimization_problem_solution_t solution = solve_lp(op_problem, solver_settings); @@ -1928,7 +1928,7 @@ TEST(pdlp_class, dual_postsolve_size) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1962,7 +1962,7 @@ TEST(dual_simplex, afiro) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); optimization_problem_solution_t solution = solve_lp(&handle_, op_problem, settings); EXPECT_EQ(solution.get_termination_status(), pdlp_termination_status_t::Optimal); @@ -1977,7 +1977,7 @@ TEST(pdlp_class, run_empty_matrix_pdlp) auto path = make_path_absolute("linear_programming/empty_matrix.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -1995,7 +1995,7 @@ TEST(pdlp_class, run_empty_matrix_dual_simplex) auto path = make_path_absolute("linear_programming/empty_matrix.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::Concurrent; @@ -2013,7 +2013,7 @@ TEST(pdlp_class, test_max) auto path = make_path_absolute("linear_programming/good-max.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2033,7 +2033,7 @@ TEST(pdlp_class, test_max_with_offset) auto path = make_path_absolute("linear_programming/max_offset.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2052,7 +2052,7 @@ TEST(pdlp_class, test_lp_no_constraints) auto path = make_path_absolute("linear_programming/lp-model-no-constraints.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path); + cuopt::linear_programming::io::read_mps(path); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.presolver = presolver_t::None; @@ -2079,7 +2079,7 @@ TEST(pdlp_class, simple_batch_afiro) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2161,7 +2161,7 @@ TEST(pdlp_class, simple_batch_different_bounds) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2217,7 +2217,7 @@ TEST(pdlp_class, more_complex_batch_different_bounds) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2310,7 +2310,7 @@ TEST(pdlp_class, simple_batch_different_objectives) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2378,7 +2378,7 @@ TEST(pdlp_class, simple_batch_different_offsets) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2418,7 +2418,7 @@ TEST(pdlp_class, simple_batch_different_objectives_and_offsets) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2476,7 +2476,7 @@ TEST(pdlp_class, simple_batch_different_constraint_bounds) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2544,7 +2544,7 @@ TEST(pdlp_class, simple_batch_everything_different) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -2664,7 +2664,7 @@ TEST(pdlp_class, run_batch_pdlp_fixed_rejects_partial_per_climber_expansion) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); constexpr int batch_size = 3; const auto n_vars = static_cast(op_problem.get_n_variables()); @@ -2746,7 +2746,7 @@ TEST(pdlp_class, run_batch_pdlp_rejects_invalid_new_bounds) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto expect_validation_error = [&](pdlp_solver_settings_t settings) { auto gpu_op = cuopt::linear_programming::mps_data_model_to_optimization_problem( @@ -2854,7 +2854,7 @@ TEST(pdlp_class, run_batch_pdlp_rejects_save_best_primal_so_far) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); // Splitting path: trigger batch mode via a non-empty new_bounds list (size > 1). { @@ -2907,7 +2907,7 @@ TEST(pdlp_class, DISABLED_cupdlpx_infeasible_detection_afiro_new_bounds) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); for (size_t i = 1; i < 8; ++i) { op_problem.get_variable_lower_bounds()[i] = 7.0; @@ -2936,7 +2936,7 @@ TEST(pdlp_class, DISABLED_cupdlpx_batch_infeasible_detection) auto path = make_path_absolute("linear_programming/good-mps-fixed-ranges.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const std::vector& variable_lower_bounds = op_problem.get_variable_lower_bounds(); const std::vector& variable_upper_bounds = op_problem.get_variable_upper_bounds(); @@ -2976,7 +2976,7 @@ TEST(pdlp_class, DISABLED_cupdlpx_infeasible_detection_batch_afiro_new_bounds) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); // Use a ref problem that is infeasible auto op_problem_ref = op_problem; @@ -3020,7 +3020,7 @@ TEST(pdlp_class, new_bounds) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -3065,7 +3065,7 @@ TEST(pdlp_class, big_batch_afiro) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -3153,7 +3153,7 @@ TEST(pdlp_class, DISABLED_simple_batch_optimal_and_infeasible) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -3185,7 +3185,7 @@ TEST(pdlp_class, DISABLED_larger_batch_optimal_and_infeasible) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -3231,7 +3231,7 @@ TEST(pdlp_class, strong_branching_test) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const std::vector fractional = {1, 2, 4}; const std::vector root_soln_x = {0.891, 0.109, 0.636429}; @@ -3338,7 +3338,7 @@ TEST(pdlp_class, strong_branching_user_api) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const std::vector fractional = {1, 2, 4}; const std::vector root_soln_x = {0.891, 0.109, 0.636429}; @@ -3426,7 +3426,7 @@ TEST(pdlp_class, strong_branching_multi_bounds_per_climber) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -3505,7 +3505,7 @@ TEST(pdlp_class, run_batch_pdlp_many_different_bounds) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const auto& variable_lower_bounds = op_problem.get_variable_lower_bounds(); const auto& variable_upper_bounds = op_problem.get_variable_upper_bounds(); @@ -3619,7 +3619,7 @@ TEST(pdlp_class, run_batch_pdlp_many_different_bounds_good_mps_some_var_bounds) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/good-mps-some-var-bounds.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const auto& variable_lower_bounds = op_problem.get_variable_lower_bounds(); const auto& variable_upper_bounds = op_problem.get_variable_upper_bounds(); @@ -3717,7 +3717,7 @@ TEST(pdlp_class, run_batch_fixed_api_many_different_bounds_good_mps_some_var_bou const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/good-mps-some-var-bounds.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const auto& variable_lower_bounds = op_problem.get_variable_lower_bounds(); const auto& variable_upper_bounds = op_problem.get_variable_upper_bounds(); @@ -3809,7 +3809,7 @@ TEST(pdlp_class, many_different_bounds) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/good-mps-some-var-bounds.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const auto& variable_lower_bounds = op_problem.get_variable_lower_bounds(); const auto& variable_upper_bounds = op_problem.get_variable_upper_bounds(); @@ -3902,7 +3902,7 @@ TEST(pdlp_class, some_climber_hit_iteration_limit) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/good-mps-some-var-bounds.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const auto& variable_lower_bounds = op_problem.get_variable_lower_bounds(); const auto& variable_upper_bounds = op_problem.get_variable_upper_bounds(); @@ -3984,7 +3984,7 @@ TEST(pdlp_class, precision_single) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -4004,7 +4004,7 @@ TEST(pdlp_class, precision_single_crossover) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -4025,7 +4025,7 @@ TEST(pdlp_class, precision_single_concurrent) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::Concurrent; @@ -4045,7 +4045,7 @@ TEST(pdlp_class, precision_single_papilo_presolve) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -4065,7 +4065,7 @@ TEST(pdlp_class, precision_single_pslp_presolve) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -4137,7 +4137,7 @@ TEST(pdlp_class, shared_sb_view_batch_pre_solved) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const std::vector fractional = {1, 2, 4}; const std::vector root_soln_x = {0.891, 0.109, 0.636429}; @@ -4197,7 +4197,7 @@ TEST(pdlp_class, shared_sb_view_concurrent_mark) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const std::vector fractional = {1, 2, 4}; const std::vector root_soln_x = {0.891, 0.109, 0.636429}; @@ -4269,7 +4269,7 @@ TEST(pdlp_class, shared_sb_view_all_infeasible) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); const std::vector fractional = {1, 2, 4}; const std::vector root_soln_x = {0.891, 0.109, 0.636429}; @@ -4336,7 +4336,7 @@ TEST(pdlp_class, big_batch_fixed_path) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; @@ -4454,7 +4454,7 @@ TEST(pdlp_class, batch_bound_objective_rescaling_factors_match_input_expansion) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); constexpr int batch_size = 3; const int n_vars = op_problem.get_n_variables(); @@ -4598,7 +4598,7 @@ TEST(pdlp_class, batch_with_optimal_size_query) auto path = make_path_absolute("linear_programming/afiro_original.mps"); cuopt::linear_programming::io::mps_data_model_t op_problem = - cuopt::linear_programming::io::parse_mps(path, true); + cuopt::linear_programming::io::read_mps(path, true); auto solver_settings = pdlp_solver_settings_t{}; solver_settings.method = cuopt::linear_programming::method_t::PDLP; diff --git a/cpp/tests/linear_programming/unit_tests/optimization_problem_test.cu b/cpp/tests/linear_programming/unit_tests/optimization_problem_test.cu index cb6eb43367..005e2e7da7 100644 --- a/cpp/tests/linear_programming/unit_tests/optimization_problem_test.cu +++ b/cpp/tests/linear_programming/unit_tests/optimization_problem_test.cu @@ -31,7 +31,7 @@ cuopt::linear_programming::io::mps_data_model_t read_from_mps( // assume relative paths are relative to RAPIDS_DATASET_ROOT_DIR const std::string& rapidsDatasetRootDir = cuopt::test::get_rapids_dataset_root_dir(); rel_file = rapidsDatasetRootDir + "/" + file; - return cuopt::linear_programming::io::parse_mps(rel_file, fixed_mps_format); + return cuopt::linear_programming::io::read_mps(rel_file, fixed_mps_format); } TEST(optimization_problem_t, good_mps_file_1) diff --git a/cpp/tests/linear_programming/unit_tests/presolve_test.cu b/cpp/tests/linear_programming/unit_tests/presolve_test.cu index fd212c4b06..449f20edae 100644 --- a/cpp/tests/linear_programming/unit_tests/presolve_test.cu +++ b/cpp/tests/linear_programming/unit_tests/presolve_test.cu @@ -108,7 +108,7 @@ TEST(pslp_presolve, postsolve_accuracy_afiro) constexpr double expected_obj = -464.75314; // Known optimal objective for afiro auto path = make_path_absolute("linear_programming/afiro_original.mps"); - auto mps_data_model = cuopt::linear_programming::io::parse_mps(path, true); + auto mps_data_model = cuopt::linear_programming::io::read_mps(path, true); // Store original problem data for later verification const auto& orig_coefficients = mps_data_model.get_constraint_matrix_values(); @@ -168,7 +168,7 @@ TEST(pslp_presolve, postsolve_dual_accuracy_afiro) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); - auto mps_data_model = cuopt::linear_programming::io::parse_mps(path, true); + auto mps_data_model = cuopt::linear_programming::io::read_mps(path, true); const int orig_n_vars = mps_data_model.get_n_variables(); const int orig_n_constraints = mps_data_model.get_n_constraints(); @@ -204,7 +204,7 @@ TEST(pslp_presolve, postsolve_accuracy_larger_problem) constexpr double tolerance = 1e-4; auto path = make_path_absolute("linear_programming/ex10/ex10.mps"); - auto mps_data_model = cuopt::linear_programming::io::parse_mps(path, false); + auto mps_data_model = cuopt::linear_programming::io::read_mps(path, false); // Store original problem dimensions const auto& orig_coefficients = mps_data_model.get_constraint_matrix_values(); @@ -254,7 +254,7 @@ TEST(pslp_presolve, compare_with_no_presolve) constexpr double obj_tolerance = 1e-3; auto path = make_path_absolute("linear_programming/afiro_original.mps"); - auto mps_data_model = cuopt::linear_programming::io::parse_mps(path, true); + auto mps_data_model = cuopt::linear_programming::io::read_mps(path, true); // Solve without presolve auto settings_no_presolve = pdlp_solver_settings_t{}; @@ -324,7 +324,7 @@ TEST(pslp_presolve, postsolve_reduced_costs) const raft::handle_t handle_{}; auto path = make_path_absolute("linear_programming/afiro_original.mps"); - auto mps_data_model = cuopt::linear_programming::io::parse_mps(path, true); + auto mps_data_model = cuopt::linear_programming::io::read_mps(path, true); const int orig_n_vars = mps_data_model.get_n_variables(); @@ -359,7 +359,7 @@ TEST(pslp_presolve, postsolve_multiple_problems) for (const auto& [name, expected_obj] : instances) { auto path = make_path_absolute("linear_programming/" + name + ".mps"); auto mps_data_model = - cuopt::linear_programming::io::parse_mps(path, name == "afiro_original"); + cuopt::linear_programming::io::read_mps(path, name == "afiro_original"); const int orig_n_vars = mps_data_model.get_n_variables(); const int orig_n_constraints = mps_data_model.get_n_constraints(); diff --git a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu index 7a29e5913a..f7f164900a 100644 --- a/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu +++ b/cpp/tests/linear_programming/unit_tests/solution_interface_test.cu @@ -368,7 +368,7 @@ TEST_F(SolutionInterfaceTest, cpu_problem_to_optimization_problem) // This test legitimately uses the MPS parser since it tests that pipeline TEST_F(SolutionInterfaceTest, mps_data_model_to_optimization_problem) { - auto mps_data = cuopt::linear_programming::io::parse_mps(lp_file_); + auto mps_data = cuopt::linear_programming::io::read_mps(lp_file_); raft::handle_t handle; auto problem = mps_data_model_to_optimization_problem(&handle, mps_data); diff --git a/cpp/tests/mip/bounds_standardization_test.cu b/cpp/tests/mip/bounds_standardization_test.cu index 0ea51af1a4..059b038f0c 100644 --- a/cpp/tests/mip/bounds_standardization_test.cu +++ b/cpp/tests/mip/bounds_standardization_test.cu @@ -46,7 +46,7 @@ void test_bounds_standardization_test(std::string test_instance) std::cout << "Running: " << test_instance << std::endl; auto path = make_path_absolute(test_instance); cuopt::linear_programming::io::mps_data_model_t problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); auto op_problem = mps_data_model_to_optimization_problem(&handle_, problem); problem_checking_t::check_problem_representation(op_problem); diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 9bd2e5353c..37ec8a1034 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -209,7 +209,7 @@ io::mps_data_model_t& get_neos8_model_cached() static std::unique_ptr> model_ptr; std::call_once(init_flag, []() { const auto neos8_path = make_path_absolute("mip/neos8.mps"); - auto neos8_model = cuopt::linear_programming::io::parse_mps(neos8_path, false); + auto neos8_model = cuopt::linear_programming::io::read_mps(neos8_path, false); model_ptr = std::make_unique>(std::move(neos8_model)); }); cuopt_assert(model_ptr != nullptr, "Failed to initialize cached neos8 model"); diff --git a/cpp/tests/mip/determinism_test.cu b/cpp/tests/mip/determinism_test.cu index 20ad338070..0accf2ba67 100644 --- a/cpp/tests/mip/determinism_test.cu +++ b/cpp/tests/mip/determinism_test.cu @@ -56,7 +56,7 @@ class DeterministicBBTest : public ::testing::Test { TEST_F(DeterministicBBTest, reproducible_objective) { auto path = make_path_absolute("/mip/gen-ip054.mps"); - auto problem = io::parse_mps(path, false); + auto problem = io::read_mps(path, false); handle_.sync_stream(); mip_solver_settings_t settings; @@ -88,7 +88,7 @@ TEST_F(DeterministicBBTest, reproducible_objective) TEST_F(DeterministicBBTest, reproducible_infeasibility) { auto path = make_path_absolute("/mip/stein9inf.mps"); - auto problem = io::parse_mps(path, false); + auto problem = io::read_mps(path, false); handle_.sync_stream(); mip_solver_settings_t settings; @@ -120,7 +120,7 @@ TEST_F(DeterministicBBTest, reproducible_infeasibility) TEST_F(DeterministicBBTest, reproducible_high_contention) { auto path = make_path_absolute("/mip/gen-ip054.mps"); - auto problem = io::parse_mps(path, false); + auto problem = io::read_mps(path, false); handle_.sync_stream(); mip_solver_settings_t settings; @@ -155,7 +155,7 @@ TEST_F(DeterministicBBTest, reproducible_high_contention) TEST_F(DeterministicBBTest, reproducible_solution_vector) { auto path = make_path_absolute("/mip/swath1.mps"); - auto problem = io::parse_mps(path, false); + auto problem = io::read_mps(path, false); handle_.sync_stream(); mip_solver_settings_t settings; @@ -188,7 +188,7 @@ TEST_P(DeterministicBBInstanceTest, deterministic_across_runs) { auto [instance_path, num_threads, time_limit, work_limit] = GetParam(); auto path = make_path_absolute(instance_path); - auto problem = io::parse_mps(path, false); + auto problem = io::read_mps(path, false); handle_.sync_stream(); // Get a random seed for each run diff --git a/cpp/tests/mip/doc_example_test.cu b/cpp/tests/mip/doc_example_test.cu index 74b8eaadbb..c198cca311 100644 --- a/cpp/tests/mip/doc_example_test.cu +++ b/cpp/tests/mip/doc_example_test.cu @@ -127,7 +127,7 @@ TEST(docs, user_problem_file) EXPECT_TRUE(std::filesystem::exists(user_problem_path)); cuopt::linear_programming::io::mps_data_model_t problem2 = - cuopt::linear_programming::io::parse_mps(user_problem_path, false); + cuopt::linear_programming::io::read_mps(user_problem_path, false); EXPECT_EQ(problem2.get_n_variables(), problem.get_n_variables()); EXPECT_EQ(problem2.get_n_constraints(), problem.get_n_constraints()); diff --git a/cpp/tests/mip/elim_var_remap_test.cu b/cpp/tests/mip/elim_var_remap_test.cu index dfab44c4f7..776ebbd310 100644 --- a/cpp/tests/mip/elim_var_remap_test.cu +++ b/cpp/tests/mip/elim_var_remap_test.cu @@ -61,7 +61,7 @@ void test_elim_var_remap(std::string test_instance) std::cout << "Running: " << test_instance << std::endl; auto path = make_path_absolute(test_instance); cuopt::linear_programming::io::mps_data_model_t mps_problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_problem); problem_checking_t::check_problem_representation(op_problem); @@ -129,7 +129,7 @@ void test_elim_var_solution(std::string test_instance) std::cout << "Running: " << test_instance << std::endl; auto path = make_path_absolute(test_instance); cuopt::linear_programming::io::mps_data_model_t mps_problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_problem); problem_checking_t::check_problem_representation(op_problem); diff --git a/cpp/tests/mip/feasibility_jump_tests.cu b/cpp/tests/mip/feasibility_jump_tests.cu index bf110c9232..7820f1f6a0 100644 --- a/cpp/tests/mip/feasibility_jump_tests.cu +++ b/cpp/tests/mip/feasibility_jump_tests.cu @@ -69,7 +69,7 @@ static fj_state_t run_fj(std::string test_instance, auto path = cuopt::test::get_rapids_dataset_root_dir() + ("/mip/" + test_instance); cuopt::linear_programming::io::mps_data_model_t mps_problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_problem); problem_checking_t::check_problem_representation(op_problem); diff --git a/cpp/tests/mip/incumbent_callback_test.cu b/cpp/tests/mip/incumbent_callback_test.cu index 95a2b0a1b3..2dce940f73 100644 --- a/cpp/tests/mip/incumbent_callback_test.cu +++ b/cpp/tests/mip/incumbent_callback_test.cu @@ -113,7 +113,7 @@ void test_incumbent_callback(std::string test_instance, bool include_set_callbac std::cout << "Running: " << test_instance << std::endl; auto path = make_path_absolute(test_instance); cuopt::linear_programming::io::mps_data_model_t mps_problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_problem); @@ -165,7 +165,7 @@ TEST(mip_solve, early_heuristic_incumbent_fallback) const raft::handle_t handle_{}; auto path = make_path_absolute("mip/pk1.mps"); cuopt::linear_programming::io::mps_data_model_t mps_problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_problem); diff --git a/cpp/tests/mip/load_balancing_test.cu b/cpp/tests/mip/load_balancing_test.cu index affbbca7dc..afdc275e15 100644 --- a/cpp/tests/mip/load_balancing_test.cu +++ b/cpp/tests/mip/load_balancing_test.cu @@ -122,7 +122,7 @@ void test_multi_probe(std::string path) rmm::mr::set_current_device_resource(memory_resource); const raft::handle_t handle_{}; cuopt::linear_programming::io::mps_data_model_t mps_problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_problem); problem_checking_t::check_problem_representation(op_problem); diff --git a/cpp/tests/mip/mip_utils.cuh b/cpp/tests/mip/mip_utils.cuh index d24d9a5be9..057047cefd 100644 --- a/cpp/tests/mip/mip_utils.cuh +++ b/cpp/tests/mip/mip_utils.cuh @@ -168,7 +168,7 @@ static std::tuple test_mps_file( auto path = make_path_absolute(test_instance); cuopt::linear_programming::io::mps_data_model_t problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); mip_solver_settings_t settings; settings.time_limit = time_limit; diff --git a/cpp/tests/mip/miplib_test.cu b/cpp/tests/mip/miplib_test.cu index 3a9a2391b0..0e255d35a9 100644 --- a/cpp/tests/mip/miplib_test.cu +++ b/cpp/tests/mip/miplib_test.cu @@ -39,7 +39,7 @@ void test_miplib_file(result_map_t test_instance, mip_solver_settings_t problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); // set the time limit depending on we are in assert mode or not #ifdef ASSERT_MODE @@ -81,7 +81,7 @@ TEST(mip_solve, low_thread_count_test) auto path = make_path_absolute("mip/dominating_set.mps"); cuopt::linear_programming::io::mps_data_model_t problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); mip_solution_t solution = solve_mip(&handle_, problem, settings); @@ -105,7 +105,7 @@ TEST(mip_solve, node_limit_test) auto path = make_path_absolute("mip/swath1.mps"); cuopt::linear_programming::io::mps_data_model_t problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); mip_solution_t solution = solve_mip(&handle_, problem, settings); diff --git a/cpp/tests/mip/multi_probe_test.cu b/cpp/tests/mip/multi_probe_test.cu index 81897e9eac..b6ffb1e592 100644 --- a/cpp/tests/mip/multi_probe_test.cu +++ b/cpp/tests/mip/multi_probe_test.cu @@ -144,7 +144,7 @@ void test_multi_probe(std::string path) rmm::mr::set_current_device_resource(memory_resource); const raft::handle_t handle_{}; cuopt::linear_programming::io::mps_data_model_t mps_problem = - cuopt::linear_programming::io::parse_mps(path, false); + cuopt::linear_programming::io::read_mps(path, false); handle_.sync_stream(); auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_problem); problem_checking_t::check_problem_representation(op_problem); diff --git a/cpp/tests/mip/presolve_test.cu b/cpp/tests/mip/presolve_test.cu index 4bd4265f34..a400d04854 100644 --- a/cpp/tests/mip/presolve_test.cu +++ b/cpp/tests/mip/presolve_test.cu @@ -34,7 +34,7 @@ TEST(problem, find_implied_integers) const raft::handle_t handle_{}; auto path = make_path_absolute("mip/fiball.mps"); - auto mps_data_model = cuopt::linear_programming::io::parse_mps(path, false); + auto mps_data_model = cuopt::linear_programming::io::read_mps(path, false); auto op_problem = mps_data_model_to_optimization_problem(&handle_, mps_data_model); auto presolver = std::make_unique>(); auto result = presolver->apply(op_problem, diff --git a/cpp/tests/qp/unit_tests/lp_parser_solve_test.cu b/cpp/tests/qp/unit_tests/lp_parser_solve_test.cu index 3dc96acbdb..31b65e3a9b 100644 --- a/cpp/tests/qp/unit_tests/lp_parser_solve_test.cu +++ b/cpp/tests/qp/unit_tests/lp_parser_solve_test.cu @@ -37,7 +37,7 @@ void expect_optimal_solution(const std::string& lp_text, const std::vector& expected_x) { raft::handle_t handle; - auto problem = io::parse_lp_from_string(lp_text); + auto problem = io::read_lp_from_string(lp_text); auto settings = pdlp_solver_settings_t(); auto solution = solve_lp(&handle, problem, settings); diff --git a/cpp/tests/routing/level0/l0_ges_test.cu b/cpp/tests/routing/level0/l0_ges_test.cu index b3e72c56e6..bcf49050bf 100644 --- a/cpp/tests/routing/level0/l0_ges_test.cu +++ b/cpp/tests/routing/level0/l0_ges_test.cu @@ -238,7 +238,7 @@ INSTANTIATE_TEST_SUITE_P(level0_ges, double_test_pdp, ::testing::Values(std::mak TEST_P(simple_routes_test_pdp, GES_PDP) { test_cvrptw(); } INSTANTIATE_TEST_SUITE_P(level0_ges, simple_routes_test_pdp, - ::testing::ValuesIn(parse_problems(simple_three_routes_))); + ::testing::ValuesIn(reads(simple_three_routes_))); TEST_P(double_test_vrp, GES_VRP) { test_cvrptw(); } INSTANTIATE_TEST_SUITE_P(level0_ges, double_test_vrp, ::testing::Values(std::make_tuple(true))); diff --git a/cpp/tests/routing/level0/l0_scross_test.cu b/cpp/tests/routing/level0/l0_scross_test.cu index 8f3ac139c3..a12b2d3968 100644 --- a/cpp/tests/routing/level0/l0_scross_test.cu +++ b/cpp/tests/routing/level0/l0_scross_test.cu @@ -207,7 +207,7 @@ typedef simple_scross_test_t scross_three_routes_tes TEST_P(scross_three_routes_test, SCROSS_GES) { test_scross(); } INSTANTIATE_TEST_SUITE_P(simple_scross_test, scross_three_routes_test, - ::testing::ValuesIn(parse_problems(scross_three_routes_))); + ::testing::ValuesIn(reads(scross_three_routes_))); } // namespace test } // namespace routing diff --git a/cpp/tests/routing/routing_test.cuh b/cpp/tests/routing/routing_test.cuh index cdafbbf1f7..4cb67a6d9d 100644 --- a/cpp/tests/routing/routing_test.cuh +++ b/cpp/tests/routing/routing_test.cuh @@ -112,7 +112,7 @@ struct test_data_t { }; template -static test_data_t parse_problem(problem_t&& problem) +static test_data_t read(problem_t&& problem) { return {problem.x_h, problem.y_h, @@ -146,9 +146,9 @@ static test_data_t parse_problem(problem_t&& problem) } template -std::vector> parse_problems(Args&&... args) +std::vector> reads(Args&&... args) { - return {parse_problem(std::forward(args))...}; + return {read(std::forward(args))...}; } template diff --git a/cpp/tests/utilities/inline_mps_test_utils.hpp b/cpp/tests/utilities/inline_mps_test_utils.hpp index 09a1bc158b..6f0370357b 100644 --- a/cpp/tests/utilities/inline_mps_test_utils.hpp +++ b/cpp/tests/utilities/inline_mps_test_utils.hpp @@ -104,7 +104,7 @@ ENDATA inline cuopt::linear_programming::io::mps_data_model_t parse_inline_mps( std::string_view mps_text) { - return cuopt::linear_programming::io::parse_mps_from_string(mps_text, false); + return cuopt::linear_programming::io::read_mps_from_string(mps_text, false); } } // namespace cuopt::test::inline_mps diff --git a/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-example.rst b/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-example.rst index dc163009ed..d4b981719c 100644 --- a/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-example.rst +++ b/docs/cuopt/source/cuopt-c/lp-qp-milp/lp-qp-example.rst @@ -153,7 +153,7 @@ Example With LP File used — it dispatches on the file extension (case-insensitive): ``.lp`` / ``.lp.gz`` / ``.lp.bz2`` → LP parser; ``.mps`` / ``.qps`` and their ``.gz`` / ``.bz2`` variants → MPS parser; unknown extensions are -rejected. See the ``parse_lp`` declaration in +rejected. See the ``read_lp`` declaration in ``cuopt/linear_programming/io/parser.hpp`` for the supported subset of the LP format. diff --git a/docs/cuopt/source/cuopt-cli/cli-examples.rst b/docs/cuopt/source/cuopt-cli/cli-examples.rst index 7596d1bf85..2a79bf920c 100644 --- a/docs/cuopt/source/cuopt-cli/cli-examples.rst +++ b/docs/cuopt/source/cuopt-cli/cli-examples.rst @@ -13,9 +13,9 @@ format is dispatched automatically from the file extension variants) → parsed as MPS / QPS Any other extension (including no extension) is rejected with an error -listing the supported suffixes. See ``parse_problem`` in +listing the supported suffixes. See ``read`` in ``cuopt/linear_programming/io/parser.hpp`` (and the Python -:func:`~cuopt.linear_programming.io.ParseProblem` wrapper). +:func:`~cuopt.linear_programming.io.Read` wrapper). The ``good-mps-1`` fixtures under ``datasets/linear_programming/`` include plain and compressed ``.mps`` / ``.lp`` files used in parser and CLI tests; diff --git a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst index 3a08418865..609e11b34b 100644 --- a/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst +++ b/docs/cuopt/source/cuopt-python/lp-qp-milp/lp-qp-milp-api.rst @@ -48,4 +48,4 @@ LP, QP and MILP API Reference :undoc-members: :exclude-members: __new__, __init__, _generate_next_value_, as_integer_ratio, bit_count, bit_length, conjugate, denominator, from_bytes, imag, is_integer, numerator, real, to_bytes -.. autofunction:: cuopt.linear_programming.io.ParseProblem +.. autofunction:: cuopt.linear_programming.io.Read diff --git a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst index 11a2df4d88..964d99680c 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp-examples.rst +++ b/docs/cuopt/source/cuopt-server/examples/lp-examples.rst @@ -271,7 +271,7 @@ Generate Datamodel using Problem File Parser -------------------------------------------- Use a :class:`~cuopt.linear_programming.data_model.DataModel` built with -:func:`~cuopt.linear_programming.io.ParseProblem` as input to ``get_LP_solve``; +:func:`~cuopt.linear_programming.io.Read` as input to ``get_LP_solve``; the client dispatches on the file extension (``.mps`` / ``.qps`` vs ``.lp``, including ``.gz`` / ``.bz2`` compressed variants). For solver settings see :doc:`LP/QP/MILP parameters <../../lp-qp-milp-settings>`. diff --git a/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py b/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py index 45ee9052e5..806f1317d2 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py +++ b/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py @@ -4,8 +4,8 @@ LP DataModel from LP file parser example This example demonstrates how to: -- Parse an LP-format file using cuopt.linear_programming.ParseProblem -- Create a DataModel from the parsed problem +- Read an LP-format file using cuopt.linear_programming.Read +- Create a DataModel from the parsed LP file - Solve using the DataModel via the server - Extract detailed solution information @@ -32,7 +32,7 @@ ThinClientSolverSettings, PDLPSolverMode, ) -from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming import Read import time @@ -56,7 +56,7 @@ def main(): print("\n=== Parsing LP File ===") parse_start = time.time() - data_model = ParseProblem(data) + data_model = Read(data) parse_time = time.time() - parse_start print(f"Parse time: {parse_time:.3f} seconds") diff --git a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py index 552feab86a..db940dd954 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py +++ b/docs/cuopt/source/cuopt-server/examples/lp/examples/mps_datamodel_example.py @@ -4,7 +4,7 @@ LP DataModel from MPS file parser example This example demonstrates how to: -- Parse an MPS file using cuopt.linear_programming.ParseProblem +- Read an MPS file using cuopt.linear_programming.Read - Create a DataModel from the parsed MPS - Solve using the DataModel via the server - Extract detailed solution information @@ -32,7 +32,7 @@ ThinClientSolverSettings, PDLPSolverMode, ) -from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming import Read import time @@ -65,7 +65,7 @@ def main(): # Parse the MPS file and measure the time spent print("\n=== Parsing MPS File ===") parse_start = time.time() - data_model = ParseProblem(data) + data_model = Read(data) parse_time = time.time() - parse_start print(f"Parse time: {parse_time:.3f} seconds") diff --git a/docs/cuopt/source/hidden/mps-api.rst b/docs/cuopt/source/hidden/mps-api.rst index a7534376f4..ec2be8df66 100644 --- a/docs/cuopt/source/hidden/mps-api.rst +++ b/docs/cuopt/source/hidden/mps-api.rst @@ -5,4 +5,4 @@ cuOpt MPS/LP Parser API Reference MPS/QPS/LP parser ------------------- -.. autofunction:: cuopt.linear_programming.io.ParseProblem +.. autofunction:: cuopt.linear_programming.io.Read diff --git a/docs/cuopt/source/hidden/parser_example.rst b/docs/cuopt/source/hidden/parser_example.rst index f19c1511a5..3b98acca31 100644 --- a/docs/cuopt/source/hidden/parser_example.rst +++ b/docs/cuopt/source/hidden/parser_example.rst @@ -7,20 +7,20 @@ Example ------- Read MPS, QPS, or LP files (including ``.gz`` / ``.bz2`` compressed variants) -with :func:`~cuopt.linear_programming.ParseProblem`: +with :func:`~cuopt.linear_programming.Read`: .. code-block:: python :linenos: - from cuopt.linear_programming import ParseProblem + from cuopt.linear_programming import Read from cuopt.linear_programming.problem import Problem # MPS / QPS - mps_model = ParseProblem("good-mps-1.mps") + mps_model = Read("good-mps-1.mps") # LP (plain or compressed) - lp_model = ParseProblem("good-mps-1.lp") - lp_gz = ParseProblem("good-mps-1.lp.gz") + lp_model = Read("good-mps-1.lp") + lp_gz = Read("good-mps-1.lp.gz") # High-level API problem = Problem.read("good-mps-1.lp") diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index f2d0aca1ce..5c8aadc574 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -3,7 +3,7 @@ from cuopt.linear_programming import internals from cuopt.linear_programming.data_model import DataModel -from cuopt.linear_programming.io import ParseProblem +from cuopt.linear_programming.io import Read from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solution import Solution from cuopt.linear_programming.solver import BatchSolve, Solve diff --git a/python/cuopt/cuopt/linear_programming/io/__init__.py b/python/cuopt/cuopt/linear_programming/io/__init__.py index 1eb1e56f10..5a28905f92 100644 --- a/python/cuopt/cuopt/linear_programming/io/__init__.py +++ b/python/cuopt/cuopt/linear_programming/io/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -from cuopt.linear_programming.io.parser import ParseProblem, toDict +from cuopt.linear_programming.io.parser import Read, toDict diff --git a/python/cuopt/cuopt/linear_programming/io/parser.pxd b/python/cuopt/cuopt/linear_programming/io/parser.pxd index 04c430ffbb..a0d27ca768 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.pxd +++ b/python/cuopt/cuopt/linear_programming/io/parser.pxd @@ -39,7 +39,7 @@ cdef extern from "cuopt/linear_programming/io/mps_data_model.hpp" namespace "cuo cdef extern from "cuopt/linear_programming/io/utilities/cython_parser.hpp" namespace "cuopt::cython": # noqa - cdef unique_ptr[mps_data_model_t[int, double]] call_parse_problem( + cdef unique_ptr[mps_data_model_t[int, double]] call_read( const string& file_path, bool fixed_mps_format ) except + diff --git a/python/cuopt/cuopt/linear_programming/io/parser.py b/python/cuopt/cuopt/linear_programming/io/parser.py index 4310fa4673..333d4b44ff 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.py +++ b/python/cuopt/cuopt/linear_programming/io/parser.py @@ -10,11 +10,11 @@ @catch_io_exception -def ParseProblem(file_path: str, fixed_mps_format: bool = False) -> DataModel: +def Read(file_path: str, fixed_mps_format: bool = False) -> DataModel: """Read an optimization problem from a file, dispatching on extension. - Dispatches to the MPS/QPS or LP parser based on the filename suffix - (case-insensitive), matching the C++ ``parse_problem`` entry point: + Dispatches to the MPS/QPS or LP reader based on the filename suffix + (case-insensitive), matching the C++ ``read`` entry point: - ``.mps``, ``.mps.gz``, ``.mps.bz2``, ``.qps``, ``.qps.gz``, ``.qps.bz2`` → MPS/QPS reader @@ -41,9 +41,9 @@ def ParseProblem(file_path: str, fixed_mps_format: bool = False) -> DataModel: ``catch_io_exception``). RuntimeError If the file extension is not one of the supported suffixes (raised by - the C++ ``parse_problem`` dispatch). + the C++ ``read`` dispatch). """ - return parser_wrapper.ParseProblem(file_path, fixed_mps_format) + return parser_wrapper.Read(file_path, fixed_mps_format) def toDict(model, json=False): diff --git a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx index c4a0e4c6a7..94c0d3f968 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx @@ -16,7 +16,7 @@ from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move -from .parser cimport call_parse_problem, mps_data_model_t +from .parser cimport call_read, mps_data_model_t import warnings import numpy as np @@ -137,10 +137,10 @@ cdef _marshal_data_model(mps_data_model_t[int, double]* dm, data_model): @catch_io_exception -def ParseProblem(file_path, fixed_mps_format=False): +def Read(file_path, fixed_mps_format=False): data_model = DataModel() dm_ret_ptr = move( - call_parse_problem( + call_read( file_path.encode('utf-8'), fixed_mps_format, ) diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index 6c2f67fdea..f6315207d0 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -10,7 +10,7 @@ from scipy.sparse import coo_matrix import cuopt.linear_programming.data_model as data_model -from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming import Read import cuopt.linear_programming.solver as solver import cuopt.linear_programming.solver_settings as solver_settings import warnings @@ -1798,7 +1798,7 @@ def read(cls, file_path, fixed_mps_format=False): """ Initialize a problem from an MPS, QPS, or LP file. - Dispatches on the file extension via the C++ ``parse_problem`` entry + Dispatches on the file extension via the C++ ``read`` entry point (case-insensitive): ``.mps`` / ``.qps`` (and ``.gz`` / ``.bz2`` variants) use the MPS/QPS reader; ``.lp`` (and compressed variants) use the LP reader. @@ -1827,7 +1827,7 @@ def read(cls, file_path, fixed_mps_format=False): raise FileNotFoundError(f"No such file: {file_path}") problem = cls() - data_model = ParseProblem(file_path, fixed_mps_format) + data_model = Read(file_path, fixed_mps_format) problem._from_data_model(data_model) problem.model = data_model return problem diff --git a/python/cuopt/cuopt/linear_programming/solver/solver.py b/python/cuopt/cuopt/linear_programming/solver/solver.py index 83f892d68b..4963d0be8a 100644 --- a/python/cuopt/cuopt/linear_programming/solver/solver.py +++ b/python/cuopt/cuopt/linear_programming/solver/solver.py @@ -17,7 +17,7 @@ def Solve(data_model, solver_settings=None): Data Model object can be construed through setters (see linear_programming.DataModel class) or through a MPS file - (see cuopt.linear_programming.ParseProblem function) + (see cuopt.linear_programming.Read function) Notes @@ -124,7 +124,7 @@ def BatchSolve(data_model_list, solver_settings=None): Data Model objects can be construed through setters (see linear_programming.DataModel class) or through a MPS file - (see cuopt.linear_programming.ParseProblem function) + (see cuopt.linear_programming.Read function) Notes @@ -160,11 +160,11 @@ def BatchSolve(data_model_list, solver_settings=None): >>> from cuopt import linear_programming >>> from cuopt.linear_programming.solver_settings import PDLPSolverMode >>> from cuopt.linear_programming.solver.solver_parameters import * - >>> from cuopt.linear_programming import ParseProblem + >>> from cuopt.linear_programming import Read >>> >>> data_models = [] >>> for i in range(...): - >>> data_models.append(ParseProblem(...)) + >>> data_models.append(Read(...)) >>> >>> # Build a solver setting object >>> settings = linear_programming.SolverSettings() diff --git a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py index a3787d4123..4453d38bcc 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_cpu_only_execution.py @@ -23,7 +23,7 @@ import sys import time -from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming import Read import pytest from cuopt import linear_programming from cuopt.linear_programming.solver.solver_parameters import CUOPT_TIME_LIMIT @@ -304,7 +304,7 @@ def _impl_lp_solve_cpu_only(): dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" - dm = ParseProblem(mps_file) + dm = Read(mps_file) n_vars = len(dm.get_objective_coefficients()) solution = linear_programming.Solve( @@ -333,7 +333,7 @@ def _impl_lp_dual_solution_cpu_only(): dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" - dm = ParseProblem(mps_file) + dm = Read(mps_file) n_vars = len(dm.get_objective_coefficients()) n_cons = len(dm.get_constraint_bounds()) @@ -365,7 +365,7 @@ def _impl_mip_solve_cpu_only(): dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/mip/bb_optimality.mps" - dm = ParseProblem(mps_file) + dm = Read(mps_file) n_vars = len(dm.get_objective_coefficients()) settings = linear_programming.SolverSettings() @@ -400,7 +400,7 @@ def _impl_warmstart_cpu_only(): dataset_root = os.environ.get("RAPIDS_DATASET_ROOT_DIR", "./") mps_file = f"{dataset_root}/linear_programming/afiro_original.mps" - dm = ParseProblem(mps_file) + dm = Read(mps_file) settings = linear_programming.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) @@ -654,7 +654,7 @@ def test_lp_solution_values(self): mps_file = ( f"{RAPIDS_DATASET_ROOT_DIR}/linear_programming/afiro_original.mps" ) - dm = ParseProblem(mps_file) + dm = Read(mps_file) n_vars = len(dm.get_objective_coefficients()) n_cons = len(dm.get_constraint_bounds()) @@ -683,7 +683,7 @@ def test_lp_solution_values(self): def test_mip_solution_values(self): """MIP solve of bb_optimality.mps returns valid stats.""" mps_file = f"{RAPIDS_DATASET_ROOT_DIR}/mip/bb_optimality.mps" - dm = ParseProblem(mps_file) + dm = Read(mps_file) n_vars = len(dm.get_objective_coefficients()) settings = linear_programming.SolverSettings() diff --git a/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py b/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py index b6729ed860..03871b22af 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_incumbent_callbacks.py @@ -3,7 +3,7 @@ import os -from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming import Read import pytest from cuopt.linear_programming import solver, solver_settings @@ -77,7 +77,7 @@ def set_solution( ) file_path = RAPIDS_DATASET_ROOT_DIR + file_name - data_model_obj = ParseProblem(file_path) + data_model_obj = Read(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_TIME_LIMIT, 10) diff --git a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py index dea2abfe9d..6fc4636b34 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_lp_solver.py @@ -5,7 +5,7 @@ import os from enum import IntEnum -from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming import Read import numpy as np import pytest @@ -93,7 +93,7 @@ def test_solver(): def test_parser_and_solver(): file_path = RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-1.mps" - data_model_obj = ParseProblem(file_path) + data_model_obj = Read(file_path) settings = solver_settings.SolverSettings() settings.set_optimality_tolerance(1e-2) @@ -365,7 +365,7 @@ def test_solver_settings_basic(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-1.mps" ) - solver.Solve(ParseProblem(file_path), settings) + solver.Solve(Read(file_path), settings) settings.set_parameter(CUOPT_PDLP_SOLVER_MODE, PDLPSolverMode.Methodical1) assert settings.get_parameter(CUOPT_PDLP_SOLVER_MODE) == int( @@ -484,7 +484,7 @@ def test_parse_var_names(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj = ParseProblem(file_path) + data_model_obj = Read(file_path) expected_names = [ "X01", @@ -583,7 +583,7 @@ def test_parser_and_batch_solver(): nb_solves = 5 for i in range(nb_solves): - data_model_list.append(ParseProblem(file_path)) + data_model_list.append(Read(file_path)) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) @@ -595,7 +595,7 @@ def test_parser_and_batch_solver(): # Call Solve on each individual data model object individual_solutions = [] for i in range(nb_solves): - individual_solution = solver.Solve(ParseProblem(file_path), settings) + individual_solution = solver.Solve(Read(file_path), settings) individual_solutions.append(individual_solution) # Verify that the results are the same @@ -608,7 +608,7 @@ def test_parser_and_batch_solver(): def test_warm_start(): file_path = RAPIDS_DATASET_ROOT_DIR + "/linear_programming/a2864/a2864.mps" - data_model_obj = ParseProblem(file_path) + data_model_obj = Read(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.PDLP) @@ -643,7 +643,7 @@ def test_warm_start(): RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj_different = ParseProblem(file_path) + data_model_obj_different = Read(file_path) with pytest.raises(Exception, match="Invalid PDLPWarmStart data"): solver.Solve(data_model_obj_different, settings) @@ -690,7 +690,7 @@ def test_solved_by(): def test_heuristics_only(): file_path = RAPIDS_DATASET_ROOT_DIR + "/mip/swath1.mps" - data_model_obj = ParseProblem(file_path) + data_model_obj = Read(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_MIP_HEURISTICS_ONLY, True) @@ -757,7 +757,7 @@ def test_write_files(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/afiro_original.mps" ) - data_model_obj = ParseProblem(file_path) + data_model_obj = Read(file_path) settings = solver_settings.SolverSettings() settings.set_parameter(CUOPT_METHOD, SolverMethod.DualSimplex) @@ -768,7 +768,7 @@ def test_write_files(): assert os.path.isfile("afiro_out.mps") - afiro = ParseProblem("afiro_out.mps") + afiro = Read("afiro_out.mps") os.remove("afiro_out.mps") settings.set_parameter(CUOPT_USER_PROBLEM_FILE, "") diff --git a/python/cuopt/cuopt/tests/linear_programming/test_parser.py b/python/cuopt/cuopt/tests/linear_programming/test_parser.py index e92e7d25cc..f40fd505ef 100644 --- a/python/cuopt/cuopt/tests/linear_programming/test_parser.py +++ b/python/cuopt/cuopt/tests/linear_programming/test_parser.py @@ -4,7 +4,7 @@ import os import tempfile -from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming import Read import numpy as np import pytest from cuopt.linear_programming.io.utilities import InputValidationError @@ -49,11 +49,11 @@ def _assert_good_mps_1_model(data_model): @pytest.mark.parametrize("filename", GOOD_MPS_1_VARIANTS) -def test_parse_problem_good_mps_1_variants(filename): +def test_read_good_mps_1_variants(filename): path = os.path.join(GOOD_MPS_1_DIR, filename) if not os.path.isfile(path): pytest.skip(f"missing dataset {path}") - _assert_good_mps_1_model(ParseProblem(path)) + _assert_good_mps_1_model(Read(path)) def test_bad_mps_files(): @@ -64,14 +64,14 @@ def test_bad_mps_files(): ) if os.path.exists(file_path): with pytest.raises(InputValidationError): - ParseProblem(file_path, fixed_mps_format=True) + Read(file_path, fixed_mps_format=True) def test_good_mps_file(): file_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.mps" ) - data_model = ParseProblem(file_path) + data_model = Read(file_path) assert not data_model.get_sense() @@ -112,7 +112,7 @@ def test_good_mps_file(): # Minimal LP content that should parse identically regardless of whether it's -# routed through ParseProblem() or the server's extension-based dispatch path. +# routed through Read() or the server's extension-based dispatch path. _MINIMAL_LP = """ Minimize x @@ -131,7 +131,7 @@ def test_parse_lp_basic(): f.write(_MINIMAL_LP) path = f.name try: - data_model = ParseProblem(path) + data_model = Read(path) finally: os.unlink(path) @@ -168,7 +168,7 @@ def test_parse_lp_rejects_unsupported_section(): path = f.name try: with pytest.raises(InputValidationError): - ParseProblem(path) + Read(path) finally: os.unlink(path) @@ -202,8 +202,8 @@ def test_parse_lp_and_parse_mps_agree_on_trivial_problem(): f.write(_MINIMAL_LP) lp_path = f.name try: - lp_model = ParseProblem(lp_path) - mps_model = ParseProblem(mps_path) + lp_model = Read(lp_path) + mps_model = Read(mps_path) finally: os.unlink(mps_path) os.unlink(lp_path) @@ -227,15 +227,15 @@ def test_parse_lp_and_parse_mps_agree_on_trivial_problem(): ) -def test_parse_problem_dispatches_mps_and_lp(): +def test_read_dispatches_mps_and_lp(): mps_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.mps" ) lp_path = ( RAPIDS_DATASET_ROOT_DIR + "/linear_programming/good-mps-free-var.lp" ) - mps_model = ParseProblem(mps_path) - lp_model = ParseProblem(lp_path) + mps_model = Read(mps_path) + lp_model = Read(lp_path) assert mps_model.get_sense() == lp_model.get_sense() assert ( mps_model.get_variable_names().tolist() @@ -243,7 +243,7 @@ def test_parse_problem_dispatches_mps_and_lp(): ) -def test_parse_problem_unrecognized_extension(): +def test_read_unrecognized_extension(): with tempfile.NamedTemporaryFile(suffix=".xyz", delete=False) as f: f.write(b"x\n") path = f.name @@ -251,6 +251,6 @@ def test_parse_problem_unrecognized_extension(): with pytest.raises( RuntimeError, match="unrecognized input file extension" ): - ParseProblem(path) + Read(path) finally: os.unlink(path) diff --git a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py index e7b7a465ae..f97a291e07 100644 --- a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py +++ b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py @@ -136,7 +136,7 @@ def is_uuid(cuopt_problem_data): # File extensions (case-insensitive, after stripping a compression suffix) that # the cuopt.linear_programming.io package can parse client-side. Matches the -# dispatch table in parse_problem() on the C++ side. +# dispatch table in read() on the C++ side. _PARSEABLE_LP_EXTS = (".lp",) _PARSEABLE_MPS_EXTS = (".mps", ".qps") _COMPRESSION_SUFFIXES = (".gz", ".bz2") @@ -180,7 +180,7 @@ def _parse_file_to_data_model(problem_input, solver_config): log.debug("Received mps_parser DataModel object") else: t0 = time.time() - model = mps_parser.ParseProblem(problem_input) + model = mps_parser.Read(problem_input) parse_time = time.time() - t0 log.debug(f"file parsing time was {parse_time}") problem_data = mps_parser.toDict(model, json=use_zlib) diff --git a/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py b/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py index 096b47f1d0..5d62b87935 100644 --- a/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py +++ b/python/cuopt_server/cuopt_server/tests/test_pdlp_warmstart.py @@ -3,7 +3,7 @@ import os -from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming import Read from cuopt.linear_programming.io import toDict import msgpack @@ -33,7 +33,7 @@ def test_warmstart(cuoptproc): # noqa RAPIDS_DATASET_ROOT_DIR, "linear_programming/square41/square41.mps", ) - data_model_obj = ParseProblem(file_path) + data_model_obj = Read(file_path) data = toDict(data_model_obj, json=True) settings = solver_settings.SolverSettings() settings.set_optimality_tolerance(1e-4) diff --git a/regression/benchmark_scripts/utils.py b/regression/benchmark_scripts/utils.py index eeae11f175..d391ace3bf 100644 --- a/regression/benchmark_scripts/utils.py +++ b/regression/benchmark_scripts/utils.py @@ -3,7 +3,7 @@ from cuopt_server.utils.utils import build_routing_datamodel_from_json -from cuopt.linear_programming import ParseProblem +from cuopt.linear_programming import Read from cuopt.linear_programming.solver_settings import SolverSettings import os import json @@ -16,7 +16,7 @@ def build_datamodel_from_mps(data): """ if os.path.isfile(data): - data_model = ParseProblem(data) + data_model = Read(data) else: raise ValueError( f"Invalid type : {type(data)} has been provided as input, " From 951c582889810010fc570145d6b85ab868c496af Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Fri, 22 May 2026 18:41:16 +0000 Subject: [PATCH 26/30] add back call_parse_mps --- .../io/utilities/cython_parser.hpp | 3 ++ cpp/src/io/utilities/cython_parser.cpp | 7 ++++ .../cuopt/linear_programming/__init__.py | 2 +- .../cuopt/linear_programming/io/__init__.py | 2 +- .../cuopt/linear_programming/io/parser.pxd | 5 +++ .../cuopt/linear_programming/io/parser.py | 34 +++++++++++++++++++ .../linear_programming/io/parser_wrapper.pyx | 14 +++++++- 7 files changed, 64 insertions(+), 3 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp index 1f5d8940f0..711f1c73b2 100644 --- a/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/utilities/cython_parser.hpp @@ -17,5 +17,8 @@ namespace cython { std::unique_ptr> call_read( const std::string& file_path, bool fixed_mps_format); +std::unique_ptr> call_parse_mps( + const std::string& mps_file_path, bool fixed_mps_format); + } // namespace cython } // namespace cuopt diff --git a/cpp/src/io/utilities/cython_parser.cpp b/cpp/src/io/utilities/cython_parser.cpp index 1cc4ea9b62..c04aee16c6 100644 --- a/cpp/src/io/utilities/cython_parser.cpp +++ b/cpp/src/io/utilities/cython_parser.cpp @@ -18,5 +18,12 @@ std::unique_ptr> ca cuopt::linear_programming::io::read(file_path, fixed_mps_format))); } +std::unique_ptr> call_parse_mps( + const std::string& mps_file_path, bool fixed_mps_format) +{ + return std::make_unique>(std::move( + cuopt::linear_programming::io::read_mps(mps_file_path, fixed_mps_format))); +} + } // namespace cython } // namespace cuopt diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index 5c8aadc574..a458499477 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -3,7 +3,7 @@ from cuopt.linear_programming import internals from cuopt.linear_programming.data_model import DataModel -from cuopt.linear_programming.io import Read +from cuopt.linear_programming.io import Read, ReadMps from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solution import Solution from cuopt.linear_programming.solver import BatchSolve, Solve diff --git a/python/cuopt/cuopt/linear_programming/io/__init__.py b/python/cuopt/cuopt/linear_programming/io/__init__.py index 5a28905f92..0f01a6cb88 100644 --- a/python/cuopt/cuopt/linear_programming/io/__init__.py +++ b/python/cuopt/cuopt/linear_programming/io/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -from cuopt.linear_programming.io.parser import Read, toDict +from cuopt.linear_programming.io.parser import Read, ReadMps, toDict diff --git a/python/cuopt/cuopt/linear_programming/io/parser.pxd b/python/cuopt/cuopt/linear_programming/io/parser.pxd index a0d27ca768..d1cc95e9d6 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.pxd +++ b/python/cuopt/cuopt/linear_programming/io/parser.pxd @@ -43,3 +43,8 @@ cdef extern from "cuopt/linear_programming/io/utilities/cython_parser.hpp" names const string& file_path, bool fixed_mps_format ) except + + + cdef unique_ptr[mps_data_model_t[int, double]] call_parse_mps( + const string& mps_file_path, + bool fixed_mps_format + ) except + diff --git a/python/cuopt/cuopt/linear_programming/io/parser.py b/python/cuopt/cuopt/linear_programming/io/parser.py index 333d4b44ff..2bc50978d6 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.py +++ b/python/cuopt/cuopt/linear_programming/io/parser.py @@ -46,6 +46,40 @@ def Read(file_path: str, fixed_mps_format: bool = False) -> DataModel: return parser_wrapper.Read(file_path, fixed_mps_format) +@catch_io_exception +def ReadMps(mps_file_path: str, fixed_mps_format: bool = False) -> DataModel: + """Read an MPS or QPS file directly via the MPS/QPS reader. + + Unlike :func:`Read`, this function bypasses extension-based dispatch + and always invokes the MPS/QPS reader (``read_mps`` on the C++ side), + regardless of the filename suffix. Compressed inputs (``.mps.gz``, + ``.mps.bz2``, ``.qps.gz``, ``.qps.bz2``) are still supported when + zlib / libbz2 are available, because compression is detected from + the file path inside the reader. + + Parameters + ---------- + mps_file_path : str + Path to an MPS or QPS file (optionally ``.gz`` / ``.bz2`` + compressed). + fixed_mps_format : bool + If the MPS/QPS reader should parse the file as fixed MPS format. + False by default. + + Returns + ------- + data_model : DataModel + A fully formed LP/MILP/QP problem. + + Raises + ------ + InputValidationError, InputRuntimeError, OutOfMemoryError + Parser errors from the underlying C++ reader (via + ``catch_io_exception``). + """ + return parser_wrapper.ReadMps(mps_file_path, fixed_mps_format) + + def toDict(model, json=False): if not isinstance(model, parser_wrapper.DataModel): raise ValueError( diff --git a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx index 94c0d3f968..bcd7dccf4e 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx @@ -16,7 +16,7 @@ from libcpp.memory cimport unique_ptr from libcpp.string cimport string from libcpp.utility cimport move -from .parser cimport call_read, mps_data_model_t +from .parser cimport call_read, call_parse_mps, mps_data_model_t import warnings import numpy as np @@ -146,3 +146,15 @@ def Read(file_path, fixed_mps_format=False): ) ) return _marshal_data_model(dm_ret_ptr.get(), data_model) + + +@catch_io_exception +def ReadMps(mps_file_path, fixed_mps_format=False): + data_model = DataModel() + dm_ret_ptr = move( + call_parse_mps( + mps_file_path.encode('utf-8'), + fixed_mps_format, + ) + ) + return _marshal_data_model(dm_ret_ptr.get(), data_model) From 62c84e02162805ef052ebf1ff340200e646cce70 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Fri, 22 May 2026 18:50:55 +0000 Subject: [PATCH 27/30] add back call_parse_mps --- .../cuopt/linear_programming/__init__.py | 2 +- .../cuopt/linear_programming/io/__init__.py | 2 +- .../cuopt/linear_programming/io/parser.py | 4 ++-- .../linear_programming/io/parser_wrapper.pyx | 2 +- .../cuopt/cuopt/linear_programming/problem.py | 19 +++++++++++++++++-- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/python/cuopt/cuopt/linear_programming/__init__.py b/python/cuopt/cuopt/linear_programming/__init__.py index a458499477..835d09d76a 100644 --- a/python/cuopt/cuopt/linear_programming/__init__.py +++ b/python/cuopt/cuopt/linear_programming/__init__.py @@ -3,7 +3,7 @@ from cuopt.linear_programming import internals from cuopt.linear_programming.data_model import DataModel -from cuopt.linear_programming.io import Read, ReadMps +from cuopt.linear_programming.io import ParseMps, Read from cuopt.linear_programming.problem import Problem from cuopt.linear_programming.solution import Solution from cuopt.linear_programming.solver import BatchSolve, Solve diff --git a/python/cuopt/cuopt/linear_programming/io/__init__.py b/python/cuopt/cuopt/linear_programming/io/__init__.py index 0f01a6cb88..c6843a9e61 100644 --- a/python/cuopt/cuopt/linear_programming/io/__init__.py +++ b/python/cuopt/cuopt/linear_programming/io/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -from cuopt.linear_programming.io.parser import Read, ReadMps, toDict +from cuopt.linear_programming.io.parser import ParseMps, Read, toDict diff --git a/python/cuopt/cuopt/linear_programming/io/parser.py b/python/cuopt/cuopt/linear_programming/io/parser.py index 2bc50978d6..a9132eaf8f 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser.py +++ b/python/cuopt/cuopt/linear_programming/io/parser.py @@ -47,7 +47,7 @@ def Read(file_path: str, fixed_mps_format: bool = False) -> DataModel: @catch_io_exception -def ReadMps(mps_file_path: str, fixed_mps_format: bool = False) -> DataModel: +def ParseMps(mps_file_path: str, fixed_mps_format: bool = False) -> DataModel: """Read an MPS or QPS file directly via the MPS/QPS reader. Unlike :func:`Read`, this function bypasses extension-based dispatch @@ -77,7 +77,7 @@ def ReadMps(mps_file_path: str, fixed_mps_format: bool = False) -> DataModel: Parser errors from the underlying C++ reader (via ``catch_io_exception``). """ - return parser_wrapper.ReadMps(mps_file_path, fixed_mps_format) + return parser_wrapper.ParseMps(mps_file_path, fixed_mps_format) def toDict(model, json=False): diff --git a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx index bcd7dccf4e..b2acff89fc 100644 --- a/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx +++ b/python/cuopt/cuopt/linear_programming/io/parser_wrapper.pyx @@ -149,7 +149,7 @@ def Read(file_path, fixed_mps_format=False): @catch_io_exception -def ReadMps(mps_file_path, fixed_mps_format=False): +def ParseMps(mps_file_path, fixed_mps_format=False): data_model = DataModel() dm_ret_ptr = move( call_parse_mps( diff --git a/python/cuopt/cuopt/linear_programming/problem.py b/python/cuopt/cuopt/linear_programming/problem.py index f6315207d0..15b5403130 100644 --- a/python/cuopt/cuopt/linear_programming/problem.py +++ b/python/cuopt/cuopt/linear_programming/problem.py @@ -10,7 +10,7 @@ from scipy.sparse import coo_matrix import cuopt.linear_programming.data_model as data_model -from cuopt.linear_programming import Read +from cuopt.linear_programming import ParseMps, Read import cuopt.linear_programming.solver as solver import cuopt.linear_programming.solver_settings as solver_settings import warnings @@ -1837,6 +1837,12 @@ def readMPS(cls, mps_file): """ Initialize a problem from an `MPS `__ file. # noqa + Always invokes the MPS/QPS reader directly (via the + ``call_parse_mps`` Cython bridge), bypassing extension-based + dispatch. Compressed ``.mps.gz`` / ``.mps.bz2`` / ``.qps.gz`` / + ``.qps.bz2`` inputs are still supported via the reader's path- + based decompression. + .. deprecated:: Use :meth:`read` instead. @@ -1850,7 +1856,16 @@ def readMPS(cls, mps_file): DeprecationWarning, stacklevel=2, ) - return cls.read(mps_file) + if not isinstance(mps_file, str) or not mps_file: + raise ValueError("mps_file must be a non-empty string") + if not os.path.isfile(mps_file): + raise FileNotFoundError(f"No such file: {mps_file}") + + problem = cls() + data_model = ParseMps(mps_file) + problem._from_data_model(data_model) + problem.model = data_model + return problem def writeMPS(self, mps_file): """ From 103bb4408d7bff004332cc9e4e3cae31c600ae8f Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Fri, 22 May 2026 19:19:16 +0000 Subject: [PATCH 28/30] rever routing hanges --- cpp/include/cuopt/linear_programming/io/parser.hpp | 7 +++---- cpp/src/io/parser.cpp | 9 ++++----- cpp/src/io/utilities/cython_parser.cpp | 4 ++-- cpp/tests/linear_programming/parser_test.cpp | 10 +++++----- cpp/tests/mip/cuts_test.cu | 4 ++-- cpp/tests/routing/level0/l0_ges_test.cu | 2 +- cpp/tests/routing/level0/l0_scross_test.cu | 4 ++-- cpp/tests/routing/routing_test.cuh | 6 +++--- 8 files changed, 22 insertions(+), 24 deletions(-) diff --git a/cpp/include/cuopt/linear_programming/io/parser.hpp b/cpp/include/cuopt/linear_programming/io/parser.hpp index 2a46b60f7d..a63e40f31f 100644 --- a/cpp/include/cuopt/linear_programming/io/parser.hpp +++ b/cpp/include/cuopt/linear_programming/io/parser.hpp @@ -41,7 +41,7 @@ namespace cuopt::linear_programming::io { */ template mps_data_model_t read_mps(const std::string& mps_file_path, - bool fixed_mps_format = false); + bool fixed_mps_format = false); /** * @brief Reads an MPS problem from in-memory file contents. @@ -56,7 +56,7 @@ mps_data_model_t read_mps(const std::string& mps_file_path, */ template mps_data_model_t read_mps_from_string(std::string_view mps_contents, - bool fixed_mps_format = false); + bool fixed_mps_format = false); /** * @brief Reads a linear, mixed-integer, or quadratic optimization problem from @@ -125,8 +125,7 @@ mps_data_model_t read_lp_from_string(std::string_view lp_contents); * @return mps_data_model_t The parsed problem. */ template -inline mps_data_model_t read(const std::string& path, - bool fixed_mps_format = false) +inline mps_data_model_t read(const std::string& path, bool fixed_mps_format = false) { std::string lower(path); std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { diff --git a/cpp/src/io/parser.cpp b/cpp/src/io/parser.cpp index a981f5ad95..93d9d9c73c 100644 --- a/cpp/src/io/parser.cpp +++ b/cpp/src/io/parser.cpp @@ -21,7 +21,7 @@ mps_data_model_t read_mps(const std::string& mps_file, bool fixed_mps_ template mps_data_model_t read_mps_from_string(std::string_view mps_contents, - bool fixed_mps_format) + bool fixed_mps_format) { mps_data_model_t problem; mps_parser_t parser(problem, mps_contents, fixed_mps_format); @@ -29,11 +29,10 @@ mps_data_model_t read_mps_from_string(std::string_view mps_contents, } template mps_data_model_t read_mps(const std::string& mps_file, bool fixed_mps_format); -template mps_data_model_t read_mps(const std::string& mps_file, - bool fixed_mps_format); +template mps_data_model_t read_mps(const std::string& mps_file, bool fixed_mps_format); template mps_data_model_t read_mps_from_string(std::string_view mps_contents, - bool fixed_mps_format); + bool fixed_mps_format); template mps_data_model_t read_mps_from_string(std::string_view mps_contents, - bool fixed_mps_format); + bool fixed_mps_format); } // namespace cuopt::linear_programming::io diff --git a/cpp/src/io/utilities/cython_parser.cpp b/cpp/src/io/utilities/cython_parser.cpp index c04aee16c6..4de1de9b6f 100644 --- a/cpp/src/io/utilities/cython_parser.cpp +++ b/cpp/src/io/utilities/cython_parser.cpp @@ -14,8 +14,8 @@ namespace cython { std::unique_ptr> call_read( const std::string& file_path, bool fixed_mps_format) { - return std::make_unique>(std::move( - cuopt::linear_programming::io::read(file_path, fixed_mps_format))); + return std::make_unique>( + std::move(cuopt::linear_programming::io::read(file_path, fixed_mps_format))); } std::unique_ptr> call_parse_mps( diff --git a/cpp/tests/linear_programming/parser_test.cpp b/cpp/tests/linear_programming/parser_test.cpp index db89b0c7f3..3b93f76f72 100644 --- a/cpp/tests/linear_programming/parser_test.cpp +++ b/cpp/tests/linear_programming/parser_test.cpp @@ -125,7 +125,7 @@ double q_entry(const mps_data_model_t& m, int row, int col) class parser_fixture_base : public ::testing::Test { protected: static mps_data_model_t read_mps_file(const std::string& file, - bool fixed_format = true) + bool fixed_format = true) { const std::string& root = cuopt::test::get_rapids_dataset_root_dir(); return read_mps(root + "/" + file, fixed_format); @@ -2550,7 +2550,7 @@ TEST(read, lp_gz_extension_dispatches_to_lp_parser) // LP path. (Routing a .lp.gz to read_mps would either fail at // decompression or fail to parse the LP content as MPS.) auto m = read(cuopt::test::get_rapids_dataset_root_dir() + - "/linear_programming/good-mps-1.lp.gz"); + "/linear_programming/good-mps-1.lp.gz"); ASSERT_EQ(m.get_variable_names().size(), 2u); EXPECT_EQ(m.get_variable_names()[0], "VAR1"); } @@ -2558,7 +2558,7 @@ TEST(read, lp_gz_extension_dispatches_to_lp_parser) TEST(read, lp_bz2_extension_dispatches_to_lp_parser) { auto m = read(cuopt::test::get_rapids_dataset_root_dir() + - "/linear_programming/good-mps-1.lp.bz2"); + "/linear_programming/good-mps-1.lp.bz2"); ASSERT_EQ(m.get_variable_names().size(), 2u); EXPECT_EQ(m.get_variable_names()[0], "VAR1"); } @@ -2583,14 +2583,14 @@ TEST(read, qps_extension_dispatches_to_mps_parser) TEST(read, mps_gz_extension_dispatches_to_mps_parser) { auto m = read(cuopt::test::get_rapids_dataset_root_dir() + - "/linear_programming/good-mps-1.mps.gz"); + "/linear_programming/good-mps-1.mps.gz"); EXPECT_EQ("good-1", m.get_problem_name()); } TEST(read, mps_bz2_extension_dispatches_to_mps_parser) { auto m = read(cuopt::test::get_rapids_dataset_root_dir() + - "/linear_programming/good-mps-1.mps.bz2"); + "/linear_programming/good-mps-1.mps.bz2"); EXPECT_EQ("good-1", m.get_problem_name()); } diff --git a/cpp/tests/mip/cuts_test.cu b/cpp/tests/mip/cuts_test.cu index 37ec8a1034..ac411f5b89 100644 --- a/cpp/tests/mip/cuts_test.cu +++ b/cpp/tests/mip/cuts_test.cu @@ -209,8 +209,8 @@ io::mps_data_model_t& get_neos8_model_cached() static std::unique_ptr> model_ptr; std::call_once(init_flag, []() { const auto neos8_path = make_path_absolute("mip/neos8.mps"); - auto neos8_model = cuopt::linear_programming::io::read_mps(neos8_path, false); - model_ptr = std::make_unique>(std::move(neos8_model)); + auto neos8_model = cuopt::linear_programming::io::read_mps(neos8_path, false); + model_ptr = std::make_unique>(std::move(neos8_model)); }); cuopt_assert(model_ptr != nullptr, "Failed to initialize cached neos8 model"); return *model_ptr; diff --git a/cpp/tests/routing/level0/l0_ges_test.cu b/cpp/tests/routing/level0/l0_ges_test.cu index bcf49050bf..b3e72c56e6 100644 --- a/cpp/tests/routing/level0/l0_ges_test.cu +++ b/cpp/tests/routing/level0/l0_ges_test.cu @@ -238,7 +238,7 @@ INSTANTIATE_TEST_SUITE_P(level0_ges, double_test_pdp, ::testing::Values(std::mak TEST_P(simple_routes_test_pdp, GES_PDP) { test_cvrptw(); } INSTANTIATE_TEST_SUITE_P(level0_ges, simple_routes_test_pdp, - ::testing::ValuesIn(reads(simple_three_routes_))); + ::testing::ValuesIn(parse_problems(simple_three_routes_))); TEST_P(double_test_vrp, GES_VRP) { test_cvrptw(); } INSTANTIATE_TEST_SUITE_P(level0_ges, double_test_vrp, ::testing::Values(std::make_tuple(true))); diff --git a/cpp/tests/routing/level0/l0_scross_test.cu b/cpp/tests/routing/level0/l0_scross_test.cu index a12b2d3968..7b71f8cf64 100644 --- a/cpp/tests/routing/level0/l0_scross_test.cu +++ b/cpp/tests/routing/level0/l0_scross_test.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ @@ -207,7 +207,7 @@ typedef simple_scross_test_t scross_three_routes_tes TEST_P(scross_three_routes_test, SCROSS_GES) { test_scross(); } INSTANTIATE_TEST_SUITE_P(simple_scross_test, scross_three_routes_test, - ::testing::ValuesIn(reads(scross_three_routes_))); + ::testing::ValuesIn(parse_problems(scross_three_routes_))); } // namespace test } // namespace routing diff --git a/cpp/tests/routing/routing_test.cuh b/cpp/tests/routing/routing_test.cuh index 4cb67a6d9d..cdafbbf1f7 100644 --- a/cpp/tests/routing/routing_test.cuh +++ b/cpp/tests/routing/routing_test.cuh @@ -112,7 +112,7 @@ struct test_data_t { }; template -static test_data_t read(problem_t&& problem) +static test_data_t parse_problem(problem_t&& problem) { return {problem.x_h, problem.y_h, @@ -146,9 +146,9 @@ static test_data_t read(problem_t&& problem) } template -std::vector> reads(Args&&... args) +std::vector> parse_problems(Args&&... args) { - return {read(std::forward(args))...}; + return {parse_problem(std::forward(args))...}; } template From cd340df0106842e19206eecd28d162170bde56a9 Mon Sep 17 00:00:00 2001 From: Ishika Roy <41401566+Iroy30@users.noreply.github.com> Date: Fri, 22 May 2026 15:24:30 -0500 Subject: [PATCH 29/30] Update docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py Co-authored-by: Ramakrishnap <42624703+rgsl888prabhu@users.noreply.github.com> --- .../cuopt-server/examples/lp/examples/lp_datamodel_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py b/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py index 806f1317d2..cf6e8913ce 100644 --- a/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py +++ b/docs/cuopt/source/cuopt-server/examples/lp/examples/lp_datamodel_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ LP DataModel from LP file parser example From af02ce4c4b1f8c9590325aad599539088b6dac52 Mon Sep 17 00:00:00 2001 From: Ishika Roy Date: Fri, 22 May 2026 20:32:58 +0000 Subject: [PATCH 30/30] address review --- cpp/tests/routing/level0/l0_scross_test.cu | 2 +- .../cuopt_sh_client/cuopt_self_host_client.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cpp/tests/routing/level0/l0_scross_test.cu b/cpp/tests/routing/level0/l0_scross_test.cu index 7b71f8cf64..8f3ac139c3 100644 --- a/cpp/tests/routing/level0/l0_scross_test.cu +++ b/cpp/tests/routing/level0/l0_scross_test.cu @@ -1,6 +1,6 @@ /* clang-format off */ /* - * SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ /* clang-format on */ diff --git a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py index f97a291e07..0e0db47366 100644 --- a/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py +++ b/python/cuopt_self_hosted/cuopt_sh_client/cuopt_self_host_client.py @@ -164,7 +164,8 @@ def _client_parseable_extension(path): def _parse_file_to_data_model(problem_input, solver_config): try: - from cuopt.linear_programming import io as mps_parser + from cuopt.linear_programming import DataModel, Read + from cuopt.linear_programming.io import toDict except ImportError as e: raise ImportError( "MPS/LP parsing on the client requires the cuopt package. " @@ -174,16 +175,16 @@ def _parse_file_to_data_model(problem_input, solver_config): "DataModel." ) from e # problem_input is either a path (str) to an MPS/LP/QPS file (optionally - # .gz / .bz2 compressed), or an mps_parser DataModel already handed to us. - if isinstance(problem_input, mps_parser.parser_wrapper.DataModel): + # .gz / .bz2 compressed), or a DataModel already handed to us. + if isinstance(problem_input, DataModel): model = problem_input - log.debug("Received mps_parser DataModel object") + log.debug("Received DataModel object") else: t0 = time.time() - model = mps_parser.Read(problem_input) + model = Read(problem_input) parse_time = time.time() - t0 log.debug(f"file parsing time was {parse_time}") - problem_data = mps_parser.toDict(model, json=use_zlib) + problem_data = toDict(model, json=use_zlib) if type(solver_config) is dict: problem_data["solver_config"] = solver_config