Fix a bug in dual variables and reduced costs when we add implied bounds on free variables#1237
Conversation
…nds on free variables When -inf <= x_j <= inf and we add implied bounds on x_j, we will compute a dual solution (u, w) that satisfies A^T u + w = c + Qx but w_j could be nonzero. When x_j is free, we need to enforce that the corresponding reduced cost is zero. We do this by transforming (u, w) into (y, z) such that A^T y + z = c + Qx with z_j = 0 for all j such that -inf < x_j < inf. We also add a unit test on a small QP to confirm the fix.
|
/ok to test 23789ff |
|
/ok to test 06eb7d2 |
📝 WalkthroughWalkthroughThis PR tracks which constraints imply finite bounds on formerly-free variables during presolve and applies post-solve dual corrections during uncrush using the original constraint matrix to update duals and reduced costs. ChangesBarrier dual correction for bounded free variables
🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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
🤖 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/dual_simplex/presolve.cpp`:
- Around line 1758-1759: The dual correction unconditionally computes du = w_j /
bfv.coefficient which can produce Inf/NaN when bfv.coefficient is zero or
near-zero; in the block around du, guard the division by checking
fabs(bfv.coefficient) against a small epsilon (e.g. machine or
domain-appropriate epsilon) and only compute and apply input_y[bfv.constraint]
+= du when the coefficient is safe; if the coefficient is too small, skip the
update or handle it deterministically (clamp, set du = 0, or record/log the
event) to avoid division-by-zero and maintain numerical stability in the dual
correction path.
🪄 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: f10782e8-e45e-4214-85b0-0c7d011d8f83
📒 Files selected for processing (5)
cpp/src/dual_simplex/presolve.cppcpp/src/dual_simplex/presolve.hppcpp/src/dual_simplex/solve.cppcpp/tests/dual_simplex/unit_tests/solve.cppcpp/tests/dual_simplex/unit_tests/solve_barrier.cu
|
/ok to test e8635f2 |
There was a problem hiding this comment.
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/dual_simplex/presolve.cpp`:
- Around line 991-1007: The code records bounded_free_variables in
current_free_variables order which breaks the postsolve "uncrush" replay because
later implied bounds can depend on earlier ones; change the recording so the
discovery/dependency order is preserved and ensure the replay applies fixes in
reverse of that discovery order (or mark and later reverse
presolve_info.bounded_free_variables before use). Specifically: when populating
presolve_info.bounded_free_variables in the loop over current_free_variables,
append entries in the actual implied-bound discovery order and then update the
uncrush/replay logic (the code that uses presolve_info.bounded_free_variables to
subtract original rows from input_z) to iterate the vector in reverse (or
reverse the vector once after filling) so previously corrected free variables
remain at zero.
🪄 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: a4ecc2e0-7143-4aee-ab2c-e9a632961f4e
⛔ Files ignored due to path filters (1)
datasets/quadratic_programming/min_x_squared.mpsis excluded by!**/*.mps
📒 Files selected for processing (3)
cpp/src/dual_simplex/presolve.cppcpp/src/dual_simplex/presolve.hppcpp/src/dual_simplex/solve.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
- cpp/src/dual_simplex/solve.cpp
- cpp/src/dual_simplex/presolve.hpp
|
/ok to test e34a62a |
|
/ok to test 594b890 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
cpp/src/dual_simplex/presolve.cpp (1)
1784-1793: ⚖️ Poor tradeoffConsider using row-wise access for the dual correction loop.
The current implementation iterates over all columns and performs a linear search within each column to find the constraint row. For large problems with many bounded free variables, this O(n × avg_col_nnz) cost per variable could be noticeable. Using a CSR representation or pre-building a row-index lookup would reduce this to O(row_nnz) per variable.
This is not blocking since the number of bounded free variables is typically small, and the fix is algorithmically correct.
🤖 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/dual_simplex/presolve.cpp` around lines 1784 - 1793, The dual correction loop over columns is doing repeated column scans to find entries for bfv.constraint (see the loop using i_t j, A.col_start/col_end, A.i and A.x) and updates input_z by subtracting A.x[p]*du; replace this by using row-wise access: either iterate over a CSR-style row view for bfv.constraint (row_start/row_end and arrays of column indices/values) or prebuild a lookup from row -> list of (col, value) before the bounded-free variable loop, then for each bfv use that row's nonzeros to apply input_z[col] -= value * du; this removes the per-column linear search and keeps the same update to input_z and use of du.
🤖 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.
Nitpick comments:
In `@cpp/src/dual_simplex/presolve.cpp`:
- Around line 1784-1793: The dual correction loop over columns is doing repeated
column scans to find entries for bfv.constraint (see the loop using i_t j,
A.col_start/col_end, A.i and A.x) and updates input_z by subtracting A.x[p]*du;
replace this by using row-wise access: either iterate over a CSR-style row view
for bfv.constraint (row_start/row_end and arrays of column indices/values) or
prebuild a lookup from row -> list of (col, value) before the bounded-free
variable loop, then for each bfv use that row's nonzeros to apply input_z[col]
-= value * du; this removes the per-column linear search and keeps the same
update to input_z and use of du.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: f60b25cc-d8a7-40ce-8060-af2f813da2e7
📒 Files selected for processing (1)
cpp/src/dual_simplex/presolve.cpp
|
/merge |
|
/ok to test b21b1bc |
|
This PR's base branch has been changed since the last |
|
/merge |
When -inf <= x_j <= inf and we add implied bounds on x_j, we will compute a dual solution (u, w) that satisfies
A^T u + w = c + Qx
but w_j could be nonzero. When x_j is free, we need to enforce that the corresponding reduced cost is zero. We do this by transforming (u, w) into (y, z) such that A^T y + z = c + Qx with z_j = 0 for all j such that -inf < x_j < inf.
We also add a unit test on a small QP to confirm the fix.
Fixes #1119