Skip to content

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

Open
yuwenchen95 wants to merge 4 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 4 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d6cba158-eaba-40f3-803b-1efd4e8b0d42

📥 Commits

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

📒 Files selected for processing (2)
  • cpp/include/cuopt/linear_programming/io/mps_data_model.hpp
  • cpp/src/io/mps_data_model.cpp

📝 Walkthrough

Walkthrough

Introduces a canonical COO representation for quadratic constraints where off-diagonal cross terms store the full coefficient rather than symmetric halves. Adds canonicalize_coo_matrix as a shared normalization function wired into all ingestion paths (LP parser, MPS parser, gRPC decoder, add_quadratic_constraint). Fixes the rotated-SOC fast-path detector to recognize a single canonical cross-term entry instead of requiring a symmetric pair, resolving a bug where natural single-term formulations are not properly detected.

Changes

Canonical QC COO representation and rotated-SOC bug fix

Layer / File(s) Summary
New canonicalize_coo_matrix interface and implementation
cpp/src/io/quadratic_constraint_coo.hpp, cpp/src/io/quadratic_constraint_coo.cpp, cpp/src/io/CMakeLists.txt
Defines canonicalize_coo_matrix template function. Implementation validates vector lengths, aggregates duplicate (row,col) entries by summing coefficients, merges symmetric off-diagonal pairs into single canonical entries (optionally enforcing pair presence and value matching within epsilon), filters near-zero contributions, and sorts output by (row,col). New file registered in the parser CMake source list.
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
append_quadratic_constraint gains optional require_symmetric_q_offdiagonal parameter to enforce MPS symmetry convention. canonicalize_quadratic_constraints helper declared for batch processing. Interface documentation specifies pre-canonicalized input for set_quadratic_constraints and guarantees on-ingest canonicalization for add_quadratic_constraint.
LP and MPS parser ingestion
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 internal build_symmetric_q_coo helper and passes raw upper-triangular triples directly to append_quadratic_constraint, deferring canonicalization. MPS parser passes require_symmetric_q_offdiagonal=true for QCMATRIX blocks. mps_data_model.cpp delegates canonicalization logic to canonicalize_coo_matrix and adds canonicalize_quadratic_constraints with explicit instantiations for int/float and int/double.
Problem-level add_quadratic_constraint canonicalization
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
All add_quadratic_constraint call sites canonicalize COO triplets before storing. populate_from_data_model_view calls canonicalize_quadratic_constraints before forwarding. gRPC code generator emits canonicalization in both unary and chunked decode paths for quadratic_constraints repeated messages.
MPS writer: re-expand canonical cross terms to symmetric halves
cpp/src/io/mps_writer.cpp
Before writing QCMATRIX, each constraint is canonicalized in a local copy. Diagonal entries are written once with value v; off-diagonal entries are split and emitted as symmetric v/2 halves for both (i,j) and (j,i) orientations, matching MPS format requirements.
Rotated-SOC fast-path: single cross-term detection
cpp/src/barrier/translate_soc.hpp
Replaces symmetric off-diagonal pair predicate with rotated_soc_cross_eligible lambda requiring exactly one off-diagonal entry. Fast-path validation derives cross_d = -v0/2, enforces positive cross_d and distinct head indices, and adjusts tail diagonal count expectation from q_nnz-2 to q_nnz-1 for the canonical single-entry representation.
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
New unit tests for canonicalize_coo_matrix cover duplicate merging, symmetric-half collapsing when enforced, asymmetric pair summation when not enforced, and rejection when required symmetry is absent. Existing LP/MPS cross-term tests and SOCP rotated-SOC tests updated to expect canonical single-entry representation. Two new C API end-to-end rotated-SOC constraint tests verify optimal solutions.
Documentation and examples
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
Rotated-SOC documentation and all example files updated to describe the single canonical cross-term input convention (e.g., Q[x0,x1] = -2 for the coefficient of 2*x0*x1). Documentation clarifies that MPS QCMATRIX requires symmetric off-diagonal halves which are validated then merged into the canonical stored form. Code examples refactored to use single cross-term entries instead of symmetric-pair splits.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.36% 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 'fix(socp): canonicalize QC Q COO for rotated SOC detection' accurately summarizes the main change: implementing canonical COO storage for quadratic constraints to fix RSOC detection.
Description check ✅ Passed The PR description clearly explains the canonicalization of quadratic-constraint Q matrices, the fix for RSOC detection, and the ingest boundaries where canonicalization is applied.
Linked Issues check ✅ Passed The PR addresses issue #1435 by implementing canonical COO storage at all ingest boundaries, allowing RSOC detection to accept single eligible cross terms instead of requiring symmetric halves, fixing the incorrect optima bug.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing canonical COO storage and fixing RSOC detection; documentation updates and tests are appropriately scoped to support this objective.

