diff --git a/.gitattributes b/.gitattributes index cd8f8830bf..f16f39ab5a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.mps linguist-detectable=false +*.qps linguist-detectable=false diff --git a/.gitignore b/.gitignore index 9755d86dce..9edc9823c1 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,5 @@ docs/cuopt/build # generated version file cpp/include/cuopt/semantic_version.hpp +!datasets/quadratic_programming +!datasets/quadratic_programming/** diff --git a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp index 05f75f7340..17f74a6c2c 100644 --- a/cpp/libmps_parser/include/mps_parser/data_model_view.hpp +++ b/cpp/libmps_parser/include/mps_parser/data_model_view.hpp @@ -229,6 +229,30 @@ class data_model_view_t { */ void set_initial_dual_solution(const f_t* initial_dual_solution, i_t size); + /** + * @brief Set the quadratic objective matrix (Q) in CSR format for QPS files. + * + * @note This is used for quadratic programming problems where the objective + * function contains quadratic terms: (1/2) * x^T * Q * x + c^T * x + * cuOpt does not own or copy this data. + * + * @param[in] Q_values Device memory pointer to values of the CSR representation of the quadratic + * objective matrix + * @param size_values Size of the Q_values array + * @param[in] Q_indices Device memory pointer to indices of the CSR representation of the + * quadratic objective matrix + * @param size_indices Size of the Q_indices array + * @param[in] Q_offsets Device memory pointer to offsets of the CSR representation of the + * quadratic objective matrix + * @param size_offsets Size of the Q_offsets array + */ + void set_quadratic_objective_matrix(const f_t* Q_values, + i_t size_values, + const i_t* Q_indices, + i_t size_indices, + const i_t* Q_offsets, + i_t size_offsets); + /** * @brief Get the sense value (false:minimize, true:maximize) * @@ -339,6 +363,32 @@ class data_model_view_t { */ std::string get_objective_name() const noexcept; + // QPS-specific getters + /** + * @brief Get the quadratic objective matrix values + * + * @return span + */ + span get_quadratic_objective_values() const noexcept; + /** + * @brief Get the quadratic objective matrix indices + * + * @return span + */ + span get_quadratic_objective_indices() const noexcept; + /** + * @brief Get the quadratic objective matrix offsets + * + * @return span + */ + span get_quadratic_objective_offsets() const noexcept; + /** + * @brief Check if the problem has quadratic objective terms + * + * @return bool + */ + bool has_quadratic_objective() const noexcept; + private: bool maximize_{false}; span A_; @@ -361,6 +411,11 @@ class data_model_view_t { span initial_primal_solution_; span initial_dual_solution_; + // QPS-specific data members for quadratic programming support + span Q_objective_; + span Q_objective_indices_; + span Q_objective_offsets_; + }; // class data_model_view_t } // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp index 9101b98cf0..9ebc480533 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -251,6 +251,26 @@ class mps_data_model_t { */ void set_initial_dual_solution(const f_t* initial_dual_solution, i_t size); + /** + * @brief Set the quadratic objective matrix (Q) in CSR format for QPS files. + * + * @note This is used for quadratic programming problems where the objective + * function contains quadratic terms: (1/2) * x^T * Q * x + c^T * x + * + * @param[in] Q_values Values of the CSR representation of the quadratic objective matrix + * @param size_values Size of the Q_values array + * @param[in] Q_indices Indices of the CSR representation of the quadratic objective matrix + * @param size_indices Size of the Q_indices array + * @param[in] Q_offsets Offsets of the CSR representation of the quadratic objective matrix + * @param size_offsets Size of the Q_offsets array + */ + void set_quadratic_objective_matrix(const f_t* Q_values, + i_t size_values, + const i_t* Q_indices, + i_t size_indices, + const i_t* Q_offsets, + i_t size_offsets); + i_t get_n_variables() const; i_t get_n_constraints() const; i_t get_nnz() const; @@ -285,6 +305,16 @@ class mps_data_model_t { const std::vector& get_variable_names() const; const std::vector& get_row_names() const; + // QPS-specific getters + const std::vector& get_quadratic_objective_values() const; + std::vector& get_quadratic_objective_values(); + const std::vector& get_quadratic_objective_indices() const; + std::vector& get_quadratic_objective_indices(); + const std::vector& get_quadratic_objective_offsets() const; + std::vector& get_quadratic_objective_offsets(); + + bool has_quadratic_objective() const noexcept; + /** whether to maximize or minimize the objective function */ bool maximize_; /** @@ -333,6 +363,13 @@ class mps_data_model_t { std::vector initial_primal_solution_; /** Initial dual solution */ std::vector initial_dual_solution_; + + // QPS-specific data members for quadratic programming support + /** Quadratic objective matrix in CSR format (for (1/2) * x^T * Q * x term) */ + std::vector Q_objective_; + std::vector Q_objective_indices_; + std::vector Q_objective_offsets_; + }; // class mps_data_model_t } // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/include/mps_parser/parser.hpp b/cpp/libmps_parser/include/mps_parser/parser.hpp index 9d55aaac0e..fedfca514e 100644 --- a/cpp/libmps_parser/include/mps_parser/parser.hpp +++ b/cpp/libmps_parser/include/mps_parser/parser.hpp @@ -22,14 +22,19 @@ namespace cuopt::mps_parser { /** - * @brief Reads the equation from the input text file which is MPS formatted + * @brief Reads the equation from the input text file which is MPS or QPS formatted * * Read this link http://lpsolve.sourceforge.net/5.5/mps-format.htm for more * details on both free and fixed MPS format. * - * @param[in] mps_file_path Path to MPS formatted file. - * @param[in] fixed_mps_format If MPS file should be parsed as fixed, false by default - * @return mps_data_model_t A fully formed LP problem which represents the given MPS file + * This function supports both standard MPS files (for linear programming) and + * QPS files (for quadratic programming). QPS files are MPS files with additional + * sections: + * - QUADOBJ: Defines quadratic terms in the objective function + * + * @param[in] mps_file_path Path to MPS or QPS formatted file. + * @param[in] fixed_mps_format If MPS/QPS file should be parsed as fixed format, false by default + * @return mps_data_model_t A fully formed LP/QP problem which represents the given MPS/QPS file */ template mps_data_model_t parse_mps(const std::string& mps_file_path, diff --git a/cpp/libmps_parser/src/data_model_view.cpp b/cpp/libmps_parser/src/data_model_view.cpp index 4bca8c0563..efbe1a0f25 100644 --- a/cpp/libmps_parser/src/data_model_view.cpp +++ b/cpp/libmps_parser/src/data_model_view.cpp @@ -152,6 +152,33 @@ void data_model_view_t::set_initial_dual_solution(const f_t* initial_d initial_dual_solution_ = span(initial_dual_solution, size); } +template +void data_model_view_t::set_quadratic_objective_matrix(const f_t* Q_values, + i_t size_values, + const i_t* Q_indices, + i_t size_indices, + const i_t* Q_offsets, + i_t size_offsets) +{ + if (size_values != 0) { + mps_parser_expects( + Q_values != nullptr, error_type_t::ValidationError, "Q_values cannot be null"); + } + Q_objective_ = span(Q_values, size_values); + + if (size_indices != 0) { + mps_parser_expects( + Q_indices != nullptr, error_type_t::ValidationError, "Q_indices cannot be null"); + } + Q_objective_indices_ = span(Q_indices, size_indices); + + mps_parser_expects( + Q_offsets != nullptr, error_type_t::ValidationError, "Q_offsets cannot be null"); + mps_parser_expects( + size_offsets > 0, error_type_t::ValidationError, "size_offsets cannot be empty"); + Q_objective_offsets_ = span(Q_offsets, size_offsets); +} + template void data_model_view_t::set_row_types(const char* row_types, i_t size) { @@ -279,6 +306,31 @@ bool data_model_view_t::get_sense() const noexcept return maximize_; } +// QPS-specific getter implementations +template +span data_model_view_t::get_quadratic_objective_values() const noexcept +{ + return Q_objective_; +} + +template +span data_model_view_t::get_quadratic_objective_indices() const noexcept +{ + return Q_objective_indices_; +} + +template +span data_model_view_t::get_quadratic_objective_offsets() const noexcept +{ + return Q_objective_offsets_; +} + +template +bool data_model_view_t::has_quadratic_objective() const noexcept +{ + return Q_objective_.size() > 0; +} + // NOTE: Explicitly instantiate all types here in order to avoid linker error template class data_model_view_t; diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index ee92cd38c2..6e756a255a 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -199,6 +199,36 @@ void mps_data_model_t::set_initial_dual_solution(const f_t* initial_du std::copy(initial_dual_solution, initial_dual_solution + size, initial_dual_solution_.data()); } +template +void mps_data_model_t::set_quadratic_objective_matrix(const f_t* Q_values, + i_t size_values, + const i_t* Q_indices, + i_t size_indices, + const i_t* Q_offsets, + i_t size_offsets) +{ + if (size_values != 0) { + mps_parser_expects( + Q_values != nullptr, error_type_t::ValidationError, "Q_values cannot be null"); + } + Q_objective_.resize(size_values); + std::copy(Q_values, Q_values + size_values, Q_objective_.data()); + + if (size_indices != 0) { + mps_parser_expects( + Q_indices != nullptr, error_type_t::ValidationError, "Q_indices cannot be null"); + } + Q_objective_indices_.resize(size_indices); + std::copy(Q_indices, Q_indices + size_indices, Q_objective_indices_.data()); + + mps_parser_expects( + Q_offsets != nullptr, error_type_t::ValidationError, "Q_offsets cannot be null"); + mps_parser_expects( + size_offsets > 0, error_type_t::ValidationError, "size_offsets cannot be empty"); + Q_objective_offsets_.resize(size_offsets); + std::copy(Q_offsets, Q_offsets + size_offsets, Q_objective_offsets_.data()); +} + template const std::vector& mps_data_model_t::get_constraint_matrix_values() const { @@ -397,6 +427,49 @@ i_t mps_data_model_t::get_nnz() const return A_.size(); } +// QPS-specific getter implementations +template +const std::vector& mps_data_model_t::get_quadratic_objective_values() const +{ + return Q_objective_; +} + +template +std::vector& mps_data_model_t::get_quadratic_objective_values() +{ + return Q_objective_; +} + +template +const std::vector& mps_data_model_t::get_quadratic_objective_indices() const +{ + return Q_objective_indices_; +} + +template +std::vector& mps_data_model_t::get_quadratic_objective_indices() +{ + return Q_objective_indices_; +} + +template +const std::vector& mps_data_model_t::get_quadratic_objective_offsets() const +{ + return Q_objective_offsets_; +} + +template +std::vector& mps_data_model_t::get_quadratic_objective_offsets() +{ + return Q_objective_offsets_; +} + +template +bool mps_data_model_t::has_quadratic_objective() const noexcept +{ + return !Q_objective_.empty(); +} + // NOTE: Explicitly instantiate all types here in order to avoid linker error template class mps_data_model_t; diff --git a/cpp/libmps_parser/src/mps_parser.cpp b/cpp/libmps_parser/src/mps_parser.cpp index b9d3daaa37..2a21937682 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -264,6 +264,77 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.set_variable_types(std::move(var_types)); problem.set_row_names(std::move(row_names)); problem.set_maximize(maximize); + + // Helper function to build CSR format using double transpose (O(m+n+nnz) instead of + // O(nnz*log(nnz))) For QUADOBJ: handles upper triangular input by expanding to full symmetric + // matrix + auto build_csr_via_transpose = [](const std::vector>& entries, + i_t num_rows, + i_t num_cols, + bool is_quadobj = false) { + struct CSRResult { + std::vector values; + std::vector indices; + std::vector offsets; + }; + + if (entries.empty()) { + CSRResult result; + result.offsets.resize(num_rows + 1, 0); + return result; + } + + // First transpose: build CSC format (entries sorted by column) + std::vector>> csc_data(num_cols); + for (const auto& entry : entries) { + i_t row = std::get<0>(entry); + i_t col = std::get<1>(entry); + f_t val = std::get<2>(entry); + + // For QUADOBJ (upper triangular), add both (row,col) and (col,row) if off-diagonal + csc_data[col].emplace_back(row, val); + if (is_quadobj && row != col) { csc_data[row].emplace_back(col, val); } + } + + // Second transpose: convert CSC to CSR (entries sorted by row, columns within rows sorted) + std::vector>> csr_data(num_rows); + for (i_t col = 0; col < num_cols; ++col) { + for (const auto& [row, val] : csc_data[col]) { + csr_data[row].emplace_back(col, val); + } + } + + // Build final CSR format + CSRResult result; + result.offsets.reserve(num_rows + 1); + result.offsets.push_back(0); + + for (i_t row = 0; row < num_rows; ++row) { + for (const auto& [col, val] : csr_data[row]) { + result.values.push_back(val); + result.indices.push_back(col); + } + result.offsets.push_back(result.values.size()); + } + + return result; + }; + + // Process QUADOBJ data if present (upper triangular format) + if (!quadobj_entries.empty()) { + // Convert quadratic objective entries to CSR format using double transpose + // QUADOBJ stores upper triangular elements, so we expand to full symmetric matrix + i_t num_vars = static_cast(var_names.size()); + auto csr_result = build_csr_via_transpose(quadobj_entries, num_vars, num_vars, true); + + // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) + problem.set_quadratic_objective_matrix(csr_result.values.data(), + csr_result.values.size(), + csr_result.indices.data(), + csr_result.indices.size(), + csr_result.offsets.data(), + csr_result.offsets.size()); + } } template @@ -426,6 +497,16 @@ void mps_parser_t::parse_string(char* buf) inside_ranges_ = false; inside_objname_ = true; inside_objsense_ = false; + } else if (line.find("QUADOBJ", 0, 7) == 0) { + encountered_sections.insert("QUADOBJ"); + inside_rows_ = false; + inside_columns_ = false; + inside_rhs_ = false; + inside_bounds_ = false; + inside_ranges_ = false; + inside_objname_ = false; + inside_objsense_ = false; + inside_quadobj_ = true; } else if (line.find("ENDATA", 0, 6) == 0) { encountered_sections.insert("ENDATA"); break; @@ -462,6 +543,8 @@ void mps_parser_t::parse_string(char* buf) parse_objsense(line); } else if (inside_objname_) { parse_objname(line); + } else if (inside_quadobj_) { + parse_quadobj(line); } else { mps_parser_expects(false, error_type_t::ValidationError, @@ -978,6 +1061,55 @@ void mps_parser_t::parse_objname(std::string_view line) } } +template +void mps_parser_t::parse_quadobj(std::string_view line) +{ + // Parse QUADOBJ section for quadratic objective terms + // Format: variable1 variable2 value + + std::string var1_name, var2_name; + f_t value; + + if (fixed_mps_format) { + mps_parser_expects(line.size() >= 25, + error_type_t::ValidationError, + "QUADOBJ should have at least 3 entities! line=%s", + std::string(line).c_str()); + + var1_name = std::string(trim(line.substr(4, 8))); // max of 8 chars allowed + var2_name = std::string(trim(line.substr(14, 8))); // max of 8 chars allowed + if (var1_name[0] == '$' || var2_name[0] == '$') return; + + i_t pos = 24; + value = get_numerical_bound(line, pos); + } else { + std::stringstream ss{std::string(line)}; + ss >> var1_name >> var2_name >> value; + if (var1_name[0] == '$' || var2_name[0] == '$') return; + } + + // Find variable indices + auto var1_it = var_names_map.find(var1_name); + auto var2_it = var_names_map.find(var2_name); + + mps_parser_expects(var1_it != var_names_map.end(), + error_type_t::ValidationError, + "Variable '%s' not found in QUADOBJ! line=%s", + var1_name.c_str(), + std::string(line).c_str()); + mps_parser_expects(var2_it != var_names_map.end(), + error_type_t::ValidationError, + "Variable '%s' not found in QUADOBJ! line=%s", + var2_name.c_str(), + std::string(line).c_str()); + + i_t var1_id = var1_it->second; + i_t var2_id = var2_it->second; + + // Store quadratic objective entry (QUADOBJ stores upper triangular elements only) + quadobj_entries.emplace_back(var1_id, var2_id, value); +} + template template f_t mps_parser_t::get_numerical_bound(std::string_view line, i_t& start) diff --git a/cpp/libmps_parser/src/mps_parser.hpp b/cpp/libmps_parser/src/mps_parser.hpp index afaf470f31..e06b2bbe63 100644 --- a/cpp/libmps_parser/src/mps_parser.hpp +++ b/cpp/libmps_parser/src/mps_parser.hpp @@ -123,6 +123,10 @@ class mps_parser_t { /** Objection function sense (maximize of minimize) */ bool maximize{false}; + // QPS-specific data for quadratic programming + /** Quadratic objective matrix entries */ + std::vector> quadobj_entries{}; + private: bool inside_rows_{false}; bool inside_columns_{false}; @@ -132,6 +136,8 @@ class mps_parser_t { bool inside_objsense_{false}; bool inside_intcapture_{false}; bool inside_objname_{false}; + // QPS-specific parsing states + bool inside_quadobj_{false}; std::unordered_set encountered_sections{}; std::unordered_map row_names_map{}; std::unordered_map var_names_map{}; @@ -169,6 +175,9 @@ class mps_parser_t { void parse_ranges(std::string_view line); i_t insert_range_value(std::string_view line, bool skip_range = true); + // QPS-specific parsing methods + void parse_quadobj(std::string_view line); + }; // class mps_parser_t } // namespace cuopt::mps_parser diff --git a/cpp/libmps_parser/tests/mps_parser_test.cpp b/cpp/libmps_parser/tests/mps_parser_test.cpp index b70e8ff290..1e7f218c82 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -757,4 +757,73 @@ TEST(mps_parser, good_mps_file_partial_bounds) EXPECT_EQ(10.0, mps.variable_upper_bounds[1]); } +// ================================================================================================ +// 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.data(), + Q_values.size(), + Q_indices.data(), + Q_indices.size(), + Q_offsets.data(), + Q_offsets.size()); + + // 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"; + } +} + } // namespace cuopt::mps_parser diff --git a/datasets/quadratic_programming/QP_Test_1.qps b/datasets/quadratic_programming/QP_Test_1.qps new file mode 100644 index 0000000000..e9fa9c9024 --- /dev/null +++ b/datasets/quadratic_programming/QP_Test_1.qps @@ -0,0 +1,20 @@ +NAME QP_Test_1 +ROWS + N OBJ.FUNC + G R------1 +COLUMNS + C------1 R------1 0.100000e+02 + C------2 R------1 -.100000e+01 +RHS + RHS OBJ.FUNC 0.100000e+03 + RHS R------1 0.100000e+02 +RANGES +BOUNDS + LO BOUNDS C------1 0.200000e+01 + UP BOUNDS C------1 0.500000e+02 + LO BOUNDS C------2 -.500000e+02 + UP BOUNDS C------2 0.500000e+02 +QUADOBJ + C------1 C------1 0.200000e-01 + C------2 C------2 0.200000e+01 +ENDATA diff --git a/datasets/quadratic_programming/QP_Test_2.qps b/datasets/quadratic_programming/QP_Test_2.qps new file mode 100644 index 0000000000..fe07c33258 --- /dev/null +++ b/datasets/quadratic_programming/QP_Test_2.qps @@ -0,0 +1,20 @@ +NAME QP_Test_2 +ROWS + N OBJ.FUNC + G R------1 +COLUMNS + C------1 OBJ.FUNC -.800000e+01 R------1 -.100000e+01 + C------2 OBJ.FUNC -.600000e+01 R------1 -.100000e+01 + C------3 OBJ.FUNC -.400000e+01 R------1 -.200000e+01 +RHS + RHS OBJ.FUNC -.900000e+01 + RHS R------1 -.300000e+01 +RANGES +BOUNDS +QUADOBJ + C------1 C------1 0.400000e+01 + C------1 C------2 0.200000e+01 + C------1 C------3 0.200000e+01 + C------2 C------2 0.400000e+01 + C------3 C------3 0.200000e+01 +ENDATA