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 6879e15d60..6022639f46 100644 --- a/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp +++ b/cpp/libmps_parser/include/mps_parser/mps_data_model.hpp @@ -262,6 +262,40 @@ class mps_data_model_t { const i_t* Q_offsets, i_t size_offsets); + /** + * @brief CSR of Q for one quadratic constraint (MPS QCMATRIX). + * + * @c constraint_row_index is the row index in the linear constraint matrix A (0-based), + * matching the order of non-objective rows in the ROWS section. + */ + struct quadratic_constraint_matrix_t { + i_t constraint_row_index{}; + std::vector values; + std::vector indices; + std::vector offsets; + }; + + /** + * @brief Append one quadratic constraint matrix (QCMATRIX) in CSR format. + * + * @param constraint_row_index Row index in A (0-based), matching non-objective ROWS order. + * @param[in] Qc_values Values of the CSR representation; copied into the model. + * @param size_values Size of the Qc_values array. + * @param[in] Qc_indices Indices of the CSR representation; copied into the model. + * @param size_indices Size of the Qc_indices array. + * @param[in] Qc_offsets Offsets of the CSR representation; copied into the model. + * @param size_offsets Size of the Qc_offsets array. + */ + void append_quadratic_constraint_matrix(i_t constraint_row_index, + const f_t* Qc_values, + i_t size_values, + const i_t* Qc_indices, + i_t size_indices, + const i_t* Qc_offsets, + i_t size_offsets); + + const std::vector& get_quadratic_constraint_matrices() const; + i_t get_n_variables() const; i_t get_n_constraints() const; i_t get_nnz() const; @@ -306,6 +340,8 @@ class mps_data_model_t { bool has_quadratic_objective() const noexcept; + bool has_quadratic_constraints() const noexcept; + /** whether to maximize or minimize the objective function */ bool maximize_; /** @@ -361,6 +397,9 @@ class mps_data_model_t { std::vector Q_objective_indices_; std::vector Q_objective_offsets_; + /** One CSR matrix per QCMATRIX block, in order of appearance in the file */ + std::vector quadratic_constraint_matrices_; + }; // 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 e8e8c342bd..6578ffb4d5 100644 --- a/cpp/libmps_parser/include/mps_parser/parser.hpp +++ b/cpp/libmps_parser/include/mps_parser/parser.hpp @@ -23,6 +23,8 @@ namespace cuopt::mps_parser { * QPS files (for quadratic programming). QPS files are MPS files with additional * sections: * - QUADOBJ: Defines quadratic terms in the objective function + * - QMATRIX: Full symmetric quadratic objective matrix (alternative to QUADOBJ) + * - QCMATRIX: Symmetric quadratic terms for a named constraint row (QCQP) * * Note: Compressed MPS files .mps.gz, .mps.bz2 can only be read if the compression * libraries zlib or libbzip2 are installed, respectively. diff --git a/cpp/libmps_parser/src/mps_data_model.cpp b/cpp/libmps_parser/src/mps_data_model.cpp index 7d0d44a038..718c8423d3 100644 --- a/cpp/libmps_parser/src/mps_data_model.cpp +++ b/cpp/libmps_parser/src/mps_data_model.cpp @@ -9,6 +9,7 @@ #include #include +#include namespace cuopt::mps_parser { @@ -219,6 +220,54 @@ void mps_data_model_t::set_quadratic_objective_matrix(const f_t* Q_val std::copy(Q_offsets, Q_offsets + size_offsets, Q_objective_offsets_.data()); } +template +void mps_data_model_t::append_quadratic_constraint_matrix(i_t constraint_row_index, + const f_t* Qc_values, + i_t size_values, + const i_t* Qc_indices, + i_t size_indices, + const i_t* Qc_offsets, + i_t size_offsets) +{ + mps_parser_expects( + constraint_row_index >= 0, error_type_t::ValidationError, "constraint_row_index must be non-negative"); + + if (size_values != 0) { + mps_parser_expects( + Qc_values != nullptr, error_type_t::ValidationError, "Qc_values cannot be null"); + } + if (size_indices != 0) { + mps_parser_expects( + Qc_indices != nullptr, error_type_t::ValidationError, "Qc_indices cannot be null"); + } + mps_parser_expects( + Qc_offsets != nullptr, error_type_t::ValidationError, "Qc_offsets cannot be null"); + mps_parser_expects( + size_offsets > 0, error_type_t::ValidationError, "size_offsets cannot be empty"); + + quadratic_constraint_matrix_t qcm; + qcm.constraint_row_index = constraint_row_index; + qcm.values.resize(size_values); + if (size_values > 0) { + std::copy(Qc_values, Qc_values + size_values, qcm.values.data()); + } + qcm.indices.resize(size_indices); + if (size_indices > 0) { + std::copy(Qc_indices, Qc_indices + size_indices, qcm.indices.data()); + } + qcm.offsets.resize(size_offsets); + std::copy(Qc_offsets, Qc_offsets + size_offsets, qcm.offsets.data()); + + quadratic_constraint_matrices_.push_back(std::move(qcm)); +} + +template +auto mps_data_model_t::get_quadratic_constraint_matrices() const + -> const std::vector& +{ + return quadratic_constraint_matrices_; +} + template const std::vector& mps_data_model_t::get_constraint_matrix_values() const { @@ -460,6 +509,12 @@ bool mps_data_model_t::has_quadratic_objective() const noexcept return !Q_objective_values_.empty(); } +template +bool mps_data_model_t::has_quadratic_constraints() const noexcept +{ + return !quadratic_constraint_matrices_.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 586544331f..f2ff0089fc 100644 --- a/cpp/libmps_parser/src/mps_parser.cpp +++ b/cpp/libmps_parser/src/mps_parser.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -432,6 +431,8 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) problem.get_constraint_upper_bounds().size()); } + const i_t num_vars_for_quad = static_cast(var_names.size()); + problem.set_problem_name(problem_name); problem.set_objective_name(objective_name); problem.set_variable_names(std::move(var_names)); @@ -439,13 +440,17 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) 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 + // 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. + // + // @p value_scale: + // QUADOBJ/QMATRIX use 0.5 (MPS ½ xᵀQx vs internal xᵀQx); + // QCMATRIX uses 1.0 (symmetric Q defines xᵀQx directly in the constraint). auto build_csr_via_transpose = [](const std::vector>& entries, i_t num_rows, i_t num_cols, - bool is_quadobj = false) { + bool symmetrize_upper_triangular, + f_t value_scale) { struct CSRResult { std::vector values; std::vector indices; @@ -467,7 +472,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) // 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); } + if (symmetrize_upper_triangular && row != col) { csc_data[row].emplace_back(col, val); } } // Second transpose: convert CSC to CSR (entries sorted by row, columns within rows sorted) @@ -485,9 +490,7 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) for (i_t row = 0; row < num_rows; ++row) { for (const auto& [col, val] : csr_data[row]) { - // While the mps format expects to optimize for 0.5 xT Q x, cuopt optimizes for xT Q x - // so we have to multiply the value by 0.5 to get the correct value. - result.values.push_back(val * 0.5); + result.values.push_back(val * value_scale); result.indices.push_back(col); } result.offsets.push_back(result.values.size()); @@ -500,8 +503,9 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) 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); + constexpr f_t k_mps_quad_half_scale = f_t(0.5); // MPS ½ xᵀQx vs internal xᵀQx + auto csr_result = + build_csr_via_transpose(quadobj_entries, num_vars_for_quad, num_vars_for_quad, true, k_mps_quad_half_scale); // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) problem.set_quadratic_objective_matrix(csr_result.values.data(), @@ -513,8 +517,9 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) } else if (!qmatrix_entries.empty()) { // Convert quadratic objective entries to CSR format using double transpose // QMATRIX stores full symmetric matrix - i_t num_vars = static_cast(var_names.size()); - auto csr_result = build_csr_via_transpose(qmatrix_entries, num_vars, num_vars, false); + constexpr f_t k_mps_quad_half_scale = f_t(0.5); + auto csr_result = + build_csr_via_transpose(qmatrix_entries, num_vars_for_quad, num_vars_for_quad, false, k_mps_quad_half_scale); // Use optimized double transpose method - O(m+n+nnz) instead of O(nnz*log(nnz)) problem.set_quadratic_objective_matrix(csr_result.values.data(), @@ -524,6 +529,20 @@ void mps_parser_t::fill_problem(mps_data_model_t& problem) csr_result.offsets.data(), csr_result.offsets.size()); } + + // QCMATRIX: one symmetric Q per constraint row (no extra ½ factor vs file coeffs) + constexpr f_t k_qcmatrix_value_scale = f_t(1); + for (const auto& block : qcmatrix_blocks_) { + auto csr_result = build_csr_via_transpose( + block.entries, num_vars_for_quad, num_vars_for_quad, false, k_qcmatrix_value_scale); + problem.append_quadratic_constraint_matrix(block.constraint_row_id, + 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 @@ -599,6 +618,11 @@ void mps_parser_t::parse_string(char* buf) // these lines mark the start of a particular "section" if (line[0] != ' ') { skip_line = false; + // Leaving QCMATRIX: any non-QCMATRIX section header ends the current block + if (inside_qcmatrix_ && line.find("QCMATRIX", 0, 8) != 0) { + flush_qcmatrix_block(); + inside_qcmatrix_ = false; + } if (line.find("NAME", 0, 4) == 0) { encountered_sections.insert("NAME"); auto name_start = line.find_first_not_of(" \t", 4); @@ -709,6 +733,7 @@ void mps_parser_t::parse_string(char* buf) inside_objname_ = false; inside_objsense_ = false; inside_qmatrix_ = false; + inside_qcmatrix_ = false; inside_quadobj_ = true; } else if (line.find("QMATRIX", 0, 7) == 0) { encountered_sections.insert("QMATRIX"); @@ -721,6 +746,21 @@ void mps_parser_t::parse_string(char* buf) inside_objsense_ = false; inside_quadobj_ = false; inside_qmatrix_ = true; + inside_qcmatrix_ = false; + } else if (line.find("QCMATRIX", 0, 8) == 0) { + encountered_sections.insert("QCMATRIX"); + flush_qcmatrix_block(); + inside_rows_ = false; + inside_columns_ = false; + inside_rhs_ = false; + inside_bounds_ = false; + inside_ranges_ = false; + inside_objname_ = false; + inside_objsense_ = false; + inside_quadobj_ = false; + inside_qmatrix_ = false; + inside_qcmatrix_ = true; + parse_qcmatrix_header(line); } else if (line.find("ENDATA", 0, 6) == 0) { encountered_sections.insert("ENDATA"); break; @@ -737,6 +777,7 @@ void mps_parser_t::parse_string(char* buf) inside_objname_ = false; inside_quadobj_ = false; inside_qmatrix_ = false; + inside_qcmatrix_ = false; } else { mps_parser_expects(false, error_type_t::ValidationError, @@ -763,6 +804,8 @@ void mps_parser_t::parse_string(char* buf) parse_quad(line, true); } else if (inside_qmatrix_) { parse_quad(line, false); + } else if (inside_qcmatrix_) { + parse_qcmatrix_data(line); } else { mps_parser_expects(false, error_type_t::ValidationError, @@ -1282,6 +1325,123 @@ void mps_parser_t::parse_objname(std::string_view line) } } +template +void mps_parser_t::flush_qcmatrix_block() +{ + if (qcmatrix_active_row_id_ < 0) { return; } + if (qcmatrix_current_entries_.empty()) { + qcmatrix_active_row_id_ = -1; + return; + } + for (const auto& b : qcmatrix_blocks_) { + mps_parser_expects(b.constraint_row_id != qcmatrix_active_row_id_, + error_type_t::ValidationError, + "Duplicate QCMATRIX block for the same constraint row (index %d)", + static_cast(qcmatrix_active_row_id_)); + } + qcmatrix_raw_block_t block; + block.constraint_row_id = qcmatrix_active_row_id_; + block.entries = std::move(qcmatrix_current_entries_); + qcmatrix_blocks_.push_back(std::move(block)); + qcmatrix_active_row_id_ = -1; +} + +template +void mps_parser_t::parse_qcmatrix_header(std::string_view line) +{ + std::string row_name; + if (fixed_mps_format) { + mps_parser_expects(line.size() >= 19, + error_type_t::ValidationError, + "QCMATRIX header line too short! line=%s", + std::string(line).c_str()); + //fixed MPS: constraint name starts in column 12 (1-based) → 0-based index 11, 8 chars + row_name = std::string(trim(line.substr(11, 8))); + } else { + std::stringstream ss{std::string(line)}; + std::string kw; + ss >> kw; + mps_parser_expects(kw == "QCMATRIX", + error_type_t::ValidationError, + "Expected QCMATRIX keyword! line=%s", + std::string(line).c_str()); + ss >> row_name; + mps_parser_expects(!row_name.empty(), + error_type_t::ValidationError, + "QCMATRIX missing constraint row name! line=%s", + std::string(line).c_str()); + } + + auto row_it = row_names_map.find(row_name); + mps_parser_expects(row_it != row_names_map.end(), + error_type_t::ValidationError, + "Unknown constraint row name '%s' in QCMATRIX! line=%s", + row_name.c_str(), + std::string(line).c_str()); + + qcmatrix_active_row_id_ = row_it->second; +} + +template +void mps_parser_t::parse_qcmatrix_data(std::string_view line) +{ + mps_parser_expects(qcmatrix_active_row_id_ >= 0, + error_type_t::ValidationError, + "QCMATRIX data line before a valid QCMATRIX header! line=%s", + std::string(line).c_str()); + + std::string var1_name, var2_name; + f_t value; + + if (fixed_mps_format) { + mps_parser_expects(line.size() >= 25, + error_type_t::ValidationError, + "QCMATRIX data line should have at least 3 entities! line=%s", + std::string(line).c_str()); + + var1_name = std::string(trim(line.substr(4, 8))); + var2_name = std::string(trim(line.substr(14, 8))); + if (var1_name[0] == '$' || var2_name[0] == '$') return; + + i_t pos = 24; + value = get_numerical_bound(line, pos); + } else { + i_t pos = 0; + i_t end = 0; + const std::string_view var1_sv = get_next_string(line, pos, end); + mps_parser_expects(!var1_sv.empty(), + error_type_t::ValidationError, + "QCMATRIX data line missing first variable name! line=%s", + std::string(line).c_str()); + if (var1_sv[0] == '$') return; + const std::string_view var2_sv = get_next_string(line, pos, end); + mps_parser_expects(!var2_sv.empty(), + error_type_t::ValidationError, + "QCMATRIX data line missing second variable name! line=%s", + std::string(line).c_str()); + if (var2_sv[0] == '$') return; + value = get_numerical_bound(line, end); + var1_name = std::string(var1_sv); + var2_name = std::string(var2_sv); + } + + 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 QCMATRIX! 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 QCMATRIX! line=%s", + var2_name.c_str(), + std::string(line).c_str()); + + qcmatrix_current_entries_.emplace_back(var1_it->second, var2_it->second, value); +} + template void mps_parser_t::parse_quad(std::string_view line, bool is_quadobj) { @@ -1304,9 +1464,23 @@ void mps_parser_t::parse_quad(std::string_view line, bool is_quadobj) 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; + i_t pos = 0; + i_t end = 0; + const std::string_view var1_sv = get_next_string(line, pos, end); + mps_parser_expects(!var1_sv.empty(), + error_type_t::ValidationError, + "QUADOBJ/QMATRIX data line missing first variable name! line=%s", + std::string(line).c_str()); + if (var1_sv[0] == '$') return; + const std::string_view var2_sv = get_next_string(line, pos, end); + mps_parser_expects(!var2_sv.empty(), + error_type_t::ValidationError, + "QUADOBJ/QMATRIX data line missing second variable name! line=%s", + std::string(line).c_str()); + if (var2_sv[0] == '$') return; + value = get_numerical_bound(line, end); + var1_name = std::string(var1_sv); + var2_name = std::string(var2_sv); } // Find variable indices diff --git a/cpp/libmps_parser/src/mps_parser.hpp b/cpp/libmps_parser/src/mps_parser.hpp index facad14c66..d73cfdd8b3 100644 --- a/cpp/libmps_parser/src/mps_parser.hpp +++ b/cpp/libmps_parser/src/mps_parser.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -130,6 +131,18 @@ class mps_parser_t { // QPS-specific parsing states bool inside_quadobj_{false}; bool inside_qmatrix_{false}; + bool inside_qcmatrix_{false}; + + /** (free-format) QCMATRIX: finalized blocks (row id + triples) */ + struct qcmatrix_raw_block_t { + i_t constraint_row_id{}; + std::vector> entries{}; + }; + std::vector qcmatrix_blocks_{}; + /** Triples for the QCMATRIX block currently being read (-1 row id means none) */ + i_t qcmatrix_active_row_id_{-1}; + std::vector> qcmatrix_current_entries_{}; + std::unordered_set encountered_sections{}; std::unordered_map row_names_map{}; std::unordered_map var_names_map{}; @@ -170,6 +183,11 @@ class mps_parser_t { // QPS-specific parsing methods void parse_quad(std::string_view line, bool is_quadobj); + // QCMATRIX-specific parsing methods + void flush_qcmatrix_block(); + void parse_qcmatrix_header(std::string_view line); + void parse_qcmatrix_data(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 f915fb2df5..e041b42419 100644 --- a/cpp/libmps_parser/tests/mps_parser_test.cpp +++ b/cpp/libmps_parser/tests/mps_parser_test.cpp @@ -855,6 +855,51 @@ TEST(qps_parser, quadratic_objective_basic) 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_matrix_t default_qcm; + EXPECT_EQ(0, default_qcm.constraint_row_index); + EXPECT_TRUE(default_qcm.values.empty()); + EXPECT_TRUE(default_qcm.indices.empty()); + EXPECT_TRUE(default_qcm.offsets.empty()); + + // 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}; + model.append_quadratic_constraint_matrix( + 0, qc0_values.data(), qc0_values.size(), qc0_indices.data(), qc0_indices.size(), qc0_offsets.data(), qc0_offsets.size()); + + // 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}; + model.append_quadratic_constraint_matrix( + 1, qc1_values.data(), qc1_values.size(), qc1_indices.data(), qc1_indices.size(), qc1_offsets.data(), qc1_offsets.size()); + + ASSERT_TRUE(model.has_quadratic_constraints()); + const auto& qcs = model.get_quadratic_constraint_matrices(); + ASSERT_EQ(2u, qcs.size()); + + EXPECT_EQ(0, qcs[0].constraint_row_index); + EXPECT_EQ(qc0_values, qcs[0].values); + EXPECT_EQ(qc0_indices, qcs[0].indices); + EXPECT_EQ(qc0_offsets, qcs[0].offsets); + + EXPECT_EQ(1, qcs[1].constraint_row_index); + EXPECT_EQ(qc1_values, qcs[1].values); + EXPECT_EQ(qc1_indices, qcs[1].indices); + EXPECT_EQ(qc1_offsets, qcs[1].offsets); +} + // Test actual QPS files from the dataset TEST(qps_parser, test_qps_files) {