From 5a3821f4af11898ce297a7e10911ad9cee8f6ad2 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 31 Aug 2026 09:39:55 +0200 Subject: [PATCH 01/13] Add utilities --- highs/mip/HighsCliqueTable.cpp | 7 +------ highs/mip/HighsMipSolverData.cpp | 10 ++-------- highs/mip/HighsPseudocost.cpp | 6 ++---- highs/presolve/HPresolve.cpp | 5 +---- highs/presolve/HighsPostsolveStack.h | 28 ++++++++++++++++++++++++++++ 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index 1f8c4fe68de..a054d1867b9 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -1290,15 +1290,10 @@ void HighsCliqueTable::extractCliques(HighsMipSolver& mipsolver, HighsDomain& globaldom = mipsolver.mipdata_->getDomain(); - for (HighsInt i = 0; i != mipsolver.numRow(); ++i) { + for (HighsInt i : mipsolver.mipdata_->postSolveStack.getNonCutRows()) { HighsInt start = mipsolver.mipdata_->ARstart_[i]; HighsInt end = mipsolver.mipdata_->ARstart_[i + 1]; - if (mipsolver.mipdata_->postSolveStack.isCutRow(i)) { - if (!mipsolver.mipdata_->postSolveStack.hasAppendedRows()) break; - continue; - } - // catch set packing and partitioning constraints that already have the form // of a clique without transformations and add those cliques with the rows // being recorded diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index bc42fc8d0a1..ecf9cac086b 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1493,19 +1493,13 @@ void HighsMipSolverData::basisTransfer() { firstrootbasis.alien = true; firstrootbasis.useful = true; - for (HighsInt i = 0; - i < static_cast(postSolveStack.getOrigRowIndex().size()); - ++i) { - if (!postSolveStack.isOrigRow(i)) break; + for (HighsInt i : postSolveStack.getOrigRows()) { HighsBasisStatus status = mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; firstrootbasis.row_status[i] = status; } - for (HighsInt i = 0; - i < static_cast(postSolveStack.getOrigColIndex().size()); - ++i) { - if (!postSolveStack.isOrigCol(i)) break; + for (HighsInt i : postSolveStack.getOrigCols()) { HighsBasisStatus status = mipsolver.rootbasis->col_status[postSolveStack.getOrigColIndex(i)]; firstrootbasis.col_status[i] = status; diff --git a/highs/mip/HighsPseudocost.cpp b/highs/mip/HighsPseudocost.cpp index 6c670c19e79..3efe48913bc 100644 --- a/highs/mip/HighsPseudocost.cpp +++ b/highs/mip/HighsPseudocost.cpp @@ -46,8 +46,7 @@ HighsPseudocost::HighsPseudocost(const HighsMipSolver& mipsolver) conflict_avg_score = mipsolver.pscostinit->conflict_avg_score * mipsolver.numCol(); - for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { - if (!mipsolver.mipdata_->postSolveStack.isOrigCol(i)) continue; + for (HighsInt i : mipsolver.mipdata_->postSolveStack.getOrigCols()) { HighsInt origCol = mipsolver.mipdata_->postSolveStack.getOrigColIndex(i); pseudocostup[i] = mipsolver.pscostinit->pseudocostup[origCol]; @@ -115,8 +114,7 @@ HighsPseudocostInitialization::HighsPseudocostInitialization( HighsInt ncols = pscost.pseudocostup.size(); conflict_avg_score /= ncols * pscost.conflict_weight; - for (HighsInt i = 0; i != ncols; ++i) { - if (!postsolveStack.isOrigCol(i)) continue; + for (HighsInt i : postsolveStack.getOrigCols()) { pseudocostup[postsolveStack.getOrigColIndex(i)] = pscost.pseudocostup[i]; pseudocostdown[postsolveStack.getOrigColIndex(i)] = pscost.pseudocostdown[i]; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 93bb0a31495..523db872875 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7264,10 +7264,7 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { cutinds.reserve(model->num_col_); cutvals.reserve(model->num_col_); HighsInt numcuts = 0; - for (HighsInt i = model->num_row_ - 1; i >= 0; --i) { - if (postsolve_stack.isOrigRow(i)) break; - if (!postsolve_stack.isCutRow(i)) continue; - + for (HighsInt i : postsolve_stack.getCutRows()) { // row is a cut, remove it from matrix but add to cutpool ++numcuts; storeRow(i); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 45311acdd69..6a70dbacfc6 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -359,6 +359,13 @@ class HighsPostsolveStack { bool isOrigCol(HighsInt col) const { return origColIndex[col] < origNumCol; } + std::vector getOrigCols() const { + std::vector cols; + for (HighsInt i = 0; i < static_cast(origColIndex.size()); ++i) + if (isOrigCol(i)) cols.push_back(i); + return cols; + } + bool isOrigRow(HighsInt row) const { return origRowType[row] == OrigRowType::kOriginal; } @@ -373,6 +380,27 @@ class HighsPostsolveStack { bool hasAppendedRows() const { return numAppendedRows > 0; } + std::vector getOrigRows() const { + std::vector rows; + for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) + if (origRowType[i] == OrigRowType::kOriginal) rows.push_back(i); + return rows; + } + + std::vector getCutRows() const { + std::vector rows; + for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) + if (origRowType[i] == OrigRowType::kCut) rows.push_back(i); + return rows; + } + + std::vector getNonCutRows() const { + std::vector rows; + for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) + if (origRowType[i] != OrigRowType::kCut) rows.push_back(i); + return rows; + } + void appendToModel(HighsInt& numRows, HighsInt numRowsToAppend, OrigRowType rowType) { if (numRowsToAppend <= 0) return; From fe3330e40e5509fcf405424905f185c80d4e9cdd Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 31 Aug 2026 09:48:27 +0200 Subject: [PATCH 02/13] Add comments --- highs/presolve/HighsPostsolveStack.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 6a70dbacfc6..ea9a3376549 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -359,6 +359,7 @@ class HighsPostsolveStack { bool isOrigCol(HighsInt col) const { return origColIndex[col] < origNumCol; } + // Returns presolved-space indices of columns from the original model std::vector getOrigCols() const { std::vector cols; for (HighsInt i = 0; i < static_cast(origColIndex.size()); ++i) @@ -380,6 +381,7 @@ class HighsPostsolveStack { bool hasAppendedRows() const { return numAppendedRows > 0; } + // Returns presolved-space indices of rows from the original model std::vector getOrigRows() const { std::vector rows; for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) @@ -387,6 +389,7 @@ class HighsPostsolveStack { return rows; } + // Returns presolved-space indices of rows that are cuts std::vector getCutRows() const { std::vector rows; for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) @@ -394,6 +397,8 @@ class HighsPostsolveStack { return rows; } + // Returns presolved-space indices of rows that are not cuts (original + + // appended) std::vector getNonCutRows() const { std::vector rows; for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) From 387372bd23e4d15e7aed429d59535e797c3c8a47 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 31 Aug 2026 10:35:29 +0200 Subject: [PATCH 03/13] Fix untransformed violations on bell3a (win64) with FM presolve --- highs/presolve/HighsPostsolveStack.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index ea9a3376549..6d943839f81 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -735,6 +735,13 @@ class HighsPostsolveStack { void duplicateRow(HighsInt row, bool rowUpperTightened, bool rowLowerTightened, HighsInt duplicateRow, double duplicateRowScale) { + // When a non-cut row is deleted as a duplicate of a cut, the cut + // becomes the sole enforcer of the constraint. Reclassify it so + // it is kept in the model and not moved to the cut pool, since + // otherwise the constraint is lost and postsolve may produce an + // infeasible solution. + if (isCutRow(row) && !isCutRow(duplicateRow)) + origRowType[row] = OrigRowType::kAppended; reductionValues.push( DuplicateRow{duplicateRowScale, origRowIndex[duplicateRow], origRowIndex[row], rowLowerTightened, rowUpperTightened}); From 5829442cbbe89947d369c4966de9e04a85e87267 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 31 Aug 2026 10:40:02 +0200 Subject: [PATCH 04/13] Clean up --- highs/presolve/HighsPostsolveStack.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 6d943839f81..00f4f53f1aa 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -385,7 +385,7 @@ class HighsPostsolveStack { std::vector getOrigRows() const { std::vector rows; for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) - if (origRowType[i] == OrigRowType::kOriginal) rows.push_back(i); + if (isOrigRow(i)) rows.push_back(i); return rows; } @@ -393,7 +393,7 @@ class HighsPostsolveStack { std::vector getCutRows() const { std::vector rows; for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) - if (origRowType[i] == OrigRowType::kCut) rows.push_back(i); + if (isCutRow(i)) rows.push_back(i); return rows; } @@ -402,7 +402,7 @@ class HighsPostsolveStack { std::vector getNonCutRows() const { std::vector rows; for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) - if (origRowType[i] != OrigRowType::kCut) rows.push_back(i); + if (!isCutRow(i)) rows.push_back(i); return rows; } From b333aa867a111d5fff3e47b851fa2650806e6a23 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 31 Aug 2026 10:45:35 +0200 Subject: [PATCH 05/13] Remove unused method --- highs/presolve/HighsPostsolveStack.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 00f4f53f1aa..3d59c44a8a8 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -379,8 +379,6 @@ class HighsPostsolveStack { return origRowType[row] == OrigRowType::kCut; } - bool hasAppendedRows() const { return numAppendedRows > 0; } - // Returns presolved-space indices of rows from the original model std::vector getOrigRows() const { std::vector rows; From f615089113a33d31f0b32a0f5fd3fd80c4e33635 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 31 Aug 2026 13:57:03 +0200 Subject: [PATCH 06/13] Clean up presolveOnOff code --- check/TestPresolveRules.cpp | 125 ++++++++++++++------------- highs/presolve/HPresolve.cpp | 123 +++++++++++++------------- highs/presolve/HPresolve.h | 2 + highs/presolve/HighsPostsolveStack.h | 4 +- 4 files changed, 134 insertions(+), 120 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 6a1717b72fe..fe7ed8bd80f 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -6,10 +6,17 @@ const bool dev_run = false; +void solveAndCheck(const std::string& message, const HighsLp& lp, Highs& h, + const std::string& solver, bool use_presolve, + const HighsInt require_presolved_model_num_col = -1, + const HighsInt require_presolved_model_num_row = -1, + const HighsInt require_presolved_model_num_nz = -1); + void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, - const HighsInt require_presolved_model_num_col = 0, - const HighsInt require_presolved_model_num_row = 0, - const HighsInt require_presolved_model_num_nz = 0); + const std::vector& solvers, + const HighsInt require_presolved_model_num_col = -1, + const HighsInt require_presolved_model_num_row = -1, + const HighsInt require_presolved_model_num_nz = -1); TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { HighsLp lp; @@ -59,89 +66,89 @@ TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { lp.a_matrix_.start_ = {0, lp.num_col_}; lp.a_matrix_.index_.resize(lp.num_col_); std::iota(lp.a_matrix_.index_.begin(), lp.a_matrix_.index_.end(), 0); + const std::vector solvers = {kSimplexString, kIpmString, + kHiPdlpString}; if (lp1) { lp.col_cost_.assign(lp.num_col_, 1); lp.a_matrix_.value_.assign(lp.num_col_, 1); - presolveOffOn("Capturing neos-787933 issue", lp, h); + presolveOffOn("Capturing neos-787933 issue", lp, h, solvers); } if (lp1a) { lp.col_cost_ = {2, 1}; lp.a_matrix_.value_.assign(lp.num_col_, 1); - presolveOffOn("Variant A neos-787933 issue", lp, h); + presolveOffOn("Variant A neos-787933 issue", lp, h, solvers); } if (lp1b) { lp.col_cost_ = {-2, -1}; lp.a_matrix_.value_.assign(lp.num_col_, 1); - presolveOffOn("Variant B neos-787933 issue", lp, h); + presolveOffOn("Variant B neos-787933 issue", lp, h, solvers); } lp.clear(); h.resetGlobalScheduler(true); } -void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, +void solveAndCheck(const std::string& message, const HighsLp& lp, Highs& h, + const std::string& solver, bool use_presolve, const HighsInt require_presolved_model_num_col, const HighsInt require_presolved_model_num_row, const HighsInt require_presolved_model_num_nz) { const HighsRunData& run_data = h.getRunData(); - bool presolve_on = false; - // If the model reduces to empty, then the output from different - // solvers cannot be tested - const bool reduce_to_empty = require_presolved_model_num_col == 0 && - require_presolved_model_num_row == 0; - const HighsInt to_k = reduce_to_empty ? 2 : 5; - for (int k = 0; k < to_k; k++) { - std::string solver = kSimplexString; - std::string run_crossover = kHighsOnString; - bool basis_postsolve = true; - if (k == 0) { - // Presolve off - to get the optimal solution to debug - // presolve - presolve_on = false; - } else { - presolve_on = true; - if (k == 1) { - solver = kSimplexString; - } else if (k == 2) { - solver = kIpmString; - } else if (k == 3) { - solver = kIpmString; - run_crossover = kHighsOffString; - basis_postsolve = false; - } else { - solver = kHiPdlpString; - basis_postsolve = false; - } - } - std::string presolve = presolve_on ? kHighsOnString : kHighsOffString; - h.setOptionValue(kPresolveString, presolve); - h.setOptionValue(kRunCrossoverString, run_crossover); - h.setOptionValue(kSolverString, solver); - if (dev_run) - printf( - "\n============\n%s: presolve = %s; solver = %s%s\n============\n\n", - message.c_str(), presolve.c_str(), solver.c_str(), - solver == kIpmString ? ("; run_crossover = " + run_crossover).c_str() - : ""); - REQUIRE(h.passModel(lp) == HighsStatus::kOk); - h.run(); - if (dev_run) h.writeSolution("", 1); - if (presolve_on) { - // Ensure that the model is reduced to empty + std::string run_crossover = kHighsOnString; + bool basis_postsolve = true; + if (solver == kIpmString) { + run_crossover = kHighsOffString; + basis_postsolve = false; + } else if (solver == kHiPdlpString) { + basis_postsolve = false; + } + std::string presolve = use_presolve ? kHighsOnString : kHighsOffString; + h.setOptionValue(kPresolveString, presolve); + h.setOptionValue(kRunCrossoverString, run_crossover); + h.setOptionValue(kSolverString, solver); + if (dev_run) + printf("\n============\n%s: presolve = %s; solver = %s%s\n============\n\n", + message.c_str(), presolve.c_str(), solver.c_str(), + solver == kIpmString ? ("; run_crossover = " + run_crossover).c_str() + : ""); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + h.run(); + if (dev_run) h.writeSolution("", 1); + if (use_presolve) { + // Ensure that the model is reduced as expected + if (require_presolved_model_num_col >= 0) REQUIRE(run_data.presolved_model_num_col == require_presolved_model_num_col); + if (require_presolved_model_num_row >= 0) REQUIRE(run_data.presolved_model_num_row == require_presolved_model_num_row); + if (require_presolved_model_num_nz >= 0) REQUIRE(run_data.presolved_model_num_nz == require_presolved_model_num_nz); - // Ensure that dual postsolve is correct - REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); - REQUIRE(h.getInfo().num_primal_infeasibilities == 0); - REQUIRE(h.getInfo().num_dual_infeasibilities == 0); + // Ensure that dual postsolve is correct + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(h.getInfo().num_primal_infeasibilities == 0); + REQUIRE(h.getInfo().num_dual_infeasibilities == 0); + if (require_presolved_model_num_col == 0 && + require_presolved_model_num_row == 0) REQUIRE(h.getInfo().simplex_iteration_count == 0); - // Ensure that any basis postsolve is correct - if (basis_postsolve) - REQUIRE(run_data.num_simplex_iterations_after_postsolve == 0); - } + // Ensure that any basis postsolve is correct + if (basis_postsolve) + REQUIRE(run_data.num_simplex_iterations_after_postsolve == 0); + } +} + +void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, + const std::vector& solvers, + const HighsInt require_presolved_model_num_col, + const HighsInt require_presolved_model_num_row, + const HighsInt require_presolved_model_num_nz) { + // Presolve off - to get the optimal solution to debug presolve + solveAndCheck(message, lp, h, kSimplexString, false); + // Presolve on with each solver + for (const std::string& solver : solvers) { + solveAndCheck(message, lp, h, solver, true, require_presolved_model_num_col, + require_presolved_model_num_row, + require_presolved_model_num_nz); } } diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 523db872875..60463c22bc7 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7195,6 +7195,68 @@ bool HPresolve::silentLog() const { return mipsolver && mipsolver->mipdata_->numRestarts > 0; } +void HPresolve::moveCutsToPool(HighsPostsolveStack& postsolve_stack) { + if (mipsolver == nullptr) return; + std::vector cutinds; + std::vector cutvals; + cutinds.reserve(model->num_col_); + cutvals.reserve(model->num_col_); + HighsInt numcuts = 0; + for (HighsInt i : postsolve_stack.getCutRows()) { + ++numcuts; + storeRow(i); + cutinds.clear(); + cutvals.clear(); + for (HighsInt j : rowpositions) { + cutinds.push_back(Acol[j]); + cutvals.push_back(Avalue[j]); + } + + if (mipsolver != nullptr) { + mipsolver->mipdata_->getCutPool().addCut( + *mipsolver, cutinds.data(), cutvals.data(), cutinds.size(), + model->row_upper_[i], + rowsizeInteger[i] + rowsizeImplInt[i] == rowsize[i] && + rowCoefficientsIntegral(i, 1.0), + true, false, false); + } + + markRowDeleted(i); + for (HighsInt j : rowpositions) unlink(j); + } + + if (numcuts == 0) return; + + // Compact deleted cut rows. shrinkProblem must not be used here + // because it replaces the cutpool with a new empty one, destroying + // the cuts that were just added above. + HighsInt oldNumRow = model->num_row_; + std::vector newRowIndex(oldNumRow); + HighsInt newNumRow = 0; + for (HighsInt i = 0; i < oldNumRow; ++i) { + if (rowDeleted[i]) + newRowIndex[i] = -1; + else + newRowIndex[i] = newNumRow++; + } + model->num_row_ = newNumRow; + + for (HighsInt i = 0; i < oldNumRow; ++i) { + if (newRowIndex[i] == -1 || newRowIndex[i] == i) continue; + model->row_lower_[newRowIndex[i]] = model->row_lower_[i]; + model->row_upper_[newRowIndex[i]] = model->row_upper_[i]; + } + model->row_lower_.resize(model->num_row_); + model->row_upper_.resize(model->num_row_); + model->row_names_.resize(model->num_row_); + + for (size_t i = 0; i < Avalue.size(); ++i) { + if (Avalue[i] == 0) continue; + assert(newRowIndex[Arow[i]] != -1); + Arow[i] = newRowIndex[Arow[i]]; + } +} + HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { presolve_status_ = HighsPresolveStatus::kNotSet; shrinkProblemEnabled = true; @@ -7258,66 +7320,7 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { mipsolver->mipdata_->getDomain().addConflictPool( mipsolver->mipdata_->getConflictPool()); - if (mipsolver->mipdata_->numRestarts != 0) { - std::vector cutinds; - std::vector cutvals; - cutinds.reserve(model->num_col_); - cutvals.reserve(model->num_col_); - HighsInt numcuts = 0; - for (HighsInt i : postsolve_stack.getCutRows()) { - // row is a cut, remove it from matrix but add to cutpool - ++numcuts; - storeRow(i); - cutinds.clear(); - cutvals.clear(); - for (HighsInt j : rowpositions) { - cutinds.push_back(Acol[j]); - cutvals.push_back(Avalue[j]); - } - - mipsolver->mipdata_->getCutPool().addCut( - *mipsolver, cutinds.data(), cutvals.data(), cutinds.size(), - model->row_upper_[i], - rowsizeInteger[i] + rowsizeImplInt[i] == rowsize[i] && - rowCoefficientsIntegral(i, 1.0), - true, false, false); - - markRowDeleted(i); - for (HighsInt j : rowpositions) unlink(j); - } - - // Compact deleted cut rows. shrinkProblem must not be used here - // because it replaces the cutpool with a new empty one, destroying - // the cuts that were just added above. - auto compactDeletedRows = [&]() { - HighsInt oldNumRow = model->num_row_; - std::vector newRowIndex(oldNumRow); - HighsInt newNumRow = 0; - for (HighsInt i = 0; i < oldNumRow; ++i) { - if (rowDeleted[i]) - newRowIndex[i] = -1; - else - newRowIndex[i] = newNumRow++; - } - model->num_row_ = newNumRow; - - for (HighsInt i = 0; i < oldNumRow; ++i) { - if (newRowIndex[i] == -1 || newRowIndex[i] == i) continue; - model->row_lower_[newRowIndex[i]] = model->row_lower_[i]; - model->row_upper_[newRowIndex[i]] = model->row_upper_[i]; - } - model->row_lower_.resize(model->num_row_); - model->row_upper_.resize(model->num_row_); - model->row_names_.resize(model->num_row_); - - for (size_t i = 0; i < Avalue.size(); ++i) { - if (Avalue[i] == 0) continue; - assert(newRowIndex[Arow[i]] != -1); - Arow[i] = newRowIndex[Arow[i]]; - } - }; - compactDeletedRows(); - } + if (mipsolver->mipdata_->numRestarts != 0) moveCutsToPool(postsolve_stack); } // Possibly populate the model matrix from the presolve matrix data diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 5fbd3e03f86..44d8e75f7f9 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -343,6 +343,8 @@ class HPresolve { void changeImplRowDualLower(HighsInt row, double newLower, HighsInt originCol); + void moveCutsToPool(HighsPostsolveStack& postsolve_stack); + Result scaleMIP(HighsPostsolveStack& postsolve_stack); Result applyConflictGraphSubstitutions(HighsPostsolveStack& postsolve_stack, diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 3d59c44a8a8..99608bfde03 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -379,6 +379,8 @@ class HighsPostsolveStack { return origRowType[row] == OrigRowType::kCut; } + void setRowType(HighsInt row, OrigRowType type) { origRowType[row] = type; } + // Returns presolved-space indices of rows from the original model std::vector getOrigRows() const { std::vector rows; @@ -739,7 +741,7 @@ class HighsPostsolveStack { // otherwise the constraint is lost and postsolve may produce an // infeasible solution. if (isCutRow(row) && !isCutRow(duplicateRow)) - origRowType[row] = OrigRowType::kAppended; + setRowType(row, OrigRowType::kAppended); reductionValues.push( DuplicateRow{duplicateRowScale, origRowIndex[duplicateRow], origRowIndex[row], rowLowerTightened, rowUpperTightened}); From 4ad17c9e345823db51ecad25b9f2a486c5484a09 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 31 Aug 2026 14:08:14 +0200 Subject: [PATCH 07/13] Move code around in test utility --- check/TestPresolveRules.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index fe7ed8bd80f..ef0c3694f03 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -114,6 +114,9 @@ void solveAndCheck(const std::string& message, const HighsLp& lp, Highs& h, REQUIRE(h.passModel(lp) == HighsStatus::kOk); h.run(); if (dev_run) h.writeSolution("", 1); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(h.getInfo().num_primal_infeasibilities == 0); + REQUIRE(h.getInfo().num_dual_infeasibilities == 0); if (use_presolve) { // Ensure that the model is reduced as expected if (require_presolved_model_num_col >= 0) @@ -125,10 +128,6 @@ void solveAndCheck(const std::string& message, const HighsLp& lp, Highs& h, if (require_presolved_model_num_nz >= 0) REQUIRE(run_data.presolved_model_num_nz == require_presolved_model_num_nz); - // Ensure that dual postsolve is correct - REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); - REQUIRE(h.getInfo().num_primal_infeasibilities == 0); - REQUIRE(h.getInfo().num_dual_infeasibilities == 0); if (require_presolved_model_num_col == 0 && require_presolved_model_num_row == 0) REQUIRE(h.getInfo().simplex_iteration_count == 0); From 34216eddbfcef01b3c36b770d3e13692b991d004 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 31 Aug 2026 14:25:56 +0200 Subject: [PATCH 08/13] Clean up removeCutsFromModel --- highs/mip/HighsMipSolverData.cpp | 2 +- highs/presolve/HighsPostsolveStack.h | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index ecf9cac086b..6d1002219a6 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1460,7 +1460,7 @@ void HighsMipSolverData::performRestart() { runSetup(); if (mipsolver.terminate()) return; - postSolveStack.removeCutsFromModel(numCuts); + postSolveStack.removeCutsFromModel(); // HighsNodeQueue oldNodeQueue; // std::swap(nodequeue, oldNodeQueue); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 99608bfde03..f3e5da18654 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -445,10 +445,9 @@ class HighsPostsolveStack { appendToModel(numAppendedRows, numRows, OrigRowType::kAppended); } - void removeCutsFromModel(HighsInt numCuts) { - if (numCuts <= 0) return; - origNumRow -= numCuts; + void removeCutsFromModel() { size_t newSize = 0; + HighsInt numRemoved = 0; for (size_t i = 0; i < origRowIndex.size(); ++i) { if (origRowType[i] != OrigRowType::kCut) { if (i != newSize) { @@ -456,8 +455,11 @@ class HighsPostsolveStack { origRowType[newSize] = origRowType[i]; } ++newSize; + } else { + ++numRemoved; } } + origNumRow -= numRemoved; origRowIndex.resize(newSize); origRowType.resize(newSize); } From 19131d13b7bc70f6231bccf285f6f79a3e93d459 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 31 Aug 2026 14:45:14 +0200 Subject: [PATCH 09/13] Revert last change --- highs/mip/HighsMipSolverData.cpp | 2 +- highs/presolve/HighsPostsolveStack.h | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 6d1002219a6..ecf9cac086b 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1460,7 +1460,7 @@ void HighsMipSolverData::performRestart() { runSetup(); if (mipsolver.terminate()) return; - postSolveStack.removeCutsFromModel(); + postSolveStack.removeCutsFromModel(numCuts); // HighsNodeQueue oldNodeQueue; // std::swap(nodequeue, oldNodeQueue); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index f3e5da18654..99608bfde03 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -445,9 +445,10 @@ class HighsPostsolveStack { appendToModel(numAppendedRows, numRows, OrigRowType::kAppended); } - void removeCutsFromModel() { + void removeCutsFromModel(HighsInt numCuts) { + if (numCuts <= 0) return; + origNumRow -= numCuts; size_t newSize = 0; - HighsInt numRemoved = 0; for (size_t i = 0; i < origRowIndex.size(); ++i) { if (origRowType[i] != OrigRowType::kCut) { if (i != newSize) { @@ -455,11 +456,8 @@ class HighsPostsolveStack { origRowType[newSize] = origRowType[i]; } ++newSize; - } else { - ++numRemoved; } } - origNumRow -= numRemoved; origRowIndex.resize(newSize); origRowType.resize(newSize); } From 5a2a3d439ac290ffd6e7fac6840d864d681f6f2c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 1 Sep 2026 13:37:02 +0200 Subject: [PATCH 10/13] Make sure that order of rows is correct in HPresolve::detectParallelRowsAndCols --- highs/presolve/HPresolve.cpp | 24 +++++++++++++++++------- highs/presolve/HighsPostsolveStack.h | 13 ++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 60463c22bc7..5142c2d2caa 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -8572,9 +8572,8 @@ HPresolve::Result HPresolve::detectParallelRowsAndCols( parallel = duplicateColRowPos != -1; if (!parallel) break; - parallel = std::abs(static_cast( - Avalue[duplicateColRowPos] - - static_cast(colScale) * colNz.value())) <= + parallel = abs(Avalue[duplicateColRowPos] - + static_cast(colScale) * colNz.value()) <= options->small_matrix_value; if (!parallel) break; } @@ -8723,7 +8722,19 @@ HPresolve::Result HPresolve::detectParallelRowsAndCols( buckets.clear(); - for (HighsInt i = 0; i != model->num_row_; ++i) { + // Iterate non-cut rows before cut rows so that when a cut is + // parallel to a non-cut, the non-cut is already in the bucket as + // the surviving row and the cut is removed. This prevents a cut + // from absorbing a non-cut constraint which would cause the + // constraint to be lost when moveCutsToPool removes it. + std::vector rowOrder(model->num_row_); + std::iota(rowOrder.begin(), rowOrder.end(), 0); + pdqsort(rowOrder.begin(), rowOrder.end(), [&](HighsInt a, HighsInt b) { + return !postsolve_stack.isCutRow(a) && postsolve_stack.isCutRow(b); + }); + + for (HighsInt rowIndex = 0; rowIndex != model->num_row_; ++rowIndex) { + HighsInt i = rowOrder[rowIndex]; if (rowDeleted[i]) continue; if (rowsize[i] <= 1 || (rowsize[i] == 2 && isEquation(i))) { HPRESOLVE_CHECKED_CALL(rowPresolve(postsolve_stack, i)); @@ -8790,9 +8801,8 @@ HPresolve::Result HPresolve::detectParallelRowsAndCols( parallel = nzPos != -1; if (!parallel) break; - parallel = std::abs(static_cast( - Avalue[nzPos] - - static_cast(rowScale) * rowNz.value())) <= + parallel = abs(Avalue[nzPos] - + static_cast(rowScale) * rowNz.value()) <= options->small_matrix_value; if (!parallel) break; } diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 99608bfde03..b8f0103afd6 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -379,8 +379,6 @@ class HighsPostsolveStack { return origRowType[row] == OrigRowType::kCut; } - void setRowType(HighsInt row, OrigRowType type) { origRowType[row] = type; } - // Returns presolved-space indices of rows from the original model std::vector getOrigRows() const { std::vector rows; @@ -735,13 +733,10 @@ class HighsPostsolveStack { void duplicateRow(HighsInt row, bool rowUpperTightened, bool rowLowerTightened, HighsInt duplicateRow, double duplicateRowScale) { - // When a non-cut row is deleted as a duplicate of a cut, the cut - // becomes the sole enforcer of the constraint. Reclassify it so - // it is kept in the model and not moved to the cut pool, since - // otherwise the constraint is lost and postsolve may produce an - // infeasible solution. - if (isCutRow(row) && !isCutRow(duplicateRow)) - setRowType(row, OrigRowType::kAppended); + // The surviving row must not be a cut absorbing a non-cut + // constraint. detectParallelRowsAndCols ensures this by + // iterating non-cut rows before cut rows. + assert(!isCutRow(row) || isCutRow(duplicateRow)); reductionValues.push( DuplicateRow{duplicateRowScale, origRowIndex[duplicateRow], origRowIndex[row], rowLowerTightened, rowUpperTightened}); From 11783eb13874dda83b85e02814f5c6bb318c6ff2 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 1 Sep 2026 14:41:04 +0200 Subject: [PATCH 11/13] Add test --- check/TestPresolveRules.cpp | 48 ++++++++++++++++++++++++++++ highs/presolve/HPresolve.cpp | 3 +- highs/presolve/HPresolve.h | 2 ++ highs/presolve/HPresolveTest.cpp | 9 ++++++ highs/presolve/HighsPostsolveStack.h | 2 ++ 5 files changed, 63 insertions(+), 1 deletion(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index ef0c3694f03..49d7532f7b8 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -3,6 +3,8 @@ #include "HCheckConfig.h" #include "Highs.h" #include "catch.hpp" +#include "presolve/HPresolve.h" +#include "presolve/HighsPostsolveStack.h" const bool dev_run = false; @@ -88,6 +90,52 @@ TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { h.resetGlobalScheduler(true); } +TEST_CASE("test-parallel-rows-cut-ordering", "[highs_test_presolve_rules]") { + // Rows 0 and 1 are parallel (both [1, 1]). Row 0 is marked as a + // cut. detectParallelRowsAndCols must remove the cut row (0) and + // keep the non-cut row (1), not the other way around. + // Row 2 involves only col 0, breaking column parallelism. + HighsLp lp; + lp.num_col_ = 2; + lp.num_row_ = 3; + lp.sense_ = ObjSense::kMinimize; + lp.col_cost_ = {1, 2}; + lp.col_lower_ = {0, 0}; + lp.col_upper_ = {10, 10}; + lp.row_lower_ = {-kHighsInf, -kHighsInf, -kHighsInf}; + lp.row_upper_ = {5, 5, 3}; + lp.a_matrix_.num_col_ = lp.num_col_; + lp.a_matrix_.num_row_ = lp.num_row_; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.start_ = {0, 3, 5}; + lp.a_matrix_.index_ = {0, 1, 2, 0, 1}; + lp.a_matrix_.value_ = {1, 1, 1, 1, 1}; + + HighsOptions options; + options.presolve_rule_test = kPresolveRuleParallelRowsAndCols; + + HighsTimer timer; + timer.start(); + + presolve::HighsPostsolveStack postsolve_stack; + postsolve_stack.initializeIndexMaps(lp.num_row_, lp.num_col_); + // Mark parallel row 0 as a cut + postsolve_stack.setRowType(0, + presolve::HighsPostsolveStack::OrigRowType::kCut); + + presolve::HPresolve presolve; + presolve.setInput(lp, options, -1, &timer); + REQUIRE(presolve.okSetupPresolveDataStructures()); + HighsModelStatus status = presolve.run(postsolve_stack); + timer.stop(); + REQUIRE(status == HighsModelStatus::kNotset); + // One row must have been removed + REQUIRE(lp.num_row_ == 1); + // The surviving row must be original row 1 (non-cut), not row 0 (cut) + REQUIRE(postsolve_stack.getOrigRowIndex(0) == 1); + REQUIRE(!postsolve_stack.isCutRow(0)); +} + void solveAndCheck(const std::string& message, const HighsLp& lp, Highs& h, const std::string& solver, bool use_presolve, const HighsInt require_presolved_model_num_col, diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5142c2d2caa..ae87068e670 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6635,7 +6635,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { time_str.c_str()); } - if (options->presolve != kHighsOffString && mipsolver == nullptr) { + if (options->presolve != kHighsOffString && mipsolver == nullptr && + !options->presolve_rule_test) { // Zero numDeletedCols and numDeletedRows since they are used to // identify reductions due to this presovle rule numDeletedCols = 0; diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 44d8e75f7f9..871a69a223b 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -546,6 +546,8 @@ class HPresolve { Result presolveRuleTest(HighsPostsolveStack& postsolve_stack); Result presolveRuleTestColStuffing(HighsPostsolveStack& postsolve_stack); + Result presolveRuleTestParallelRowsAndCols( + HighsPostsolveStack& postsolve_stack); // Not currently called static void debug(const HighsLp& lp, const HighsOptions& options); diff --git a/highs/presolve/HPresolveTest.cpp b/highs/presolve/HPresolveTest.cpp index 77a24d1e98d..ff06d91285a 100644 --- a/highs/presolve/HPresolveTest.cpp +++ b/highs/presolve/HPresolveTest.cpp @@ -14,6 +14,8 @@ HPresolve::Result HPresolve::presolveRuleTest( assert(options->presolve_rule_test); if (options->presolve_rule_test == kPresolveRuleColStuffing) { return presolveRuleTestColStuffing(postsolve_stack); + } else if (options->presolve_rule_test == kPresolveRuleParallelRowsAndCols) { + return presolveRuleTestParallelRowsAndCols(postsolve_stack); } return Result::kOk; } @@ -36,4 +38,11 @@ HPresolve::Result HPresolve::presolveRuleTestColStuffing( // Possibly remove the row return rowPresolve(postsolve_stack, 0); } +HPresolve::Result HPresolve::presolveRuleTestParallelRowsAndCols( + HighsPostsolveStack& postsolve_stack) { + assert(options->presolve_rule_test == kPresolveRuleParallelRowsAndCols); + highsLogUser(options->log_options, HighsLogType::kInfo, + "HPresolve::presolveRuleTestParallelRowsAndCols\n"); + return detectParallelRowsAndCols(postsolve_stack); +} } // namespace presolve diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index b8f0103afd6..d46428b814f 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -379,6 +379,8 @@ class HighsPostsolveStack { return origRowType[row] == OrigRowType::kCut; } + void setRowType(HighsInt row, OrigRowType type) { origRowType[row] = type; } + // Returns presolved-space indices of rows from the original model std::vector getOrigRows() const { std::vector rows; From 72e8d2ac2f312344834324c9bae44f5a72af9fe7 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 1 Sep 2026 15:02:20 +0200 Subject: [PATCH 12/13] Sort by increasing index --- highs/presolve/HPresolve.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ae87068e670..be9efd1346e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -8731,7 +8731,9 @@ HPresolve::Result HPresolve::detectParallelRowsAndCols( std::vector rowOrder(model->num_row_); std::iota(rowOrder.begin(), rowOrder.end(), 0); pdqsort(rowOrder.begin(), rowOrder.end(), [&](HighsInt a, HighsInt b) { - return !postsolve_stack.isCutRow(a) && postsolve_stack.isCutRow(b); + if (postsolve_stack.isCutRow(a) != postsolve_stack.isCutRow(b)) + return !postsolve_stack.isCutRow(a); + return a < b; }); for (HighsInt rowIndex = 0; rowIndex != model->num_row_; ++rowIndex) { From a47ac64d9ece250da0558fe9c24dc7116964bdcc Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 1 Sep 2026 15:21:40 +0200 Subject: [PATCH 13/13] Add missing change --- highs/presolve/HighsPostsolveStack.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index d46428b814f..1a9fdb0a7aa 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -392,7 +392,9 @@ class HighsPostsolveStack { // Returns presolved-space indices of rows that are cuts std::vector getCutRows() const { std::vector rows; - for (HighsInt i = 0; i < static_cast(origRowType.size()); ++i) + // keep reverse loop to avoid behavior changes + for (HighsInt i = static_cast(origRowType.size()) - 1; i >= 0; + --i) if (isCutRow(i)) rows.push_back(i); return rows; }