Skip to content

fix(socp): canonicalize QC Q COO for rotated SOC detection (#1435)#1439

Open
yuwenchen95 wants to merge 6 commits into
NVIDIA:mainfrom
yuwenchen95:fix-qcqp-canonical-coo
Open

fix(socp): canonicalize QC Q COO for rotated SOC detection (#1435)#1439
yuwenchen95 wants to merge 6 commits into
NVIDIA:mainfrom
yuwenchen95:fix-qcqp-canonical-coo

Conversation

@yuwenchen95

Copy link
Copy Markdown
Contributor

Description

Quadratic-constraint Q is now stored in canonical COO internally: one coefficient per variable pair (e.g. a single -2 for 2·x₀·x₁), with symmetric MPS halves merged at ingest. Canonicalization runs at ingest boundaries (MPS/LP parser, C API, Python→solver, gRPC, PDLP CPU/GPU problem setup). MPS QCMATRIX still accepts symmetric halves; the MPS writer expands canonical cross terms back to symmetric form on export.

The RSOC fast path now accepts a single eligible cross term (e.g. -2·t·u for ||tail||² ≤ 2·t·u) instead of requiring two symmetric off-diagonal entries. Previously, natural Python/API forms were misrouted to the general path or failed pattern matching, producing wrong optima (e.g. ~0 instead of √2).

Issue

closes #1435

Checklist

  • I am familiar with the Contributing Guidelines.
  • Testing
    • New or existing tests cover these changes
    • Added tests
    • Created an issue to follow-up
    • NA
  • Documentation
    • The documentation is up to date with these changes
    • Added new documentation
    • NA

Test plan

  • ./build.sh libcuopt with conda env active
  • C++: ctest --test-dir cpp/build -R 'SOCP_TEST|MPS_PARSER_TEST|C_API_TEST'
  • Parser: qc_cross_term_stored_canonical and related LP/MPS QC COO tests
  • C API: rotated SOC / canonical QC COO tests in C_API_TEST
  • Python: pytest -v cuopt/cuopt/tests/socp/test_socp.py -k rotated_soc
  • Manual: rotated SOC with single -2*t*u cross term → objective ≈ √2 (not ~0)

Store quadratic-constraint Q in canonical COO (one cross coefficient per
variable pair) and run canonicalization at ingest boundaries (MPS/LP,
C API, Python→solver, gRPC, MPS export). The RSOC fast path now accepts
a single cross term (e.g. -2*x0*x1 for ||tail||^2 <= 2*x0*x1) instead of
requiring symmetric MPS-style halves, which previously routed natural
API forms to the general QC path and produced wrong optima.
MPS QCMATRIX still accepts symmetric halves; the writer expands canonical
cross terms back to MPS form on export. Adds tests and doc updates for
C API, parser, and examples.

Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>
@yuwenchen95 yuwenchen95 self-assigned this Jun 15, 2026
@yuwenchen95 yuwenchen95 requested review from a team as code owners June 15, 2026 18:35
@yuwenchen95 yuwenchen95 added bug Something isn't working non-breaking Introduces a non-breaking change labels Jun 15, 2026
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes a bug where rotated second-order cone detection incorrectly required symmetric off-diagonal pairs. It introduces canonicalize_coo_matrix as the single normalization point that merges duplicate and symmetric COO entries into one canonical upper-triangular cross-term. Every quadratic constraint ingestion path (LP parser, MPS parser, gRPC decoder, add_quadratic_constraint) is wired through this canonicalization. The MPS writer re-expands canonical entries to symmetric halves on output. The rotated-SOC fast-path detector is updated to recognize the new single-entry form.

Changes

Canonical QC COO and rotated-SOC bug fix

Layer / File(s) Summary
Core COO canonicalization utilities
cpp/src/io/mps_parser_internal.hpp, cpp/src/io/mps_parser.cpp
Declares and implements check_symmetric_offdiagonal_pairs (validates matching symmetric off-diagonal pairs within epsilon) and canonicalize_coo_matrix (aggregates duplicates, merges symmetric off-diagonal pairs into single upper-triangular cross-term entries, sorts output). Adds explicit int/float and int/double instantiations.
Public header contracts for canonical QC storage
cpp/include/cuopt/linear_programming/io/mps_data_model.hpp, cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp, cpp/include/cuopt/linear_programming/io/data_model_view.hpp, cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp
Updates quadratic_constraint_t and append_quadratic_constraint docs to specify canonical COO (one triplet per variable pair, full coefficient, upper-triangular). Declares new canonicalize_quadratic_constraints free function template. Updates add_quadratic_constraint and set_quadratic_constraints documentation for on-ingest canonicalization guarantee.
LP and MPS parser ingestion with canonicalization
cpp/src/io/lp_parser.hpp, cpp/src/io/lp_parser.cpp, cpp/src/io/mps_data_model.cpp, cpp/src/io/mps_parser.cpp
LP parser removes build_symmetric_q_coo and forwards raw face-value triples to append_quadratic_constraint. MPS parser calls check_symmetric_offdiagonal_pairs before appending each QCMATRIX block. mps_data_model.cpp replaces manual permutation-sort canonicalization with canonicalize_coo_matrix, adds canonicalize_quadratic_constraints template and instantiations.
Problem-level add_quadratic_constraint and gRPC wiring
cpp/src/pdlp/optimization_problem.cu, cpp/src/pdlp/cpu_optimization_problem.cpp, cpp/src/grpc/grpc_problem_mapper.cpp, cpp/src/grpc/codegen/generate_conversions.py, cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp, cpp/src/io/data_model_view.cpp
Inserts io::canonicalize_coo_matrix into both GPU and CPU add_quadratic_constraint implementations. gRPC code generator emits canonicalization in unary and chunked decode paths for quadratic_constraints. populate_from_data_model_view calls io::canonicalize_quadratic_constraints before setting constraints on the problem interface.
MPS writer: re-expand canonical cross terms to symmetric halves
cpp/src/io/mps_writer.cpp
Before writing QCMATRIX, canonicalizes a local copy of each constraint. Diagonal entries write v; off-diagonal entries write v/2 for both (i,j) and (j,i) to restore MPS symmetric convention.
Rotated-SOC fast-path: single canonical cross-term detection
cpp/src/barrier/translate_soc.hpp
Replaces the symmetric-pair use_general_path predicate with a rotated_soc_cross_eligible lambda checking for exactly one off-diagonal entry. Fast-path now requires a single cross term, derives cross_d = -v0/2, and expects q_nnz - 1 tail diagonals (down from q_nnz - 2).
Parser, SOCP, and C API tests
cpp/tests/linear_programming/parser_test.cpp, cpp/tests/socp/general_quadratic_test.cu, cpp/tests/linear_programming/c_api_tests/c_api_test.c, cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp, cpp/tests/linear_programming/c_api_tests/c_api_tests.h, python/cuopt/cuopt/tests/socp/test_socp.py
Parser tests add canonicalization unit tests (duplicate merging, symmetric collapsing, rejection of incomplete pairs) and update LP cross-term expectations to single-entry canonical COO. SOCP tests change rotated-SOC fixtures from 4-entry symmetric to 3-entry canonical form. Two new C API end-to-end tests verify optimal solutions for rotated-SOC with different cross-term conventions. New Python barrier test validates the natural v0^2 + v1^2 - 2*t*u <= 0 idiom.
Documentation and examples for canonical cross-term convention
docs/cuopt/source/convex-features.rst, docs/cuopt/source/cuopt-c/convex/convex-c-api.rst, docs/cuopt/source/cuopt-c/convex/convex-examples.rst, docs/cuopt/source/cuopt-c/convex/examples/*, docs/cuopt/source/cuopt-python/convex/convex-examples.rst, docs/cuopt/source/cuopt-python/convex/examples/rotated_socp_example.py
All rotated-SOC docs and examples updated to describe single canonical cross-term input (Q[x0,x1] = -2 for 2*x0*x1). MPS QCMATRIX docs clarify symmetric-halves validation. Example files reduce quadratic triplet arrays from 4-entry symmetric to 3-entry canonical form with matching API call argument counts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

improvement

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: canonicalizing QC Q COO for rotated SOC detection, which is the core fix addressing issue #1435.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the canonical COO storage for quadratic constraints and how it fixes the RSOC detection bug.
Linked Issues check ✅ Passed The PR successfully addresses issue #1435 by implementing canonical COO storage for quadratic constraints and fixing RSOC detection to accept single eligible cross terms instead of requiring symmetric pairs.
Out of Scope Changes check ✅ Passed All changes are directly related to the canonicalization of quadratic-constraint Q in COO format and fixing RSOC detection, with no apparent out-of-scope modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/src/barrier/translate_soc.hpp (1)

500-510: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Allow one-tail rotated cones.

Line 507 still rejects q_nnz == 2, but canonical x² - 2*y*z <= 0 has exactly one tail diagonal plus one cross term. The preceding tail_vars.size() == q_nnz - 1 check already validates the canonical count; require at least one tail instead.

🐛 Proposed fix
-        cuopt_expects(q_nnz >= 3,
+        cuopt_expects(!tail_vars.empty(),
                       error_type_t::ValidationError,
                       "Quadratic constraint '%s' rotated SOC Q must have at least 1 tail entry",
                       qc.constraint_row_name.c_str());

Please also add a one-tail RSOC regression such as x0^2 - 2*y*z <= 0.

As per coding guidelines, incorrect constraint handling/objective computation for RSOC/Q canonicalization is CRITICAL.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/barrier/translate_soc.hpp` around lines 500 - 510, The second
cuopt_expects check currently requires q_nnz >= 3, but canonical rotated SOC
forms like x² - 2*y*z <= 0 have exactly q_nnz == 2 (one tail diagonal and one
cross term). Since the preceding tail_vars.size() == q_nnz - 1 check already
validates the correct count of tail variables, change the minimum requirement
from q_nnz >= 3 to q_nnz >= 2 to allow at least one tail entry. Additionally,
add a regression test case with a one-tail RSOC constraint matching the
canonical form x0^2 - 2*y*z <= 0.

Source: Coding guidelines

🧹 Nitpick comments (2)
cpp/tests/linear_programming/parser_test.cpp (1)

2768-2777: ⚡ Quick win

Cover mismatched MPS symmetric halves too.

Line 2768 covers a missing reverse entry, but the strict MPS path should also reject both halves when their values differ. Add a small test for {(0,1,2), (1,0,3)} with require_symmetric_offdiagonal_pairs=true.

🧪 Proposed test coverage
 TEST(qc_coo_canonicalize, mps_requires_matching_symmetric_half)
 {
   std::vector<int> rows    = {0};
   std::vector<int> cols    = {1};
   std::vector<double> vals = {2.0};
   qc_coo_canonicalize_options_t<double> opts;
   opts.require_symmetric_offdiagonal_pairs = true;
   opts.constraint_name                     = "QC0";
   EXPECT_THROW(canonicalize_qc_coo(rows, cols, vals, opts), std::logic_error);
 }
+
+TEST(qc_coo_canonicalize, mps_rejects_mismatched_symmetric_halves)
+{
+  std::vector<int> rows    = {0, 1};
+  std::vector<int> cols    = {1, 0};
+  std::vector<double> vals = {2.0, 3.0};
+  qc_coo_canonicalize_options_t<double> opts;
+  opts.require_symmetric_offdiagonal_pairs = true;
+  opts.constraint_name                     = "QC0";
+  EXPECT_THROW(canonicalize_qc_coo(rows, cols, vals, opts), std::logic_error);
+}

As per coding guidelines, parser/SOCP tests for RSOC-related changes should cover edge cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/tests/linear_programming/parser_test.cpp` around lines 2768 - 2777, The
current test mps_requires_matching_symmetric_half only covers the case where a
reverse entry is missing. Add another test function to cover the case where both
symmetric halves exist but with mismatched values. Create a new test that calls
canonicalize_qc_coo with row indices {0, 1}, column indices {1, 0}, values {2.0,
3.0}, and require_symmetric_offdiagonal_pairs set to true, then verify it throws
std::logic_error. This ensures the strict MPS path rejects both matching and
mismatching symmetric pairs when the pairs have different values.

Source: Coding guidelines

cpp/src/io/quadratic_constraint_coo.cpp (1)

115-117: 💤 Low value

Near-zero diagonal entries not filtered after aggregation.

Diagonal entries that aggregate to near-zero values (e.g., +1 and -1 for the same (i,i)) are still emitted to the output. The aggregation phase filters near-zero inputs (line 63), but aggregated near-zero results on the diagonal are not filtered, unlike off-diagonal entries which use lookup_coeff with tolerance gating.

This is a minor inconsistency; downstream consumers likely handle near-zero coefficients gracefully, but for cleanliness you could add a tolerance check before emitting diagonals.

♻️ Optional: filter near-zero diagonal aggregates
   for (const auto& [rc, v] : agg) {
-    if (rc.first == rc.second) { out.emplace_back(rc.first, rc.second, v); }
+    if (rc.first == rc.second && std::abs(v) > opts.tol) {
+      out.emplace_back(rc.first, rc.second, v);
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/io/quadratic_constraint_coo.cpp` around lines 115 - 117, The loop
iterating through aggregated entries emits diagonal entries (where rc.first ==
rc.second) without filtering near-zero aggregated values, while the aggregation
phase filters near-zero inputs at line 63 and off-diagonal entries use tolerance
gating via lookup_coeff. Add a tolerance check before emitting diagonal entries
in the emplace_back call to filter out near-zero aggregated diagonal values,
ensuring consistency with how off-diagonal entries are handled and maintaining
code cleanliness.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cpp/src/barrier/translate_soc.hpp`:
- Around line 500-510: The second cuopt_expects check currently requires q_nnz
>= 3, but canonical rotated SOC forms like x² - 2*y*z <= 0 have exactly q_nnz ==
2 (one tail diagonal and one cross term). Since the preceding tail_vars.size()
== q_nnz - 1 check already validates the correct count of tail variables, change
the minimum requirement from q_nnz >= 3 to q_nnz >= 2 to allow at least one tail
entry. Additionally, add a regression test case with a one-tail RSOC constraint
matching the canonical form x0^2 - 2*y*z <= 0.

---

Nitpick comments:
In `@cpp/src/io/quadratic_constraint_coo.cpp`:
- Around line 115-117: The loop iterating through aggregated entries emits
diagonal entries (where rc.first == rc.second) without filtering near-zero
aggregated values, while the aggregation phase filters near-zero inputs at line
63 and off-diagonal entries use tolerance gating via lookup_coeff. Add a
tolerance check before emitting diagonal entries in the emplace_back call to
filter out near-zero aggregated diagonal values, ensuring consistency with how
off-diagonal entries are handled and maintaining code cleanliness.

In `@cpp/tests/linear_programming/parser_test.cpp`:
- Around line 2768-2777: The current test mps_requires_matching_symmetric_half
only covers the case where a reverse entry is missing. Add another test function
to cover the case where both symmetric halves exist but with mismatched values.
Create a new test that calls canonicalize_qc_coo with row indices {0, 1}, column
indices {1, 0}, values {2.0, 3.0}, and require_symmetric_offdiagonal_pairs set
to true, then verify it throws std::logic_error. This ensures the strict MPS
path rejects both matching and mismatching symmetric pairs when the pairs have
different values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 08edaebe-2c73-4a04-be32-3d00d718d07d

📥 Commits

Reviewing files that changed from the base of the PR and between 03fe3fc and 7063060.

⛔ Files ignored due to path filters (2)
  • cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc is excluded by !**/generated/**
  • cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc is excluded by !**/generated/**
📒 Files selected for processing (31)
  • cpp/include/cuopt/linear_programming/io/data_model_view.hpp
  • cpp/include/cuopt/linear_programming/io/mps_data_model.hpp
  • cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp
  • cpp/include/cuopt/linear_programming/optimization_problem_utils.hpp
  • cpp/src/barrier/translate_soc.hpp
  • cpp/src/grpc/codegen/generate_conversions.py
  • cpp/src/grpc/grpc_problem_mapper.cpp
  • cpp/src/io/CMakeLists.txt
  • cpp/src/io/data_model_view.cpp
  • cpp/src/io/lp_parser.cpp
  • cpp/src/io/lp_parser.hpp
  • cpp/src/io/mps_data_model.cpp
  • cpp/src/io/mps_parser.cpp
  • cpp/src/io/mps_writer.cpp
  • cpp/src/io/quadratic_constraint_coo.cpp
  • cpp/src/io/quadratic_constraint_coo.hpp
  • cpp/src/pdlp/cpu_optimization_problem.cpp
  • cpp/src/pdlp/optimization_problem.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
  • cpp/tests/linear_programming/c_api_tests/c_api_tests.cpp
  • cpp/tests/linear_programming/c_api_tests/c_api_tests.h
  • cpp/tests/linear_programming/parser_test.cpp
  • cpp/tests/socp/general_quadratic_test.cu
  • docs/cuopt/source/convex-features.rst
  • docs/cuopt/source/cuopt-c/convex/convex-c-api.rst
  • docs/cuopt/source/cuopt-c/convex/convex-examples.rst
  • docs/cuopt/source/cuopt-c/convex/examples/general_quadratic_example.c
  • docs/cuopt/source/cuopt-c/convex/examples/rotated_socp_example.c
  • docs/cuopt/source/cuopt-python/convex/convex-examples.rst
  • docs/cuopt/source/cuopt-python/convex/examples/rotated_socp_example.py
  • python/cuopt/cuopt/tests/socp/test_socp.py

Comment thread cpp/include/cuopt/linear_programming/io/data_model_view.hpp Outdated
* @param vals, rows, cols COO triplets for Q; same length; may all be empty if Q is empty.
* Stored sorted by (row, col).
* Stored sorted by (row, col) in canonical form (one entry per variable pair).
* @param require_symmetric_q_offdiagonal When true (MPS QCMATRIX), each off-diagonal pair must

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for this extra argument? Is it an assertion or does the change the behavior of the function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the flag for additional check with MPS file input that requires symmetric halved inputs for cross terms.

template <typename i_t, typename f_t>
void canonicalize_quadratic_constraints(
std::vector<typename mps_data_model_t<i_t, f_t>::quadratic_constraint_t>& constraints,
bool require_symmetric_q_offdiagonal = false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment on require_symmetric_q_offdiagonal. Is it an assertion or does the change the behavior of the function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the flag for additional check with MPS file input that requires symmetric halved inputs for cross terms.

Comment thread cpp/include/cuopt/linear_programming/io/mps_data_model.hpp Outdated
Comment thread cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp Outdated
Comment thread cpp/tests/linear_programming/c_api_tests/c_api_test.c Outdated
Comment thread docs/cuopt/source/cuopt-c/convex/convex-c-api.rst Outdated
(supply ``Q[x_0, x_1] = -2`` in COO, or ``-x_0*x_1`` for the ``<= x_0*x_1`` cone form; see :doc:`/convex-features`)

For the rotated second-order cone constraints, cuOpt expects the quadratic matrix to be symmetric.
MPS QCMATRIX requires symmetric off-diagonal halves; the C API accepts a single cross coefficient per variable pair (canonicalized on ingest).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be clear what's accepted. If the inputs are canonicalized then it sounds like the C API accepts more than just a single cross coefficient per variable pair.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We allow users to give duplicate cross coefficients per variable pair and then merge them in the canonicalization step.

Comment thread docs/cuopt/source/cuopt-c/convex/convex-examples.rst Outdated
Comment thread docs/cuopt/source/convex-features.rst Outdated
…im redundant canonicalization comments

Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>
Replace qc_coo options/wrappers with canonicalize_coo_matrix(rows,cols,vals,bool).
Use machine epsilon for zero checks; sum both orientations into one (lo,hi) entry.
Test via append_quadratic_constraint; update gRPC codegen call sites.

Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/io/quadratic_constraint_coo.cpp`:
- Around line 22-31: The lookup_coeff function uses
std::numeric_limits<f_t>::epsilon() as a bare absolute threshold for filtering
zero coefficients, which is numerically inappropriate for coefficients with
varying magnitudes. Replace the hardcoded epsilon threshold with a fixed
tolerance value of 1e-9, which is consistent with the existing compare_equal()
function used elsewhere in the codebase. Specifically, modify the line where eps
is declared and update the condition in the if statement that checks
std::abs(it->second) <= eps to use the appropriate tolerance instead of epsilon.
🪄 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: Enterprise

Run ID: de147a41-97d7-4cb3-bee0-acb904661cd9

📥 Commits

Reviewing files that changed from the base of the PR and between 7063060 and 9ff9b03.

⛔ Files ignored due to path filters (2)
  • cpp/src/grpc/codegen/generated/generated_chunked_arrays_to_problem.inc is excluded by !**/generated/**
  • cpp/src/grpc/codegen/generated/generated_proto_to_problem.inc is excluded by !**/generated/**
📒 Files selected for processing (19)
  • cpp/include/cuopt/linear_programming/io/data_model_view.hpp
  • cpp/include/cuopt/linear_programming/io/mps_data_model.hpp
  • cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp
  • cpp/src/grpc/codegen/generate_conversions.py
  • cpp/src/io/lp_parser.cpp
  • cpp/src/io/mps_data_model.cpp
  • cpp/src/io/mps_parser.cpp
  • cpp/src/io/mps_writer.cpp
  • cpp/src/io/quadratic_constraint_coo.cpp
  • cpp/src/io/quadratic_constraint_coo.hpp
  • cpp/src/pdlp/cpu_optimization_problem.cpp
  • cpp/src/pdlp/optimization_problem.cu
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
  • cpp/tests/linear_programming/parser_test.cpp
  • docs/cuopt/source/convex-features.rst
  • docs/cuopt/source/cuopt-c/convex/convex-c-api.rst
  • docs/cuopt/source/cuopt-c/convex/convex-examples.rst
  • docs/cuopt/source/cuopt-c/convex/examples/general_quadratic_example.c
  • docs/cuopt/source/cuopt-c/convex/examples/rotated_socp_example.c
💤 Files with no reviewable changes (2)
  • cpp/include/cuopt/linear_programming/optimization_problem_interface.hpp
  • cpp/tests/linear_programming/c_api_tests/c_api_test.c
✅ Files skipped from review due to trivial changes (3)
  • docs/cuopt/source/cuopt-c/convex/convex-c-api.rst
  • docs/cuopt/source/cuopt-c/convex/convex-examples.rst
  • cpp/include/cuopt/linear_programming/io/data_model_view.hpp
🚧 Files skipped from review as they are similar to previous changes (8)
  • cpp/src/pdlp/cpu_optimization_problem.cpp
  • docs/cuopt/source/cuopt-c/convex/examples/rotated_socp_example.c
  • cpp/src/io/mps_parser.cpp
  • cpp/src/io/mps_writer.cpp
  • cpp/include/cuopt/linear_programming/io/mps_data_model.hpp
  • cpp/src/io/mps_data_model.cpp
  • cpp/src/io/lp_parser.cpp
  • cpp/src/grpc/codegen/generate_conversions.py

Comment thread cpp/src/io/quadratic_constraint_coo.cpp Outdated
Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>

@mlubin mlubin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big improvement!

We have test coverage of the C interface, what about also adding tests covering the python example in the original issue?

Comment thread cpp/tests/linear_programming/parser_test.cpp Outdated
Comment thread cpp/include/cuopt/linear_programming/io/mps_data_model.hpp Outdated
Comment thread cpp/include/cuopt/linear_programming/io/mps_data_model.hpp Outdated
Comment thread cpp/src/io/quadratic_constraint_coo.hpp Outdated
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights
* reserved. SPDX-License-Identifier: Apache-2.0
*/
#pragma once

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming this file; the contents don't mention anything about quadratic constraints.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the canonicalization function into mps_parser_* files though it is also used for lp file input.

Comment thread docs/cuopt/source/convex-features.rst Outdated
…out only for mps files

Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>
Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
cpp/src/pdlp/optimization_problem.cu (1)

13-13: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Move the canonical COO declaration out of the MPS parser-internal header.

optimization_problem.cu is a PDLP ingestion path, so depending on mps_parser_internal.hpp leaks parser internals into solver code. Since this helper is now shared across PDLP, gRPC, data-model, and writer paths, expose canonicalize_coo_matrix from a neutral quadratic-COO utility header and keep MPS-only validation in the parser internals.

Suggested include direction after moving the declaration
-#include <mps_parser_internal.hpp>
+#include <quadratic_constraint_coo.hpp>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/pdlp/optimization_problem.cu` at line 13, The include of
`mps_parser_internal.hpp` in optimization_problem.cu exposes parser internals to
the solver code. Move the `canonicalize_coo_matrix` declaration from the
mps_parser_internal header to a neutral quadratic-COO utility header (either
create a new one or use an existing shared header), keeping MPS-specific
validation logic in mps_parser_internal.hpp. Then update the include statement
in optimization_problem.cu to reference the new neutral header instead of
mps_parser_internal.hpp to properly separate solver concerns from parser
implementation details.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/io/mps_parser.cpp`:
- Around line 172-174: The code uses machine epsilon (via
std::numeric_limits<f_t>::epsilon()) as an absolute zero cutoff threshold for Q
coefficients, which aggressively removes valid small non-zero values and
incorrectly changes the quadratic constraint during canonicalization. Replace
this hardcoded machine epsilon check with an explicit parser or solver tolerance
constant that employs relative scaling instead of absolute machine epsilon as
the storage-level zero threshold. Update the condition in the zero check that
compares std::abs(it->second) <= eps to use this new explicit tolerance, and
apply the same fix to all other occurrences of similar epsilon-based comparisons
in the file (around lines 216-217, 276-277, and 295-300 as noted in the
comment).

---

Nitpick comments:
In `@cpp/src/pdlp/optimization_problem.cu`:
- Line 13: The include of `mps_parser_internal.hpp` in optimization_problem.cu
exposes parser internals to the solver code. Move the `canonicalize_coo_matrix`
declaration from the mps_parser_internal header to a neutral quadratic-COO
utility header (either create a new one or use an existing shared header),
keeping MPS-specific validation logic in mps_parser_internal.hpp. Then update
the include statement in optimization_problem.cu to reference the new neutral
header instead of mps_parser_internal.hpp to properly separate solver concerns
from parser implementation details.
🪄 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: Enterprise

Run ID: d403fc47-c696-4cf8-bfd3-0119c147e514

📥 Commits

Reviewing files that changed from the base of the PR and between b555adf and aa69c54.

📒 Files selected for processing (12)
  • cpp/include/cuopt/linear_programming/io/mps_data_model.hpp
  • cpp/src/grpc/grpc_problem_mapper.cpp
  • cpp/src/io/mps_data_model.cpp
  • cpp/src/io/mps_parser.cpp
  • cpp/src/io/mps_parser_internal.hpp
  • cpp/src/io/mps_writer.cpp
  • cpp/src/pdlp/cpu_optimization_problem.cpp
  • cpp/src/pdlp/optimization_problem.cu
  • cpp/tests/linear_programming/parser_test.cpp
  • cpp/tests/socp/general_quadratic_test.cu
  • docs/cuopt/source/convex-features.rst
  • python/cuopt/cuopt/tests/socp/test_socp.py
✅ Files skipped from review due to trivial changes (1)
  • cpp/src/grpc/grpc_problem_mapper.cpp
🚧 Files skipped from review as they are similar to previous changes (4)
  • cpp/tests/socp/general_quadratic_test.cu
  • cpp/src/pdlp/cpu_optimization_problem.cpp
  • docs/cuopt/source/convex-features.rst
  • cpp/tests/linear_programming/parser_test.cpp

Comment thread cpp/src/io/mps_parser.cpp
Comment on lines +172 to +174
const f_t eps = std::numeric_limits<f_t>::epsilon();
const auto it = agg.find({r, c});
if (it == agg.end() || std::abs(it->second) <= eps) { return f_t(0); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve non-zero Q coefficients during canonicalization.

Using numeric_limits<f_t>::epsilon() as an absolute zero cutoff drops valid small coefficients; with float, any |Q_ij| <= ~1e-7 is silently removed, so canonicalization can change the quadratic constraint instead of only normalizing its COO representation.

Suggested direction
-  const f_t eps = std::numeric_limits<f_t>::epsilon();
   const auto it = agg.find({r, c});
-  if (it == agg.end() || std::abs(it->second) <= eps) { return f_t(0); }
+  if (it == agg.end()) { return f_t(0); }
   return it->second;
-    if (std::abs(v) <= std::numeric_limits<f_t>::epsilon()) { continue; }
+    if (v == f_t(0)) { continue; }
-    const f_t eps     = std::numeric_limits<f_t>::epsilon();
     const f_t v_lo_hi = qcmatrix_lookup_coeff(agg, lo, hi);
     const f_t v_hi_lo = qcmatrix_lookup_coeff(agg, hi, lo);
 
     const f_t cross = v_lo_hi + v_hi_lo;
-    if (std::abs(cross) > eps) { out.emplace_back(lo, hi, cross); }
+    if (cross != f_t(0)) { out.emplace_back(lo, hi, cross); }

If symmetry comparison still needs tolerance, use an explicit parser/solver tolerance with relative scaling rather than treating machine epsilon as a storage-level zero threshold.

As per coding guidelines, “Prevent numerical instability (overflow, underflow, precision loss) producing wrong results” and avoid hardcoded tolerances that fail on degenerate problems.

Also applies to: 216-217, 276-277, 295-300

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/io/mps_parser.cpp` around lines 172 - 174, The code uses machine
epsilon (via std::numeric_limits<f_t>::epsilon()) as an absolute zero cutoff
threshold for Q coefficients, which aggressively removes valid small non-zero
values and incorrectly changes the quadratic constraint during canonicalization.
Replace this hardcoded machine epsilon check with an explicit parser or solver
tolerance constant that employs relative scaling instead of absolute machine
epsilon as the storage-level zero threshold. Update the condition in the zero
check that compares std::abs(it->second) <= eps to use this new explicit
tolerance, and apply the same fix to all other occurrences of similar
epsilon-based comparisons in the file (around lines 216-217, 276-277, and
295-300 as noted in the comment).

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Rotated second order cone detection assumes symmetrized inputs

2 participants