✏️ 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 on lines +22 to +31
template <typename i_t, typename f_t>
f_t lookup_coeff(const std::unordered_map<std::pair<i_t, i_t>, f_t, pair_hash<i_t>>& agg,
i_t r,
i_t c)
{
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); }
return it->second;
}

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check test coverage for canonicalize_coo_matrix with different coefficient scales
rg -n -A5 -B5 'canonicalize_coo_matrix|canonicalize_quadratic_constraints' cpp/tests/linear_programming/parser_test.cpp

# Check if solver tolerances are defined that could inform this threshold
rg -n 'tolerance|epsilon.*threshold|zero.*tolerance' cpp/include/cuopt/linear_programming/ cpp/src/pdlp/

Repository: NVIDIA/cuopt

Length of output: 17676


🏁 Script executed:

# Find where lookup_coeff is used
rg -n 'lookup_coeff' cpp/src/io/ cpp/include/

# Find the complete quadratic_constraint_coo.cpp file to see full context
head -100 cpp/src/io/quadratic_constraint_coo.cpp

# Search for quadratic constraint parsing and coefficient handling
rg -n -B3 -A3 'quadratic.*constraint|aggregat.*coeff' cpp/src/io/

Repository: NVIDIA/cuopt

Length of output: 26068


🏁 Script executed:

# Check if there are any issue discussions or test cases related to coefficient scaling
rg -n 'coefficient.*scale|scale.*coefficient|magnitude.*coeff' cpp/tests/ cpp/src/

# Look for any problem scaling or normalization code
rg -n 'scale.*problem|normalize.*coeff|coefficient.*range' cpp/src/ cpp/include/

Repository: NVIDIA/cuopt

Length of output: 13073


🏁 Script executed:

# Check if there's a standard tolerance used elsewhere in I/O for matrix comparisons
rg -n 'csr_matrix_utils|compare.*tol|matrix.*compar' cpp/include/cuopt/linear_programming/io/ cpp/src/io/ -A2 -B2

# Look for TODO or FIXME comments related to epsilon, tolerance, or zero handling in I/O
rg -n 'TODO|FIXME|XXX' cpp/src/io/quadratic_constraint_coo.cpp cpp/include/cuopt/linear_programming/io/quadratic_constraint_coo.hpp

# Check parser documentation or comments about input requirements
rg -n 'scale|normalize|coefficient.*range|input.*range' cpp/src/io/mps_parser.cpp cpp/src/io/lp_parser.cpp -B2 -A2 | head -80

Repository: NVIDIA/cuopt

Length of output: 4368


🏁 Script executed:

# Search for how other I/O code handles near-zero values
rg -n 'abs.*<=|numeric_limits.*epsilon' cpp/src/io/ -B1 -A1

Repository: NVIDIA/cuopt

Length of output: 1505


🏁 Script executed:

# Check all callers of canonicalize_coo_matrix
rg -n 'canonicalize_coo_matrix' cpp/src/io/ -B5 -A2

# Check mps_data_model append_quadratic_constraint to see if any pre-processing occurs
sed -n '145,210p' cpp/src/io/mps_data_model.cpp

Repository: NVIDIA/cuopt

Length of output: 7564


🏁 Script executed:

# Check if the LP/MPS parsers read coefficients and any normalization happens before canonicalize
rg -n 'parse.*coefficient|read.*value|value.*scale' cpp/src/io/mps_parser.cpp cpp/src/io/lp_parser.cpp | head -50

Repository: NVIDIA/cuopt

Length of output: 1496


🏁 Script executed:

# Check if there's any documentation about input coefficient handling in comments
rg -n 'coefficient.*range|scale.*input|input.*normali|magnitude' cpp/src/io/ cpp/include/cuopt/linear_programming/io/ -B2 -A2 | head -60

# Check the header for canonicalize_coo_matrix to see if there's any contract/documentation
cat cpp/src/io/quadratic_constraint_coo.hpp

Repository: NVIDIA/cuopt

Length of output: 1404


Replace bare machine epsilon with appropriate I/O tolerance for coefficient thresholding.

The lookup_coeff function uses std::numeric_limits<f_t>::epsilon() (~2.2e-16 for double) as an absolute threshold to determine zero coefficients. This is numerically inappropriate for general coefficient magnitudes; epsilon-based absolute comparisons only make sense for values near 1.0. Coefficients from MPS/LP input can range widely and will be incorrectly filtered or retained.

The codebase already uses 1e-9 as the default tolerance in csr_matrix_utils.hpp::compare_equal() for matrix comparisons. For consistency and robustness, replace the epsilon threshold with a fixed tolerance (e.g., 1e-9) or scale epsilon by the maximum aggregated coefficient magnitude.

🤖 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 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.

Signed-off-by: yuwenchen95 <yuwchen@nvidia.com>
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