Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmarks/linear_programming/run_mps_files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ else
mapfile -t mps_files < <(find "$MPS_DIR" -type f \( -name "*.mps" -o -name "*.MPS" -o -name "*.SIF" \) | sort)
else
# Gather .mps/.MPS and .SIF files in the directory
mapfile -t mps_files < <(ls "$MPS_DIR"/*.mps "$MPS_DIR"/*.MPS "$MPS_DIR"/*.SIF 2>/dev/null)
mapfile -t mps_files < <(ls "$MPS_DIR"/*.mps "$MPS_DIR"/*.MPS "$MPS_DIR"/*.SIF "$MPS_DIR"/*.mps.gz "$MPS_DIR"/*.MPS.gz "$MPS_DIR"/*.SIF.gz 2>/dev/null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Normalize names for all newly supported extensions.

Line 363 adds .MPS.gz and .SIF.gz, but benchmarks/linear_programming/cuopt/run_mip.cpp strips only lowercase .mps when it creates log names. For these new inputs, the lookup returns npos, so names retain the compressed suffix, such as model.SIF.gz.log. Normalize .gz first, then strip .mps, .MPS, or .SIF consistently before generating benchmark artifacts.

🤖 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 `@benchmarks/linear_programming/run_mps_files.sh` at line 363, Update the
input-name normalization in the benchmark artifact-generation logic, including
the relevant naming code in run_mip.cpp, to remove a trailing .gz first and then
strip .mps, .MPS, or .SIF suffixes case-consistently. Ensure all newly supported
compressed and uppercase extensions produce artifact names without the source
suffix.

fi

echo "Found ${#mps_files[@]} .mps and .SIF files in $MPS_DIR"
Expand Down
72 changes: 72 additions & 0 deletions cpp/src/dual_simplex/scaling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,78 @@ i_t scaling(const lp_problem_t<i_t, f_t>& unscaled,
q_ratio);
}

// -----------------------------------------------------------------------
// Bound-magnitude column pre-scaling.
// -----------------------------------------------------------------------
// Ruiz equilibration only balances the *coefficient* magnitudes of A and Q.
// On problems whose variable BOUNDS span many orders of magnitude (e.g. a
// network-flow QP with |bound| ranging from ~1e4 to ~1e11), the constraint
// matrix can already be perfectly balanced (all +/-1) while the variables
// themselves live at wildly different scales. The interior-point diagonal
// D = z/x then spans those same many orders of magnitude, wrecking the
// conditioning of the KKT factorization and stalling convergence.
//
// We fix this by first scaling each column so that the variable it
// represents becomes O(1): c0[j] = (geometric mean of the finite bound
// magnitudes of x_j). Substituting x_j = c0[j] * x'_j leaves the feasible
// region shape unchanged but brings every variable to a common scale, which
// Ruiz then finishes off on the coefficient side. Columns with no finite,
// nonzero bound are left at scale 1.
{
std::vector<f_t> c0(n, 1.0);
const i_t cone_start0 =
unscaled.second_order_cone_dims.empty() ? n : unscaled.cone_var_start;
f_t geo_sum = 0.0;
i_t geo_count = 0;
for (i_t j = 0; j < cone_start0; ++j) {
f_t lo = std::abs(scaled.lower[j]);
f_t hi = std::abs(scaled.upper[j]);
f_t mag;
bool lo_fin = scaled.lower[j] > -1e20 && lo > 0;
bool hi_fin = scaled.upper[j] < 1e20 && hi > 0;
if (lo_fin && hi_fin) {
mag = std::sqrt(lo * hi);
} else if (lo_fin) {
mag = lo;
} else if (hi_fin) {
mag = hi;
} else {
continue; // free / one-sided-zero: leave at scale 1
}
c0[j] = mag;
geo_sum += std::log(mag);
geo_count++;
}
if (geo_count > 0) {
// Normalize so the average column scale is 1, keeping the overall
// problem magnitude centered rather than uniformly shrinking it.
const f_t geo_mean = std::exp(geo_sum / static_cast<f_t>(geo_count));
for (i_t j = 0; j < cone_start0; ++j) {
c0[j] /= geo_mean;
}
Comment on lines +136 to +166

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Normalization loop rescales columns that have no finite bound, contradicting the "leave at scale 1" intent.

The comment at line 134 states: "Columns with no finite, nonzero bound are left at scale 1." The first loop (lines 141-159) honors that by continue-ing past free columns, leaving their c0[j] at the default 1.0. But the normalization loop at lines 164-166 divides every c0[j] in [0, cone_start0) by geo_mean, including the untouched default-1.0 entries. This turns free columns' c0[j] into 1.0 / geo_mean, which then fails the if (c0[j] == 1.0) continue; check at line 170 and causes scaled.A, scaled.objective, scaled.lower/scaled.upper, and scaled.Q entries for free variables to be scaled incorrectly.

Track which columns actually received a finite-bound magnitude and only normalize those.

🐛 Proposed fix using an explicit "has bound" mask
       std::vector<f_t> c0(n, 1.0);
       const i_t cone_start0 =
         unscaled.second_order_cone_dims.empty() ? n : unscaled.cone_var_start;
       f_t geo_sum = 0.0;
       i_t geo_count = 0;
+      std::vector<bool> has_bound(n, false);
       for (i_t j = 0; j < cone_start0; ++j) {
         f_t lo = std::abs(scaled.lower[j]);
         f_t hi = std::abs(scaled.upper[j]);
         f_t mag;
         bool lo_fin = scaled.lower[j] > -1e20 && lo > 0;
         bool hi_fin = scaled.upper[j] < 1e20 && hi > 0;
         if (lo_fin && hi_fin) {
           mag = std::sqrt(lo * hi);
         } else if (lo_fin) {
           mag = lo;
         } else if (hi_fin) {
           mag = hi;
         } else {
           continue;  // free / one-sided-zero: leave at scale 1
         }
         c0[j] = mag;
+        has_bound[j] = true;
         geo_sum += std::log(mag);
         geo_count++;
       }
       if (geo_count > 0) {
         const f_t geo_mean = std::exp(geo_sum / static_cast<f_t>(geo_count));
         for (i_t j = 0; j < cone_start0; ++j) {
-          c0[j] /= geo_mean;
+          if (has_bound[j]) c0[j] /= geo_mean;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
std::vector<f_t> c0(n, 1.0);
const i_t cone_start0 =
unscaled.second_order_cone_dims.empty() ? n : unscaled.cone_var_start;
f_t geo_sum = 0.0;
i_t geo_count = 0;
for (i_t j = 0; j < cone_start0; ++j) {
f_t lo = std::abs(scaled.lower[j]);
f_t hi = std::abs(scaled.upper[j]);
f_t mag;
bool lo_fin = scaled.lower[j] > -1e20 && lo > 0;
bool hi_fin = scaled.upper[j] < 1e20 && hi > 0;
if (lo_fin && hi_fin) {
mag = std::sqrt(lo * hi);
} else if (lo_fin) {
mag = lo;
} else if (hi_fin) {
mag = hi;
} else {
continue; // free / one-sided-zero: leave at scale 1
}
c0[j] = mag;
geo_sum += std::log(mag);
geo_count++;
}
if (geo_count > 0) {
// Normalize so the average column scale is 1, keeping the overall
// problem magnitude centered rather than uniformly shrinking it.
const f_t geo_mean = std::exp(geo_sum / static_cast<f_t>(geo_count));
for (i_t j = 0; j < cone_start0; ++j) {
c0[j] /= geo_mean;
}
std::vector<f_t> c0(n, 1.0);
const i_t cone_start0 =
unscaled.second_order_cone_dims.empty() ? n : unscaled.cone_var_start;
f_t geo_sum = 0.0;
i_t geo_count = 0;
std::vector<bool> has_bound(n, false);
for (i_t j = 0; j < cone_start0; ++j) {
f_t lo = std::abs(scaled.lower[j]);
f_t hi = std::abs(scaled.upper[j]);
f_t mag;
bool lo_fin = scaled.lower[j] > -1e20 && lo > 0;
bool hi_fin = scaled.upper[j] < 1e20 && hi > 0;
if (lo_fin && hi_fin) {
mag = std::sqrt(lo * hi);
} else if (lo_fin) {
mag = lo;
} else if (hi_fin) {
mag = hi;
} else {
continue; // free / one-sided-zero: leave at scale 1
}
c0[j] = mag;
has_bound[j] = true;
geo_sum += std::log(mag);
geo_count++;
}
if (geo_count > 0) {
// Normalize so the average column scale is 1, keeping the overall
// problem magnitude centered rather than uniformly shrinking it.
const f_t geo_mean = std::exp(geo_sum / static_cast<f_t>(geo_count));
for (i_t j = 0; j < cone_start0; ++j) {
if (has_bound[j]) c0[j] /= geo_mean;
}
🤖 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/scaling.cpp` around lines 136 - 166, Track which columns
receive a finite-bound magnitude in the scaling setup, using a per-column mask
alongside c0. In the normalization loop associated with geo_mean, divide only
masked columns by geo_mean so columns skipped by the first loop remain exactly
1.0 and continue to bypass later scaling.

// Apply x_j = c0[j] * x'_j : A(:,j) *= c0[j], obj[j] *= c0[j],
// bounds /= c0[j], Q(i,j) *= c0[i]*c0[j], accumulate into col_scale.
for (i_t j = 0; j < n; ++j) {
if (c0[j] == 1.0) continue;
for (i_t p = scaled.A.col_start[j]; p < scaled.A.col_start[j + 1]; ++p) {
scaled.A.x[p] *= c0[j];
}
scaled.objective[j] *= c0[j];
if (scaled.lower[j] > -1e20) scaled.lower[j] /= c0[j];
if (scaled.upper[j] < 1e20) scaled.upper[j] /= c0[j];
col_scale[j] *= c0[j];
}
if (scaled.Q.n > 0) {
for (i_t row = 0; row < scaled.Q.m; ++row) {
for (i_t p = scaled.Q.row_start[row]; p < scaled.Q.row_start[row + 1]; ++p) {
i_t col = scaled.Q.j[p];
scaled.Q.x[p] *= c0[row] * c0[col];
}
}
}
}
}

// Apply Ruiz equilibration
csr_matrix_t<i_t, f_t> Arow(0, 0, 0);
scaled.A.to_compressed_row(Arow);
Expand Down