Add QCMATRIX: parsing and quadratic-constraint model#1105
Add QCMATRIX: parsing and quadratic-constraint model#1105yuwenchen95 wants to merge 2 commits intoNVIDIA:mainfrom
Conversation
📝 WalkthroughWalkthroughAdded QCMATRIX support: a CSR-based quadratic_constraint_matrix_t in the data model, public append/get/has APIs, parser state and parsing logic for QCMATRIX blocks, CSR-builder refactor for configurable symmetrization/scale, and a unit test for the new API. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
cpp/libmps_parser/src/mps_parser.cpp (1)
1328-1347: Consider using a map for O(1) duplicate detection.The
flush_qcmatrix_blockfunction uses a linear scan to detect duplicate QCMATRIX blocks for the same constraint row. For files with many quadratic constraints, this results in O(n²) complexity.Since this is in the parser (not a hot path during solving), the impact is likely minimal for typical use cases. This is a minor optimization opportunity.
♻️ Optional: Use unordered_set for duplicate detection
Add a member
std::unordered_set<i_t> qcmatrix_seen_rows_;and check/insert in O(1):+ // In mps_parser.hpp, add: + std::unordered_set<i_t> qcmatrix_seen_rows_{}; // In flush_qcmatrix_block: - 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<int>(qcmatrix_active_row_id_)); - } + mps_parser_expects(qcmatrix_seen_rows_.insert(qcmatrix_active_row_id_).second, + error_type_t::ValidationError, + "Duplicate QCMATRIX block for the same constraint row (index %d)", + static_cast<int>(qcmatrix_active_row_id_));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/libmps_parser/src/mps_parser.cpp` around lines 1328 - 1347, The current flush_qcmatrix_block implementation does O(n) scans over qcmatrix_blocks_ to detect duplicate qcmatrix_active_row_id_, causing O(n^2) behavior; change to use an unordered_set to track seen constraint row ids: add a member std::unordered_set<i_t> qcmatrix_seen_rows_, on flush check if qcmatrix_active_row_id_ is already in qcmatrix_seen_rows_ (and throw the same mps_parser_expects error if found), otherwise insert the id and append the block to qcmatrix_blocks_ as before, then clear qcmatrix_current_entries_ and reset qcmatrix_active_row_id_. Ensure the set is updated exactly where blocks are added so duplicate detection becomes O(1) per block.cpp/libmps_parser/tests/mps_parser_test.cpp (1)
862-901: Good API test coverage, consider adding edge case tests.The test validates the basic
append_quadratic_constraint_matrixAPI with two matrices and checks storage correctness. This provides good coverage of the happy path.Consider adding tests for:
- Empty QCMATRIX block (0 entries)
- Duplicate constraint row detection (should fail per
flush_qcmatrix_blocklogic)- Parser integration test with actual QCMATRIX file content (could be added in follow-up)
💡 Example edge case test
TEST(qps_parser, qcmatrix_empty_entries) { using model_t = mps_data_model_t<int, double>; model_t model; // Empty CSR with just offsets const std::vector<double> empty_values; const std::vector<int> empty_indices; const std::vector<int> offsets = {0, 0}; // 1 row, 0 entries model.append_quadratic_constraint_matrix( 0, empty_values.data(), 0, empty_indices.data(), 0, offsets.data(), offsets.size()); ASSERT_TRUE(model.has_quadratic_constraints()); const auto& qcs = model.get_quadratic_constraint_matrices(); ASSERT_EQ(1u, qcs.size()); EXPECT_TRUE(qcs[0].values.empty()); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/libmps_parser/tests/mps_parser_test.cpp` around lines 862 - 901, Add edge-case tests for the quadratic-constraint API: (1) an "empty entries" test that calls append_quadratic_constraint_matrix with empty values/indices and offsets like {0,0} and then asserts has_quadratic_constraints() is true and get_quadratic_constraint_matrices() contains a matrix with empty values/indices; (2) a "duplicate constraint row" test that attempts to append two matrices with the same constraint_row_index and asserts the parser/model rejects or flags the duplicate consistent with flush_qcmatrix_block behavior; and (3) a lightweight parser integration test that feeds a small QCMATRIX-formatted input through the parser and validates the resulting model via has_quadratic_constraints() and get_quadratic_constraint_matrices() to ensure end-to-end behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cpp/libmps_parser/src/mps_data_model.cpp`:
- Around line 223-259: Validate that constraint_row_index is non-negative and
that CSR offsets are consistent before constructing
quadratic_constraint_matrix_t: in append_quadratic_constraint_matrix check
constraint_row_index >= 0 (reject negative with mps_parser_expects and an
appropriate ValidationError), and verify CSR consistency by asserting
Qc_offsets[size_offsets-1] == size_values (and optionally that offsets are
non-decreasing and within [0,size_values]); if any check fails, return a
ValidationError. Mirror the same style and error messages used in
set_quadratic_objective_matrix and push_back into quadratic_constraint_matrices_
only after all validations pass.
---
Nitpick comments:
In `@cpp/libmps_parser/src/mps_parser.cpp`:
- Around line 1328-1347: The current flush_qcmatrix_block implementation does
O(n) scans over qcmatrix_blocks_ to detect duplicate qcmatrix_active_row_id_,
causing O(n^2) behavior; change to use an unordered_set to track seen constraint
row ids: add a member std::unordered_set<i_t> qcmatrix_seen_rows_, on flush
check if qcmatrix_active_row_id_ is already in qcmatrix_seen_rows_ (and throw
the same mps_parser_expects error if found), otherwise insert the id and append
the block to qcmatrix_blocks_ as before, then clear qcmatrix_current_entries_
and reset qcmatrix_active_row_id_. Ensure the set is updated exactly where
blocks are added so duplicate detection becomes O(1) per block.
In `@cpp/libmps_parser/tests/mps_parser_test.cpp`:
- Around line 862-901: Add edge-case tests for the quadratic-constraint API: (1)
an "empty entries" test that calls append_quadratic_constraint_matrix with empty
values/indices and offsets like {0,0} and then asserts
has_quadratic_constraints() is true and get_quadratic_constraint_matrices()
contains a matrix with empty values/indices; (2) a "duplicate constraint row"
test that attempts to append two matrices with the same constraint_row_index and
asserts the parser/model rejects or flags the duplicate consistent with
flush_qcmatrix_block behavior; and (3) a lightweight parser integration test
that feeds a small QCMATRIX-formatted input through the parser and validates the
resulting model via has_quadratic_constraints() and
get_quadratic_constraint_matrices() to ensure end-to-end behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5d6dc11a-8039-473b-89c2-20ca25ccfc28
📒 Files selected for processing (6)
cpp/libmps_parser/include/mps_parser/mps_data_model.hppcpp/libmps_parser/include/mps_parser/parser.hppcpp/libmps_parser/src/mps_data_model.cppcpp/libmps_parser/src/mps_parser.cppcpp/libmps_parser/src/mps_parser.hppcpp/libmps_parser/tests/mps_parser_test.cpp
|
/ok to test a114099 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
cpp/libmps_parser/src/mps_parser.cpp (2)
1336-1341: Consider using a set for O(1) duplicate detection.The linear scan for duplicate constraint row IDs results in O(n²) complexity when processing many QCMATRIX blocks. While unlikely to be a bottleneck in practice, using a
std::unordered_setfor already-seen row IDs would provide O(1) lookup.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/libmps_parser/src/mps_parser.cpp` around lines 1336 - 1341, Replace the O(n²) duplicate check over qcmatrix_blocks_ with an O(1) lookup using a std::unordered_set: create a seen set (e.g., std::unordered_set<size_t> seen_row_ids), iterate qcmatrix_blocks_, and for each b check if b.constraint_row_id is already in seen_row_ids; if it is, call mps_parser_expects(..., error_type_t::ValidationError, ...) using qcmatrix_active_row_id_ (or b.constraint_row_id) as before, otherwise insert b.constraint_row_id into seen_row_ids; update the check site that currently compares every element to qcmatrix_active_row_id_ to use the set membership test instead.
1342-1346: Clearqcmatrix_current_entries_after move for defensive coding.After
std::move(qcmatrix_current_entries_), the vector is in a valid but unspecified state. While subsequentemplace_backcalls will work, explicitly clearing it makes the intent clear and prevents potential issues if the implementation changes.🔧 Proposed fix
block.entries = std::move(qcmatrix_current_entries_); + qcmatrix_current_entries_.clear(); qcmatrix_blocks_.push_back(std::move(block)); qcmatrix_active_row_id_ = -1;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/libmps_parser/src/mps_parser.cpp` around lines 1342 - 1346, After moving qcmatrix_current_entries_ into the qcmatrix_raw_block_t and pushing it into qcmatrix_blocks_, explicitly reset the source vector by calling qcmatrix_current_entries_.clear() (or clear + shrink_to_fit if desired) so the vector is in a known empty state before further use; apply this change around the block creation code that sets block.constraint_row_id = qcmatrix_active_row_id_, block.entries = std::move(qcmatrix_current_entries_), qcmatrix_blocks_.push_back(std::move(block)), and qcmatrix_active_row_id_ = -1.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cpp/libmps_parser/src/mps_parser.cpp`:
- Around line 1408-1412: In the free-format branch where you parse into
var1_name, var2_name and value, guard against failed numeric extraction:
initialize value and verify that (ss >> value) succeeds; if it fails, call
mps_parser_no_except (consistent with the fixed-format path using
get_numerical_bound) or otherwise report the parse error and return/skip
processing to avoid using an uninitialized value; update the block that
references var1_name, var2_name and value to perform this check before
continuing.
---
Nitpick comments:
In `@cpp/libmps_parser/src/mps_parser.cpp`:
- Around line 1336-1341: Replace the O(n²) duplicate check over qcmatrix_blocks_
with an O(1) lookup using a std::unordered_set: create a seen set (e.g.,
std::unordered_set<size_t> seen_row_ids), iterate qcmatrix_blocks_, and for each
b check if b.constraint_row_id is already in seen_row_ids; if it is, call
mps_parser_expects(..., error_type_t::ValidationError, ...) using
qcmatrix_active_row_id_ (or b.constraint_row_id) as before, otherwise insert
b.constraint_row_id into seen_row_ids; update the check site that currently
compares every element to qcmatrix_active_row_id_ to use the set membership test
instead.
- Around line 1342-1346: After moving qcmatrix_current_entries_ into the
qcmatrix_raw_block_t and pushing it into qcmatrix_blocks_, explicitly reset the
source vector by calling qcmatrix_current_entries_.clear() (or clear +
shrink_to_fit if desired) so the vector is in a known empty state before further
use; apply this change around the block creation code that sets
block.constraint_row_id = qcmatrix_active_row_id_, block.entries =
std::move(qcmatrix_current_entries_),
qcmatrix_blocks_.push_back(std::move(block)), and qcmatrix_active_row_id_ = -1.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 0b68e278-9861-44ca-8945-9e624b0213b8
📒 Files selected for processing (6)
cpp/libmps_parser/include/mps_parser/mps_data_model.hppcpp/libmps_parser/include/mps_parser/parser.hppcpp/libmps_parser/src/mps_data_model.cppcpp/libmps_parser/src/mps_parser.cppcpp/libmps_parser/src/mps_parser.hppcpp/libmps_parser/tests/mps_parser_test.cpp
✅ Files skipped from review due to trivial changes (2)
- cpp/libmps_parser/include/mps_parser/parser.hpp
- cpp/libmps_parser/tests/mps_parser_test.cpp
🚧 Files skipped from review as they are similar to previous changes (3)
- cpp/libmps_parser/src/mps_parser.hpp
- cpp/libmps_parser/src/mps_data_model.cpp
- cpp/libmps_parser/include/mps_parser/mps_data_model.hpp
|
@yuwenchen95 do these changes include the mps writer as well? |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
cpp/libmps_parser/src/mps_data_model.cpp (1)
223-262:⚠️ Potential issue | 🟠 MajorEnforce CSR structural invariants before storing QCMATRIX.
The function still accepts structurally invalid CSR (e.g., mismatched values/indices count, non-monotonic offsets, terminal offset not equal to nnz). This can break downstream constraint handling.
Proposed fix
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"); + mps_parser_expects( + size_values == size_indices, + error_type_t::ValidationError, + "size_values and size_indices must match for CSR"); + mps_parser_expects( + Qc_offsets[0] == 0, + error_type_t::ValidationError, + "Qc_offsets must start at 0"); + for (i_t k = 1; k < size_offsets; ++k) { + mps_parser_expects( + Qc_offsets[k] >= Qc_offsets[k - 1], + error_type_t::ValidationError, + "Qc_offsets must be non-decreasing"); + } + mps_parser_expects( + Qc_offsets[size_offsets - 1] == size_values, + error_type_t::ValidationError, + "last Qc_offsets entry must equal size_values");As per coding guidelines, "Validate algorithm correctness in optimization logic: ... constraint/objective handling must produce correct results."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/libmps_parser/src/mps_data_model.cpp` around lines 223 - 262, append_quadratic_constraint_matrix currently accepts CSR inputs without validating CSR invariants; update it to validate that size_values == size_indices (or both zero), that size_offsets > 0, that offsets are non-decreasing, that offsets.front() == 0 and offsets.back() == size_values (nnz), and that every offset is in [0, size_values]; perform these checks in append_quadratic_constraint_matrix using mps_parser_expects before constructing quadratic_constraint_matrix_t and push_back into quadratic_constraint_matrices_, so invalid Qc_values/Qc_indices/Qc_offsets are rejected early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cpp/libmps_parser/src/mps_data_model.cpp`:
- Around line 223-262: In append_quadratic_constraint_matrix, validate that
size_values and size_indices are non-negative before calling std::vector::resize
and before using Qc_values/Qc_indices: add mps_parser_expects(size_values >= 0,
...) and mps_parser_expects(size_indices >= 0, ...) (similar to the existing
checks for constraint_row_index and size_offsets), then only call
qcm.values.resize(size_values) and std::copy(Qc_values, Qc_values + size_values,
...) when size_values > 0, and likewise only resize/copy indices when
size_indices > 0; this prevents signed-to-unsigned conversion into huge sizes
and avoids resource exhaustion in append_quadratic_constraint_matrix.
---
Duplicate comments:
In `@cpp/libmps_parser/src/mps_data_model.cpp`:
- Around line 223-262: append_quadratic_constraint_matrix currently accepts CSR
inputs without validating CSR invariants; update it to validate that size_values
== size_indices (or both zero), that size_offsets > 0, that offsets are
non-decreasing, that offsets.front() == 0 and offsets.back() == size_values
(nnz), and that every offset is in [0, size_values]; perform these checks in
append_quadratic_constraint_matrix using mps_parser_expects before constructing
quadratic_constraint_matrix_t and push_back into quadratic_constraint_matrices_,
so invalid Qc_values/Qc_indices/Qc_offsets are rejected early.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e13f07ef-d48d-486d-bd6d-10fb0d9fc8e7
📒 Files selected for processing (2)
cpp/libmps_parser/src/mps_data_model.cppcpp/libmps_parser/src/mps_parser.cpp
✅ Files skipped from review due to trivial changes (1)
- cpp/libmps_parser/src/mps_parser.cpp
| template <typename i_t, typename f_t> | ||
| void mps_data_model_t<i_t, f_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)); | ||
| } |
There was a problem hiding this comment.
Validate non-negative CSR sizes before resize.
size_values and size_indices are signed (i_t). If either is negative, std::vector::resize can convert to a huge unsigned size and trigger resource exhaustion.
Proposed fix
void mps_data_model_t<i_t, f_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");
+ mps_parser_expects(
+ size_values >= 0, error_type_t::ValidationError, "size_values cannot be negative");
+ mps_parser_expects(
+ size_indices >= 0, error_type_t::ValidationError, "size_indices cannot be negative");
+ mps_parser_expects(
+ size_offsets > 0, error_type_t::ValidationError, "size_offsets cannot be empty");
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");As per coding guidelines, "Validate input sanitization to prevent buffer overflows and resource exhaustion attacks; avoid unsafe deserialization of problem files."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cpp/libmps_parser/src/mps_data_model.cpp` around lines 223 - 262, In
append_quadratic_constraint_matrix, validate that size_values and size_indices
are non-negative before calling std::vector::resize and before using
Qc_values/Qc_indices: add mps_parser_expects(size_values >= 0, ...) and
mps_parser_expects(size_indices >= 0, ...) (similar to the existing checks for
constraint_row_index and size_offsets), then only call
qcm.values.resize(size_values) and std::copy(Qc_values, Qc_values + size_values,
...) when size_values > 0, and likewise only resize/copy indices when
size_indices > 0; this prevents signed-to-unsigned conversion into huge sizes
and avoids resource exhaustion in append_quadratic_constraint_matrix.
It's only relevant to the change for the constructor of |
Can you add the code to write the mps file as well? |
| EXPECT_EQ(1.0, model.get_quadratic_objective_values()[1]); | ||
| } | ||
|
|
||
| // ================================================================================================ |
There was a problem hiding this comment.
Please add a test that parses a .MPS file containing a QCMATRIX section.
chris-maes
left a comment
There was a problem hiding this comment.
You should probably add support for QCMAATRIX to the MPS writer as well. That way you can verify that you can write out and read in the same problem.
| * @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 { |
There was a problem hiding this comment.
I think this should be quadratic_constraint_t and include the linear term, sense, and rhs.
| * @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, |
There was a problem hiding this comment.
I think this should be append_quadratic_constraint and you should provide the matrix, linear term, rhs, and sense.
chris-maes
left a comment
There was a problem hiding this comment.
Thanks for the PR @yuwenchen95 .I think the data model should deal with quadratic constraints. Not quadratic constraint matrices. The parser needs to sort out which row corresponds to the linear term and the sense and rhs, and feed this to the data model.
The parser also needs to make sure that the row associated with the linear term of a quadratic constraint doesn't get pulled into the A matrix accidently.
Description
This change adds support for reading QCMATRIX blocks from MPS/QPS-style input and representing each quadratic constraint matrix in CSR form on
mps_data_model_t.Included
QCMATRIX(fixed and free format), validate row/variable names, and reject duplicate blocks for the same constraint row.append_quadratic_constraint_matrix/get_quadratic_constraint_matrices, plushas_quadratic_constraints().QCMATRIXuses symmetric CSR without the extra ½ factor).qps_parser.qcmatrix_append_apifor the model append API.Not included (follow-up work)
Issue
N/A — incremental parser/model support; follow-up PR(s) will wire algorithms.
Checklist