From 8db6a3db9eb247c0c7026c4081599263c13e1d4f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 28 Apr 2026 10:11:30 +0200 Subject: [PATCH 001/196] WIP --- highs/presolve/HPresolve.cpp | 53 ++++++++++++++++++++++++++++++++++++ highs/presolve/HPresolve.h | 10 +++++++ 2 files changed, 63 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 1ebf6d9ca52..5560649d749 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2121,6 +2121,59 @@ void HPresolve::addToMatrix(const HighsInt row, const HighsInt col, } } +void HPresolve::addToMatrix( + HighsPostsolveStack& postsolve_stack, + const std::vector& row_lower, const std::vector& row_upper, + const std::vector>& row_entries) { + HighsInt num_rows = static_cast(row_entries.size()); + HighsInt oldNumRows = model->num_row_; + model->num_row_ += num_rows; + postsolve_stack.appendCutsToModel(num_rows); + + model->row_lower_.insert(model->row_lower_.end(), row_lower.begin(), + row_lower.end()); + model->row_upper_.insert(model->row_upper_.end(), row_upper.begin(), + row_upper.end()); + + rowroot.resize(model->num_row_, -1); + rowsize.resize(model->num_row_, 0); + rowsizeInteger.resize(model->num_row_, 0); + rowsizeImplInt.resize(model->num_row_, 0); + + rowDualLower.resize(model->num_row_); + rowDualUpper.resize(model->num_row_); + std::transform( + row_lower.begin(), row_lower.end(), rowDualLower.begin() + oldNumRows, + [](double lower) { return lower == -kHighsInf ? 0.0 : -kHighsInf; }); + std::transform( + row_upper.begin(), row_upper.end(), rowDualUpper.begin() + oldNumRows, + [](double upper) { return upper == kHighsInf ? 0.0 : kHighsInf; }); + implRowDualLower.resize(model->num_row_, -kHighsInf); + implRowDualUpper.resize(model->num_row_, kHighsInf); + rowDualLowerSource.resize(model->num_row_, -1); + rowDualUpperSource.resize(model->num_row_, -1); + + colImplSourceByRow.resize(model->num_row_); + + changedRowFlag.resize(model->num_row_, false); + rowDeleted.resize(model->num_row_, false); + + impliedRowBounds.setNumSums(model->num_row_); + + eqiters.resize(model->num_row_, equations.end()); + + for (HighsInt i = 0; i < num_rows; ++i) { + HighsInt row = oldNumRows + i; + for (const auto& entry : row_entries[i]) + addToMatrix(row, entry.col, entry.val); + + if (rowsize[row] == 1) singletonRows.push_back(row); + + if (isEquation(row)) + eqiters[row] = equations.emplace(rowsize[row], row).first; + } +} + HighsTripletListSlice HPresolve::getColumnVector(HighsInt col) const { return HighsTripletListSlice(Arow.data(), Avalue.data(), Anext.data(), colhead[col]); diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 6825701e7de..51572b7611c 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -156,6 +156,11 @@ class HPresolve { explicit operator Result() const { return my_result; }; }; + struct row_entry { + HighsInt col; + double val; + }; + HighsPresolveStatus presolve_status_; HPresolveAnalysis analysis_; @@ -374,6 +379,11 @@ class HPresolve { void addToMatrix(const HighsInt row, const HighsInt col, const double val); + void addToMatrix(HighsPostsolveStack& postsolve_stack, + const std::vector& row_lower, + const std::vector& row_upper, + const std::vector>& row_entries); + Result prepareProbing(HighsPostsolveStack& postsolve_stack, bool& firstCall); Result finaliseProbing(HighsPostsolveStack& postsolve_stack, bool firstCall, From 98183bcd935bc52ae4f1e2ae1f237604eaf2ca4d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 28 Apr 2026 10:27:24 +0200 Subject: [PATCH 002/196] Fix format --- highs/presolve/HPresolve.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5560649d749..9a9648be907 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2122,8 +2122,8 @@ void HPresolve::addToMatrix(const HighsInt row, const HighsInt col, } void HPresolve::addToMatrix( - HighsPostsolveStack& postsolve_stack, - const std::vector& row_lower, const std::vector& row_upper, + HighsPostsolveStack& postsolve_stack, const std::vector& row_lower, + const std::vector& row_upper, const std::vector>& row_entries) { HighsInt num_rows = static_cast(row_entries.size()); HighsInt oldNumRows = model->num_row_; From 8b05196805bd9475a012314e92e9c138730bcd1b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 28 Apr 2026 15:37:03 +0200 Subject: [PATCH 003/196] Simplify --- highs/presolve/HPresolve.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 9a9648be907..24a5782e8ed 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2140,14 +2140,13 @@ void HPresolve::addToMatrix( rowsizeInteger.resize(model->num_row_, 0); rowsizeImplInt.resize(model->num_row_, 0); - rowDualLower.resize(model->num_row_); - rowDualUpper.resize(model->num_row_); - std::transform( - row_lower.begin(), row_lower.end(), rowDualLower.begin() + oldNumRows, - [](double lower) { return lower == -kHighsInf ? 0.0 : -kHighsInf; }); - std::transform( - row_upper.begin(), row_upper.end(), rowDualUpper.begin() + oldNumRows, - [](double upper) { return upper == kHighsInf ? 0.0 : kHighsInf; }); + rowDualLower.resize(model->num_row_, -kHighsInf); + rowDualUpper.resize(model->num_row_, kHighsInf); + for (HighsInt i = oldNumRows; i < model->num_row_; i++) { + if (model->row_lower_[i] == -kHighsInf) rowDualLower[i] = 0.0; + if (model->row_upper_[i] == kHighsInf) rowDualUpper[i] = 0.0; + } + implRowDualLower.resize(model->num_row_, -kHighsInf); implRowDualUpper.resize(model->num_row_, kHighsInf); rowDualLowerSource.resize(model->num_row_, -1); From 4b880d58ce9c8c8314cfc32eee8ac8cba78f9dde Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 28 Apr 2026 15:42:16 +0200 Subject: [PATCH 004/196] Add comments --- highs/presolve/HPresolve.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 24a5782e8ed..85eaf24b81f 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2125,21 +2125,27 @@ void HPresolve::addToMatrix( HighsPostsolveStack& postsolve_stack, const std::vector& row_lower, const std::vector& row_upper, const std::vector>& row_entries) { + // update number of rows HighsInt num_rows = static_cast(row_entries.size()); HighsInt oldNumRows = model->num_row_; model->num_row_ += num_rows; + + // resize postsolve vectors postsolve_stack.appendCutsToModel(num_rows); + // add row bounds model->row_lower_.insert(model->row_lower_.end(), row_lower.begin(), row_lower.end()); model->row_upper_.insert(model->row_upper_.end(), row_upper.begin(), row_upper.end()); + // initialise row sizes rowroot.resize(model->num_row_, -1); rowsize.resize(model->num_row_, 0); rowsizeInteger.resize(model->num_row_, 0); rowsizeImplInt.resize(model->num_row_, 0); + // initialise row duals rowDualLower.resize(model->num_row_, -kHighsInf); rowDualUpper.resize(model->num_row_, kHighsInf); for (HighsInt i = oldNumRows; i < model->num_row_; i++) { @@ -2147,27 +2153,35 @@ void HPresolve::addToMatrix( if (model->row_upper_[i] == kHighsInf) rowDualUpper[i] = 0.0; } + // initialise implied row duals implRowDualLower.resize(model->num_row_, -kHighsInf); implRowDualUpper.resize(model->num_row_, kHighsInf); rowDualLowerSource.resize(model->num_row_, -1); rowDualUpperSource.resize(model->num_row_, -1); - colImplSourceByRow.resize(model->num_row_); + // initialise flags changedRowFlag.resize(model->num_row_, false); rowDeleted.resize(model->num_row_, false); + // resize vectors for implied row bounds impliedRowBounds.setNumSums(model->num_row_); + // resize vector for equations eqiters.resize(model->num_row_, equations.end()); - for (HighsInt i = 0; i < num_rows; ++i) { + for (HighsInt i = 0; i < num_rows; i++) { + // new row index HighsInt row = oldNumRows + i; + + // add non-zeros for (const auto& entry : row_entries[i]) addToMatrix(row, entry.col, entry.val); + // add row singleton if (rowsize[row] == 1) singletonRows.push_back(row); + // add equation if (isEquation(row)) eqiters[row] = equations.emplace(rowsize[row], row).first; } From 1cb0450991e44ce39ff459998c49a368c66b5a66 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 28 Apr 2026 16:02:36 +0200 Subject: [PATCH 005/196] Use okResize --- highs/presolve/HPresolve.cpp | 42 ++++++++++++++++++++---------------- highs/presolve/HPresolve.h | 2 +- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 85eaf24b81f..7d25cbecb11 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1027,8 +1027,6 @@ void HPresolve::shrinkProblem(HighsPostsolveStack& postsolve_stack) { mipsolver->mipdata_->debugSolution.shrink(newColIndex); numProbes.resize(model->num_col_); - // Need to set the constraint matrix dimensions - model->setMatrixDimensions(); } // Need to set the constraint matrix dimensions model->setMatrixDimensions(); @@ -2121,12 +2119,13 @@ void HPresolve::addToMatrix(const HighsInt row, const HighsInt col, } } -void HPresolve::addToMatrix( +bool HPresolve::addToMatrix( HighsPostsolveStack& postsolve_stack, const std::vector& row_lower, const std::vector& row_upper, const std::vector>& row_entries) { // update number of rows HighsInt num_rows = static_cast(row_entries.size()); + if (num_rows == 0) return true; HighsInt oldNumRows = model->num_row_; model->num_row_ += num_rows; @@ -2140,35 +2139,38 @@ void HPresolve::addToMatrix( row_upper.end()); // initialise row sizes - rowroot.resize(model->num_row_, -1); - rowsize.resize(model->num_row_, 0); - rowsizeInteger.resize(model->num_row_, 0); - rowsizeImplInt.resize(model->num_row_, 0); + if (!okResize(rowroot, model->num_row_, HighsInt{-1})) return false; + if (!okResize(rowsize, model->num_row_, HighsInt{0})) return false; + if (!okResize(rowsizeInteger, model->num_row_, HighsInt{0})) return false; + if (!okResize(rowsizeImplInt, model->num_row_, HighsInt{0})) return false; // initialise row duals - rowDualLower.resize(model->num_row_, -kHighsInf); - rowDualUpper.resize(model->num_row_, kHighsInf); + if (!okResize(rowDualLower, model->num_row_, -kHighsInf)) return false; + if (!okResize(rowDualUpper, model->num_row_, kHighsInf)) return false; for (HighsInt i = oldNumRows; i < model->num_row_; i++) { - if (model->row_lower_[i] == -kHighsInf) rowDualLower[i] = 0.0; - if (model->row_upper_[i] == kHighsInf) rowDualUpper[i] = 0.0; + if (model->row_lower_[i] == -kHighsInf) rowDualUpper[i] = 0; + if (model->row_upper_[i] == kHighsInf) rowDualLower[i] = 0; } // initialise implied row duals - implRowDualLower.resize(model->num_row_, -kHighsInf); - implRowDualUpper.resize(model->num_row_, kHighsInf); - rowDualLowerSource.resize(model->num_row_, -1); - rowDualUpperSource.resize(model->num_row_, -1); - colImplSourceByRow.resize(model->num_row_); + if (!okResize(implRowDualLower, model->num_row_, -kHighsInf)) return false; + if (!okResize(implRowDualUpper, model->num_row_, kHighsInf)) return false; + if (!okResize(rowDualLowerSource, model->num_row_, HighsInt{-1})) + return false; + if (!okResize(rowDualUpperSource, model->num_row_, HighsInt{-1})) + return false; + if (!okResize(colImplSourceByRow, model->num_row_, std::set{})) + return false; // initialise flags - changedRowFlag.resize(model->num_row_, false); - rowDeleted.resize(model->num_row_, false); + if (!okResize(changedRowFlag, model->num_row_, uint8_t{1})) return false; + if (!okResize(rowDeleted, model->num_row_, uint8_t{0})) return false; // resize vectors for implied row bounds impliedRowBounds.setNumSums(model->num_row_); // resize vector for equations - eqiters.resize(model->num_row_, equations.end()); + if (!okResize(eqiters, model->num_row_, equations.end())) return false; for (HighsInt i = 0; i < num_rows; i++) { // new row index @@ -2185,6 +2187,8 @@ void HPresolve::addToMatrix( if (isEquation(row)) eqiters[row] = equations.emplace(rowsize[row], row).first; } + + return true; } HighsTripletListSlice HPresolve::getColumnVector(HighsInt col) const { diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 51572b7611c..fdc4210c8c2 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -379,7 +379,7 @@ class HPresolve { void addToMatrix(const HighsInt row, const HighsInt col, const double val); - void addToMatrix(HighsPostsolveStack& postsolve_stack, + bool addToMatrix(HighsPostsolveStack& postsolve_stack, const std::vector& row_lower, const std::vector& row_upper, const std::vector>& row_entries); From 60a80ffcb53874923d17778e44cfff49cfc0d85a Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 29 Apr 2026 08:18:15 +0200 Subject: [PATCH 006/196] More WIP --- highs/presolve/HPresolve.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 7d25cbecb11..1bf92755743 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2166,6 +2166,10 @@ bool HPresolve::addToMatrix( if (!okResize(changedRowFlag, model->num_row_, uint8_t{1})) return false; if (!okResize(rowDeleted, model->num_row_, uint8_t{0})) return false; + // initialise row names + if (!okResize(model->row_names_, model->num_row_, std::string{})) + return false; + // resize vectors for implied row bounds impliedRowBounds.setNumSums(model->num_row_); @@ -5926,6 +5930,20 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { // Start of main presolve loop // while (true) { + std::vector row_lower, row_upper; + std::vector> rows; + for (HighsInt i = 0; i < static_cast(0.01 * model->num_row_); + i++) { + std::vector row; + for (const auto& rowNz : getRowVector(i)) { + row.push_back(row_entry{rowNz.index(), rowNz.value()}); + } + row_lower.push_back(model->row_lower_[i]); + row_upper.push_back(model->row_upper_[i]); + rows.push_back(row); + } + if (!addToMatrix(postsolve_stack, row_lower, row_upper, rows)) return; + HighsInt currSize = model->num_col_ - numDeletedCols + model->num_row_ - numDeletedRows; if (currSize < 0.85 * lastPrintSize) { From ee737b5a8d2394fa50d864d1689514f4ca13987f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 29 Apr 2026 14:44:11 +0200 Subject: [PATCH 007/196] Return Result::kOk --- highs/presolve/HPresolve.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 1bf92755743..9a57f994ef7 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2128,6 +2128,7 @@ bool HPresolve::addToMatrix( if (num_rows == 0) return true; HighsInt oldNumRows = model->num_row_; model->num_row_ += num_rows; + model->a_matrix_.num_row_ += num_rows; // resize postsolve vectors postsolve_stack.appendCutsToModel(num_rows); @@ -5930,6 +5931,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { // Start of main presolve loop // while (true) { + // FOR DEBUGGING NEW METHOD! std::vector row_lower, row_upper; std::vector> rows; for (HighsInt i = 0; i < static_cast(0.01 * model->num_row_); @@ -5942,7 +5944,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { row_upper.push_back(model->row_upper_[i]); rows.push_back(row); } - if (!addToMatrix(postsolve_stack, row_lower, row_upper, rows)) return; + if (!addToMatrix(postsolve_stack, row_lower, row_upper, rows)) + return Result::kOk; HighsInt currSize = model->num_col_ - numDeletedCols + model->num_row_ - numDeletedRows; From 6aa8fd59e64d8f4372d3bd08106dbb66784071e1 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 29 Apr 2026 16:22:56 +0200 Subject: [PATCH 008/196] WIP --- highs/mip/HighsMipSolverData.cpp | 2 +- highs/presolve/HPresolve.cpp | 16 ++++++++++++---- highs/presolve/HPresolve.h | 3 +++ highs/presolve/HighsPostsolveStack.h | 2 ++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index f44c7cfa0dc..9ce7337d141 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1278,7 +1278,7 @@ void HighsMipSolverData::performRestart() { HighsInt numLpRows = lp.getLp().num_row_; HighsInt numModelRows = mipsolver.numRow(); HighsInt numCuts = numLpRows - numModelRows; - if (numCuts > 0) postSolveStack.appendCutsToModel(numCuts); + postSolveStack.appendCutsToModel(numCuts); auto integrality = std::move(presolvedModel.integrality_); double offset = presolvedModel.offset_; presolvedModel = lp.getLp(); diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 9a57f994ef7..a3fc53ef9c0 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -125,6 +125,7 @@ bool HPresolve::okSetInput(HighsLp& model_, const HighsOptions& options_, if (!okReserve(liftingOpportunities, model->num_row_)) return false; numDeletedCols = 0; numDeletedRows = 0; + numAppendedRows = 0; // initialize substitution opportunities for (HighsInt row = 0; row != model->num_row_; ++row) { if (!isDualImpliedFree(row)) continue; @@ -686,9 +687,13 @@ HPresolve::Result HPresolve::updateColImpliedBounds(HighsInt row, HighsInt col, // do not use the implied bound if this a not a model row, since the // row can be removed and should not be used, e.g., to identify a // column as implied free - bool useImplBound = mipsolver == nullptr || - mipsolver->mipdata_->postSolveStack.getOrigRowIndex( - row) < mipsolver->orig_model_->num_row_; + bool useImplBound = + mipsolver == nullptr || + mipsolver->mipdata_->postSolveStack.getOrigRowIndex(row) < + mipsolver->orig_model_->num_row_ || + mipsolver->mipdata_->postSolveStack.getOrigRowIndex(row) >= + mipsolver->mipdata_->postSolveStack.getOrigNumRow() - + numAppendedRows; if (direction * val > 0) { // upper bound @@ -2127,6 +2132,7 @@ bool HPresolve::addToMatrix( HighsInt num_rows = static_cast(row_entries.size()); if (num_rows == 0) return true; HighsInt oldNumRows = model->num_row_; + numAppendedRows += num_rows; model->num_row_ += num_rows; model->a_matrix_.num_row_ += num_rows; @@ -6426,6 +6432,8 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { shrinkProblem(postsolve_stack); + postsolve_stack.removeCutsFromModel(numAppendedRows); + if (mipsolver != nullptr) { mipsolver->mipdata_->cliquetable.setPresolveFlag(false); mipsolver->mipdata_->cliquetable.setMaxEntries(numNonzeros()); @@ -6439,7 +6447,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) { + for (HighsInt i = model->num_row_ - numAppendedRows - 1; i >= 0; --i) { // check if we already reached the original rows if (postsolve_stack.getOrigRowIndex(i) < mipsolver->orig_model_->num_row_) diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index fdc4210c8c2..bcbe3cdd85d 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -127,6 +127,9 @@ class HPresolve { HighsInt numDeletedRows; HighsInt numDeletedCols; + // counter for number of appended rows + HighsInt numAppendedRows; + // store old problem sizes to compute percentage reductions in // presolve loop HighsInt oldNumCol; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 1b23df745e9..8674d33a14a 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -277,6 +277,7 @@ class HighsPostsolveStack { } void appendCutsToModel(HighsInt numCuts) { + if (numCuts <= 0) return; size_t currNumRow = origRowIndex.size(); size_t newNumRow = currNumRow + numCuts; origRowIndex.resize(newNumRow); @@ -285,6 +286,7 @@ class HighsPostsolveStack { } void removeCutsFromModel(HighsInt numCuts) { + if (numCuts <= 0) return; origNumRow -= numCuts; size_t origRowIndexSize = origRowIndex.size(); From 58378c69df7c5e5c8791d16da1d470a01e82f028 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 30 Apr 2026 09:13:23 +0200 Subject: [PATCH 009/196] Re-set pointers for bound vectors --- highs/presolve/HPresolve.cpp | 10 ++++++++-- highs/presolve/HighsPostsolveStack.cpp | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index a3fc53ef9c0..40820826af9 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2177,11 +2177,17 @@ bool HPresolve::addToMatrix( if (!okResize(model->row_names_, model->num_row_, std::string{})) return false; + // resize vector for equations + if (!okResize(eqiters, model->num_row_, equations.end())) return false; + // resize vectors for implied row bounds impliedRowBounds.setNumSums(model->num_row_); - // resize vector for equations - if (!okResize(eqiters, model->num_row_, equations.end())) return false; + // set bound arrays again (pointers may get invalidated by reallocation) + impliedDualRowBounds.setBoundArrays( + rowDualLower.data(), rowDualUpper.data(), implRowDualLower.data(), + implRowDualUpper.data(), rowDualLowerSource.data(), + rowDualUpperSource.data()); for (HighsInt i = 0; i < num_rows; i++) { // new row index diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index cab0c2fbdfb..17fdd3e21c3 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -527,8 +527,8 @@ void HighsPostsolveStack::DuplicateRow::undo(const HighsOptions& options, : computeStatus(solution.row_dual[row], basis.row_status[row], options.dual_feasibility_tolerance); - auto computeRowDualAndStatus = [&](bool tighened) { - if (tighened) { + auto computeRowDualAndStatus = [&](bool tightened) { + if (tightened) { if (solution.isModelRow(duplicateRow)) { solution.row_dual[duplicateRow] = solution.row_dual[row] / duplicateRowScale; From 1774ba29b62a43a91da7e0bb1a018cd6be106169 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 30 Apr 2026 11:01:06 +0200 Subject: [PATCH 010/196] Skip deleted rows --- highs/presolve/HPresolve.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 40820826af9..ef6387f4b7c 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5946,8 +5946,9 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { // FOR DEBUGGING NEW METHOD! std::vector row_lower, row_upper; std::vector> rows; - for (HighsInt i = 0; i < static_cast(0.01 * model->num_row_); + for (HighsInt i = 0; i < static_cast(0.1 * model->num_row_); i++) { + if (rowDeleted[i]) continue; std::vector row; for (const auto& rowNz : getRowVector(i)) { row.push_back(row_entry{rowNz.index(), rowNz.value()}); From e1065df1b102b735b8029a6501ced95e17c32e7c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 5 May 2026 16:15:11 +0200 Subject: [PATCH 011/196] WIP --- highs/lp_data/HConst.h | 3 +- highs/lp_data/Highs.cpp | 2 + highs/lp_data/HighsLp.cpp | 1 + highs/lp_data/HighsLp.h | 1 + highs/mip/HighsMipSolver.cpp | 4 ++ highs/mip/HighsMipSolver.h | 1 + highs/mip/HighsMipSolverData.h | 3 + highs/presolve/HPresolve.cpp | 7 ++- highs/presolve/HighsPostsolveStack.h | 85 ++++++++++++++++++++++++++-- 9 files changed, 99 insertions(+), 8 deletions(-) diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index 32b9fc50075..43662fbd823 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -252,8 +252,9 @@ enum class HighsBasisStatus : uint8_t { kBasic, // (slack) variable is basic kUpper, // (slack) variable is at its upper bound kZero, // free variable is nonbasic and set to zero - kNonbasic // nonbasic with no specific bound information - useful for users + kNonbasic, // nonbasic with no specific bound information - useful for users // and postsolve + kNotSet }; // Types of LP presolve rules diff --git a/highs/lp_data/Highs.cpp b/highs/lp_data/Highs.cpp index 92f568dd79c..aa626d45d77 100644 --- a/highs/lp_data/Highs.cpp +++ b/highs/lp_data/Highs.cpp @@ -3627,6 +3627,7 @@ HighsPresolveStatus Highs::runPresolve(const bool force_lp_presolve, presolve_.data_.reduced_lp_ = solver.getPresolvedModel(); presolve_.data_.postSolveStack = solver.getPostsolveStack(); presolve_.presolve_status_ = presolve_return_status; + HighsInt numActiveAddedRows = solver.getNumAppendedRows(); // presolve_.data_.presolve_log_ = } else { // Use presolve for LP @@ -3696,6 +3697,7 @@ HighsPostsolveStatus Highs::runPostsolve() { return HighsPostsolveStatus::kNoPrimalSolutionError; const bool have_dual_solution = presolve_.data_.recovered_solution_.dual_valid; + //presolve_.data_.postSolveStack.removeCutsFromModel(presolve_.data_.reduced_lp_.rows_appended_by_presolve_); presolve_.data_.postSolveStack.undo(options_, presolve_.data_.recovered_solution_, presolve_.data_.recovered_basis_); diff --git a/highs/lp_data/HighsLp.cpp b/highs/lp_data/HighsLp.cpp index ebcc3c2a3b6..8fe1897df96 100644 --- a/highs/lp_data/HighsLp.cpp +++ b/highs/lp_data/HighsLp.cpp @@ -226,6 +226,7 @@ void HighsLp::clear() { this->cost_row_location_ = -1; this->has_infinite_cost_ = false; this->mods_.clear(); + this->rows_appended_by_presolve_ = 0; } void HighsLp::clearScale() { diff --git a/highs/lp_data/HighsLp.h b/highs/lp_data/HighsLp.h index e3dc241593e..21edf287ca4 100644 --- a/highs/lp_data/HighsLp.h +++ b/highs/lp_data/HighsLp.h @@ -56,6 +56,7 @@ class HighsLp { HighsInt cost_row_location_; bool has_infinite_cost_; HighsLpMods mods_; + HighsInt rows_appended_by_presolve_; bool operator==(const HighsLp& lp) const; bool equalButForNames(const HighsLp& lp) const; diff --git a/highs/mip/HighsMipSolver.cpp b/highs/mip/HighsMipSolver.cpp index 2c7fdd845f6..19442e479d1 100644 --- a/highs/mip/HighsMipSolver.cpp +++ b/highs/mip/HighsMipSolver.cpp @@ -844,6 +844,10 @@ presolve::HighsPostsolveStack HighsMipSolver::getPostsolveStack() const { return mipdata_->postSolveStack; } + HighsInt HighsMipSolver::getNumAppendedRows() const { + return model_->num_row_ - mipdata_->postSolveStack.getOrigRowIndexSize(); + } + void HighsMipSolver::callbackGetCutPool() const { assert(callback_->user_callback); assert(callback_->callbackActive(kCallbackMipGetCutPool)); diff --git a/highs/mip/HighsMipSolver.h b/highs/mip/HighsMipSolver.h index a70e8736998..783a2eee67e 100644 --- a/highs/mip/HighsMipSolver.h +++ b/highs/mip/HighsMipSolver.h @@ -151,6 +151,7 @@ class HighsMipSolver { HighsModelStatus terminationStatus() const { return this->termination_status_; } + HighsInt getNumAppendedRows() const; }; std::array getGapString(const double gap_, diff --git a/highs/mip/HighsMipSolverData.h b/highs/mip/HighsMipSolverData.h index f06da7ca6b1..bb7ec1778c2 100644 --- a/highs/mip/HighsMipSolverData.h +++ b/highs/mip/HighsMipSolverData.h @@ -308,6 +308,9 @@ struct HighsMipSolverData { void terminatorTerminate(); bool terminatorTerminated() const; void terminatorReport() const; + HighsInt getNumActiveAppendedRows() const { + return mipsolver.numRow() - postSolveStack.getOrigRowIndexSize(); + } }; #endif diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ef6387f4b7c..0e164384d92 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2137,7 +2137,7 @@ bool HPresolve::addToMatrix( model->a_matrix_.num_row_ += num_rows; // resize postsolve vectors - postsolve_stack.appendCutsToModel(num_rows); + postsolve_stack.appendRowsToModel(num_rows); // add row bounds model->row_lower_.insert(model->row_lower_.end(), row_lower.begin(), @@ -6439,7 +6439,10 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { shrinkProblem(postsolve_stack); - postsolve_stack.removeCutsFromModel(numAppendedRows); + /*postsolve_stack.removeCutsFromModel(model->rows_appended_by_presolve_ + + numAppendedRows);*/ + model->rows_appended_by_presolve_ += + model->num_row_ - postsolve_stack.computeNumOrigRows(numAppendedRows); if (mipsolver != nullptr) { mipsolver->mipdata_->cliquetable.setPresolveFlag(false); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 8674d33a14a..f66bfefd76f 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -255,6 +255,7 @@ class HighsPostsolveStack { std::vector colValues; HighsInt origNumCol = -1; HighsInt origNumRow = -1; + HighsInt numRowsAppendedByPresolve = 0; void reductionAdded(ReductionType type) { size_t position = reductionValues.getCurrentDataSize(); @@ -276,6 +277,10 @@ class HighsPostsolveStack { return origColIndex[col]; } + HighsInt getOrigRowIndexSize() const { + return static_cast(origRowIndex.size()); + } + void appendCutsToModel(HighsInt numCuts) { if (numCuts <= 0) return; size_t currNumRow = origRowIndex.size(); @@ -287,21 +292,41 @@ class HighsPostsolveStack { void removeCutsFromModel(HighsInt numCuts) { if (numCuts <= 0) return; + HighsInt numOrigRows = computeNumOrigRows(numCuts); origNumRow -= numCuts; + origRowIndex.resize(numOrigRows); + } + + void appendRowsToModel(HighsInt numCuts) { + if (numCuts <= 0) return; + size_t currNumRow = origRowIndex.size(); + size_t newNumRow = currNumRow + numCuts; + origRowIndex.resize(newNumRow); + for (size_t i = currNumRow; i != newNumRow; ++i) origRowIndex[i] = i; + numRowsAppendedByPresolve += numCuts; + } + + HighsInt computeNumOrigRows(HighsInt numRowsAppended) { + HighsInt origRowIndexSize = static_cast(origRowIndex.size()); + HighsInt oldOrigRowIndexSize = origRowIndexSize; + if (numRowsAppended <= 0) return origRowIndexSize; - size_t origRowIndexSize = origRowIndex.size(); for (size_t i = origRowIndex.size(); i > 0; --i) { if (origRowIndex[i - 1] < origNumRow) break; --origRowIndexSize; } - origRowIndex.resize(origRowIndexSize); + return origRowIndexSize; } HighsInt getOrigNumRow() const { return origNumRow; } HighsInt getOrigNumCol() const { return origNumCol; } + HighsInt getNumRowsAppendedByPresolve() const { + return numRowsAppendedByPresolve; + } + void initializeIndexMaps(HighsInt numRow, HighsInt numCol); void compressIndexMaps(const std::vector& newRowIndex, @@ -602,6 +627,28 @@ class HighsPostsolveStack { #endif } + template + void undoIterateBackwards2(std::vector& values, + const std::vector& index, + HighsInt origSize) { + values.resize(origSize); + // #ifdef DEBUG_EXTRA + // Fill vector with NaN for debugging purposes + std::vector valuesNew; + valuesNew.resize(origSize, HighsBasisStatus::kNotSet); + for (size_t i = index.size(); i > 0; --i) { + assert(static_cast(index[i - 1]) >= i - 1); + valuesNew[index[i - 1]] = values[i - 1]; + } + std::copy(valuesNew.cbegin(), valuesNew.cend(), values.begin()); + // #else + /* for (size_t i = index.size(); i > 0; --i) { + assert(static_cast(index[i - 1]) >= i - 1); + values[index[i - 1]] = values[i - 1]; + }*/ + // #endif + } + /// check if vector contains NaN or Inf bool containsNanOrInf(const std::vector& v) const { return std::find_if(v.cbegin(), v.cend(), [](const double& d) { @@ -624,21 +671,25 @@ class HighsPostsolveStack { undoIterateBackwards(solution.col_value, origColIndex, origNumCol); assert(origNumRow >= 0); - undoIterateBackwards(solution.row_value, origRowIndex, origNumRow); + assert(numRowsAppendedByPresolve >= 0); + undoIterateBackwards(solution.row_value, origRowIndex, + origNumRow + numRowsAppendedByPresolve); if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); - undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow); + undoIterateBackwards(solution.row_dual, origRowIndex, + origNumRow + numRowsAppendedByPresolve); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - undoIterateBackwards(basis.row_status, origRowIndex, origNumRow); + undoIterateBackwards2(basis.row_status, origRowIndex, + origNumRow + numRowsAppendedByPresolve); } // now undo the changes @@ -753,6 +804,30 @@ class HighsPostsolveStack { printf("After last reduction: col_value[%2d] = %g\n", int(report_col), solution.col_value[report_col]); + std::vector row_value; + row_value.resize(origNumRow); + for (size_t i = 0; i < origNumRow; i++) { + row_value[i] = solution.row_value[origRowIndex[i]]; + } + solution.row_value = std::move(row_value); + + std::vector row_dual; + row_dual.resize(origNumRow); + for (size_t i = 0; i < origNumRow; i++) { + row_dual[i] = solution.row_dual[origRowIndex[i]]; + } + solution.row_dual = std::move(row_dual); + + std::vector row_status; + row_status.resize(origNumRow); + for (size_t i = 0; i < origNumRow; i++) { + if (origRowIndex[i] > origNumRow) + row_status[i] = HighsBasisStatus::kNonbasic; + else + row_status[i] = basis.row_status[origRowIndex[i]]; + } + basis.row_status = std::move(row_status); + #ifdef DEBUG_EXTRA // solution should not contain NaN or Inf assert(!containsNanOrInf(solution.col_value)); From 5c1bcb6ee95aba7243bb0fb85a80b46b3406570b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 6 May 2026 09:55:07 +0200 Subject: [PATCH 012/196] WIP --- highs/lp_data/HighsLp.cpp | 2 +- highs/lp_data/HighsLp.h | 2 +- highs/mip/HighsMipSolverData.cpp | 2 +- highs/presolve/HPresolve.cpp | 8 +-- highs/presolve/HighsPostsolveStack.cpp | 5 +- highs/presolve/HighsPostsolveStack.h | 67 +++++++++++++------------- highs/presolve/PresolveComponent.cpp | 2 +- 7 files changed, 45 insertions(+), 43 deletions(-) diff --git a/highs/lp_data/HighsLp.cpp b/highs/lp_data/HighsLp.cpp index 8fe1897df96..2799524a5ff 100644 --- a/highs/lp_data/HighsLp.cpp +++ b/highs/lp_data/HighsLp.cpp @@ -226,7 +226,7 @@ void HighsLp::clear() { this->cost_row_location_ = -1; this->has_infinite_cost_ = false; this->mods_.clear(); - this->rows_appended_by_presolve_ = 0; + this->num_rows_appended_by_presolve_ = 0; } void HighsLp::clearScale() { diff --git a/highs/lp_data/HighsLp.h b/highs/lp_data/HighsLp.h index 21edf287ca4..dc5984a8e24 100644 --- a/highs/lp_data/HighsLp.h +++ b/highs/lp_data/HighsLp.h @@ -56,7 +56,7 @@ class HighsLp { HighsInt cost_row_location_; bool has_infinite_cost_; HighsLpMods mods_; - HighsInt rows_appended_by_presolve_; + HighsInt num_rows_appended_by_presolve_; bool operator==(const HighsLp& lp) const; bool equalButForNames(const HighsLp& lp) const; diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 9ce7337d141..598d5518eac 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -692,7 +692,7 @@ void HighsMipSolverData::removeFixedIndices() { } void HighsMipSolverData::init() { - postSolveStack.initializeIndexMaps(mipsolver.numRow(), mipsolver.numCol()); + postSolveStack.initializeIndexMaps(mipsolver.numRow(), mipsolver.numCol(), mipsolver.model_->num_rows_appended_by_presolve_); mipsolver.orig_model_ = mipsolver.model_; feastol = mipsolver.options_mip_->mip_feasibility_tolerance; epsilon = mipsolver.options_mip_->small_matrix_value; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 0e164384d92..dd893f1422a 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6441,7 +6441,7 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { /*postsolve_stack.removeCutsFromModel(model->rows_appended_by_presolve_ + numAppendedRows);*/ - model->rows_appended_by_presolve_ += + model->num_rows_appended_by_presolve_ += model->num_row_ - postsolve_stack.computeNumOrigRows(numAppendedRows); if (mipsolver != nullptr) { @@ -6538,7 +6538,7 @@ void HPresolve::computeIntermediateMatrix(std::vector& flagRow, size_t& numreductions) { shrinkProblemEnabled = false; HighsPostsolveStack stack; - stack.initializeIndexMaps(flagRow.size(), flagCol.size()); + stack.initializeIndexMaps(flagRow.size(), flagCol.size(), model->num_rows_appended_by_presolve_); setReductionLimit(numreductions); presolve(stack); numreductions = stack.numReductions(); @@ -8227,7 +8227,7 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { model.integrality_.assign(lp.num_col_, HighsVarType::kContinuous); HighsPostsolveStack postsolve_stack; - postsolve_stack.initializeIndexMaps(lp.num_row_, lp.num_col_); + postsolve_stack.initializeIndexMaps(lp.num_row_, lp.num_col_, lp.num_rows_appended_by_presolve_); { HPresolve presolve; presolve.okSetInput(model, options, options.presolve_reduction_limit); @@ -8303,7 +8303,7 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { HPresolve presolve; presolve.okSetInput(model, options, options.presolve_reduction_limit); HighsPostsolveStack tmp; - tmp.initializeIndexMaps(model.num_row_, model.num_col_); + tmp.initializeIndexMaps(model.num_row_, model.num_col_, model.num_rows_appended_by_presolve_); presolve.setReductionLimit(reductionLim); presolve.run(tmp); diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 17fdd3e21c3..16c40cb6b8b 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -18,8 +18,9 @@ namespace presolve { void HighsPostsolveStack::initializeIndexMaps(HighsInt numRow, - HighsInt numCol) { - origNumRow = numRow; + HighsInt numCol, HighsInt numRowPresolve) { + numRowsAppendedByPresolve = numRowPresolve; + origNumRow = numRow - numRowPresolve; origNumCol = numCol; origRowIndex.resize(numRow); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index f66bfefd76f..629a6265cbd 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -302,8 +302,8 @@ class HighsPostsolveStack { size_t currNumRow = origRowIndex.size(); size_t newNumRow = currNumRow + numCuts; origRowIndex.resize(newNumRow); - for (size_t i = currNumRow; i != newNumRow; ++i) origRowIndex[i] = i; - numRowsAppendedByPresolve += numCuts; + for (size_t i = currNumRow; i != newNumRow; ++i) origRowIndex[i] = origNumRow + numRowsAppendedByPresolve + static_cast(i - currNumRow); + //numRowsAppendedByPresolve += numCuts; } HighsInt computeNumOrigRows(HighsInt numRowsAppended) { @@ -327,7 +327,8 @@ class HighsPostsolveStack { return numRowsAppendedByPresolve; } - void initializeIndexMaps(HighsInt numRow, HighsInt numCol); + void initializeIndexMaps(HighsInt numRow, HighsInt numCol, + HighsInt numRowPresolve); void compressIndexMaps(const std::vector& newRowIndex, const std::vector& newColIndex); @@ -622,7 +623,7 @@ class HighsPostsolveStack { #else for (size_t i = index.size(); i > 0; --i) { assert(static_cast(index[i - 1]) >= i - 1); - values[index[i - 1]] = values[i - 1]; + if (index[i - 1] < origSize) values[index[i - 1]] = values[i - 1]; } #endif } @@ -638,7 +639,7 @@ class HighsPostsolveStack { valuesNew.resize(origSize, HighsBasisStatus::kNotSet); for (size_t i = index.size(); i > 0; --i) { assert(static_cast(index[i - 1]) >= i - 1); - valuesNew[index[i - 1]] = values[i - 1]; + if (index[i - 1] < origSize) valuesNew[index[i - 1]] = values[i - 1]; } std::copy(valuesNew.cbegin(), valuesNew.cend(), values.begin()); // #else @@ -672,24 +673,21 @@ class HighsPostsolveStack { assert(origNumRow >= 0); assert(numRowsAppendedByPresolve >= 0); - undoIterateBackwards(solution.row_value, origRowIndex, - origNumRow + numRowsAppendedByPresolve); + undoIterateBackwards(solution.row_value, origRowIndex, origNumRow); if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); - undoIterateBackwards(solution.row_dual, origRowIndex, - origNumRow + numRowsAppendedByPresolve); + undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - undoIterateBackwards2(basis.row_status, origRowIndex, - origNumRow + numRowsAppendedByPresolve); + undoIterateBackwards2(basis.row_status, origRowIndex, origNumRow); } // now undo the changes @@ -804,29 +802,32 @@ class HighsPostsolveStack { printf("After last reduction: col_value[%2d] = %g\n", int(report_col), solution.col_value[report_col]); - std::vector row_value; - row_value.resize(origNumRow); - for (size_t i = 0; i < origNumRow; i++) { - row_value[i] = solution.row_value[origRowIndex[i]]; - } - solution.row_value = std::move(row_value); + solution.row_value.resize(origNumRow + numRowsAppendedByPresolve); + solution.row_dual.resize(origNumRow + numRowsAppendedByPresolve); + basis.row_status.resize(origNumRow + numRowsAppendedByPresolve); + /*std::vector row_value; + row_value.resize(origNumRow); + for (size_t i = 0; i < origNumRow; i++) { + row_value[i] = solution.row_value[origRowIndex[i]]; + } + solution.row_value = std::move(row_value); - std::vector row_dual; - row_dual.resize(origNumRow); - for (size_t i = 0; i < origNumRow; i++) { - row_dual[i] = solution.row_dual[origRowIndex[i]]; - } - solution.row_dual = std::move(row_dual); - - std::vector row_status; - row_status.resize(origNumRow); - for (size_t i = 0; i < origNumRow; i++) { - if (origRowIndex[i] > origNumRow) - row_status[i] = HighsBasisStatus::kNonbasic; - else - row_status[i] = basis.row_status[origRowIndex[i]]; - } - basis.row_status = std::move(row_status); + std::vector row_dual; + row_dual.resize(origNumRow); + for (size_t i = 0; i < origNumRow; i++) { + row_dual[i] = solution.row_dual[origRowIndex[i]]; + } + solution.row_dual = std::move(row_dual); + + std::vector row_status; + row_status.resize(origNumRow); + for (size_t i = 0; i < origNumRow; i++) { + if (origRowIndex[i] > origNumRow) + row_status[i] = HighsBasisStatus::kNonbasic; + else + row_status[i] = basis.row_status[origRowIndex[i]]; + } + basis.row_status = std::move(row_status);*/ #ifdef DEBUG_EXTRA // solution should not contain NaN or Inf diff --git a/highs/presolve/PresolveComponent.cpp b/highs/presolve/PresolveComponent.cpp index dcaa16d7364..9090dd33365 100644 --- a/highs/presolve/PresolveComponent.cpp +++ b/highs/presolve/PresolveComponent.cpp @@ -15,7 +15,7 @@ HighsStatus PresolveComponent::init(const HighsLp& lp, HighsTimer& timer, bool mip) { - data_.postSolveStack.initializeIndexMaps(lp.num_row_, lp.num_col_); + data_.postSolveStack.initializeIndexMaps(lp.num_row_, lp.num_col_, lp.num_rows_appended_by_presolve_); data_.reduced_lp_ = lp; this->timer = &timer; return HighsStatus::kOk; From f4ff79e51e546437557f84891105e1cc351ed3f6 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 6 May 2026 10:56:32 +0200 Subject: [PATCH 013/196] More WIP --- highs/lp_data/HConst.h | 3 +- highs/lp_data/Highs.cpp | 2 - highs/mip/HighsMipSolver.cpp | 4 -- highs/mip/HighsMipSolver.h | 1 - highs/mip/HighsMipSolverData.h | 3 -- highs/presolve/HighsPostsolveStack.cpp | 6 +-- highs/presolve/HighsPostsolveStack.h | 65 ++++---------------------- 7 files changed, 12 insertions(+), 72 deletions(-) diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index 43662fbd823..32b9fc50075 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -252,9 +252,8 @@ enum class HighsBasisStatus : uint8_t { kBasic, // (slack) variable is basic kUpper, // (slack) variable is at its upper bound kZero, // free variable is nonbasic and set to zero - kNonbasic, // nonbasic with no specific bound information - useful for users + kNonbasic // nonbasic with no specific bound information - useful for users // and postsolve - kNotSet }; // Types of LP presolve rules diff --git a/highs/lp_data/Highs.cpp b/highs/lp_data/Highs.cpp index aa626d45d77..92f568dd79c 100644 --- a/highs/lp_data/Highs.cpp +++ b/highs/lp_data/Highs.cpp @@ -3627,7 +3627,6 @@ HighsPresolveStatus Highs::runPresolve(const bool force_lp_presolve, presolve_.data_.reduced_lp_ = solver.getPresolvedModel(); presolve_.data_.postSolveStack = solver.getPostsolveStack(); presolve_.presolve_status_ = presolve_return_status; - HighsInt numActiveAddedRows = solver.getNumAppendedRows(); // presolve_.data_.presolve_log_ = } else { // Use presolve for LP @@ -3697,7 +3696,6 @@ HighsPostsolveStatus Highs::runPostsolve() { return HighsPostsolveStatus::kNoPrimalSolutionError; const bool have_dual_solution = presolve_.data_.recovered_solution_.dual_valid; - //presolve_.data_.postSolveStack.removeCutsFromModel(presolve_.data_.reduced_lp_.rows_appended_by_presolve_); presolve_.data_.postSolveStack.undo(options_, presolve_.data_.recovered_solution_, presolve_.data_.recovered_basis_); diff --git a/highs/mip/HighsMipSolver.cpp b/highs/mip/HighsMipSolver.cpp index 19442e479d1..2c7fdd845f6 100644 --- a/highs/mip/HighsMipSolver.cpp +++ b/highs/mip/HighsMipSolver.cpp @@ -844,10 +844,6 @@ presolve::HighsPostsolveStack HighsMipSolver::getPostsolveStack() const { return mipdata_->postSolveStack; } - HighsInt HighsMipSolver::getNumAppendedRows() const { - return model_->num_row_ - mipdata_->postSolveStack.getOrigRowIndexSize(); - } - void HighsMipSolver::callbackGetCutPool() const { assert(callback_->user_callback); assert(callback_->callbackActive(kCallbackMipGetCutPool)); diff --git a/highs/mip/HighsMipSolver.h b/highs/mip/HighsMipSolver.h index 783a2eee67e..a70e8736998 100644 --- a/highs/mip/HighsMipSolver.h +++ b/highs/mip/HighsMipSolver.h @@ -151,7 +151,6 @@ class HighsMipSolver { HighsModelStatus terminationStatus() const { return this->termination_status_; } - HighsInt getNumAppendedRows() const; }; std::array getGapString(const double gap_, diff --git a/highs/mip/HighsMipSolverData.h b/highs/mip/HighsMipSolverData.h index bb7ec1778c2..f06da7ca6b1 100644 --- a/highs/mip/HighsMipSolverData.h +++ b/highs/mip/HighsMipSolverData.h @@ -308,9 +308,6 @@ struct HighsMipSolverData { void terminatorTerminate(); bool terminatorTerminated() const; void terminatorReport() const; - HighsInt getNumActiveAppendedRows() const { - return mipsolver.numRow() - postSolveStack.getOrigRowIndexSize(); - } }; #endif diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 16c40cb6b8b..448ee11d170 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -17,9 +17,9 @@ namespace presolve { -void HighsPostsolveStack::initializeIndexMaps(HighsInt numRow, - HighsInt numCol, HighsInt numRowPresolve) { - numRowsAppendedByPresolve = numRowPresolve; +void HighsPostsolveStack::initializeIndexMaps(HighsInt numRow, HighsInt numCol, + HighsInt numRowPresolve) { + numRowsAppendedByPresolve = numRowPresolve; origNumRow = numRow - numRowPresolve; origNumCol = numCol; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 629a6265cbd..657a40a0b8e 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -277,10 +277,6 @@ class HighsPostsolveStack { return origColIndex[col]; } - HighsInt getOrigRowIndexSize() const { - return static_cast(origRowIndex.size()); - } - void appendCutsToModel(HighsInt numCuts) { if (numCuts <= 0) return; size_t currNumRow = origRowIndex.size(); @@ -302,21 +298,21 @@ class HighsPostsolveStack { size_t currNumRow = origRowIndex.size(); size_t newNumRow = currNumRow + numCuts; origRowIndex.resize(newNumRow); - for (size_t i = currNumRow; i != newNumRow; ++i) origRowIndex[i] = origNumRow + numRowsAppendedByPresolve + static_cast(i - currNumRow); - //numRowsAppendedByPresolve += numCuts; + for (size_t i = currNumRow; i != newNumRow; ++i) + origRowIndex[i] = origNumRow + numRowsAppendedByPresolve + + static_cast(i - currNumRow); } HighsInt computeNumOrigRows(HighsInt numRowsAppended) { - HighsInt origRowIndexSize = static_cast(origRowIndex.size()); - HighsInt oldOrigRowIndexSize = origRowIndexSize; - if (numRowsAppended <= 0) return origRowIndexSize; + HighsInt numOrig = static_cast(origRowIndex.size()); + if (numRowsAppended <= 0) return numOrig; for (size_t i = origRowIndex.size(); i > 0; --i) { if (origRowIndex[i - 1] < origNumRow) break; - --origRowIndexSize; + --numOrig; } - return origRowIndexSize; + return numOrig; } HighsInt getOrigNumRow() const { return origNumRow; } @@ -628,28 +624,6 @@ class HighsPostsolveStack { #endif } - template - void undoIterateBackwards2(std::vector& values, - const std::vector& index, - HighsInt origSize) { - values.resize(origSize); - // #ifdef DEBUG_EXTRA - // Fill vector with NaN for debugging purposes - std::vector valuesNew; - valuesNew.resize(origSize, HighsBasisStatus::kNotSet); - for (size_t i = index.size(); i > 0; --i) { - assert(static_cast(index[i - 1]) >= i - 1); - if (index[i - 1] < origSize) valuesNew[index[i - 1]] = values[i - 1]; - } - std::copy(valuesNew.cbegin(), valuesNew.cend(), values.begin()); - // #else - /* for (size_t i = index.size(); i > 0; --i) { - assert(static_cast(index[i - 1]) >= i - 1); - values[index[i - 1]] = values[i - 1]; - }*/ - // #endif - } - /// check if vector contains NaN or Inf bool containsNanOrInf(const std::vector& v) const { return std::find_if(v.cbegin(), v.cend(), [](const double& d) { @@ -687,7 +661,7 @@ class HighsPostsolveStack { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - undoIterateBackwards2(basis.row_status, origRowIndex, origNumRow); + undoIterateBackwards(basis.row_status, origRowIndex, origNumRow); } // now undo the changes @@ -805,29 +779,6 @@ class HighsPostsolveStack { solution.row_value.resize(origNumRow + numRowsAppendedByPresolve); solution.row_dual.resize(origNumRow + numRowsAppendedByPresolve); basis.row_status.resize(origNumRow + numRowsAppendedByPresolve); - /*std::vector row_value; - row_value.resize(origNumRow); - for (size_t i = 0; i < origNumRow; i++) { - row_value[i] = solution.row_value[origRowIndex[i]]; - } - solution.row_value = std::move(row_value); - - std::vector row_dual; - row_dual.resize(origNumRow); - for (size_t i = 0; i < origNumRow; i++) { - row_dual[i] = solution.row_dual[origRowIndex[i]]; - } - solution.row_dual = std::move(row_dual); - - std::vector row_status; - row_status.resize(origNumRow); - for (size_t i = 0; i < origNumRow; i++) { - if (origRowIndex[i] > origNumRow) - row_status[i] = HighsBasisStatus::kNonbasic; - else - row_status[i] = basis.row_status[origRowIndex[i]]; - } - basis.row_status = std::move(row_status);*/ #ifdef DEBUG_EXTRA // solution should not contain NaN or Inf From 8e609dbce055c91e13a0a6c40de02b4239e34861 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 6 May 2026 11:08:52 +0200 Subject: [PATCH 014/196] More WIP --- highs/mip/HighsMipSolverData.cpp | 2 +- highs/presolve/HighsPostsolveStack.h | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 598d5518eac..bef67b71c8f 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1304,7 +1304,7 @@ void HighsMipSolverData::performRestart() { root_basis.col_status[postSolveStack.getOrigColIndex(i)] = basis.col_status[i]; - HighsInt numRow = basis.row_status.size(); + HighsInt numRow = basis.row_status.size() - lp.getLp().num_rows_appended_by_presolve_; for (HighsInt i = 0; i < numRow; ++i) root_basis.row_status[postSolveStack.getOrigRowIndex(i)] = basis.row_status[i]; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 657a40a0b8e..b82065e60db 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -319,10 +319,6 @@ class HighsPostsolveStack { HighsInt getOrigNumCol() const { return origNumCol; } - HighsInt getNumRowsAppendedByPresolve() const { - return numRowsAppendedByPresolve; - } - void initializeIndexMaps(HighsInt numRow, HighsInt numCol, HighsInt numRowPresolve); From 001e7aae0d4a4f9ea25a199010a68bec420838af Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 6 May 2026 15:16:11 +0200 Subject: [PATCH 015/196] WIP --- highs/mip/HighsMipSolverData.cpp | 56 +++++++++++++++++++------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index bef67b71c8f..93a2f8a4ee8 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -692,7 +692,9 @@ void HighsMipSolverData::removeFixedIndices() { } void HighsMipSolverData::init() { - postSolveStack.initializeIndexMaps(mipsolver.numRow(), mipsolver.numCol(), mipsolver.model_->num_rows_appended_by_presolve_); + postSolveStack.initializeIndexMaps( + mipsolver.numRow(), mipsolver.numCol(), + mipsolver.model_->num_rows_appended_by_presolve_); mipsolver.orig_model_ = mipsolver.model_; feastol = mipsolver.options_mip_->mip_feasibility_tolerance; epsilon = mipsolver.options_mip_->small_matrix_value; @@ -1304,7 +1306,8 @@ void HighsMipSolverData::performRestart() { root_basis.col_status[postSolveStack.getOrigColIndex(i)] = basis.col_status[i]; - HighsInt numRow = basis.row_status.size() - lp.getLp().num_rows_appended_by_presolve_; + HighsInt numRow = + basis.row_status.size() - lp.getLp().num_rows_appended_by_presolve_; for (HighsInt i = 0; i < numRow; ++i) root_basis.row_status[postSolveStack.getOrigRowIndex(i)] = basis.row_status[i]; @@ -1405,26 +1408,35 @@ void HighsMipSolverData::performRestart() { void HighsMipSolverData::basisTransfer() { // if a root basis is given, construct a basis for the root LP from // in the reduced problem space after presolving - if (mipsolver.rootbasis) { - const HighsInt numRow = mipsolver.numRow(); - const HighsInt numCol = mipsolver.numCol(); - firstrootbasis.col_status.assign(numCol, HighsBasisStatus::kNonbasic); - firstrootbasis.row_status.assign(numRow, HighsBasisStatus::kNonbasic); - firstrootbasis.valid = true; - firstrootbasis.alien = true; - firstrootbasis.useful = true; - - for (HighsInt i = 0; i < numRow; ++i) { - HighsBasisStatus status = - mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; - firstrootbasis.row_status[i] = status; - } - - for (HighsInt i = 0; i < numCol; ++i) { - HighsBasisStatus status = - mipsolver.rootbasis->col_status[postSolveStack.getOrigColIndex(i)]; - firstrootbasis.col_status[i] = status; - } + if (mipsolver.rootbasis == nullptr) return; + + const HighsInt numRow = mipsolver.numRow(); + const HighsInt numCol = mipsolver.numCol(); + firstrootbasis.col_status.assign(numCol, HighsBasisStatus::kNonbasic); + firstrootbasis.row_status.assign(numRow, HighsBasisStatus::kNonbasic); + firstrootbasis.valid = true; + firstrootbasis.alien = true; + firstrootbasis.useful = true; + HighsInt numBasicVars = 0; + + auto sol = lp.getLpSolver().getSolution(); + + for (HighsInt i = 0; i < numCol; ++i) { + HighsBasisStatus status = + mipsolver.rootbasis->col_status[postSolveStack.getOrigColIndex(i)]; + firstrootbasis.col_status[i] = status; + if (status == HighsBasisStatus::kBasic) numBasicVars++; + } + for (HighsInt i = 0; i < postSolveStack.getOrigNumRow(); ++i) { + HighsBasisStatus status = + mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; + firstrootbasis.row_status[i] = status; + if (status == HighsBasisStatus::kBasic) numBasicVars++; + } + + for (HighsInt i = 0; i < numRow - numBasicVars; i++) { + firstrootbasis.row_status[postSolveStack.getOrigNumRow() + i] = + HighsBasisStatus::kBasic; } } From 60dd349828231e626e3b4f6c9289129948b6dcf5 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 7 May 2026 16:05:56 +0200 Subject: [PATCH 016/196] Restarts still not working --- highs/lp_data/HighsLp.cpp | 2 +- highs/lp_data/HighsLp.h | 2 +- highs/mip/HighsMipSolverData.cpp | 22 ++++++--- highs/presolve/HPresolve.cpp | 19 ++++--- highs/presolve/HighsPostsolveStack.cpp | 19 ++++--- highs/presolve/HighsPostsolveStack.h | 68 +++++++++++++++++++------- highs/presolve/PresolveComponent.cpp | 2 +- 7 files changed, 92 insertions(+), 42 deletions(-) diff --git a/highs/lp_data/HighsLp.cpp b/highs/lp_data/HighsLp.cpp index 2799524a5ff..f2c5d2dc75f 100644 --- a/highs/lp_data/HighsLp.cpp +++ b/highs/lp_data/HighsLp.cpp @@ -226,7 +226,7 @@ void HighsLp::clear() { this->cost_row_location_ = -1; this->has_infinite_cost_ = false; this->mods_.clear(); - this->num_rows_appended_by_presolve_ = 0; + this->rows_appended_by_presolve_.clear(); } void HighsLp::clearScale() { diff --git a/highs/lp_data/HighsLp.h b/highs/lp_data/HighsLp.h index dc5984a8e24..c464b5b9b42 100644 --- a/highs/lp_data/HighsLp.h +++ b/highs/lp_data/HighsLp.h @@ -56,7 +56,7 @@ class HighsLp { HighsInt cost_row_location_; bool has_infinite_cost_; HighsLpMods mods_; - HighsInt num_rows_appended_by_presolve_; + std::vector rows_appended_by_presolve_; bool operator==(const HighsLp& lp) const; bool equalButForNames(const HighsLp& lp) const; diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 93a2f8a4ee8..53c48a0d5ba 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -694,7 +694,7 @@ void HighsMipSolverData::removeFixedIndices() { void HighsMipSolverData::init() { postSolveStack.initializeIndexMaps( mipsolver.numRow(), mipsolver.numCol(), - mipsolver.model_->num_rows_appended_by_presolve_); + mipsolver.model_->rows_appended_by_presolve_); mipsolver.orig_model_ = mipsolver.model_; feastol = mipsolver.options_mip_->mip_feasibility_tolerance; epsilon = mipsolver.options_mip_->small_matrix_value; @@ -1279,7 +1279,7 @@ void HighsMipSolverData::performRestart() { sb_lp_iterations_before_run = sb_lp_iterations; HighsInt numLpRows = lp.getLp().num_row_; HighsInt numModelRows = mipsolver.numRow(); - HighsInt numCuts = numLpRows - numModelRows; + HighsInt numCuts = numLpRows - numModelRows + postSolveStack.numAppended(); postSolveStack.appendCutsToModel(numCuts); auto integrality = std::move(presolvedModel.integrality_); double offset = presolvedModel.offset_; @@ -1298,7 +1298,7 @@ void HighsMipSolverData::performRestart() { // for the presolved model after the restart root_basis.col_status.resize(postSolveStack.getOrigNumCol()); root_basis.row_status.resize(postSolveStack.getOrigNumRow(), - HighsBasisStatus::kBasic); + HighsBasisStatus::kNonbasic); root_basis.valid = true; root_basis.useful = true; @@ -1306,11 +1306,10 @@ void HighsMipSolverData::performRestart() { root_basis.col_status[postSolveStack.getOrigColIndex(i)] = basis.col_status[i]; - HighsInt numRow = - basis.row_status.size() - lp.getLp().num_rows_appended_by_presolve_; - for (HighsInt i = 0; i < numRow; ++i) - root_basis.row_status[postSolveStack.getOrigRowIndex(i)] = - basis.row_status[i]; + for (HighsInt i = 0; i < mipsolver.numRow(); ++i) + //if (postSolveStack.getOrigRowIndex(i) < root_basis.row_status.size()) + root_basis.row_status[postSolveStack.getOrigRowIndex(i)] = + basis.row_status[i]; mipsolver.rootbasis = &root_basis; } @@ -1438,6 +1437,13 @@ void HighsMipSolverData::basisTransfer() { firstrootbasis.row_status[postSolveStack.getOrigNumRow() + i] = HighsBasisStatus::kBasic; } + + /*for (HighsInt i = 0; i < numRow; ++i) { + HighsBasisStatus status = + mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; + firstrootbasis.row_status[i] = status; + if (status == HighsBasisStatus::kBasic) numBasicVars++; + }*/ } const std::vector& HighsMipSolverData::getSolution() const { diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index dd893f1422a..130668175e3 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2132,12 +2132,13 @@ bool HPresolve::addToMatrix( HighsInt num_rows = static_cast(row_entries.size()); if (num_rows == 0) return true; HighsInt oldNumRows = model->num_row_; + HighsInt oldNumRowsAppended = numAppendedRows; numAppendedRows += num_rows; model->num_row_ += num_rows; model->a_matrix_.num_row_ += num_rows; // resize postsolve vectors - postsolve_stack.appendRowsToModel(num_rows); + postsolve_stack.appendRowsToModel2(num_rows); // add row bounds model->row_lower_.insert(model->row_lower_.end(), row_lower.begin(), @@ -6439,10 +6440,9 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { shrinkProblem(postsolve_stack); - /*postsolve_stack.removeCutsFromModel(model->rows_appended_by_presolve_ + - numAppendedRows);*/ - model->num_rows_appended_by_presolve_ += - model->num_row_ - postsolve_stack.computeNumOrigRows(numAppendedRows); + postsolve_stack.removeCutsFromModel(numAppendedRows); + + postsolve_stack.getAppendedRows(model->rows_appended_by_presolve_); if (mipsolver != nullptr) { mipsolver->mipdata_->cliquetable.setPresolveFlag(false); @@ -6538,7 +6538,8 @@ void HPresolve::computeIntermediateMatrix(std::vector& flagRow, size_t& numreductions) { shrinkProblemEnabled = false; HighsPostsolveStack stack; - stack.initializeIndexMaps(flagRow.size(), flagCol.size(), model->num_rows_appended_by_presolve_); + stack.initializeIndexMaps(flagRow.size(), flagCol.size(), + model->rows_appended_by_presolve_); setReductionLimit(numreductions); presolve(stack); numreductions = stack.numReductions(); @@ -8227,7 +8228,8 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { model.integrality_.assign(lp.num_col_, HighsVarType::kContinuous); HighsPostsolveStack postsolve_stack; - postsolve_stack.initializeIndexMaps(lp.num_row_, lp.num_col_, lp.num_rows_appended_by_presolve_); + postsolve_stack.initializeIndexMaps(lp.num_row_, lp.num_col_, + lp.rows_appended_by_presolve_); { HPresolve presolve; presolve.okSetInput(model, options, options.presolve_reduction_limit); @@ -8303,7 +8305,8 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { HPresolve presolve; presolve.okSetInput(model, options, options.presolve_reduction_limit); HighsPostsolveStack tmp; - tmp.initializeIndexMaps(model.num_row_, model.num_col_, model.num_rows_appended_by_presolve_); + tmp.initializeIndexMaps(model.num_row_, model.num_col_, + model.rows_appended_by_presolve_); presolve.setReductionLimit(reductionLim); presolve.run(tmp); diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 448ee11d170..b4ff357e496 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -17,12 +17,14 @@ namespace presolve { -void HighsPostsolveStack::initializeIndexMaps(HighsInt numRow, HighsInt numCol, - HighsInt numRowPresolve) { - numRowsAppendedByPresolve = numRowPresolve; - origNumRow = numRow - numRowPresolve; +void HighsPostsolveStack::initializeIndexMaps( + HighsInt numRow, HighsInt numCol, + const std::vector& rowsAppendedByPresolve) { + origNumRow = numRow; origNumCol = numCol; + for (HighsInt row : rowsAppendedByPresolve) rowsAppended[row] = row; + origRowIndex.resize(numRow); std::iota(origRowIndex.begin(), origRowIndex.end(), 0); @@ -39,10 +41,15 @@ void HighsPostsolveStack::compressIndexMaps( // store original index at new index position otherwise HighsInt numRow = origRowIndex.size(); for (size_t i = 0; i != newRowIndex.size(); ++i) { - if (newRowIndex[i] == -1) + bool rowIsAppended = + rowsAppended.find(origRowIndex[i]) != rowsAppended.end(); + if (newRowIndex[i] == -1) { --numRow; - else + if (rowIsAppended) rowsAppended[origRowIndex[i]] = -1; + } else { origRowIndex[newRowIndex[i]] = origRowIndex[i]; + if (rowIsAppended) rowsAppended[origRowIndex[i]] = newRowIndex[i]; + } } // resize original index array to new size origRowIndex.resize(numRow); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index b82065e60db..00f2df27563 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -255,7 +255,7 @@ class HighsPostsolveStack { std::vector colValues; HighsInt origNumCol = -1; HighsInt origNumRow = -1; - HighsInt numRowsAppendedByPresolve = 0; + std::unordered_map rowsAppended; void reductionAdded(ReductionType type) { size_t position = reductionValues.getCurrentDataSize(); @@ -293,14 +293,24 @@ class HighsPostsolveStack { origRowIndex.resize(numOrigRows); } - void appendRowsToModel(HighsInt numCuts) { - if (numCuts <= 0) return; + void appendRowsToModel(HighsInt startRow, HighsInt numRows) { + if (numRows <= 0) return; size_t currNumRow = origRowIndex.size(); - size_t newNumRow = currNumRow + numCuts; + size_t newNumRow = currNumRow + numRows; origRowIndex.resize(newNumRow); - for (size_t i = currNumRow; i != newNumRow; ++i) - origRowIndex[i] = origNumRow + numRowsAppendedByPresolve + - static_cast(i - currNumRow); + for (size_t i = currNumRow; i != newNumRow; ++i) { + origRowIndex[i] = + origNumRow + startRow + static_cast(i - currNumRow); + rowsAppended[origRowIndex[i]] = static_cast(i); + } + } + + void appendRowsToModel2(HighsInt numRows) { + if (numRows <= 0) return; + size_t currNumRow = origRowIndex.size(); + appendCutsToModel(numRows); + for (size_t i = currNumRow; i != origRowIndex.size(); ++i) + rowsAppended[origRowIndex[i]] = static_cast(i); } HighsInt computeNumOrigRows(HighsInt numRowsAppended) { @@ -308,19 +318,33 @@ class HighsPostsolveStack { if (numRowsAppended <= 0) return numOrig; for (size_t i = origRowIndex.size(); i > 0; --i) { - if (origRowIndex[i - 1] < origNumRow) break; + if (origRowIndex[i - 1] < origNumRow - numRowsAppended) break; --numOrig; } return numOrig; } + void getAppendedRows(std::vector& appendedRows) { + appendedRows.clear(); + for (const auto& elm : rowsAppended) + if (elm.second != -1) appendedRows.push_back(elm.second); + } + + HighsInt numAppended() { + /*HighsInt cnt = 0; + for (const auto& elm : rowsAppended) + if (elm.second != -1) cnt++; + return cnt;*/ + return rowsAppended.size(); + } + HighsInt getOrigNumRow() const { return origNumRow; } HighsInt getOrigNumCol() const { return origNumCol; } void initializeIndexMaps(HighsInt numRow, HighsInt numCol, - HighsInt numRowPresolve); + const std::vector& rowsAppendedByPresolve); void compressIndexMaps(const std::vector& newRowIndex, const std::vector& newColIndex); @@ -615,7 +639,7 @@ class HighsPostsolveStack { #else for (size_t i = index.size(); i > 0; --i) { assert(static_cast(index[i - 1]) >= i - 1); - if (index[i - 1] < origSize) values[index[i - 1]] = values[i - 1]; + values[index[i - 1]] = values[i - 1]; } #endif } @@ -642,22 +666,24 @@ class HighsPostsolveStack { undoIterateBackwards(solution.col_value, origColIndex, origNumCol); assert(origNumRow >= 0); - assert(numRowsAppendedByPresolve >= 0); - undoIterateBackwards(solution.row_value, origRowIndex, origNumRow); + undoIterateBackwards(solution.row_value, origRowIndex, + origNumRow + rowsAppended.size()); if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); - undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow); + undoIterateBackwards(solution.row_dual, origRowIndex, + origNumRow + rowsAppended.size()); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - undoIterateBackwards(basis.row_status, origRowIndex, origNumRow); + undoIterateBackwards(basis.row_status, origRowIndex, + origNumRow + rowsAppended.size()); } // now undo the changes @@ -772,9 +798,17 @@ class HighsPostsolveStack { printf("After last reduction: col_value[%2d] = %g\n", int(report_col), solution.col_value[report_col]); - solution.row_value.resize(origNumRow + numRowsAppendedByPresolve); - solution.row_dual.resize(origNumRow + numRowsAppendedByPresolve); - basis.row_status.resize(origNumRow + numRowsAppendedByPresolve); + for (const auto& elm : rowsAppended) { + if (elm.second == -1) { + solution.row_value[elm.first] = 0.0; + if (perform_dual_postsolve) solution.row_dual[elm.first] = 0.0; + if (perform_basis_postsolve) + basis.row_status[elm.first] = HighsBasisStatus::kLower; + } + } + solution.row_value.resize(origNumRow); + solution.row_dual.resize(origNumRow); + basis.row_status.resize(origNumRow); #ifdef DEBUG_EXTRA // solution should not contain NaN or Inf diff --git a/highs/presolve/PresolveComponent.cpp b/highs/presolve/PresolveComponent.cpp index 9090dd33365..599022289a3 100644 --- a/highs/presolve/PresolveComponent.cpp +++ b/highs/presolve/PresolveComponent.cpp @@ -15,7 +15,7 @@ HighsStatus PresolveComponent::init(const HighsLp& lp, HighsTimer& timer, bool mip) { - data_.postSolveStack.initializeIndexMaps(lp.num_row_, lp.num_col_, lp.num_rows_appended_by_presolve_); + data_.postSolveStack.initializeIndexMaps(lp.num_row_, lp.num_col_, lp.rows_appended_by_presolve_); data_.reduced_lp_ = lp; this->timer = &timer; return HighsStatus::kOk; From a6249a5de64f8b887b672b2a3c25f8a9d332e2b0 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 8 May 2026 10:55:55 +0200 Subject: [PATCH 017/196] WIP --- highs/mip/HighsMipSolverData.cpp | 24 +++++++++++++----------- highs/presolve/HPresolve.cpp | 7 ++----- highs/presolve/HighsPostsolveStack.h | 2 ++ 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 55a1e7bfce8..ae37f9ea64d 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1302,9 +1302,9 @@ void HighsMipSolverData::performRestart() { basis.col_status[i]; for (HighsInt i = 0; i < mipsolver.numRow(); ++i) - //if (postSolveStack.getOrigRowIndex(i) < root_basis.row_status.size()) - root_basis.row_status[postSolveStack.getOrigRowIndex(i)] = - basis.row_status[i]; + // if (postSolveStack.getOrigRowIndex(i) < root_basis.row_status.size()) + root_basis.row_status[postSolveStack.getOrigRowIndex(i)] = + basis.row_status[i]; mipsolver.rootbasis = &root_basis; } @@ -1408,20 +1408,18 @@ void HighsMipSolverData::basisTransfer() { const HighsInt numCol = mipsolver.numCol(); firstrootbasis.col_status.assign(numCol, HighsBasisStatus::kNonbasic); firstrootbasis.row_status.assign(numRow, HighsBasisStatus::kNonbasic); - firstrootbasis.valid = true; + firstrootbasis.valid = false; firstrootbasis.alien = true; - firstrootbasis.useful = true; + firstrootbasis.useful = false; HighsInt numBasicVars = 0; - auto sol = lp.getLpSolver().getSolution(); - - for (HighsInt i = 0; i < numCol; ++i) { + /*for (HighsInt i = 0; i < numCol; ++i) { HighsBasisStatus status = mipsolver.rootbasis->col_status[postSolveStack.getOrigColIndex(i)]; firstrootbasis.col_status[i] = status; if (status == HighsBasisStatus::kBasic) numBasicVars++; } - for (HighsInt i = 0; i < postSolveStack.getOrigNumRow(); ++i) { + for (HighsInt i = 0; i < postSolveStack.getOrigRowIndexSize(); ++i) { HighsBasisStatus status = mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; firstrootbasis.row_status[i] = status; @@ -1429,9 +1427,9 @@ void HighsMipSolverData::basisTransfer() { } for (HighsInt i = 0; i < numRow - numBasicVars; i++) { - firstrootbasis.row_status[postSolveStack.getOrigNumRow() + i] = + firstrootbasis.row_status[postSolveStack.getOrigRowIndexSize() + i] = HighsBasisStatus::kBasic; - } + }*/ /*for (HighsInt i = 0; i < numRow; ++i) { HighsBasisStatus status = @@ -1439,6 +1437,10 @@ void HighsMipSolverData::basisTransfer() { firstrootbasis.row_status[i] = status; if (status == HighsBasisStatus::kBasic) numBasicVars++; }*/ + + for (HighsInt i = 0; i < numRow; ++i) { + firstrootbasis.row_status[i] = HighsBasisStatus::kBasic; + } } const std::vector& HighsMipSolverData::getSolution() const { diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 130668175e3..b0f5fbe27e8 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6457,7 +6457,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_ - numAppendedRows - 1; i >= 0; --i) { + for (HighsInt i = postsolve_stack.getOrigRowIndexSize() - 1; i >= 0; --i) { // check if we already reached the original rows if (postsolve_stack.getOrigRowIndex(i) < mipsolver->orig_model_->num_row_) @@ -6484,10 +6484,7 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { for (HighsInt j : rowpositions) unlink(j); } - model->num_row_ -= numcuts; - model->row_lower_.resize(model->num_row_); - model->row_upper_.resize(model->num_row_); - model->row_names_.resize(model->num_row_); + shrinkProblem(postsolve_stack); } } diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 00f2df27563..08ec24720c2 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -267,6 +267,8 @@ class HighsPostsolveStack { const HighsInt* getOrigColsIndex() const { return origColIndex.data(); } + size_t getOrigRowIndexSize() const { return origRowIndex.size(); } + HighsInt getOrigRowIndex(HighsInt row) const { assert(static_cast(row) < origRowIndex.size()); return origRowIndex[row]; From 7fb42415c0c6de762815aea87a012e515f1f73f6 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 8 May 2026 11:19:06 +0200 Subject: [PATCH 018/196] Still WIP --- highs/mip/HighsMipSolverData.cpp | 66 ++++++++++------------------ highs/presolve/HPresolve.cpp | 7 +-- highs/presolve/HighsPostsolveStack.h | 45 ++----------------- highs/presolve/PresolveComponent.cpp | 3 +- 4 files changed, 34 insertions(+), 87 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index ae37f9ea64d..3d1a2037968 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1274,7 +1274,7 @@ void HighsMipSolverData::performRestart() { sb_lp_iterations_before_run = sb_lp_iterations; HighsInt numLpRows = lp.getLp().num_row_; HighsInt numModelRows = mipsolver.numRow(); - HighsInt numCuts = numLpRows - numModelRows + postSolveStack.numAppended(); + HighsInt numCuts = numLpRows - numModelRows; postSolveStack.appendCutsToModel(numCuts); auto integrality = std::move(presolvedModel.integrality_); double offset = presolvedModel.offset_; @@ -1293,7 +1293,7 @@ void HighsMipSolverData::performRestart() { // for the presolved model after the restart root_basis.col_status.resize(postSolveStack.getOrigNumCol()); root_basis.row_status.resize(postSolveStack.getOrigNumRow(), - HighsBasisStatus::kNonbasic); + HighsBasisStatus::kBasic); root_basis.valid = true; root_basis.useful = true; @@ -1301,8 +1301,8 @@ void HighsMipSolverData::performRestart() { root_basis.col_status[postSolveStack.getOrigColIndex(i)] = basis.col_status[i]; - for (HighsInt i = 0; i < mipsolver.numRow(); ++i) - // if (postSolveStack.getOrigRowIndex(i) < root_basis.row_status.size()) + HighsInt numRow = basis.row_status.size(); + for (HighsInt i = 0; i < numRow; ++i) root_basis.row_status[postSolveStack.getOrigRowIndex(i)] = basis.row_status[i]; @@ -1402,44 +1402,26 @@ void HighsMipSolverData::performRestart() { void HighsMipSolverData::basisTransfer() { // if a root basis is given, construct a basis for the root LP from // in the reduced problem space after presolving - if (mipsolver.rootbasis == nullptr) return; - - const HighsInt numRow = mipsolver.numRow(); - const HighsInt numCol = mipsolver.numCol(); - firstrootbasis.col_status.assign(numCol, HighsBasisStatus::kNonbasic); - firstrootbasis.row_status.assign(numRow, HighsBasisStatus::kNonbasic); - firstrootbasis.valid = false; - firstrootbasis.alien = true; - firstrootbasis.useful = false; - HighsInt numBasicVars = 0; - - /*for (HighsInt i = 0; i < numCol; ++i) { - HighsBasisStatus status = - mipsolver.rootbasis->col_status[postSolveStack.getOrigColIndex(i)]; - firstrootbasis.col_status[i] = status; - if (status == HighsBasisStatus::kBasic) numBasicVars++; - } - for (HighsInt i = 0; i < postSolveStack.getOrigRowIndexSize(); ++i) { - HighsBasisStatus status = - mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; - firstrootbasis.row_status[i] = status; - if (status == HighsBasisStatus::kBasic) numBasicVars++; - } - - for (HighsInt i = 0; i < numRow - numBasicVars; i++) { - firstrootbasis.row_status[postSolveStack.getOrigRowIndexSize() + i] = - HighsBasisStatus::kBasic; - }*/ - - /*for (HighsInt i = 0; i < numRow; ++i) { - HighsBasisStatus status = - mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; - firstrootbasis.row_status[i] = status; - if (status == HighsBasisStatus::kBasic) numBasicVars++; - }*/ - - for (HighsInt i = 0; i < numRow; ++i) { - firstrootbasis.row_status[i] = HighsBasisStatus::kBasic; + if (mipsolver.rootbasis) { + const HighsInt numRow = mipsolver.numRow(); + const HighsInt numCol = mipsolver.numCol(); + firstrootbasis.col_status.assign(numCol, HighsBasisStatus::kNonbasic); + firstrootbasis.row_status.assign(numRow, HighsBasisStatus::kNonbasic); + firstrootbasis.valid = true; + firstrootbasis.alien = true; + firstrootbasis.useful = true; + + for (HighsInt i = 0; i < numRow; ++i) { + HighsBasisStatus status = + mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; + firstrootbasis.row_status[i] = status; + } + + for (HighsInt i = 0; i < numCol; ++i) { + HighsBasisStatus status = + mipsolver.rootbasis->col_status[postSolveStack.getOrigColIndex(i)]; + firstrootbasis.col_status[i] = status; + } } } diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b0f5fbe27e8..0d9c72909e0 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2138,7 +2138,7 @@ bool HPresolve::addToMatrix( model->a_matrix_.num_row_ += num_rows; // resize postsolve vectors - postsolve_stack.appendRowsToModel2(num_rows); + postsolve_stack.appendRowsToModel(num_rows); // add row bounds model->row_lower_.insert(model->row_lower_.end(), row_lower.begin(), @@ -6440,7 +6440,7 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { shrinkProblem(postsolve_stack); - postsolve_stack.removeCutsFromModel(numAppendedRows); + postsolve_stack.removeCutsFromModel(numAppendedRows); postsolve_stack.getAppendedRows(model->rows_appended_by_presolve_); @@ -6457,7 +6457,8 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { cutinds.reserve(model->num_col_); cutvals.reserve(model->num_col_); HighsInt numcuts = 0; - for (HighsInt i = postsolve_stack.getOrigRowIndexSize() - 1; i >= 0; --i) { + for (HighsInt i = postsolve_stack.getOrigRowIndexSize() - 1; i >= 0; + --i) { // check if we already reached the original rows if (postsolve_stack.getOrigRowIndex(i) < mipsolver->orig_model_->num_row_) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 08ec24720c2..87ebd4dbf8e 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -295,19 +295,7 @@ class HighsPostsolveStack { origRowIndex.resize(numOrigRows); } - void appendRowsToModel(HighsInt startRow, HighsInt numRows) { - if (numRows <= 0) return; - size_t currNumRow = origRowIndex.size(); - size_t newNumRow = currNumRow + numRows; - origRowIndex.resize(newNumRow); - for (size_t i = currNumRow; i != newNumRow; ++i) { - origRowIndex[i] = - origNumRow + startRow + static_cast(i - currNumRow); - rowsAppended[origRowIndex[i]] = static_cast(i); - } - } - - void appendRowsToModel2(HighsInt numRows) { + void appendRowsToModel(HighsInt numRows) { if (numRows <= 0) return; size_t currNumRow = origRowIndex.size(); appendCutsToModel(numRows); @@ -318,12 +306,10 @@ class HighsPostsolveStack { HighsInt computeNumOrigRows(HighsInt numRowsAppended) { HighsInt numOrig = static_cast(origRowIndex.size()); if (numRowsAppended <= 0) return numOrig; - for (size_t i = origRowIndex.size(); i > 0; --i) { if (origRowIndex[i - 1] < origNumRow - numRowsAppended) break; --numOrig; } - return numOrig; } @@ -333,14 +319,6 @@ class HighsPostsolveStack { if (elm.second != -1) appendedRows.push_back(elm.second); } - HighsInt numAppended() { - /*HighsInt cnt = 0; - for (const auto& elm : rowsAppended) - if (elm.second != -1) cnt++; - return cnt;*/ - return rowsAppended.size(); - } - HighsInt getOrigNumRow() const { return origNumRow; } HighsInt getOrigNumCol() const { return origNumCol; } @@ -668,24 +646,21 @@ class HighsPostsolveStack { undoIterateBackwards(solution.col_value, origColIndex, origNumCol); assert(origNumRow >= 0); - undoIterateBackwards(solution.row_value, origRowIndex, - origNumRow + rowsAppended.size()); + undoIterateBackwards(solution.row_value, origRowIndex, origNumRow); if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); - undoIterateBackwards(solution.row_dual, origRowIndex, - origNumRow + rowsAppended.size()); + undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - undoIterateBackwards(basis.row_status, origRowIndex, - origNumRow + rowsAppended.size()); + undoIterateBackwards(basis.row_status, origRowIndex, origNumRow); } // now undo the changes @@ -800,18 +775,6 @@ class HighsPostsolveStack { printf("After last reduction: col_value[%2d] = %g\n", int(report_col), solution.col_value[report_col]); - for (const auto& elm : rowsAppended) { - if (elm.second == -1) { - solution.row_value[elm.first] = 0.0; - if (perform_dual_postsolve) solution.row_dual[elm.first] = 0.0; - if (perform_basis_postsolve) - basis.row_status[elm.first] = HighsBasisStatus::kLower; - } - } - solution.row_value.resize(origNumRow); - solution.row_dual.resize(origNumRow); - basis.row_status.resize(origNumRow); - #ifdef DEBUG_EXTRA // solution should not contain NaN or Inf assert(!containsNanOrInf(solution.col_value)); diff --git a/highs/presolve/PresolveComponent.cpp b/highs/presolve/PresolveComponent.cpp index 599022289a3..5461335964d 100644 --- a/highs/presolve/PresolveComponent.cpp +++ b/highs/presolve/PresolveComponent.cpp @@ -15,7 +15,8 @@ HighsStatus PresolveComponent::init(const HighsLp& lp, HighsTimer& timer, bool mip) { - data_.postSolveStack.initializeIndexMaps(lp.num_row_, lp.num_col_, lp.rows_appended_by_presolve_); + data_.postSolveStack.initializeIndexMaps(lp.num_row_, lp.num_col_, + lp.rows_appended_by_presolve_); data_.reduced_lp_ = lp; this->timer = &timer; return HighsStatus::kOk; From dd7ba14b5db5f97d307f1284701ddf5c402c5b57 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 8 May 2026 13:57:46 +0200 Subject: [PATCH 019/196] WIP --- highs/presolve/HighsPostsolveStack.cpp | 1 + highs/presolve/HighsPostsolveStack.h | 2 ++ 2 files changed, 3 insertions(+) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index b4ff357e496..f5476129b4d 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -513,6 +513,7 @@ void HighsPostsolveStack::ForcingRow::undo( void HighsPostsolveStack::DuplicateRow::undo(const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) const { + bool ismodel = isOrigRow(row); // (removed) cuts may have been used in this reduction. if (!solution.isModelRow(row)) return; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 87ebd4dbf8e..5f7d33c51a9 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -64,6 +64,8 @@ class HighsPostsolveStack { double debug_prev_row_lower = 0; double debug_prev_row_upper = 0; + bool isOrigRow(HighsInt row) const { return false; } + private: /// transform a column x by a linear mapping with a new column x'. /// I.e. substitute x = a * x' + b From d240363f5805ccee6ef5fc29a0791a27a06fa5e6 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 8 May 2026 15:37:58 +0200 Subject: [PATCH 020/196] Pass reference to postsolve stack to undo --- highs/lp_data/HStruct.h | 3 - highs/presolve/HighsPostsolveStack.cpp | 122 +++++++++++++------------ highs/presolve/HighsPostsolveStack.h | 90 ++++++++++-------- 3 files changed, 117 insertions(+), 98 deletions(-) diff --git a/highs/lp_data/HStruct.h b/highs/lp_data/HStruct.h index 14fc62e1c00..5c12c017c08 100644 --- a/highs/lp_data/HStruct.h +++ b/highs/lp_data/HStruct.h @@ -28,9 +28,6 @@ struct HighsSolution { void clear(); void print(const std::string& prefix = "", const std::string& message = "") const; - bool isModelRow(HighsInt row) const { - return static_cast(row) < row_value.size(); - } }; struct HighsObjectiveSolution { diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index f5476129b4d..eea29607a85 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -90,7 +90,8 @@ static HighsBasisStatus computeRowStatus(double dual, } void HighsPostsolveStack::FreeColSubstitution::undo( - const HighsOptions& options, const std::vector& rowValues, + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& rowValues, const std::vector& colValues, HighsSolution& solution, HighsBasis& basis) { // compute primal values @@ -105,7 +106,7 @@ void HighsPostsolveStack::FreeColSubstitution::undo( assert(colCoef != 0); // Row values aren't fully postsolved, so why do this? - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) solution.row_value[row] = static_cast(rowValue + colCoef * solution.col_value[col]); solution.col_value[col] = static_cast((rhs - rowValue) / colCoef); @@ -114,11 +115,11 @@ void HighsPostsolveStack::FreeColSubstitution::undo( if (!solution.dual_valid) return; // compute the row dual value such that reduced cost of basic column is 0 - if (solution.isModelRow(row)) { + if (postsolveStack.isOrigRow(row)) { solution.row_dual[row] = 0; HighsCDouble dualval = colCost; for (const auto& colVal : colValues) { - if (solution.isModelRow(colVal.index)) + if (postsolveStack.isOrigRow(colVal.index)) dualval -= colVal.value * solution.row_dual[colVal.index]; } solution.row_dual[row] = static_cast(dualval / colCoef); @@ -130,7 +131,7 @@ void HighsPostsolveStack::FreeColSubstitution::undo( if (!basis.valid) return; basis.col_status[col] = HighsBasisStatus::kBasic; - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) basis.row_status[row] = computeRowStatus(solution.row_dual[row], rowType); } @@ -155,8 +156,9 @@ static HighsBasisStatus computeStatus(double dual, } void HighsPostsolveStack::DoubletonEquation::undo( - const HighsOptions& options, const std::vector& colValues, - HighsSolution& solution, HighsBasis& basis) const { + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& colValues, HighsSolution& solution, + HighsBasis& basis) const { // retrieve the row and column index, the row side and the two // coefficients then compute the primal values solution.col_value[colSubst] = static_cast( @@ -180,10 +182,10 @@ void HighsPostsolveStack::DoubletonEquation::undo( // multiplier of this row i implicitly increases the dual multiplier of this // doubleton equation row with that scale. HighsCDouble rowDual = 0.0; - if (solution.isModelRow(row)) { + if (postsolveStack.isOrigRow(row)) { solution.row_dual[row] = 0; for (const auto& colVal : colValues) { - if (solution.isModelRow(colVal.index)) + if (postsolveStack.isOrigRow(colVal.index)) rowDual -= colVal.value * solution.row_dual[colVal.index]; } rowDual /= coefSubst; @@ -200,7 +202,7 @@ void HighsPostsolveStack::DoubletonEquation::undo( // so alter the dual multiplier of the row to make the dual multiplier of // column zero double rowDualDelta = solution.col_dual[col] / coef; - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) solution.row_dual[row] = static_cast(rowDual + rowDualDelta); solution.col_dual[col] = 0.0; solution.col_dual[colSubst] = static_cast( @@ -221,7 +223,7 @@ void HighsPostsolveStack::DoubletonEquation::undo( // otherwise make the reduced cost of the substituted column zero and make // that column basic double rowDualDelta = solution.col_dual[colSubst] / coefSubst; - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) solution.row_dual[row] = static_cast(rowDual + rowDualDelta); solution.col_dual[colSubst] = 0.0; solution.col_dual[col] = @@ -232,15 +234,17 @@ void HighsPostsolveStack::DoubletonEquation::undo( if (!basis.valid) return; - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) basis.row_status[row] = computeRowStatus(solution.row_dual[row], rowType); } void HighsPostsolveStack::EqualityRowAddition::undo( - const HighsOptions& options, const std::vector& eqRowValues, - HighsSolution& solution, HighsBasis& basis) const { + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& eqRowValues, HighsSolution& solution, + HighsBasis& basis) const { // (removed) cuts may have been used in this reduction. - if (!solution.isModelRow(row) || !solution.isModelRow(addedEqRow)) return; + if (!postsolveStack.isOrigRow(row) || !postsolveStack.isOrigRow(addedEqRow)) + return; // nothing more to do if the row is zero in the dual solution or there is // no dual solution @@ -256,11 +260,12 @@ void HighsPostsolveStack::EqualityRowAddition::undo( } void HighsPostsolveStack::EqualityRowAdditions::undo( - const HighsOptions& options, const std::vector& eqRowValues, + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& eqRowValues, const std::vector& targetRows, HighsSolution& solution, HighsBasis& basis) const { // a (removed) cut may have been used in this reduction. - if (!solution.isModelRow(addedEqRow)) return; + if (!postsolveStack.isOrigRow(addedEqRow)) return; // nothing more to do if the row is zero in the dual solution or there is // no dual solution @@ -271,7 +276,7 @@ void HighsPostsolveStack::EqualityRowAdditions::undo( // used for adding the equation HighsCDouble eqRowDual = solution.row_dual[addedEqRow]; for (const auto& targetRow : targetRows) { - if (solution.isModelRow(targetRow.index)) + if (postsolveStack.isOrigRow(targetRow.index)) eqRowDual += static_cast(targetRow.value) * solution.row_dual[targetRow.index]; } @@ -281,8 +286,9 @@ void HighsPostsolveStack::EqualityRowAdditions::undo( } void HighsPostsolveStack::ForcingColumn::undo( - const HighsOptions& options, const std::vector& colValues, - HighsSolution& solution, HighsBasis& basis) const { + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& colValues, HighsSolution& solution, + HighsBasis& basis) const { HighsInt nonbasicRow = -1; HighsBasisStatus nonbasicRowStatus = HighsBasisStatus::kNonbasic; double colValFromNonbasicRow = colBound; @@ -295,7 +301,7 @@ void HighsPostsolveStack::ForcingColumn::undo( for (const auto& colVal : colValues) { // Row values aren't fully postsolved, so how can this work? debug_num_use_row_value++; - if (solution.isModelRow(colVal.index)) { + if (postsolveStack.isOrigRow(colVal.index)) { double colValFromRow = solution.row_value[colVal.index] / colVal.value; if (direction * colValFromRow > direction * colValFromNonbasicRow) { nonbasicRow = colVal.index; @@ -345,10 +351,11 @@ void HighsPostsolveStack::ForcingColumn::undo( } void HighsPostsolveStack::ForcingColumnRemovedRow::undo( - const HighsOptions& options, const std::vector& rowValues, - HighsSolution& solution, HighsBasis& basis) const { + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& rowValues, HighsSolution& solution, + HighsBasis& basis) const { // a (removed) cut may have been used in this reduction. - if (!solution.isModelRow(row)) return; + if (!postsolveStack.isOrigRow(row)) return; // we use the row value as storage for the scaled value implied on the // column dual @@ -363,9 +370,9 @@ void HighsPostsolveStack::ForcingColumnRemovedRow::undo( if (basis.valid) basis.row_status[row] = HighsBasisStatus::kBasic; } -void HighsPostsolveStack::SingletonRow::undo(const HighsOptions& options, - HighsSolution& solution, - HighsBasis& basis) const { +void HighsPostsolveStack::SingletonRow::undo( + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + HighsSolution& solution, HighsBasis& basis) const { // nothing to do if the rows dual value is zero in the dual solution or // there is no dual solution if (!solution.dual_valid) return; @@ -381,7 +388,7 @@ void HighsPostsolveStack::SingletonRow::undo(const HighsOptions& options, (!colUpperTightened || colStatus != HighsBasisStatus::kUpper)) { // the tightened bound is not used in the basic solution // hence we simply make the row basic and give it a dual multiplier of 0 - if (solution.isModelRow(row)) { + if (postsolveStack.isOrigRow(row)) { if (basis.valid) basis.row_status[row] = HighsBasisStatus::kBasic; solution.row_dual[row] = 0; } @@ -390,13 +397,13 @@ void HighsPostsolveStack::SingletonRow::undo(const HighsOptions& options, // choose the row dual value such that the columns reduced cost becomes // zero - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) solution.row_dual[row] = solution.col_dual[col] / coef; solution.col_dual[col] = 0; if (!basis.valid) return; - if (solution.isModelRow(row)) { + if (postsolveStack.isOrigRow(row)) { switch (colStatus) { case HighsBasisStatus::kLower: assert(colLowerTightened); @@ -426,10 +433,10 @@ void HighsPostsolveStack::SingletonRow::undo(const HighsOptions& options, } // column fixed to lower or upper bound -void HighsPostsolveStack::FixedCol::undo(const HighsOptions& options, - const std::vector& colValues, - HighsSolution& solution, - HighsBasis& basis) const { +void HighsPostsolveStack::FixedCol::undo( + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& colValues, HighsSolution& solution, + HighsBasis& basis) const { // set solution value solution.col_value[col] = fixValue; @@ -439,7 +446,7 @@ void HighsPostsolveStack::FixedCol::undo(const HighsOptions& options, HighsCDouble reducedCost = colCost; for (const auto& colVal : colValues) { - if (solution.isModelRow(colVal.index)) + if (postsolveStack.isOrigRow(colVal.index)) reducedCost -= colVal.value * solution.row_dual[colVal.index]; } @@ -455,11 +462,11 @@ void HighsPostsolveStack::FixedCol::undo(const HighsOptions& options, } } -void HighsPostsolveStack::RedundantRow::undo(const HighsOptions& options, - HighsSolution& solution, - HighsBasis& basis) const { +void HighsPostsolveStack::RedundantRow::undo( + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + HighsSolution& solution, HighsBasis& basis) const { // a (removed) cut may have been used in this reduction. - if (!solution.isModelRow(row)) return; + if (!postsolveStack.isOrigRow(row)) return; // set row dual to zero if dual solution requested if (!solution.dual_valid) return; @@ -470,8 +477,9 @@ void HighsPostsolveStack::RedundantRow::undo(const HighsOptions& options, } void HighsPostsolveStack::ForcingRow::undo( - const HighsOptions& options, const std::vector& rowValues, - HighsSolution& solution, HighsBasis& basis) const { + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& rowValues, HighsSolution& solution, + HighsBasis& basis) const { if (!solution.dual_valid) return; // compute the row dual multiplier and determine the new basic column @@ -490,7 +498,7 @@ void HighsPostsolveStack::ForcingRow::undo( } if (basicCol != -1) { - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) solution.row_dual[row] = solution.row_dual[row] + dualDelta; for (const auto& rowVal : rowValues) { solution.col_dual[rowVal.index] = static_cast( @@ -500,7 +508,7 @@ void HighsPostsolveStack::ForcingRow::undo( solution.col_dual[basicCol] = 0; if (basis.valid) { - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) basis.row_status[row] = (rowType == RowType::kGeq ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper); @@ -510,18 +518,17 @@ void HighsPostsolveStack::ForcingRow::undo( } } -void HighsPostsolveStack::DuplicateRow::undo(const HighsOptions& options, - HighsSolution& solution, - HighsBasis& basis) const { - bool ismodel = isOrigRow(row); +void HighsPostsolveStack::DuplicateRow::undo( + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + HighsSolution& solution, HighsBasis& basis) const { // (removed) cuts may have been used in this reduction. - if (!solution.isModelRow(row)) return; + if (!postsolveStack.isOrigRow(row)) return; if (!solution.dual_valid) return; if (!rowUpperTightened && !rowLowerTightened) { // simple case of row2 being redundant, in which case it just gets a // dual multiplier of 0 and is made basic - if (solution.isModelRow(duplicateRow)) { + if (postsolveStack.isOrigRow(duplicateRow)) { solution.row_dual[duplicateRow] = 0.0; if (basis.valid) basis.row_status[duplicateRow] = HighsBasisStatus::kBasic; @@ -538,7 +545,7 @@ void HighsPostsolveStack::DuplicateRow::undo(const HighsOptions& options, auto computeRowDualAndStatus = [&](bool tightened) { if (tightened) { - if (solution.isModelRow(duplicateRow)) { + if (postsolveStack.isOrigRow(duplicateRow)) { solution.row_dual[duplicateRow] = solution.row_dual[row] / duplicateRowScale; if (basis.valid) { @@ -550,7 +557,7 @@ void HighsPostsolveStack::DuplicateRow::undo(const HighsOptions& options, } solution.row_dual[row] = 0.0; if (basis.valid) basis.row_status[row] = HighsBasisStatus::kBasic; - } else if (solution.isModelRow(duplicateRow)) { + } else if (postsolveStack.isOrigRow(duplicateRow)) { solution.row_dual[duplicateRow] = 0.0; if (basis.valid) basis.row_status[duplicateRow] = HighsBasisStatus::kBasic; @@ -564,7 +571,7 @@ void HighsPostsolveStack::DuplicateRow::undo(const HighsOptions& options, switch (rowStatus) { case HighsBasisStatus::kBasic: // if row is basic the parallel row is also basic - if (solution.isModelRow(duplicateRow)) { + if (postsolveStack.isOrigRow(duplicateRow)) { solution.row_dual[duplicateRow] = 0.0; if (basis.valid) basis.row_status[duplicateRow] = HighsBasisStatus::kBasic; @@ -1300,8 +1307,9 @@ void HighsPostsolveStack::DuplicateColumn::transformToPresolvedSpace( } void HighsPostsolveStack::SlackColSubstitution::undo( - const HighsOptions& options, const std::vector& rowValues, - HighsSolution& solution, HighsBasis& basis) { + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& rowValues, HighsSolution& solution, + HighsBasis& basis) { bool debug_print = false; // May have to determine row dual and basis status unless doing // primal-only transformation in MIP solver, in which case row may @@ -1320,7 +1328,7 @@ void HighsPostsolveStack::SlackColSubstitution::undo( assert(colCoef != 0); // Row values aren't fully postsolved, so why do this? - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) solution.row_value[row] = static_cast(rowValue + colCoef * solution.col_value[col]); @@ -1330,14 +1338,14 @@ void HighsPostsolveStack::SlackColSubstitution::undo( if (!solution.dual_valid) return; // Row retains its dual value, and column has this dual value scaled by coeff - if (solution.isModelRow(row)) + if (postsolveStack.isOrigRow(row)) solution.col_dual[col] = -solution.row_dual[row] / colCoef; // Set basis status if necessary if (!basis.valid) return; // If row is basic, then slack is basic, otherwise row retains its status - if (solution.isModelRow(row)) { + if (postsolveStack.isOrigRow(row)) { HighsBasisStatus save_row_basis_status = basis.row_status[row]; if (basis.row_status[row] == HighsBasisStatus::kBasic) { basis.col_status[col] = HighsBasisStatus::kBasic; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 5f7d33c51a9..6c5184e8dc8 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -64,8 +64,6 @@ class HighsPostsolveStack { double debug_prev_row_lower = 0; double debug_prev_row_upper = 0; - bool isOrigRow(HighsInt row) const { return false; } - private: /// transform a column x by a linear mapping with a new column x'. /// I.e. substitute x = a * x' + b @@ -86,7 +84,8 @@ class HighsPostsolveStack { HighsInt col; RowType rowType; - void undo(const HighsOptions& options, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, const std::vector& rowValues, const std::vector& colValues, HighsSolution& solution, HighsBasis& basis); @@ -106,7 +105,8 @@ class HighsPostsolveStack { bool upperTightened; RowType rowType; - void undo(const HighsOptions& options, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, const std::vector& colValues, HighsSolution& solution, HighsBasis& basis) const; }; @@ -116,7 +116,8 @@ class HighsPostsolveStack { HighsInt addedEqRow; double eqRowScale; - void undo(const HighsOptions& options, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, const std::vector& eqRowValues, HighsSolution& solution, HighsBasis& basis) const; }; @@ -124,7 +125,8 @@ class HighsPostsolveStack { struct EqualityRowAdditions { HighsInt addedEqRow; - void undo(const HighsOptions& options, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, const std::vector& eqRowValues, const std::vector& targetRows, HighsSolution& solution, HighsBasis& basis) const; @@ -136,7 +138,8 @@ class HighsPostsolveStack { bool colLowerTightened; bool colUpperTightened; - void undo(const HighsOptions& options, HighsSolution& solution, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) const; }; @@ -147,7 +150,8 @@ class HighsPostsolveStack { HighsInt col; HighsBasisStatus fixType; - void undo(const HighsOptions& options, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, const std::vector& colValues, HighsSolution& solution, HighsBasis& basis) const; }; @@ -155,7 +159,8 @@ class HighsPostsolveStack { struct RedundantRow { HighsInt row; - void undo(const HighsOptions& options, HighsSolution& solution, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) const; }; @@ -164,7 +169,8 @@ class HighsPostsolveStack { HighsInt row; RowType rowType; - void undo(const HighsOptions& options, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, const std::vector& rowValues, HighsSolution& solution, HighsBasis& basis) const; }; @@ -176,7 +182,8 @@ class HighsPostsolveStack { bool atInfiniteUpper; bool colIntegral; - void undo(const HighsOptions& options, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, const std::vector& colValues, HighsSolution& solution, HighsBasis& basis) const; }; @@ -184,7 +191,8 @@ class HighsPostsolveStack { struct ForcingColumnRemovedRow { double rhs; HighsInt row; - void undo(const HighsOptions& options, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, const std::vector& rowValues, HighsSolution& solution, HighsBasis& basis) const; }; @@ -196,7 +204,8 @@ class HighsPostsolveStack { bool rowLowerTightened; bool rowUpperTightened; - void undo(const HighsOptions& options, HighsSolution& solution, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) const; }; @@ -224,7 +233,8 @@ class HighsPostsolveStack { HighsInt row; HighsInt col; - void undo(const HighsOptions& options, + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, const std::vector& rowValues, HighsSolution& solution, HighsBasis& basis); }; @@ -264,6 +274,10 @@ class HighsPostsolveStack { reductions.emplace_back(type, position); } + bool isOrigRow(HighsInt row) const { + return row < origNumRow && rowsAppended.find(row) == rowsAppended.end(); + } + public: const HighsInt* getOrigRowsIndex() const { return origRowIndex.data(); } @@ -683,21 +697,21 @@ class HighsPostsolveStack { reductionValues.pop(colValues); reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, colValues, solution, basis); + reduction.undo(*this, options, rowValues, colValues, solution, basis); break; } case ReductionType::kDoubletonEquation: { DoubletonEquation reduction; reductionValues.pop(colValues); reductionValues.pop(reduction); - reduction.undo(options, colValues, solution, basis); + reduction.undo(*this, options, colValues, solution, basis); break; } case ReductionType::kEqualityRowAddition: { EqualityRowAddition reduction; reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reduction.undo(*this, options, rowValues, solution, basis); break; } case ReductionType::kEqualityRowAdditions: { @@ -705,53 +719,53 @@ class HighsPostsolveStack { reductionValues.pop(colValues); reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, colValues, solution, basis); + reduction.undo(*this, options, rowValues, colValues, solution, basis); break; } case ReductionType::kSingletonRow: { SingletonRow reduction; reductionValues.pop(reduction); - reduction.undo(options, solution, basis); + reduction.undo(*this, options, solution, basis); break; } case ReductionType::kFixedCol: { FixedCol reduction; reductionValues.pop(colValues); reductionValues.pop(reduction); - reduction.undo(options, colValues, solution, basis); + reduction.undo(*this, options, colValues, solution, basis); break; } case ReductionType::kRedundantRow: { RedundantRow reduction; reductionValues.pop(reduction); - reduction.undo(options, solution, basis); + reduction.undo(*this, options, solution, basis); break; } case ReductionType::kForcingRow: { ForcingRow reduction; reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reduction.undo(*this, options, rowValues, solution, basis); break; } case ReductionType::kForcingColumn: { ForcingColumn reduction; reductionValues.pop(colValues); reductionValues.pop(reduction); - reduction.undo(options, colValues, solution, basis); + reduction.undo(*this, options, colValues, solution, basis); break; } case ReductionType::kForcingColumnRemovedRow: { ForcingColumnRemovedRow reduction; reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reduction.undo(*this, options, rowValues, solution, basis); break; } case ReductionType::kDuplicateRow: { DuplicateRow reduction; reductionValues.pop(reduction); - reduction.undo(options, solution, basis); + reduction.undo(*this, options, solution, basis); break; } case ReductionType::kDuplicateColumn: { @@ -764,7 +778,7 @@ class HighsPostsolveStack { SlackColSubstitution reduction; reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reduction.undo(*this, options, rowValues, solution, basis); break; } default: @@ -866,21 +880,21 @@ class HighsPostsolveStack { reductionValues.pop(colValues); reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, colValues, solution, basis); + reduction.undo(*this, options, rowValues, colValues, solution, basis); break; } case ReductionType::kDoubletonEquation: { DoubletonEquation reduction; reductionValues.pop(colValues); reductionValues.pop(reduction); - reduction.undo(options, colValues, solution, basis); + reduction.undo(*this, options, colValues, solution, basis); break; } case ReductionType::kEqualityRowAddition: { EqualityRowAddition reduction; reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reduction.undo(*this, options, rowValues, solution, basis); break; } case ReductionType::kEqualityRowAdditions: { @@ -888,53 +902,53 @@ class HighsPostsolveStack { reductionValues.pop(colValues); reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, colValues, solution, basis); + reduction.undo(*this, options, rowValues, colValues, solution, basis); break; } case ReductionType::kSingletonRow: { SingletonRow reduction; reductionValues.pop(reduction); - reduction.undo(options, solution, basis); + reduction.undo(*this, options, solution, basis); break; } case ReductionType::kFixedCol: { FixedCol reduction; reductionValues.pop(colValues); reductionValues.pop(reduction); - reduction.undo(options, colValues, solution, basis); + reduction.undo(*this, options, colValues, solution, basis); break; } case ReductionType::kRedundantRow: { RedundantRow reduction; reductionValues.pop(reduction); - reduction.undo(options, solution, basis); + reduction.undo(*this, options, solution, basis); break; } case ReductionType::kForcingRow: { ForcingRow reduction; reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reduction.undo(*this, options, rowValues, solution, basis); break; } case ReductionType::kForcingColumn: { ForcingColumn reduction; reductionValues.pop(colValues); reductionValues.pop(reduction); - reduction.undo(options, colValues, solution, basis); + reduction.undo(*this, options, colValues, solution, basis); break; } case ReductionType::kForcingColumnRemovedRow: { ForcingColumnRemovedRow reduction; reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reduction.undo(*this, options, rowValues, solution, basis); break; } case ReductionType::kDuplicateRow: { DuplicateRow reduction; reductionValues.pop(reduction); - reduction.undo(options, solution, basis); + reduction.undo(*this, options, solution, basis); break; } case ReductionType::kDuplicateColumn: { @@ -946,7 +960,7 @@ class HighsPostsolveStack { SlackColSubstitution reduction; reductionValues.pop(rowValues); reductionValues.pop(reduction); - reduction.undo(options, rowValues, solution, basis); + reduction.undo(*this, options, rowValues, solution, basis); break; } } From 4917aee898af4651ed5ca4f1ed13a1542af1b92c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 8 May 2026 16:09:50 +0200 Subject: [PATCH 021/196] Still WIP --- highs/mip/HighsMipSolverData.cpp | 3 ++- highs/presolve/HighsPostsolveStack.h | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 3d1a2037968..1a7be720109 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1411,7 +1411,8 @@ void HighsMipSolverData::basisTransfer() { firstrootbasis.alien = true; firstrootbasis.useful = true; - for (HighsInt i = 0; i < numRow; ++i) { + for (HighsInt i = 0; + i < static_cast(postSolveStack.getOrigRowIndexSize()); ++i) { HighsBasisStatus status = mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; firstrootbasis.row_status[i] = status; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 6c5184e8dc8..924eb83e443 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -636,6 +636,7 @@ class HighsPostsolveStack { for (size_t i = index.size(); i > 0; --i) { assert(static_cast(index[i - 1]) >= i - 1); values[index[i - 1]] = values[i - 1]; + values[i - 1] = T{}; } #endif } From f501b7452f7afcdaba68e72c8ac17bbfbf10668b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 8 May 2026 17:24:09 +0200 Subject: [PATCH 022/196] Undo change --- highs/presolve/HighsPostsolveStack.h | 1 - 1 file changed, 1 deletion(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 924eb83e443..6c5184e8dc8 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -636,7 +636,6 @@ class HighsPostsolveStack { for (size_t i = index.size(); i > 0; --i) { assert(static_cast(index[i - 1]) >= i - 1); values[index[i - 1]] = values[i - 1]; - values[i - 1] = T{}; } #endif } From b4b501c323709535a6159ca78426e94a5a4bcfe4 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 11 May 2026 10:20:46 +0200 Subject: [PATCH 023/196] WIP --- highs/presolve/HighsPostsolveStack.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 6c5184e8dc8..44c5f3c8ce3 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -669,6 +669,8 @@ class HighsPostsolveStack { // index space undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); + for (const auto& elm : rowsAppended) + if (elm.second != -1) solution.row_dual[elm.second] = 0; undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow); } @@ -676,6 +678,9 @@ class HighsPostsolveStack { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); + for (const auto& elm : rowsAppended) + if (elm.second != -1) + basis.row_status[elm.second] = HighsBasisStatus::kLower; undoIterateBackwards(basis.row_status, origRowIndex, origNumRow); } From 8c33b10afb5e199da88efa8b32a0d4f37163f2de Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 12 May 2026 14:17:45 +0200 Subject: [PATCH 024/196] Argh, still WIP --- highs/lp_data/HConst.h | 3 +- highs/mip/HighsMipSolverData.cpp | 5 +- highs/presolve/HPresolve.cpp | 4 +- highs/presolve/HighsPostsolveStack.cpp | 1 + highs/presolve/HighsPostsolveStack.h | 95 ++++++++++++++++++++++---- 5 files changed, 90 insertions(+), 18 deletions(-) diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index ed5372ae54f..019b290dfdf 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -248,8 +248,9 @@ enum class HighsBasisStatus : uint8_t { kBasic, // (slack) variable is basic kUpper, // (slack) variable is at its upper bound kZero, // free variable is nonbasic and set to zero - kNonbasic // nonbasic with no specific bound information - useful for users + kNonbasic, // nonbasic with no specific bound information - useful for users // and postsolve + kUninit }; // Types of LP presolve rules diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 1a7be720109..ff92af0e39f 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1413,8 +1413,9 @@ void HighsMipSolverData::basisTransfer() { for (HighsInt i = 0; i < static_cast(postSolveStack.getOrigRowIndexSize()); ++i) { - HighsBasisStatus status = - mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; + HighsInt origIndex = postSolveStack.getOrigRowIndex(i); + if (origIndex >= numRow) break; + HighsBasisStatus status = mipsolver.rootbasis->row_status[origIndex]; firstrootbasis.row_status[i] = status; } diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 0d9c72909e0..5f41fe1db66 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2138,7 +2138,7 @@ bool HPresolve::addToMatrix( model->a_matrix_.num_row_ += num_rows; // resize postsolve vectors - postsolve_stack.appendRowsToModel(num_rows); + postsolve_stack.appendRowsToModel2(num_rows); // add row bounds model->row_lower_.insert(model->row_lower_.end(), row_lower.begin(), @@ -6440,7 +6440,7 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { shrinkProblem(postsolve_stack); - postsolve_stack.removeCutsFromModel(numAppendedRows); + //postsolve_stack.removeCutsFromModel(numAppendedRows - postsolve_stack.getAppendedRows().size()); postsolve_stack.getAppendedRows(model->rows_appended_by_presolve_); diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index eea29607a85..31a09848af6 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -45,6 +45,7 @@ void HighsPostsolveStack::compressIndexMaps( rowsAppended.find(origRowIndex[i]) != rowsAppended.end(); if (newRowIndex[i] == -1) { --numRow; + //if (rowIsAppended) rowsAppended.erase(origRowIndex[i]); if (rowIsAppended) rowsAppended[origRowIndex[i]] = -1; } else { origRowIndex[newRowIndex[i]] = origRowIndex[i]; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 44c5f3c8ce3..c31d837ebf0 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -268,6 +268,7 @@ class HighsPostsolveStack { HighsInt origNumCol = -1; HighsInt origNumRow = -1; std::unordered_map rowsAppended; + HighsInt numRowsAppended = 0; void reductionAdded(ReductionType type) { size_t position = reductionValues.getCurrentDataSize(); @@ -275,7 +276,7 @@ class HighsPostsolveStack { } bool isOrigRow(HighsInt row) const { - return row < origNumRow && rowsAppended.find(row) == rowsAppended.end(); + return row < origNumRow + numRowsAppended; } public: @@ -304,6 +305,15 @@ class HighsPostsolveStack { origRowIndex[i] = origNumRow++; } + void appendCutsToModel2(HighsInt numCuts) { + if (numCuts <= 0) return; + size_t currNumRow = origRowIndex.size(); + size_t newNumRow = currNumRow + numCuts; + origRowIndex.resize(newNumRow); + for (size_t i = currNumRow; i != newNumRow; ++i) + origRowIndex[i] = origNumRow + (numRowsAppended++); + } + void removeCutsFromModel(HighsInt numCuts) { if (numCuts <= 0) return; HighsInt numOrigRows = computeNumOrigRows(numCuts); @@ -319,6 +329,14 @@ class HighsPostsolveStack { rowsAppended[origRowIndex[i]] = static_cast(i); } + void appendRowsToModel2(HighsInt numRows) { + if (numRows <= 0) return; + size_t currNumRow = origRowIndex.size(); + appendCutsToModel2(numRows); + for (size_t i = currNumRow; i != origRowIndex.size(); ++i) + rowsAppended[origRowIndex[i]] = static_cast(i); + } + HighsInt computeNumOrigRows(HighsInt numRowsAppended) { HighsInt numOrig = static_cast(origRowIndex.size()); if (numRowsAppended <= 0) return numOrig; @@ -329,16 +347,18 @@ class HighsPostsolveStack { return numOrig; } - void getAppendedRows(std::vector& appendedRows) { - appendedRows.clear(); + void getAppendedRows(std::vector& rows) const { + rows.clear(); for (const auto& elm : rowsAppended) - if (elm.second != -1) appendedRows.push_back(elm.second); + if (elm.second != -1) rows.push_back(elm.second); } HighsInt getOrigNumRow() const { return origNumRow; } HighsInt getOrigNumCol() const { return origNumCol; } + HighsInt getNumRowsAppended() const { return numRowsAppended; } + void initializeIndexMaps(HighsInt numRow, HighsInt numCol, const std::vector& rowsAppendedByPresolve); @@ -621,8 +641,8 @@ class HighsPostsolveStack { template void undoIterateBackwards(std::vector& values, const std::vector& index, - HighsInt origSize) { - values.resize(origSize); + HighsInt origSize, HighsInt numAppended = 0) { + values.resize(origSize + numAppended); #ifdef DEBUG_EXTRA // Fill vector with NaN for debugging purposes std::vector valuesNew; @@ -640,6 +660,22 @@ class HighsPostsolveStack { #endif } + template + void undoIterateBackwards2(std::vector& values, + const std::vector& index, + HighsInt origSize, HighsInt numAppended = 0) { + values.resize(origSize + numAppended); + + // Fill vector with NaN for debugging purposes + std::vector valuesNew; + valuesNew.resize(origSize + numAppended, HighsBasisStatus::kUninit); + for (size_t i = index.size(); i > 0; --i) { + assert(static_cast(index[i - 1]) >= i - 1); + valuesNew[index[i - 1]] = values[i - 1]; + } + std::copy(valuesNew.cbegin(), valuesNew.cend(), values.begin()); + } + /// check if vector contains NaN or Inf bool containsNanOrInf(const std::vector& v) const { return std::find_if(v.cbegin(), v.cend(), [](const double& d) { @@ -662,26 +698,29 @@ class HighsPostsolveStack { undoIterateBackwards(solution.col_value, origColIndex, origNumCol); assert(origNumRow >= 0); - undoIterateBackwards(solution.row_value, origRowIndex, origNumRow); + undoIterateBackwards(solution.row_value, origRowIndex, origNumRow, + numRowsAppended); if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); - for (const auto& elm : rowsAppended) - if (elm.second != -1) solution.row_dual[elm.second] = 0; - undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow); + /*for (const auto& elm : rowsAppended) + if (elm.second != -1) solution.row_dual[elm.second] = 0;*/ + undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow, + numRowsAppended); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - for (const auto& elm : rowsAppended) + /*for (const auto& elm : rowsAppended) if (elm.second != -1) - basis.row_status[elm.second] = HighsBasisStatus::kLower; - undoIterateBackwards(basis.row_status, origRowIndex, origNumRow); + basis.row_status[elm.second] = HighsBasisStatus::kLower;*/ + undoIterateBackwards2(basis.row_status, origRowIndex, origNumRow, + numRowsAppended); } // now undo the changes @@ -796,6 +835,36 @@ class HighsPostsolveStack { printf("After last reduction: col_value[%2d] = %g\n", int(report_col), solution.col_value[report_col]); + solution.row_value.resize(origNumRow); + if (perform_dual_postsolve) solution.row_dual.resize(origNumRow); + + if (perform_basis_postsolve) { + HighsInt numBasics = 0; + for (auto e : basis.col_status) + if (e == HighsBasisStatus::kBasic) numBasics++; + for (auto e : basis.row_status) + if (e == HighsBasisStatus::kBasic) numBasics++; + assert(numBasics == origNumRow + numRowsAppended); + + /*for (HighsInt i = 0; i < static_cast(origRowIndex.size()); + ++i) { + HighsInt origIndex = origRowIndex[i]; + if (origIndex >= origNumRow) break; + row_status[i] = basis.row_status[origIndex]; + }*/ + + /*for (const auto& elm : rowsAppended) + basis.row_status[elm.first] = HighsBasisStatus::kLower;*/ + basis.row_status.resize(origNumRow); + + numBasics = 0; + for (auto e : basis.col_status) + if (e == HighsBasisStatus::kBasic) numBasics++; + for (auto e : basis.row_status) + if (e == HighsBasisStatus::kBasic) numBasics++; + assert(numBasics == origNumRow); + } + #ifdef DEBUG_EXTRA // solution should not contain NaN or Inf assert(!containsNanOrInf(solution.col_value)); From 86382415e6cf9fca6fee6ba002e07103301b1bca Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 12 May 2026 15:17:19 +0200 Subject: [PATCH 025/196] WIP --- highs/presolve/HighsPostsolveStack.h | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index c31d837ebf0..c16a8440dd8 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -839,12 +839,13 @@ class HighsPostsolveStack { if (perform_dual_postsolve) solution.row_dual.resize(origNumRow); if (perform_basis_postsolve) { - HighsInt numBasics = 0; + HighsInt numColBasics = 0; + HighsInt numRowBasics = 0; for (auto e : basis.col_status) - if (e == HighsBasisStatus::kBasic) numBasics++; + if (e == HighsBasisStatus::kBasic) numColBasics++; for (auto e : basis.row_status) - if (e == HighsBasisStatus::kBasic) numBasics++; - assert(numBasics == origNumRow + numRowsAppended); + if (e == HighsBasisStatus::kBasic) numRowBasics++; + assert(numColBasics + numRowBasics == origNumRow + numRowsAppended); /*for (HighsInt i = 0; i < static_cast(origRowIndex.size()); ++i) { @@ -853,16 +854,18 @@ class HighsPostsolveStack { row_status[i] = basis.row_status[origIndex]; }*/ - /*for (const auto& elm : rowsAppended) - basis.row_status[elm.first] = HighsBasisStatus::kLower;*/ + for (const auto& elm : rowsAppended) + if (elm.first < origNumRow) + basis.row_status[elm.first] = HighsBasisStatus::kLower; basis.row_status.resize(origNumRow); - numBasics = 0; + numColBasics = 0; + numRowBasics = 0; for (auto e : basis.col_status) - if (e == HighsBasisStatus::kBasic) numBasics++; + if (e == HighsBasisStatus::kBasic) numColBasics++; for (auto e : basis.row_status) - if (e == HighsBasisStatus::kBasic) numBasics++; - assert(numBasics == origNumRow); + if (e == HighsBasisStatus::kBasic) numRowBasics++; + assert(numColBasics + numRowBasics == origNumRow); } #ifdef DEBUG_EXTRA From 67377ce273acfc4bcbba00016d6d2ff850d9dedf Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 13 May 2026 10:51:40 +0200 Subject: [PATCH 026/196] WIP --- highs/presolve/HighsPostsolveStack.h | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index c16a8440dd8..b7a987e90fa 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -847,17 +847,7 @@ class HighsPostsolveStack { if (e == HighsBasisStatus::kBasic) numRowBasics++; assert(numColBasics + numRowBasics == origNumRow + numRowsAppended); - /*for (HighsInt i = 0; i < static_cast(origRowIndex.size()); - ++i) { - HighsInt origIndex = origRowIndex[i]; - if (origIndex >= origNumRow) break; - row_status[i] = basis.row_status[origIndex]; - }*/ - - for (const auto& elm : rowsAppended) - if (elm.first < origNumRow) - basis.row_status[elm.first] = HighsBasisStatus::kLower; - basis.row_status.resize(origNumRow); + // Add code to shrink basis (without appended rows) numColBasics = 0; numRowBasics = 0; From f6d3ab94a25531e3da81af88fec09749e178a665 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 13 May 2026 13:52:30 +0200 Subject: [PATCH 027/196] WIP --- highs/presolve/HPresolve.cpp | 30 ++++++++++++++++------------ highs/presolve/HighsPostsolveStack.h | 12 ++--------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5f41fe1db66..b37525fa790 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5943,23 +5943,27 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { // Start of main presolve loop // + bool tryAppendRows = + mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; while (true) { // FOR DEBUGGING NEW METHOD! - std::vector row_lower, row_upper; - std::vector> rows; - for (HighsInt i = 0; i < static_cast(0.1 * model->num_row_); - i++) { - if (rowDeleted[i]) continue; - std::vector row; - for (const auto& rowNz : getRowVector(i)) { - row.push_back(row_entry{rowNz.index(), rowNz.value()}); + if (tryAppendRows) { + std::vector row_lower, row_upper; + std::vector> rows; + for (HighsInt i = 0; i < static_cast(0.1 * model->num_row_); + i++) { + if (rowDeleted[i]) continue; + std::vector row; + for (const auto& rowNz : getRowVector(i)) { + row.push_back(row_entry{rowNz.index(), rowNz.value()}); + } + row_lower.push_back(model->row_lower_[i]); + row_upper.push_back(model->row_upper_[i]); + rows.push_back(row); } - row_lower.push_back(model->row_lower_[i]); - row_upper.push_back(model->row_upper_[i]); - rows.push_back(row); + if (!addToMatrix(postsolve_stack, row_lower, row_upper, rows)) + return Result::kOk; } - if (!addToMatrix(postsolve_stack, row_lower, row_upper, rows)) - return Result::kOk; HighsInt currSize = model->num_col_ - numDeletedCols + model->num_row_ - numDeletedRows; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index b7a987e90fa..067cb21849d 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -839,18 +839,10 @@ class HighsPostsolveStack { if (perform_dual_postsolve) solution.row_dual.resize(origNumRow); if (perform_basis_postsolve) { + assert(numRowsAppended == 0); + basis.row_status.resize(origNumRow); HighsInt numColBasics = 0; HighsInt numRowBasics = 0; - for (auto e : basis.col_status) - if (e == HighsBasisStatus::kBasic) numColBasics++; - for (auto e : basis.row_status) - if (e == HighsBasisStatus::kBasic) numRowBasics++; - assert(numColBasics + numRowBasics == origNumRow + numRowsAppended); - - // Add code to shrink basis (without appended rows) - - numColBasics = 0; - numRowBasics = 0; for (auto e : basis.col_status) if (e == HighsBasisStatus::kBasic) numColBasics++; for (auto e : basis.row_status) From 7708fc1e579b07b3d93ed87e4664bd9b878669e4 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 13 May 2026 14:36:30 +0200 Subject: [PATCH 028/196] And WIP again --- highs/Highs.h | 4 +- highs/lp_data/HConst.h | 3 +- highs/lp_data/HighsLp.cpp | 1 - highs/lp_data/HighsLp.h | 1 - highs/mip/HighsCliqueTable.cpp | 2 +- highs/mip/HighsMipSolverData.cpp | 14 ++-- highs/mip/HighsPseudocost.cpp | 23 ++++--- highs/presolve/HPresolve.cpp | 43 +++++------- highs/presolve/HPresolve.h | 3 - highs/presolve/HighsPostsolveStack.cpp | 17 ++--- highs/presolve/HighsPostsolveStack.h | 94 ++++---------------------- highs/presolve/PresolveComponent.cpp | 3 +- 12 files changed, 60 insertions(+), 148 deletions(-) diff --git a/highs/Highs.h b/highs/Highs.h index aff0eec0c08..43020154efa 100644 --- a/highs/Highs.h +++ b/highs/Highs.h @@ -466,7 +466,7 @@ class Highs { * the presolved model */ const HighsInt* getPresolveOrigColsIndex() const { - return presolve_.data_.postSolveStack.getOrigColsIndex(); + return presolve_.data_.postSolveStack.getOrigColIndex().data(); } /** @@ -474,7 +474,7 @@ class Highs { * presolved model */ const HighsInt* getPresolveOrigRowsIndex() const { - return presolve_.data_.postSolveStack.getOrigRowsIndex(); + return presolve_.data_.postSolveStack.getOrigRowIndex().data(); } /** diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index 019b290dfdf..ed5372ae54f 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -248,9 +248,8 @@ enum class HighsBasisStatus : uint8_t { kBasic, // (slack) variable is basic kUpper, // (slack) variable is at its upper bound kZero, // free variable is nonbasic and set to zero - kNonbasic, // nonbasic with no specific bound information - useful for users + kNonbasic // nonbasic with no specific bound information - useful for users // and postsolve - kUninit }; // Types of LP presolve rules diff --git a/highs/lp_data/HighsLp.cpp b/highs/lp_data/HighsLp.cpp index d6fbb52369f..ceb4de3e51c 100644 --- a/highs/lp_data/HighsLp.cpp +++ b/highs/lp_data/HighsLp.cpp @@ -227,7 +227,6 @@ void HighsLp::clear() { this->cost_row_location_ = -1; this->has_infinite_cost_ = false; this->mods_.clear(); - this->rows_appended_by_presolve_.clear(); } void HighsLp::clearScale() { diff --git a/highs/lp_data/HighsLp.h b/highs/lp_data/HighsLp.h index c464b5b9b42..e3dc241593e 100644 --- a/highs/lp_data/HighsLp.h +++ b/highs/lp_data/HighsLp.h @@ -56,7 +56,6 @@ class HighsLp { HighsInt cost_row_location_; bool has_infinite_cost_; HighsLpMods mods_; - std::vector rows_appended_by_presolve_; bool operator==(const HighsLp& lp) const; bool equalButForNames(const HighsLp& lp) const; diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index 7907cf90e83..03963d733ec 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -1282,7 +1282,7 @@ void HighsCliqueTable::extractCliques(HighsMipSolver& mipsolver, HighsInt start = mipsolver.mipdata_->ARstart_[i]; HighsInt end = mipsolver.mipdata_->ARstart_[i + 1]; - if (mipsolver.mipdata_->postSolveStack.getOrigRowIndex(i) >= + if (mipsolver.mipdata_->postSolveStack.getOrigRowIndex()[i] >= mipsolver.orig_model_->num_row_) break; diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 8f3f6347e4e..5b7b375d3e1 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -689,8 +689,7 @@ void HighsMipSolverData::removeFixedIndices() { void HighsMipSolverData::init() { postSolveStack.initializeIndexMaps( - mipsolver.numRow(), mipsolver.numCol(), - mipsolver.model_->rows_appended_by_presolve_); + mipsolver.numRow(), mipsolver.numCol()); mipsolver.orig_model_ = mipsolver.model_; feastol = mipsolver.options_mip_->mip_feasibility_tolerance; epsilon = mipsolver.options_mip_->small_matrix_value; @@ -1298,12 +1297,12 @@ void HighsMipSolverData::performRestart() { root_basis.useful = true; for (HighsInt i = 0; i < mipsolver.numCol(); ++i) - root_basis.col_status[postSolveStack.getOrigColIndex(i)] = + root_basis.col_status[postSolveStack.getOrigColIndex()[i]] = basis.col_status[i]; HighsInt numRow = basis.row_status.size(); for (HighsInt i = 0; i < numRow; ++i) - root_basis.row_status[postSolveStack.getOrigRowIndex(i)] = + root_basis.row_status[postSolveStack.getOrigRowIndex()[i]] = basis.row_status[i]; mipsolver.rootbasis = &root_basis; @@ -1412,8 +1411,9 @@ void HighsMipSolverData::basisTransfer() { firstrootbasis.useful = true; for (HighsInt i = 0; - i < static_cast(postSolveStack.getOrigRowIndexSize()); ++i) { - HighsInt origIndex = postSolveStack.getOrigRowIndex(i); + i < static_cast(postSolveStack.getOrigRowIndex().size()); + ++i) { + HighsInt origIndex = postSolveStack.getOrigRowIndex()[i]; if (origIndex >= numRow) break; HighsBasisStatus status = mipsolver.rootbasis->row_status[origIndex]; firstrootbasis.row_status[i] = status; @@ -1421,7 +1421,7 @@ void HighsMipSolverData::basisTransfer() { for (HighsInt i = 0; i < numCol; ++i) { HighsBasisStatus status = - mipsolver.rootbasis->col_status[postSolveStack.getOrigColIndex(i)]; + 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 a702eed2423..07dcb78501d 100644 --- a/highs/mip/HighsPseudocost.cpp +++ b/highs/mip/HighsPseudocost.cpp @@ -41,7 +41,8 @@ HighsPseudocost::HighsPseudocost(const HighsMipSolver& mipsolver) mipsolver.pscostinit->conflict_avg_score * mipsolver.numCol(); for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { - HighsInt origCol = mipsolver.mipdata_->postSolveStack.getOrigColIndex(i); + HighsInt origCol = + mipsolver.mipdata_->postSolveStack.getOrigColIndex()[i]; pseudocostup[i] = mipsolver.pscostinit->pseudocostup[origCol]; nsamplesup[i] = mipsolver.pscostinit->nsamplesup[origCol]; @@ -109,21 +110,21 @@ HighsPseudocostInitialization::HighsPseudocostInitialization( conflict_avg_score /= ncols * pscost.conflict_weight; for (HighsInt i = 0; i != ncols; ++i) { - pseudocostup[postsolveStack.getOrigColIndex(i)] = pscost.pseudocostup[i]; - pseudocostdown[postsolveStack.getOrigColIndex(i)] = + pseudocostup[postsolveStack.getOrigColIndex()[i]] = pscost.pseudocostup[i]; + pseudocostdown[postsolveStack.getOrigColIndex()[i]] = pscost.pseudocostdown[i]; - nsamplesup[postsolveStack.getOrigColIndex(i)] = + nsamplesup[postsolveStack.getOrigColIndex()[i]] = std::min(maxCount, pscost.nsamplesup[i]); - nsamplesdown[postsolveStack.getOrigColIndex(i)] = + nsamplesdown[postsolveStack.getOrigColIndex()[i]] = std::min(maxCount, pscost.nsamplesdown[i]); - inferencesup[postsolveStack.getOrigColIndex(i)] = pscost.inferencesup[i]; - inferencesdown[postsolveStack.getOrigColIndex(i)] = + inferencesup[postsolveStack.getOrigColIndex()[i]] = pscost.inferencesup[i]; + inferencesdown[postsolveStack.getOrigColIndex()[i]] = pscost.inferencesdown[i]; - ninferencesup[postsolveStack.getOrigColIndex(i)] = 1; - ninferencesdown[postsolveStack.getOrigColIndex(i)] = 1; - conflictscoreup[postsolveStack.getOrigColIndex(i)] = + ninferencesup[postsolveStack.getOrigColIndex()[i]] = 1; + ninferencesdown[postsolveStack.getOrigColIndex()[i]] = 1; + conflictscoreup[postsolveStack.getOrigColIndex()[i]] = pscost.conflictscoreup[i] / pscost.conflict_weight; - conflictscoredown[postsolveStack.getOrigColIndex(i)] = + conflictscoredown[postsolveStack.getOrigColIndex()[i]] = pscost.conflictscoredown[i] / pscost.conflict_weight; } } diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b37525fa790..5915cf86095 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -48,7 +48,7 @@ namespace presolve { void HPresolve::debugPrintRow(HighsPostsolveStack& postsolve_stack, HighsInt row) { printf("(row %" HIGHSINT_FORMAT ") %.15g (impl: %.15g) <= ", - postsolve_stack.getOrigRowIndex(row), model->row_lower_[row], + postsolve_stack.getOrigRowIndex()[row], model->row_lower_[row], impliedRowBounds.getSumLower(row)); for (const HighsSliceNonzero& nonzero : getSortedRowVector(row)) { @@ -59,7 +59,7 @@ void HPresolve::debugPrintRow(HighsPostsolveStack& postsolve_stack, : 'x'; char signchar = nonzero.value() < 0 ? '-' : '+'; printf("%c%g %c%" HIGHSINT_FORMAT " ", signchar, std::abs(nonzero.value()), - colchar, postsolve_stack.getOrigColIndex(nonzero.index())); + colchar, postsolve_stack.getOrigColIndex()[nonzero.index()]); } printf("<= %.15g (impl: %.15g)\n", model->row_upper_[row], @@ -125,7 +125,6 @@ bool HPresolve::okSetInput(HighsLp& model_, const HighsOptions& options_, if (!okReserve(liftingOpportunities, model->num_row_)) return false; numDeletedCols = 0; numDeletedRows = 0; - numAppendedRows = 0; // initialize substitution opportunities for (HighsInt row = 0; row != model->num_row_; ++row) { if (!isDualImpliedFree(row)) continue; @@ -689,11 +688,11 @@ HPresolve::Result HPresolve::updateColImpliedBounds(HighsInt row, HighsInt col, // column as implied free bool useImplBound = mipsolver == nullptr || - mipsolver->mipdata_->postSolveStack.getOrigRowIndex(row) < + mipsolver->mipdata_->postSolveStack.getOrigRowIndex()[row] < mipsolver->orig_model_->num_row_ || - mipsolver->mipdata_->postSolveStack.getOrigRowIndex(row) >= + mipsolver->mipdata_->postSolveStack.getOrigRowIndex()[row] >= mipsolver->mipdata_->postSolveStack.getOrigNumRow() - - numAppendedRows; + mipsolver->mipdata_->postSolveStack.getNumAppendedRows(); if (direction * val > 0) { // upper bound @@ -2132,13 +2131,12 @@ bool HPresolve::addToMatrix( HighsInt num_rows = static_cast(row_entries.size()); if (num_rows == 0) return true; HighsInt oldNumRows = model->num_row_; - HighsInt oldNumRowsAppended = numAppendedRows; - numAppendedRows += num_rows; + HighsInt oldNumRowsAppended = postsolve_stack.getNumAppendedRows(); model->num_row_ += num_rows; model->a_matrix_.num_row_ += num_rows; // resize postsolve vectors - postsolve_stack.appendRowsToModel2(num_rows); + postsolve_stack.appendRowsToModel(num_rows); // add row bounds model->row_lower_.insert(model->row_lower_.end(), row_lower.begin(), @@ -6444,10 +6442,6 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { shrinkProblem(postsolve_stack); - //postsolve_stack.removeCutsFromModel(numAppendedRows - postsolve_stack.getAppendedRows().size()); - - postsolve_stack.getAppendedRows(model->rows_appended_by_presolve_); - if (mipsolver != nullptr) { mipsolver->mipdata_->cliquetable.setPresolveFlag(false); mipsolver->mipdata_->cliquetable.setMaxEntries(numNonzeros()); @@ -6461,10 +6455,10 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { cutinds.reserve(model->num_col_); cutvals.reserve(model->num_col_); HighsInt numcuts = 0; - for (HighsInt i = postsolve_stack.getOrigRowIndexSize() - 1; i >= 0; + for (HighsInt i = postsolve_stack.getOrigRowIndex().size() - 1; i >= 0; --i) { // check if we already reached the original rows - if (postsolve_stack.getOrigRowIndex(i) < + if (postsolve_stack.getOrigRowIndex()[i] < mipsolver->orig_model_->num_row_) break; @@ -6540,8 +6534,7 @@ void HPresolve::computeIntermediateMatrix(std::vector& flagRow, size_t& numreductions) { shrinkProblemEnabled = false; HighsPostsolveStack stack; - stack.initializeIndexMaps(flagRow.size(), flagCol.size(), - model->rows_appended_by_presolve_); + stack.initializeIndexMaps(flagRow.size(), flagCol.size()); setReductionLimit(numreductions); presolve(stack); numreductions = stack.numReductions(); @@ -8230,8 +8223,7 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { model.integrality_.assign(lp.num_col_, HighsVarType::kContinuous); HighsPostsolveStack postsolve_stack; - postsolve_stack.initializeIndexMaps(lp.num_row_, lp.num_col_, - lp.rows_appended_by_presolve_); + postsolve_stack.initializeIndexMaps(lp.num_row_, lp.num_col_); { HPresolve presolve; presolve.okSetInput(model, options, options.presolve_reduction_limit); @@ -8307,8 +8299,7 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { HPresolve presolve; presolve.okSetInput(model, options, options.presolve_reduction_limit); HighsPostsolveStack tmp; - tmp.initializeIndexMaps(model.num_row_, model.num_col_, - model.rows_appended_by_presolve_); + tmp.initializeIndexMaps(model.num_row_, model.num_col_); presolve.setReductionLimit(reductionLim); presolve.run(tmp); @@ -8322,16 +8313,16 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { temp_sol.col_dual.resize(model.num_col_); temp_sol.col_value.resize(model.num_col_); for (HighsInt i = 0; i != model.num_col_; ++i) { - temp_sol.col_dual[i] = sol.col_dual[tmp.getOrigColIndex(i)]; - temp_sol.col_value[i] = sol.col_value[tmp.getOrigColIndex(i)]; - temp_basis.col_status[i] = basis.col_status[tmp.getOrigColIndex(i)]; + temp_sol.col_dual[i] = sol.col_dual[tmp.getOrigColIndex()[i]]; + temp_sol.col_value[i] = sol.col_value[tmp.getOrigColIndex()[i]]; + temp_basis.col_status[i] = basis.col_status[tmp.getOrigColIndex()[i]]; } temp_basis.row_status.resize(model.num_row_); temp_sol.row_dual.resize(model.num_row_); for (HighsInt i = 0; i != model.num_row_; ++i) { - temp_sol.row_dual[i] = sol.row_dual[tmp.getOrigRowIndex(i)]; - temp_basis.row_status[i] = basis.row_status[tmp.getOrigRowIndex(i)]; + temp_sol.row_dual[i] = sol.row_dual[tmp.getOrigRowIndex()[i]]; + temp_basis.row_status[i] = basis.row_status[tmp.getOrigRowIndex()[i]]; } temp_sol.row_value.resize(model.num_row_); calculateRowValuesQuad(model, sol); diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index bcbe3cdd85d..fdc4210c8c2 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -127,9 +127,6 @@ class HPresolve { HighsInt numDeletedRows; HighsInt numDeletedCols; - // counter for number of appended rows - HighsInt numAppendedRows; - // store old problem sizes to compute percentage reductions in // presolve loop HighsInt oldNumCol; diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 4f23da3355d..def7217073a 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -17,14 +17,11 @@ namespace presolve { -void HighsPostsolveStack::initializeIndexMaps( - HighsInt numRow, HighsInt numCol, - const std::vector& rowsAppendedByPresolve) { +void HighsPostsolveStack::initializeIndexMaps(HighsInt numRow, + HighsInt numCol) { origNumRow = numRow; origNumCol = numCol; - for (HighsInt row : rowsAppendedByPresolve) rowsAppended[row] = row; - origRowIndex.resize(numRow); std::iota(origRowIndex.begin(), origRowIndex.end(), 0); @@ -41,16 +38,10 @@ void HighsPostsolveStack::compressIndexMaps( // store original index at new index position otherwise HighsInt numRow = origRowIndex.size(); for (size_t i = 0; i != newRowIndex.size(); ++i) { - bool rowIsAppended = - rowsAppended.find(origRowIndex[i]) != rowsAppended.end(); - if (newRowIndex[i] == -1) { + if (newRowIndex[i] == -1) --numRow; - //if (rowIsAppended) rowsAppended.erase(origRowIndex[i]); - if (rowIsAppended) rowsAppended[origRowIndex[i]] = -1; - } else { + else origRowIndex[newRowIndex[i]] = origRowIndex[i]; - if (rowIsAppended) rowsAppended[origRowIndex[i]] = newRowIndex[i]; - } } // resize original index array to new size origRowIndex.resize(numRow); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 0745ee08044..81a7fd70f32 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -267,8 +267,7 @@ class HighsPostsolveStack { std::vector colValues; HighsInt origNumCol = -1; HighsInt origNumRow = -1; - std::unordered_map rowsAppended; - HighsInt numRowsAppended = 0; + HighsInt numAppendedRows = 0; void reductionAdded(ReductionType type) { size_t position = reductionValues.getCurrentDataSize(); @@ -276,25 +275,13 @@ class HighsPostsolveStack { } bool isOrigRow(HighsInt row) const { - return row < origNumRow + numRowsAppended; + return row < origNumRow + numAppendedRows; } public: - const HighsInt* getOrigRowsIndex() const { return origRowIndex.data(); } + const std::vector& getOrigColIndex() const { return origColIndex; } - const HighsInt* getOrigColsIndex() const { return origColIndex.data(); } - - size_t getOrigRowIndexSize() const { return origRowIndex.size(); } - - HighsInt getOrigRowIndex(HighsInt row) const { - assert(static_cast(row) < origRowIndex.size()); - return origRowIndex[row]; - } - - HighsInt getOrigColIndex(HighsInt col) const { - assert(static_cast(col) < origColIndex.size()); - return origColIndex[col]; - } + const std::vector& getOrigRowIndex() const { return origRowIndex; } void appendCutsToModel(HighsInt numCuts) { if (numCuts <= 0) return; @@ -305,13 +292,13 @@ class HighsPostsolveStack { origRowIndex[i] = origNumRow++; } - void appendCutsToModel2(HighsInt numCuts) { - if (numCuts <= 0) return; + void appendRowsToModel(HighsInt numRows) { + if (numRows <= 0) return; size_t currNumRow = origRowIndex.size(); - size_t newNumRow = currNumRow + numCuts; + size_t newNumRow = currNumRow + numRows; origRowIndex.resize(newNumRow); for (size_t i = currNumRow; i != newNumRow; ++i) - origRowIndex[i] = origNumRow + (numRowsAppended++); + origRowIndex[i] = origNumRow + (numAppendedRows++); } void removeCutsFromModel(HighsInt numCuts) { @@ -321,22 +308,6 @@ class HighsPostsolveStack { origRowIndex.resize(numOrigRows); } - void appendRowsToModel(HighsInt numRows) { - if (numRows <= 0) return; - size_t currNumRow = origRowIndex.size(); - appendCutsToModel(numRows); - for (size_t i = currNumRow; i != origRowIndex.size(); ++i) - rowsAppended[origRowIndex[i]] = static_cast(i); - } - - void appendRowsToModel2(HighsInt numRows) { - if (numRows <= 0) return; - size_t currNumRow = origRowIndex.size(); - appendCutsToModel2(numRows); - for (size_t i = currNumRow; i != origRowIndex.size(); ++i) - rowsAppended[origRowIndex[i]] = static_cast(i); - } - HighsInt computeNumOrigRows(HighsInt numRowsAppended) { HighsInt numOrig = static_cast(origRowIndex.size()); if (numRowsAppended <= 0) return numOrig; @@ -347,20 +318,13 @@ class HighsPostsolveStack { return numOrig; } - void getAppendedRows(std::vector& rows) const { - rows.clear(); - for (const auto& elm : rowsAppended) - if (elm.second != -1) rows.push_back(elm.second); - } - HighsInt getOrigNumRow() const { return origNumRow; } HighsInt getOrigNumCol() const { return origNumCol; } - HighsInt getNumRowsAppended() const { return numRowsAppended; } + HighsInt getNumAppendedRows() const { return numAppendedRows; } - void initializeIndexMaps(HighsInt numRow, HighsInt numCol, - const std::vector& rowsAppendedByPresolve); + void initializeIndexMaps(HighsInt numRow, HighsInt numCol); void compressIndexMaps(const std::vector& newRowIndex, const std::vector& newColIndex); @@ -660,22 +624,6 @@ class HighsPostsolveStack { #endif } - template - void undoIterateBackwards2(std::vector& values, - const std::vector& index, - HighsInt origSize, HighsInt numAppended = 0) { - values.resize(origSize + numAppended); - - // Fill vector with NaN for debugging purposes - std::vector valuesNew; - valuesNew.resize(origSize + numAppended, HighsBasisStatus::kUninit); - for (size_t i = index.size(); i > 0; --i) { - assert(static_cast(index[i - 1]) >= i - 1); - valuesNew[index[i - 1]] = values[i - 1]; - } - std::copy(valuesNew.cbegin(), valuesNew.cend(), values.begin()); - } - /// check if vector contains NaN or Inf bool containsNanOrInf(const std::vector& v) const { return std::find_if(v.cbegin(), v.cend(), [](const double& d) { @@ -699,28 +647,23 @@ class HighsPostsolveStack { assert(origNumRow >= 0); undoIterateBackwards(solution.row_value, origRowIndex, origNumRow, - numRowsAppended); + numAppendedRows); if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); - /*for (const auto& elm : rowsAppended) - if (elm.second != -1) solution.row_dual[elm.second] = 0;*/ undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow, - numRowsAppended); + numAppendedRows); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - /*for (const auto& elm : rowsAppended) - if (elm.second != -1) - basis.row_status[elm.second] = HighsBasisStatus::kLower;*/ - undoIterateBackwards2(basis.row_status, origRowIndex, origNumRow, - numRowsAppended); + undoIterateBackwards(basis.row_status, origRowIndex, origNumRow, + numAppendedRows); } // now undo the changes @@ -839,15 +782,8 @@ class HighsPostsolveStack { if (perform_dual_postsolve) solution.row_dual.resize(origNumRow); if (perform_basis_postsolve) { - assert(numRowsAppended == 0); + assert(numAppendedRows == 0); basis.row_status.resize(origNumRow); - HighsInt numColBasics = 0; - HighsInt numRowBasics = 0; - for (auto e : basis.col_status) - if (e == HighsBasisStatus::kBasic) numColBasics++; - for (auto e : basis.row_status) - if (e == HighsBasisStatus::kBasic) numRowBasics++; - assert(numColBasics + numRowBasics == origNumRow); } #ifdef DEBUG_EXTRA diff --git a/highs/presolve/PresolveComponent.cpp b/highs/presolve/PresolveComponent.cpp index 5461335964d..dcaa16d7364 100644 --- a/highs/presolve/PresolveComponent.cpp +++ b/highs/presolve/PresolveComponent.cpp @@ -15,8 +15,7 @@ HighsStatus PresolveComponent::init(const HighsLp& lp, HighsTimer& timer, bool mip) { - data_.postSolveStack.initializeIndexMaps(lp.num_row_, lp.num_col_, - lp.rows_appended_by_presolve_); + data_.postSolveStack.initializeIndexMaps(lp.num_row_, lp.num_col_); data_.reduced_lp_ = lp; this->timer = &timer; return HighsStatus::kOk; From 6a231052731e2555a6313a12ffdadab71ab26e3a Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 13 May 2026 14:40:41 +0200 Subject: [PATCH 029/196] Fix issue in unsed code --- highs/presolve/HighsPostsolveStack.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 81a7fd70f32..70901ec1d4f 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -610,7 +610,8 @@ class HighsPostsolveStack { #ifdef DEBUG_EXTRA // Fill vector with NaN for debugging purposes std::vector valuesNew; - valuesNew.resize(origSize, std::numeric_limits::signaling_NaN()); + valuesNew.resize(origSize + numAppended, + std::numeric_limits::signaling_NaN()); for (size_t i = index.size(); i > 0; --i) { assert(static_cast(index[i - 1]) >= i - 1); valuesNew[index[i - 1]] = values[i - 1]; From 3ac0def5aa1f85ce631208b6678fbba9f5306b21 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 13 May 2026 16:14:13 +0200 Subject: [PATCH 030/196] Add indicator for row type --- highs/mip/HighsMipSolverData.cpp | 2 +- highs/presolve/HPresolve.cpp | 8 +-- highs/presolve/HighsPostsolveStack.cpp | 8 ++- highs/presolve/HighsPostsolveStack.h | 68 ++++++++++++++------------ 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 5b7b375d3e1..8df9eae62fe 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1291,7 +1291,7 @@ void HighsMipSolverData::performRestart() { // original space so that it can be used for constructing a starting basis // for the presolved model after the restart root_basis.col_status.resize(postSolveStack.getOrigNumCol()); - root_basis.row_status.resize(postSolveStack.getOrigNumRow(), + root_basis.row_status.resize(postSolveStack.getNextRowIndex(), HighsBasisStatus::kBasic); root_basis.valid = true; root_basis.useful = true; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5915cf86095..c4b5596b2fa 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -688,11 +688,8 @@ HPresolve::Result HPresolve::updateColImpliedBounds(HighsInt row, HighsInt col, // column as implied free bool useImplBound = mipsolver == nullptr || - mipsolver->mipdata_->postSolveStack.getOrigRowIndex()[row] < - mipsolver->orig_model_->num_row_ || - mipsolver->mipdata_->postSolveStack.getOrigRowIndex()[row] >= - mipsolver->mipdata_->postSolveStack.getOrigNumRow() - - mipsolver->mipdata_->postSolveStack.getNumAppendedRows(); + mipsolver->mipdata_->postSolveStack.getOrigRowType()[row] != + HighsPostsolveStack::OrigRowType::kCut; if (direction * val > 0) { // upper bound @@ -2131,7 +2128,6 @@ bool HPresolve::addToMatrix( HighsInt num_rows = static_cast(row_entries.size()); if (num_rows == 0) return true; HighsInt oldNumRows = model->num_row_; - HighsInt oldNumRowsAppended = postsolve_stack.getNumAppendedRows(); model->num_row_ += num_rows; model->a_matrix_.num_row_ += num_rows; diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index def7217073a..856173f19cf 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -21,10 +21,13 @@ void HighsPostsolveStack::initializeIndexMaps(HighsInt numRow, HighsInt numCol) { origNumRow = numRow; origNumCol = numCol; + nextRowIndex = numRow; origRowIndex.resize(numRow); std::iota(origRowIndex.begin(), origRowIndex.end(), 0); + origRowType.resize(numRow, OrigRowType::kOriginal); + origColIndex.resize(numCol); std::iota(origColIndex.begin(), origColIndex.end(), 0); @@ -40,11 +43,14 @@ void HighsPostsolveStack::compressIndexMaps( for (size_t i = 0; i != newRowIndex.size(); ++i) { if (newRowIndex[i] == -1) --numRow; - else + else { origRowIndex[newRowIndex[i]] = origRowIndex[i]; + origRowType[newRowIndex[i]] = origRowType[i]; + } } // resize original index array to new size origRowIndex.resize(numRow); + origRowType.resize(numRow); // now compress the column array HighsInt numCol = origColIndex.size(); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 70901ec1d4f..37ba7504029 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -45,6 +45,8 @@ class HighsPostsolveStack { // the constructor call, and should restore primal/dual solution values, as // well as the basis status as appropriate. public: + enum class OrigRowType : uint8_t { kOriginal, kCut, kAppended }; + enum class RowType { kGeq, kLeq, @@ -261,6 +263,7 @@ class HighsPostsolveStack { std::vector> reductions; std::vector origColIndex; std::vector origRowIndex; + std::vector origRowType; std::vector linearlyTransformable; std::vector rowValues; @@ -268,15 +271,14 @@ class HighsPostsolveStack { HighsInt origNumCol = -1; HighsInt origNumRow = -1; HighsInt numAppendedRows = 0; + HighsInt nextRowIndex = -1; void reductionAdded(ReductionType type) { size_t position = reductionValues.getCurrentDataSize(); reductions.emplace_back(type, position); } - bool isOrigRow(HighsInt row) const { - return row < origNumRow + numAppendedRows; - } + bool isOrigRow(HighsInt row) const { return row < nextRowIndex; } public: const std::vector& getOrigColIndex() const { return origColIndex; } @@ -288,8 +290,10 @@ class HighsPostsolveStack { size_t currNumRow = origRowIndex.size(); size_t newNumRow = currNumRow + numCuts; origRowIndex.resize(newNumRow); + origRowType.resize(newNumRow, OrigRowType::kCut); for (size_t i = currNumRow; i != newNumRow; ++i) - origRowIndex[i] = origNumRow++; + origRowIndex[i] = nextRowIndex++; + origNumRow += numCuts; } void appendRowsToModel(HighsInt numRows) { @@ -297,25 +301,27 @@ class HighsPostsolveStack { size_t currNumRow = origRowIndex.size(); size_t newNumRow = currNumRow + numRows; origRowIndex.resize(newNumRow); + origRowType.resize(newNumRow, OrigRowType::kAppended); for (size_t i = currNumRow; i != newNumRow; ++i) - origRowIndex[i] = origNumRow + (numAppendedRows++); + origRowIndex[i] = nextRowIndex++; + numAppendedRows += numRows; } void removeCutsFromModel(HighsInt numCuts) { if (numCuts <= 0) return; - HighsInt numOrigRows = computeNumOrigRows(numCuts); origNumRow -= numCuts; - origRowIndex.resize(numOrigRows); - } - - HighsInt computeNumOrigRows(HighsInt numRowsAppended) { - HighsInt numOrig = static_cast(origRowIndex.size()); - if (numRowsAppended <= 0) return numOrig; - for (size_t i = origRowIndex.size(); i > 0; --i) { - if (origRowIndex[i - 1] < origNumRow - numRowsAppended) break; - --numOrig; + size_t write = 0; + for (size_t read = 0; read < origRowIndex.size(); ++read) { + if (origRowType[read] != OrigRowType::kCut) { + if (read != write) { + origRowIndex[write] = origRowIndex[read]; + origRowType[write] = origRowType[read]; + } + ++write; + } } - return numOrig; + origRowIndex.resize(write); + origRowType.resize(write); } HighsInt getOrigNumRow() const { return origNumRow; } @@ -324,6 +330,12 @@ class HighsPostsolveStack { HighsInt getNumAppendedRows() const { return numAppendedRows; } + HighsInt getNextRowIndex() const { return nextRowIndex; } + + const std::vector& getOrigRowType() const { + return origRowType; + } + void initializeIndexMaps(HighsInt numRow, HighsInt numCol); void compressIndexMaps(const std::vector& newRowIndex, @@ -605,13 +617,12 @@ class HighsPostsolveStack { template void undoIterateBackwards(std::vector& values, const std::vector& index, - HighsInt origSize, HighsInt numAppended = 0) { - values.resize(origSize + numAppended); + HighsInt origSize) { + values.resize(origSize); #ifdef DEBUG_EXTRA // Fill vector with NaN for debugging purposes std::vector valuesNew; - valuesNew.resize(origSize + numAppended, - std::numeric_limits::signaling_NaN()); + valuesNew.resize(origSize, std::numeric_limits::signaling_NaN()); for (size_t i = index.size(); i > 0; --i) { assert(static_cast(index[i - 1]) >= i - 1); valuesNew[index[i - 1]] = values[i - 1]; @@ -646,25 +657,22 @@ class HighsPostsolveStack { assert(origNumCol > 0); undoIterateBackwards(solution.col_value, origColIndex, origNumCol); - assert(origNumRow >= 0); - undoIterateBackwards(solution.row_value, origRowIndex, origNumRow, - numAppendedRows); + assert(nextRowIndex >= 0); + undoIterateBackwards(solution.row_value, origRowIndex, nextRowIndex); if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); - undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow, - numAppendedRows); + undoIterateBackwards(solution.row_dual, origRowIndex, nextRowIndex); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - undoIterateBackwards(basis.row_status, origRowIndex, origNumRow, - numAppendedRows); + undoIterateBackwards(basis.row_status, origRowIndex, nextRowIndex); } // now undo the changes @@ -845,21 +853,21 @@ class HighsPostsolveStack { // expand solution to original index space undoIterateBackwards(solution.col_value, origColIndex, origNumCol); - undoIterateBackwards(solution.row_value, origRowIndex, origNumRow); + undoIterateBackwards(solution.row_value, origRowIndex, nextRowIndex); if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); - undoIterateBackwards(solution.row_dual, origRowIndex, origNumRow); + undoIterateBackwards(solution.row_dual, origRowIndex, nextRowIndex); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - undoIterateBackwards(basis.row_status, origRowIndex, origNumRow); + undoIterateBackwards(basis.row_status, origRowIndex, nextRowIndex); } // now undo the changes From 37e91ef9fe1a4b589a3cf843b21a87251fec1e37 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 13 May 2026 16:25:55 +0200 Subject: [PATCH 031/196] WIP --- highs/presolve/HighsPostsolveStack.h | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 37ba7504029..1a19b5d434a 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -285,6 +285,8 @@ class HighsPostsolveStack { const std::vector& getOrigRowIndex() const { return origRowIndex; } + const std::vector& getOrigRowType() const { return origRowType; } + void appendCutsToModel(HighsInt numCuts) { if (numCuts <= 0) return; size_t currNumRow = origRowIndex.size(); @@ -310,32 +312,26 @@ class HighsPostsolveStack { void removeCutsFromModel(HighsInt numCuts) { if (numCuts <= 0) return; origNumRow -= numCuts; - size_t write = 0; - for (size_t read = 0; read < origRowIndex.size(); ++read) { - if (origRowType[read] != OrigRowType::kCut) { - if (read != write) { - origRowIndex[write] = origRowIndex[read]; - origRowType[write] = origRowType[read]; + size_t newSize = 0; + for (size_t i = 0; i < origRowIndex.size(); ++i) { + if (origRowType[i] != OrigRowType::kCut) { + if (i != newSize) { + origRowIndex[newSize] = origRowIndex[i]; + origRowType[newSize] = origRowType[i]; } - ++write; + ++newSize; } } - origRowIndex.resize(write); - origRowType.resize(write); + origRowIndex.resize(newSize); + origRowType.resize(newSize); } HighsInt getOrigNumRow() const { return origNumRow; } HighsInt getOrigNumCol() const { return origNumCol; } - HighsInt getNumAppendedRows() const { return numAppendedRows; } - HighsInt getNextRowIndex() const { return nextRowIndex; } - const std::vector& getOrigRowType() const { - return origRowType; - } - void initializeIndexMaps(HighsInt numRow, HighsInt numCol); void compressIndexMaps(const std::vector& newRowIndex, From 8e673c38c3cc82d44d83206c6ea513a9b3f1993d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 13 May 2026 16:49:14 +0200 Subject: [PATCH 032/196] WIP --- highs/mip/HighsCliqueTable.cpp | 4 ++-- highs/mip/HighsMipSolverData.cpp | 4 +++- highs/presolve/HPresolve.cpp | 8 +++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index 03963d733ec..35be2d5509a 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -1282,8 +1282,8 @@ void HighsCliqueTable::extractCliques(HighsMipSolver& mipsolver, HighsInt start = mipsolver.mipdata_->ARstart_[i]; HighsInt end = mipsolver.mipdata_->ARstart_[i + 1]; - if (mipsolver.mipdata_->postSolveStack.getOrigRowIndex()[i] >= - mipsolver.orig_model_->num_row_) + if (mipsolver.mipdata_->postSolveStack.getOrigRowType()[i] != + presolve::HighsPostsolveStack::OrigRowType::kOriginal) break; // catch set packing and partitioning constraints that already have the form diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 8df9eae62fe..ce4528e397d 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1413,8 +1413,10 @@ void HighsMipSolverData::basisTransfer() { for (HighsInt i = 0; i < static_cast(postSolveStack.getOrigRowIndex().size()); ++i) { + if (postSolveStack.getOrigRowType()[i] != + presolve::HighsPostsolveStack::OrigRowType::kOriginal) + break; HighsInt origIndex = postSolveStack.getOrigRowIndex()[i]; - if (origIndex >= numRow) break; HighsBasisStatus status = mipsolver.rootbasis->row_status[origIndex]; firstrootbasis.row_status[i] = status; } diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index c4b5596b2fa..fcb50758dba 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6453,10 +6453,12 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { HighsInt numcuts = 0; for (HighsInt i = postsolve_stack.getOrigRowIndex().size() - 1; i >= 0; --i) { - // check if we already reached the original rows - if (postsolve_stack.getOrigRowIndex()[i] < - mipsolver->orig_model_->num_row_) + if (postsolve_stack.getOrigRowType()[i] == + HighsPostsolveStack::OrigRowType::kOriginal) break; + if (postsolve_stack.getOrigRowType()[i] != + HighsPostsolveStack::OrigRowType::kCut) + continue; // row is a cut, remove it from matrix but add to cutpool ++numcuts; From dc9565cd2413eb37d3eada68d45dba43e1bace01 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 13 May 2026 17:00:26 +0200 Subject: [PATCH 033/196] Add utility methods --- highs/mip/HighsCliqueTable.cpp | 4 +--- highs/mip/HighsMipSolverData.cpp | 7 ++----- highs/presolve/HPresolve.cpp | 17 +++++------------ highs/presolve/HighsPostsolveStack.h | 10 +++++++++- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index 35be2d5509a..64426e50c32 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -1282,9 +1282,7 @@ void HighsCliqueTable::extractCliques(HighsMipSolver& mipsolver, HighsInt start = mipsolver.mipdata_->ARstart_[i]; HighsInt end = mipsolver.mipdata_->ARstart_[i + 1]; - if (mipsolver.mipdata_->postSolveStack.getOrigRowType()[i] != - presolve::HighsPostsolveStack::OrigRowType::kOriginal) - break; + if (!mipsolver.mipdata_->postSolveStack.isRowOrig(i)) break; // catch set packing and partitioning constraints that already have the form // of a clique without transformations and add those cliques with the rows diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index ce4528e397d..57671783127 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -688,8 +688,7 @@ void HighsMipSolverData::removeFixedIndices() { } void HighsMipSolverData::init() { - postSolveStack.initializeIndexMaps( - mipsolver.numRow(), mipsolver.numCol()); + postSolveStack.initializeIndexMaps(mipsolver.numRow(), mipsolver.numCol()); mipsolver.orig_model_ = mipsolver.model_; feastol = mipsolver.options_mip_->mip_feasibility_tolerance; epsilon = mipsolver.options_mip_->small_matrix_value; @@ -1413,9 +1412,7 @@ void HighsMipSolverData::basisTransfer() { for (HighsInt i = 0; i < static_cast(postSolveStack.getOrigRowIndex().size()); ++i) { - if (postSolveStack.getOrigRowType()[i] != - presolve::HighsPostsolveStack::OrigRowType::kOriginal) - break; + if (!postSolveStack.isRowOrig(i)) break; HighsInt origIndex = postSolveStack.getOrigRowIndex()[i]; HighsBasisStatus status = mipsolver.rootbasis->row_status[origIndex]; firstrootbasis.row_status[i] = status; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index fcb50758dba..9db8128d922 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -686,10 +686,8 @@ HPresolve::Result HPresolve::updateColImpliedBounds(HighsInt row, HighsInt col, // do not use the implied bound if this a not a model row, since the // row can be removed and should not be used, e.g., to identify a // column as implied free - bool useImplBound = - mipsolver == nullptr || - mipsolver->mipdata_->postSolveStack.getOrigRowType()[row] != - HighsPostsolveStack::OrigRowType::kCut; + bool useImplBound = mipsolver == nullptr || + !mipsolver->mipdata_->postSolveStack.isRowCut(row); if (direction * val > 0) { // upper bound @@ -6451,14 +6449,9 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { cutinds.reserve(model->num_col_); cutvals.reserve(model->num_col_); HighsInt numcuts = 0; - for (HighsInt i = postsolve_stack.getOrigRowIndex().size() - 1; i >= 0; - --i) { - if (postsolve_stack.getOrigRowType()[i] == - HighsPostsolveStack::OrigRowType::kOriginal) - break; - if (postsolve_stack.getOrigRowType()[i] != - HighsPostsolveStack::OrigRowType::kCut) - continue; + for (HighsInt i = model->num_row_ - 1; i >= 0; --i) { + if (postsolve_stack.isRowOrig(i)) break; + if (!postsolve_stack.isRowCut(i)) continue; // row is a cut, remove it from matrix but add to cutpool ++numcuts; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 1a19b5d434a..8185abe2af3 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -285,7 +285,15 @@ class HighsPostsolveStack { const std::vector& getOrigRowIndex() const { return origRowIndex; } - const std::vector& getOrigRowType() const { return origRowType; } + bool isRowOrig(HighsInt row) const { + return origRowType[row] == OrigRowType::kOriginal; + } + bool isRowAppended(HighsInt row) const { + return origRowType[row] == OrigRowType::kAppended; + } + bool isRowCut(HighsInt row) const { + return origRowType[row] == OrigRowType::kCut; + } void appendCutsToModel(HighsInt numCuts) { if (numCuts <= 0) return; From ac29c0bd3833c2d0fc55609e6fad4e317fbf5cd1 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 18 May 2026 10:40:25 +0200 Subject: [PATCH 034/196] Rename --- highs/mip/HighsCliqueTable.cpp | 2 +- highs/mip/HighsMipSolverData.cpp | 2 +- highs/presolve/HPresolve.cpp | 6 +++--- highs/presolve/HighsPostsolveStack.h | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index 64426e50c32..cec319e8d52 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -1282,7 +1282,7 @@ void HighsCliqueTable::extractCliques(HighsMipSolver& mipsolver, HighsInt start = mipsolver.mipdata_->ARstart_[i]; HighsInt end = mipsolver.mipdata_->ARstart_[i + 1]; - if (!mipsolver.mipdata_->postSolveStack.isRowOrig(i)) break; + if (!mipsolver.mipdata_->postSolveStack.isModelRow(i)) break; // catch set packing and partitioning constraints that already have the form // of a clique without transformations and add those cliques with the rows diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 57671783127..b4660394246 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1412,7 +1412,7 @@ void HighsMipSolverData::basisTransfer() { for (HighsInt i = 0; i < static_cast(postSolveStack.getOrigRowIndex().size()); ++i) { - if (!postSolveStack.isRowOrig(i)) break; + if (!postSolveStack.isModelRow(i)) break; HighsInt origIndex = postSolveStack.getOrigRowIndex()[i]; HighsBasisStatus status = mipsolver.rootbasis->row_status[origIndex]; firstrootbasis.row_status[i] = status; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 9db8128d922..d0669571d5e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -687,7 +687,7 @@ HPresolve::Result HPresolve::updateColImpliedBounds(HighsInt row, HighsInt col, // row can be removed and should not be used, e.g., to identify a // column as implied free bool useImplBound = mipsolver == nullptr || - !mipsolver->mipdata_->postSolveStack.isRowCut(row); + !mipsolver->mipdata_->postSolveStack.isCutRow(row); if (direction * val > 0) { // upper bound @@ -6450,8 +6450,8 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { cutvals.reserve(model->num_col_); HighsInt numcuts = 0; for (HighsInt i = model->num_row_ - 1; i >= 0; --i) { - if (postsolve_stack.isRowOrig(i)) break; - if (!postsolve_stack.isRowCut(i)) continue; + if (postsolve_stack.isModelRow(i)) break; + if (!postsolve_stack.isCutRow(i)) continue; // row is a cut, remove it from matrix but add to cutpool ++numcuts; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 8185abe2af3..13dae2f84b7 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -285,13 +285,13 @@ class HighsPostsolveStack { const std::vector& getOrigRowIndex() const { return origRowIndex; } - bool isRowOrig(HighsInt row) const { + bool isModelRow(HighsInt row) const { return origRowType[row] == OrigRowType::kOriginal; } - bool isRowAppended(HighsInt row) const { + bool isAppendedRow(HighsInt row) const { return origRowType[row] == OrigRowType::kAppended; } - bool isRowCut(HighsInt row) const { + bool isCutRow(HighsInt row) const { return origRowType[row] == OrigRowType::kCut; } From 4e9e51e0daca9df5f65f2d0fe24402dfd4fba60f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 18 May 2026 11:09:34 +0200 Subject: [PATCH 035/196] WIP --- highs/mip/HighsCliqueTable.cpp | 6 +++++- highs/presolve/HighsPostsolveStack.h | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index cec319e8d52..91e64d1a2db 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -1282,7 +1282,11 @@ void HighsCliqueTable::extractCliques(HighsMipSolver& mipsolver, HighsInt start = mipsolver.mipdata_->ARstart_[i]; HighsInt end = mipsolver.mipdata_->ARstart_[i + 1]; - if (!mipsolver.mipdata_->postSolveStack.isModelRow(i)) break; + 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 diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 13dae2f84b7..44099f76bf5 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -294,6 +294,7 @@ class HighsPostsolveStack { bool isCutRow(HighsInt row) const { return origRowType[row] == OrigRowType::kCut; } + bool hasAppendedRows() const { return numAppendedRows > 0; } void appendCutsToModel(HighsInt numCuts) { if (numCuts <= 0) return; From 3fec1694cd1ca940ffec7ae0e3b086054f70278f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 18 May 2026 11:12:36 +0200 Subject: [PATCH 036/196] Format --- highs/mip/HighsCliqueTable.cpp | 3 +-- highs/presolve/HighsPostsolveStack.h | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index 91e64d1a2db..012d9780f1e 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -1283,8 +1283,7 @@ void HighsCliqueTable::extractCliques(HighsMipSolver& mipsolver, HighsInt end = mipsolver.mipdata_->ARstart_[i + 1]; if (mipsolver.mipdata_->postSolveStack.isCutRow(i)) { - if (!mipsolver.mipdata_->postSolveStack.hasAppendedRows()) - break; + if (!mipsolver.mipdata_->postSolveStack.hasAppendedRows()) break; continue; } diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 44099f76bf5..410533b6a9f 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -288,12 +288,15 @@ class HighsPostsolveStack { bool isModelRow(HighsInt row) const { return origRowType[row] == OrigRowType::kOriginal; } + bool isAppendedRow(HighsInt row) const { return origRowType[row] == OrigRowType::kAppended; } + bool isCutRow(HighsInt row) const { return origRowType[row] == OrigRowType::kCut; } + bool hasAppendedRows() const { return numAppendedRows > 0; } void appendCutsToModel(HighsInt numCuts) { From 965b66a706df970c08204d26f2d6b68cca34af6c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 18 May 2026 11:30:42 +0200 Subject: [PATCH 037/196] WIP --- highs/presolve/HPresolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index d0669571d5e..b8f0eef99f9 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2163,7 +2163,7 @@ bool HPresolve::addToMatrix( return false; // initialise flags - if (!okResize(changedRowFlag, model->num_row_, uint8_t{1})) return false; + if (!okResize(changedRowFlag, model->num_row_, uint8_t{0})) return false; if (!okResize(rowDeleted, model->num_row_, uint8_t{0})) return false; // initialise row names From 96fe5625da34f8fe9e2eac3bf436aaf18e369cc5 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 18 May 2026 13:54:56 +0200 Subject: [PATCH 038/196] WIP --- highs/mip/HighsMipSolverData.cpp | 2 +- highs/presolve/HPresolve.cpp | 2 +- highs/presolve/HighsPostsolveStack.cpp | 58 +++++++++++++------------- highs/presolve/HighsPostsolveStack.h | 4 +- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index b4660394246..dc9618764af 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1412,7 +1412,7 @@ void HighsMipSolverData::basisTransfer() { for (HighsInt i = 0; i < static_cast(postSolveStack.getOrigRowIndex().size()); ++i) { - if (!postSolveStack.isModelRow(i)) break; + if (!postSolveStack.isOrigRow(i)) break; HighsInt origIndex = postSolveStack.getOrigRowIndex()[i]; HighsBasisStatus status = mipsolver.rootbasis->row_status[origIndex]; firstrootbasis.row_status[i] = status; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b8f0eef99f9..997bccffefc 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6450,7 +6450,7 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { cutvals.reserve(model->num_col_); HighsInt numcuts = 0; for (HighsInt i = model->num_row_ - 1; i >= 0; --i) { - if (postsolve_stack.isModelRow(i)) break; + if (postsolve_stack.isOrigRow(i)) break; if (!postsolve_stack.isCutRow(i)) continue; // row is a cut, remove it from matrix but add to cutpool diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 856173f19cf..f3489208e8d 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -104,7 +104,7 @@ void HighsPostsolveStack::FreeColSubstitution::undo( assert(colCoef != 0); // Row values aren't fully postsolved, so why do this? - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) solution.row_value[row] = static_cast(rowValue + colCoef * solution.col_value[col]); solution.col_value[col] = static_cast((rhs - rowValue) / colCoef); @@ -113,11 +113,11 @@ void HighsPostsolveStack::FreeColSubstitution::undo( if (!solution.dual_valid) return; // compute the row dual value such that reduced cost of basic column is 0 - if (postsolveStack.isOrigRow(row)) { + if (postsolveStack.isModelRow(row)) { solution.row_dual[row] = 0; HighsCDouble dualval = colCost; for (const auto& colVal : colValues) { - if (postsolveStack.isOrigRow(colVal.index)) + if (postsolveStack.isModelRow(colVal.index)) dualval -= colVal.value * solution.row_dual[colVal.index]; } solution.row_dual[row] = static_cast(dualval / colCoef); @@ -129,7 +129,7 @@ void HighsPostsolveStack::FreeColSubstitution::undo( if (!basis.valid) return; basis.col_status[col] = HighsBasisStatus::kBasic; - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) basis.row_status[row] = computeRowStatus(solution.row_dual[row], rowType); } @@ -180,10 +180,10 @@ void HighsPostsolveStack::DoubletonEquation::undo( // multiplier of this row i implicitly increases the dual multiplier of this // doubleton equation row with that scale. HighsCDouble rowDual = 0.0; - if (postsolveStack.isOrigRow(row)) { + if (postsolveStack.isModelRow(row)) { solution.row_dual[row] = 0; for (const auto& colVal : colValues) { - if (postsolveStack.isOrigRow(colVal.index)) + if (postsolveStack.isModelRow(colVal.index)) rowDual -= colVal.value * solution.row_dual[colVal.index]; } rowDual /= coefSubst; @@ -200,7 +200,7 @@ void HighsPostsolveStack::DoubletonEquation::undo( // so alter the dual multiplier of the row to make the dual multiplier of // column zero double rowDualDelta = solution.col_dual[col] / coef; - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) solution.row_dual[row] = static_cast(rowDual + rowDualDelta); solution.col_dual[col] = 0.0; solution.col_dual[colSubst] = static_cast( @@ -221,7 +221,7 @@ void HighsPostsolveStack::DoubletonEquation::undo( // otherwise make the reduced cost of the substituted column zero and make // that column basic double rowDualDelta = solution.col_dual[colSubst] / coefSubst; - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) solution.row_dual[row] = static_cast(rowDual + rowDualDelta); solution.col_dual[colSubst] = 0.0; solution.col_dual[col] = @@ -232,7 +232,7 @@ void HighsPostsolveStack::DoubletonEquation::undo( if (!basis.valid) return; - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) basis.row_status[row] = computeRowStatus(solution.row_dual[row], rowType); } @@ -241,7 +241,7 @@ void HighsPostsolveStack::EqualityRowAddition::undo( const std::vector& eqRowValues, HighsSolution& solution, HighsBasis& basis) const { // (removed) cuts may have been used in this reduction. - if (!postsolveStack.isOrigRow(row) || !postsolveStack.isOrigRow(addedEqRow)) + if (!postsolveStack.isModelRow(row) || !postsolveStack.isModelRow(addedEqRow)) return; // nothing more to do if the row is zero in the dual solution or there is @@ -263,7 +263,7 @@ void HighsPostsolveStack::EqualityRowAdditions::undo( const std::vector& targetRows, HighsSolution& solution, HighsBasis& basis) const { // a (removed) cut may have been used in this reduction. - if (!postsolveStack.isOrigRow(addedEqRow)) return; + if (!postsolveStack.isModelRow(addedEqRow)) return; // nothing more to do if the row is zero in the dual solution or there is // no dual solution @@ -274,7 +274,7 @@ void HighsPostsolveStack::EqualityRowAdditions::undo( // used for adding the equation HighsCDouble eqRowDual = solution.row_dual[addedEqRow]; for (const auto& targetRow : targetRows) { - if (postsolveStack.isOrigRow(targetRow.index)) + if (postsolveStack.isModelRow(targetRow.index)) eqRowDual += static_cast(targetRow.value) * solution.row_dual[targetRow.index]; } @@ -299,7 +299,7 @@ void HighsPostsolveStack::ForcingColumn::undo( for (const auto& colVal : colValues) { // Row values aren't fully postsolved, so how can this work? debug_num_use_row_value++; - if (postsolveStack.isOrigRow(colVal.index)) { + if (postsolveStack.isModelRow(colVal.index)) { double colValFromRow = solution.row_value[colVal.index] / colVal.value; if (direction * colValFromRow > direction * colValFromNonbasicRow) { nonbasicRow = colVal.index; @@ -353,7 +353,7 @@ void HighsPostsolveStack::ForcingColumnRemovedRow::undo( const std::vector& rowValues, HighsSolution& solution, HighsBasis& basis) const { // a (removed) cut may have been used in this reduction. - if (!postsolveStack.isOrigRow(row)) return; + if (!postsolveStack.isModelRow(row)) return; // we use the row value as storage for the scaled value implied on the // column dual @@ -386,7 +386,7 @@ void HighsPostsolveStack::SingletonRow::undo( (!colUpperTightened || colStatus != HighsBasisStatus::kUpper)) { // the tightened bound is not used in the basic solution // hence we simply make the row basic and give it a dual multiplier of 0 - if (postsolveStack.isOrigRow(row)) { + if (postsolveStack.isModelRow(row)) { if (basis.valid) basis.row_status[row] = HighsBasisStatus::kBasic; solution.row_dual[row] = 0; } @@ -395,13 +395,13 @@ void HighsPostsolveStack::SingletonRow::undo( // choose the row dual value such that the columns reduced cost becomes // zero - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) solution.row_dual[row] = solution.col_dual[col] / coef; solution.col_dual[col] = 0; if (!basis.valid) return; - if (postsolveStack.isOrigRow(row)) { + if (postsolveStack.isModelRow(row)) { switch (colStatus) { case HighsBasisStatus::kLower: assert(colLowerTightened); @@ -444,7 +444,7 @@ void HighsPostsolveStack::FixedCol::undo( HighsCDouble reducedCost = colCost; for (const auto& colVal : colValues) { - if (postsolveStack.isOrigRow(colVal.index)) + if (postsolveStack.isModelRow(colVal.index)) reducedCost -= colVal.value * solution.row_dual[colVal.index]; } @@ -464,7 +464,7 @@ void HighsPostsolveStack::RedundantRow::undo( const HighsPostsolveStack& postsolveStack, const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) const { // a (removed) cut may have been used in this reduction. - if (!postsolveStack.isOrigRow(row)) return; + if (!postsolveStack.isModelRow(row)) return; // set row dual to zero if dual solution requested if (!solution.dual_valid) return; @@ -496,7 +496,7 @@ void HighsPostsolveStack::ForcingRow::undo( } if (basicCol != -1) { - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) solution.row_dual[row] = solution.row_dual[row] + dualDelta; for (const auto& rowVal : rowValues) { solution.col_dual[rowVal.index] = static_cast( @@ -506,7 +506,7 @@ void HighsPostsolveStack::ForcingRow::undo( solution.col_dual[basicCol] = 0; if (basis.valid) { - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) basis.row_status[row] = (rowType == RowType::kGeq ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper); @@ -520,13 +520,13 @@ void HighsPostsolveStack::DuplicateRow::undo( const HighsPostsolveStack& postsolveStack, const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) const { // (removed) cuts may have been used in this reduction. - if (!postsolveStack.isOrigRow(row)) return; + if (!postsolveStack.isModelRow(row)) return; if (!solution.dual_valid) return; if (!rowUpperTightened && !rowLowerTightened) { // simple case of row2 being redundant, in which case it just gets a // dual multiplier of 0 and is made basic - if (postsolveStack.isOrigRow(duplicateRow)) { + if (postsolveStack.isModelRow(duplicateRow)) { solution.row_dual[duplicateRow] = 0.0; if (basis.valid) basis.row_status[duplicateRow] = HighsBasisStatus::kBasic; @@ -543,7 +543,7 @@ void HighsPostsolveStack::DuplicateRow::undo( auto computeRowDualAndStatus = [&](bool tightened) { if (tightened) { - if (postsolveStack.isOrigRow(duplicateRow)) { + if (postsolveStack.isModelRow(duplicateRow)) { solution.row_dual[duplicateRow] = solution.row_dual[row] / duplicateRowScale; if (basis.valid) { @@ -555,7 +555,7 @@ void HighsPostsolveStack::DuplicateRow::undo( } solution.row_dual[row] = 0.0; if (basis.valid) basis.row_status[row] = HighsBasisStatus::kBasic; - } else if (postsolveStack.isOrigRow(duplicateRow)) { + } else if (postsolveStack.isModelRow(duplicateRow)) { solution.row_dual[duplicateRow] = 0.0; if (basis.valid) basis.row_status[duplicateRow] = HighsBasisStatus::kBasic; @@ -569,7 +569,7 @@ void HighsPostsolveStack::DuplicateRow::undo( switch (rowStatus) { case HighsBasisStatus::kBasic: // if row is basic the parallel row is also basic - if (postsolveStack.isOrigRow(duplicateRow)) { + if (postsolveStack.isModelRow(duplicateRow)) { solution.row_dual[duplicateRow] = 0.0; if (basis.valid) basis.row_status[duplicateRow] = HighsBasisStatus::kBasic; @@ -1360,7 +1360,7 @@ void HighsPostsolveStack::SlackColSubstitution::undo( assert(colCoef != 0); // Row values aren't fully postsolved, so why do this? - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) solution.row_value[row] = static_cast(rowValue + colCoef * solution.col_value[col]); @@ -1370,14 +1370,14 @@ void HighsPostsolveStack::SlackColSubstitution::undo( if (!solution.dual_valid) return; // Row retains its dual value, and column has this dual value scaled by coeff - if (postsolveStack.isOrigRow(row)) + if (postsolveStack.isModelRow(row)) solution.col_dual[col] = -solution.row_dual[row] / colCoef; // Set basis status if necessary if (!basis.valid) return; // If row is basic, then slack is basic, otherwise row retains its status - if (postsolveStack.isOrigRow(row)) { + if (postsolveStack.isModelRow(row)) { HighsBasisStatus save_row_basis_status = basis.row_status[row]; if (basis.row_status[row] == HighsBasisStatus::kBasic) { basis.col_status[col] = HighsBasisStatus::kBasic; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 410533b6a9f..92a36559a47 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -278,14 +278,14 @@ class HighsPostsolveStack { reductions.emplace_back(type, position); } - bool isOrigRow(HighsInt row) const { return row < nextRowIndex; } + bool isModelRow(HighsInt row) const { return row < nextRowIndex; } public: const std::vector& getOrigColIndex() const { return origColIndex; } const std::vector& getOrigRowIndex() const { return origRowIndex; } - bool isModelRow(HighsInt row) const { + bool isOrigRow(HighsInt row) const { return origRowType[row] == OrigRowType::kOriginal; } From 9569e1e1b6b3630ddc182182fd2e0c21e0e56067 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 18 May 2026 14:17:59 +0200 Subject: [PATCH 039/196] WIP --- highs/presolve/HPresolve.cpp | 4 +- highs/presolve/HighsPostsolveStack.h | 157 +-------------------------- 2 files changed, 6 insertions(+), 155 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 997bccffefc..2410c22b3d2 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -8296,7 +8296,7 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { sol = reducedsol; basis = reducedbasis; - postsolve_stack.undoUntil(options, sol, basis, tmp.numReductions()); + postsolve_stack.undo(options, sol, basis, tmp.numReductions()); HighsBasis temp_basis; HighsSolution temp_sol; @@ -8342,7 +8342,7 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { ARstart, ARindex, ARvalue); sol = reducedsol; basis = reducedbasis; - postsolve_stack.undoUntil(options, sol, basis, reductionLim); + postsolve_stack.undo(options, sol, basis, reductionLim); calculateRowValuesQuad(model, sol); kktinfo = dev_kkt_check::initInfo(); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 92a36559a47..1ac37bdb7a3 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -653,7 +653,8 @@ class HighsPostsolveStack { /// undo presolve steps for primal dual solution and basis void undo(const HighsOptions& options, HighsSolution& solution, - HighsBasis& basis, const HighsInt report_col = -1) { + HighsBasis& basis, size_t numReductions = 0, + const HighsInt report_col = -1) { reductionValues.resetPosition(); // Verify that undo can be performed @@ -684,7 +685,7 @@ class HighsPostsolveStack { } // now undo the changes - for (size_t i = reductions.size(); i > 0; --i) { + for (size_t i = reductions.size(); i > numReductions; --i) { if (report_col >= 0) printf("Before reduction %2d (type %2d): col_value[%2d] = %g\n", int(i - 1), int(reductions[i - 1].first), int(report_col), @@ -822,7 +823,7 @@ class HighsPostsolveStack { HighsBasis basis; basis.valid = false; solution.dual_valid = false; - undo(options, solution, basis, report_col); + undo(options, solution, basis, 0, report_col); } /* @@ -838,156 +839,6 @@ class HighsPostsolveStack { } */ - // Only used for debugging - void undoUntil(const HighsOptions& options, HighsSolution& solution, - HighsBasis& basis, size_t numReductions) { - reductionValues.resetPosition(); - - // Do these returns ever happen? How is it known that undo has not - // been performed? - assert(solution.col_value.size() == origColIndex.size()); - assert(solution.row_value.size() == origRowIndex.size()); - // This should be a better measure of whether undo can be - // performed - assert(solution.value_valid); - if (solution.col_value.size() != origColIndex.size()) return; - if (solution.row_value.size() != origRowIndex.size()) return; - - bool perform_dual_postsolve = solution.dual_valid; - assert((solution.col_dual.size() == solution.col_value.size()) == - perform_dual_postsolve); - bool perform_basis_postsolve = basis.valid; - - // expand solution to original index space - undoIterateBackwards(solution.col_value, origColIndex, origNumCol); - - undoIterateBackwards(solution.row_value, origRowIndex, nextRowIndex); - - if (perform_dual_postsolve) { - // if dual solution is given, expand dual solution and basis to original - // index space - undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); - - undoIterateBackwards(solution.row_dual, origRowIndex, nextRowIndex); - } - - if (perform_basis_postsolve) { - // if basis is given, expand basis status values to original index space - undoIterateBackwards(basis.col_status, origColIndex, origNumCol); - - undoIterateBackwards(basis.row_status, origRowIndex, nextRowIndex); - } - - // now undo the changes - for (size_t i = reductions.size(); i > numReductions; --i) { - switch (reductions[i - 1].first) { - case ReductionType::kLinearTransform: { - LinearTransform reduction; - reductionValues.pop(reduction); - reduction.undo(options, solution); - break; - } - case ReductionType::kFreeColSubstitution: { - FreeColSubstitution reduction; - reductionValues.pop(colValues); - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(*this, options, rowValues, colValues, solution, basis); - break; - } - case ReductionType::kDoubletonEquation: { - DoubletonEquation reduction; - reductionValues.pop(colValues); - reductionValues.pop(reduction); - reduction.undo(*this, options, colValues, solution, basis); - break; - } - case ReductionType::kEqualityRowAddition: { - EqualityRowAddition reduction; - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(*this, options, rowValues, solution, basis); - break; - } - case ReductionType::kEqualityRowAdditions: { - EqualityRowAdditions reduction; - reductionValues.pop(colValues); - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(*this, options, rowValues, colValues, solution, basis); - break; - } - case ReductionType::kSingletonRow: { - SingletonRow reduction; - reductionValues.pop(reduction); - reduction.undo(*this, options, solution, basis); - break; - } - case ReductionType::kFixedCol: { - FixedCol reduction; - reductionValues.pop(colValues); - reductionValues.pop(reduction); - reduction.undo(*this, options, colValues, solution, basis); - break; - } - case ReductionType::kRedundantRow: { - RedundantRow reduction; - reductionValues.pop(reduction); - reduction.undo(*this, options, solution, basis); - break; - } - case ReductionType::kForcingRow: { - ForcingRow reduction; - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(*this, options, rowValues, solution, basis); - break; - } - case ReductionType::kForcingColumn: { - ForcingColumn reduction; - reductionValues.pop(colValues); - reductionValues.pop(reduction); - reduction.undo(*this, options, colValues, solution, basis); - break; - } - case ReductionType::kForcingColumnRemovedRow: { - ForcingColumnRemovedRow reduction; - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(*this, options, rowValues, solution, basis); - break; - } - case ReductionType::kDuplicateRow: { - DuplicateRow reduction; - reductionValues.pop(reduction); - reduction.undo(*this, options, solution, basis); - break; - } - case ReductionType::kDuplicateColumn: { - DuplicateColumn reduction; - reductionValues.pop(reduction); - reduction.undo(options, solution, basis); - break; - } - case ReductionType::kSlackColSubstitution: { - SlackColSubstitution reduction; - reductionValues.pop(rowValues); - reductionValues.pop(reduction); - reduction.undo(*this, options, rowValues, solution, basis); - break; - } - } - } -#ifdef DEBUG_EXTRA - // solution should not contain NaN or Inf - assert(!containsNanOrInf(solution.col_value)); - // row values are not determined by postsolve - // assert(!containsNanOrInf(solution.row_value)); - assert(!containsNanOrInf(solution.col_dual)); - assert(!containsNanOrInf(solution.row_dual)); -#endif - } - size_t numReductions() const { return reductions.size(); } }; From 9017acb93bb669b0010104c1ac4654f5995317ca Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 18 May 2026 14:31:21 +0200 Subject: [PATCH 040/196] WIP --- highs/presolve/HighsPostsolveStack.h | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 1ac37bdb7a3..e4bf403f16d 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -299,26 +299,24 @@ class HighsPostsolveStack { bool hasAppendedRows() const { return numAppendedRows > 0; } - void appendCutsToModel(HighsInt numCuts) { - if (numCuts <= 0) return; + void appendToModel(HighsInt& numRows, HighsInt numRowsToAppend, + OrigRowType rowType) { + if (numRowsToAppend <= 0) return; size_t currNumRow = origRowIndex.size(); - size_t newNumRow = currNumRow + numCuts; + size_t newNumRow = currNumRow + numRowsToAppend; origRowIndex.resize(newNumRow); - origRowType.resize(newNumRow, OrigRowType::kCut); + origRowType.resize(newNumRow, rowType); for (size_t i = currNumRow; i != newNumRow; ++i) origRowIndex[i] = nextRowIndex++; - origNumRow += numCuts; + numRows += numRowsToAppend; + } + + void appendCutsToModel(HighsInt numCuts) { + appendToModel(origNumRow, numCuts, OrigRowType::kCut); } void appendRowsToModel(HighsInt numRows) { - if (numRows <= 0) return; - size_t currNumRow = origRowIndex.size(); - size_t newNumRow = currNumRow + numRows; - origRowIndex.resize(newNumRow); - origRowType.resize(newNumRow, OrigRowType::kAppended); - for (size_t i = currNumRow; i != newNumRow; ++i) - origRowIndex[i] = nextRowIndex++; - numAppendedRows += numRows; + appendToModel(numAppendedRows, numRows, OrigRowType::kAppended); } void removeCutsFromModel(HighsInt numCuts) { From 195305c17b20d5dc6ee661ef9db85aaf391ad296 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 18 May 2026 14:34:27 +0200 Subject: [PATCH 041/196] Remove extra variable --- highs/mip/HighsMipSolverData.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index dc9618764af..a0068a0d0fb 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1413,8 +1413,8 @@ void HighsMipSolverData::basisTransfer() { i < static_cast(postSolveStack.getOrigRowIndex().size()); ++i) { if (!postSolveStack.isOrigRow(i)) break; - HighsInt origIndex = postSolveStack.getOrigRowIndex()[i]; - HighsBasisStatus status = mipsolver.rootbasis->row_status[origIndex]; + HighsBasisStatus status = + mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex()[i]]; firstrootbasis.row_status[i] = status; } From a577431fdc13dca3bc281c398b51ff441fdff382 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 18 May 2026 14:37:07 +0200 Subject: [PATCH 042/196] Remove debugging code --- highs/presolve/HPresolve.cpp | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 2410c22b3d2..d8ce55b597e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5935,28 +5935,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { // Start of main presolve loop // - bool tryAppendRows = - mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; while (true) { - // FOR DEBUGGING NEW METHOD! - if (tryAppendRows) { - std::vector row_lower, row_upper; - std::vector> rows; - for (HighsInt i = 0; i < static_cast(0.1 * model->num_row_); - i++) { - if (rowDeleted[i]) continue; - std::vector row; - for (const auto& rowNz : getRowVector(i)) { - row.push_back(row_entry{rowNz.index(), rowNz.value()}); - } - row_lower.push_back(model->row_lower_[i]); - row_upper.push_back(model->row_upper_[i]); - rows.push_back(row); - } - if (!addToMatrix(postsolve_stack, row_lower, row_upper, rows)) - return Result::kOk; - } - HighsInt currSize = model->num_col_ - numDeletedCols + model->num_row_ - numDeletedRows; if (currSize < 0.85 * lastPrintSize) { From 08b3adc14c6624e2e433dc139b8561f8c05042a7 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 19 May 2026 14:42:42 +0200 Subject: [PATCH 043/196] WIP --- highs/lp_data/HConst.h | 3 +- highs/lp_data/HighsModelUtils.cpp | 2 + highs/presolve/HPresolve.cpp | 354 ++++++++++++++++++++++++++++++ highs/presolve/HPresolve.h | 2 + 4 files changed, 360 insertions(+), 1 deletion(-) diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index ed5372ae54f..1d293a0fd94 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -275,7 +275,8 @@ enum PresolveRuleType : int { kPresolveRuleSparsify, kPresolveRuleProbing, kPresolveRuleEnumeration, - kPresolveRuleMax = kPresolveRuleEnumeration, + kPresolveRuleFourierMotzkin, + kPresolveRuleMax = kPresolveRuleFourierMotzkin, kPresolveRuleLastAllowOff = kPresolveRuleMax, kPresolveRuleCount }; diff --git a/highs/lp_data/HighsModelUtils.cpp b/highs/lp_data/HighsModelUtils.cpp index a9310a4ea20..bea30952064 100644 --- a/highs/lp_data/HighsModelUtils.cpp +++ b/highs/lp_data/HighsModelUtils.cpp @@ -1515,6 +1515,8 @@ std::string utilPresolveRuleTypeToString(const HighsInt rule_type) { return "Probing"; } else if (rule_type == kPresolveRuleEnumeration) { return "Enumeration"; + } else if (rule_type == kPresolveRuleFourierMotzkin) { + return "Fourier-Motzkin"; } assert(1 == 0); return "????"; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index d8ce55b597e..e210bf54111 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5956,6 +5956,9 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { applyConflictGraphSubstitutions(postsolve_stack, numDelCol)); } + if (analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) + HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); + if (analysis_.allow_rule_[kPresolveRuleAggregator]) HPRESOLVE_CHECKED_CALL(aggregator(postsolve_stack)); @@ -6850,6 +6853,357 @@ HPresolve::Result HPresolve::aggregator(HighsPostsolveStack& postsolve_stack) { return Result::kOk; } +HPresolve::Result HPresolve::fourierMotzkin( + HighsPostsolveStack& postsolve_stack) { + assert(analysis_.allow_rule_[kPresolveRuleFourierMotzkin]); + const bool logging_on = analysis_.logging_on_; + if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleFourierMotzkin); + + // max. absolute coefficient + const double maxCoef = 1e3; + + // structs + struct candidate { + HighsInt col; + int64_t neRed; + int64_t mrRed; + }; + + struct newRowEntry { + HighsInt col; + HighsCDouble val; + }; + + struct newRow { + std::vector entries; + double lower; + double upper; + }; + + auto finalise = [&]() { + analysis_.logging_on_ = logging_on; + if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleFourierMotzkin); + return Result::kOk; + }; + + auto computeCandidates = [&](std::vector& candidates) { + for (HighsInt col = 0; col < model->num_col_; col++) { + if (colDeleted[col]) continue; + if (colsize[col] == 0) continue; + if (model->integrality_[col] != HighsVarType::kContinuous) continue; + + bool isCandidate = true; + for (const auto& nz : getColumnVector(col)) { + isCandidate = !isEquation(nz.index()); + if (!isCandidate) break; + double absval = std::abs(nz.value()); + isCandidate = absval >= 1.0 / maxCoef && absval <= maxCoef; + if (!isCandidate) break; + } + if (isCandidate) candidates.push_back(col); + } + }; + + auto checkRows = [&](HighsInt col, std::vector& iPlus, + std::vector& iMinus, int64_t& nePlus, + int64_t& neMinus) { + nePlus = 0; + neMinus = 0; + iPlus.clear(); + iMinus.clear(); + for (const auto& nz : getColumnVector(col)) { + HighsInt row = nz.index(); + if (rowDeleted[row]) continue; + + if (isRanged(row)) { + iPlus.push_back(row); + nePlus += rowsize[row]; + iMinus.push_back(row); + neMinus += rowsize[row]; + } else { + HighsInt direction; + if (model->row_lower_[row] == -kHighsInf && + model->row_upper_[row] != kHighsInf) + direction = 1; + else + direction = -1; + + if (direction * nz.value() > 0) { + iPlus.push_back(row); + nePlus += rowsize[row]; + } else { + iMinus.push_back(row); + neMinus += rowsize[row]; + } + } + } + }; + + auto checkNonZeros = [&](HighsInt col, std::vector& iPlus, + std::vector& iMinus, + std::vector& pPlus, + std::vector& pMinus, + std::vector& otherCols, int64_t& neRed, + int64_t& mrRed) { + int64_t nePlus; + int64_t neMinus; + checkRows(col, iPlus, iMinus, nePlus, neMinus); + + if (iPlus.size() == 0 || iMinus.size() == 0) { + iPlus.clear(); + iMinus.clear(); + return false; + } + + // take into account other variables present in the rows + for (HighsInt row : iPlus) { + for (const auto& nz : getRowVector(row)) { + HighsInt k = nz.index(); + if (k == col) continue; + if (pPlus[k] == 0 && pMinus[k] == 0) otherCols.push_back(k); + pPlus[k]++; + } + } + for (HighsInt row : iMinus) { + for (const auto& nz : getRowVector(row)) { + HighsInt k = nz.index(); + if (k == col) continue; + if (pPlus[k] == 0 && pMinus[k] == 0) otherCols.push_back(k); + pMinus[k]++; + } + } + + // compute correction term + int64_t correction = 0; + for (HighsInt k : otherCols) { + correction += static_cast(pPlus[k]) * pMinus[k]; + pPlus[k] = 0; + pMinus[k] = 0; + } + otherCols.clear(); + + int64_t neOld = nePlus + neMinus; + int64_t neNew = static_cast(iPlus.size()) * neMinus + + static_cast(iMinus.size()) * nePlus - correction; + neRed = neOld - neNew; + mrRed = iPlus.size() + iMinus.size() - + static_cast(iPlus.size()) * iMinus.size(); + return true; + }; + + auto checkNewRow = [&](const newRow& nr, bool& isRedundant) { + HighsCDouble impliedLower = 0; + HighsCDouble impliedUpper = 0; + bool lowerFinite = true; + bool upperFinite = true; + isRedundant = false; + for (const auto& e : nr.entries) { + double lb = model->col_lower_[e.col]; + double ub = model->col_upper_[e.col]; + if (e.val > 0) { + lowerFinite = lowerFinite && lb != -kHighsInf; + if (lowerFinite) impliedLower += e.val * lb; + upperFinite = upperFinite && ub != kHighsInf; + if (upperFinite) impliedUpper += e.val * ub; + } else { + lowerFinite = lowerFinite && ub != kHighsInf; + if (lowerFinite) impliedLower += e.val * ub; + upperFinite = upperFinite && lb != -kHighsInf; + if (upperFinite) impliedUpper += e.val * lb; + } + if (!lowerFinite && !upperFinite) return Result::kOk; + } + + double lower = lowerFinite ? static_cast(impliedLower) : -kHighsInf; + double upper = upperFinite ? static_cast(impliedUpper) : kHighsInf; + + // check for infeasibility + if (lower > nr.upper + primal_feastol || upper < nr.lower - primal_feastol) + return Result::kPrimalInfeasible; + + // check for redundancy + isRedundant = lower >= nr.lower - primal_feastol && + upper <= nr.upper + primal_feastol; + + return Result::kOk; + }; + + // collect candidate variables + std::vector candidates; + computeCandidates(candidates); + if (candidates.empty()) return finalise(); + + // candidate vector + std::vector scored; + scored.reserve(candidates.size()); + + // workspace vectors + std::vector iPlus; + std::vector iMinus; + iPlus.reserve(model->num_row_); + iMinus.reserve(model->num_row_); + std::vector pPlus(model->num_col_, 0); + std::vector pMinus(model->num_col_, 0); + std::vector otherCols; + otherCols.reserve(model->num_col_); + + for (HighsInt col : candidates) { + int64_t neRed; + int64_t mrRed; + if (!checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, otherCols, neRed, + mrRed)) + continue; + + if (neRed > 0 || (neRed == 0 && mrRed > 0)) + scored.push_back({col, neRed, mrRed}); + } + + if (scored.empty()) return finalise(); + + // sort candidates + pdqsort(scored.begin(), scored.end(), + [](const candidate& a, const candidate& b) { + if (a.neRed != b.neRed) return a.neRed > b.neRed; + return a.mrRed > b.mrRed; + }); + + // vectors for computing new row entries + std::vector newRowEntries; + std::vector newRowMark(model->num_col_, -1); + + // vector for storing new rows + std::vector newRows; + newRows.reserve(iPlus.size() * iMinus.size()); + + // main loop: eliminate variables + for (const candidate& c : scored) { + HighsInt col = c.col; + if (colDeleted[col]) continue; + + // recompute reduction numbers + int64_t neRed; + int64_t mrRed; + if (!checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, otherCols, neRed, + mrRed)) + continue; + + if (neRed < 0) continue; + if (neRed == 0 && mrRed <= 0) continue; + + for (HighsInt pRow : iPlus) { + HighsInt pPos = findNonzero(pRow, col); + assert(pPos != -1); + HighsInt pDirection = Avalue[pPos] > 0 ? HighsInt{1} : HighsInt{-1}; + double pCoefAbs = std::abs(Avalue[pPos]); + double pBound = + pDirection > 0 ? model->row_upper_[pRow] : -model->row_lower_[pRow]; + + for (HighsInt mRow : iMinus) { + HighsInt mPos = findNonzero(mRow, col); + assert(mPos != -1); + HighsInt mDirection = Avalue[mPos] < 0 ? HighsInt{1} : HighsInt{-1}; + double mCoefAbs = std::abs(Avalue[mPos]); + double mBound = + mDirection > 0 ? model->row_upper_[mRow] : -model->row_lower_[mRow]; + + // scale factor to preserve violation tolerances (see section 4.3): + double s = (pCoefAbs * mCoefAbs) / (pCoefAbs + mCoefAbs); + double pScale = s / pCoefAbs; + double mScale = s / mCoefAbs; + + storeRow(pRow); + for (HighsInt rowiter : rowpositions) { + if (Acol[rowiter] == col) continue; + double val = pDirection * pScale * Avalue[rowiter]; + newRowMark[Acol[rowiter]] = + static_cast(newRowEntries.size()); + newRowEntries.push_back({Acol[rowiter], val}); + } + + storeRow(mRow); + for (HighsInt rowiter : rowpositions) { + if (Acol[rowiter] == col) continue; + double val = mDirection * mScale * Avalue[rowiter]; + if (newRowMark[Acol[rowiter]] == -1) { + newRowMark[Acol[rowiter]] = + static_cast(newRowEntries.size()); + newRowEntries.push_back({Acol[rowiter], val}); + } else + newRowEntries[newRowMark[Acol[rowiter]]].val += val; + } + + // reset marker before removing near-zeros + for (const auto& e : newRowEntries) newRowMark[e.col] = -1; + + // remove near-zero entries + newRowEntries.erase( + std::remove_if(newRowEntries.begin(), newRowEntries.end(), + [&](const auto& e) { + return abs(e.val) <= options->small_matrix_value; + }), + newRowEntries.end()); + + // store new row + double new_upper = + static_cast(static_cast(pScale) * pBound + + static_cast(mScale) * mBound); + newRows.push_back({newRowEntries, -kHighsInf, new_upper}); + + // clear vector + newRowEntries.clear(); + } + } + + // add new rows, filtering out redundant ones + std::vector rowLower; + std::vector rowUpper; + std::vector> rowEntries; + + for (const auto& nr : newRows) { + // check whether new row is infeasible or redundant + bool redundant; + HPRESOLVE_CHECKED_CALL(checkNewRow(nr, redundant)); + + // skip redundant rows + if (redundant) continue; + + // add the row + std::vector entries; + entries.reserve(nr.entries.size()); + for (const auto& e : nr.entries) + entries.push_back({e.col, static_cast(e.val)}); + rowLower.push_back(nr.lower); + rowUpper.push_back(nr.upper); + rowEntries.push_back(std::move(entries)); + } + + // clear vector + newRows.clear(); + + // add new rows to matrix + if (!addToMatrix(postsolve_stack, rowLower, rowUpper, rowEntries)) + return finalise(); + + // remove old rows containing col + for (HighsInt rp : iPlus) { + postsolve_stack.redundantRow(rp); + removeRow(rp); + } + for (HighsInt rm : iMinus) { + if (rowDeleted[rm]) continue; + postsolve_stack.redundantRow(rm); + removeRow(rm); + } + + // mark column as deleted + markColDeleted(col); + + HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); + } + + return finalise(); +} + void HPresolve::substitute(HighsInt substcol, HighsInt staycol, double offset, double scale) { // substitute the column in each row where it occurs diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index fdc4210c8c2..009d2bef3f5 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -468,6 +468,8 @@ class HPresolve { Result aggregator(HighsPostsolveStack& postsolve_stack); + Result fourierMotzkin(HighsPostsolveStack& postsolve_stack); + Result removeRowSingletons(HighsPostsolveStack& postsolve_stack); Result presolveColSingletons(HighsPostsolveStack& postsolve_stack); From ca383bd062fa40cedb2082807c506c5a70114c0d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 26 May 2026 15:30:16 +0200 Subject: [PATCH 044/196] Cannot use shrinkProblem --- highs/presolve/HPresolve.cpp | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index df656a1b646..ea4f6fb7e48 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6481,7 +6481,37 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { for (HighsInt j : rowpositions) unlink(j); } - shrinkProblem(postsolve_stack); + // 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(); } } From ce57eeab2e0efe2d257ceb6364b09707374fbedb Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 26 May 2026 15:40:09 +0200 Subject: [PATCH 045/196] Remove debugging code --- highs/presolve/HPresolve.cpp | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ea4f6fb7e48..a808c5b7f69 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5942,28 +5942,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { // Start of main presolve loop // - bool tryAppendRows = - mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; while (true) { - // FOR DEBUGGING NEW METHOD! - if (tryAppendRows) { - std::vector row_lower, row_upper; - std::vector> rows; - for (HighsInt i = 0; i < static_cast(0.1 * model->num_row_); - i++) { - if (rowDeleted[i]) continue; - std::vector row; - for (const auto& rowNz : getRowVector(i)) { - row.push_back(row_entry{rowNz.index(), rowNz.value()}); - } - row_lower.push_back(model->row_lower_[i]); - row_upper.push_back(model->row_upper_[i]); - rows.push_back(row); - } - if (!addToMatrix(postsolve_stack, row_lower, row_upper, rows)) - return Result::kOk; - } - HighsInt currSize = model->num_col_ - numDeletedCols + model->num_row_ - numDeletedRows; if (currSize < 0.85 * lastPrintSize) { From 52ee49ae354ed6da14a36e4f3ac8136d294f8fd4 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 12:24:31 +0200 Subject: [PATCH 046/196] WIP --- highs/presolve/HPresolve.cpp | 158 ++++++++++++++++++++++++++++++----- 1 file changed, 136 insertions(+), 22 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 3d90cbd63de..fd590cdd28e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7065,15 +7065,79 @@ HPresolve::Result HPresolve::fourierMotzkin( return Result::kOk; }; + auto isReduction = [](int64_t neRed, int64_t mrRed) { + return neRed > 0 || (neRed == 0 && mrRed > 0); + }; + + auto heapBetter = [](const candidate& a, const candidate& b) { + if (a.neRed != b.neRed) return a.neRed > b.neRed; + return a.mrRed > b.mrRed; + }; + + auto heapSwap = [&](std::vector& heap, + std::vector& heapPos, HighsInt i, HighsInt j) { + std::swap(heap[i], heap[j]); + heapPos[heap[i].col] = i; + heapPos[heap[j].col] = j; + }; + + auto heapBubbleUp = [&](std::vector& heap, + std::vector& heapPos, HighsInt i) { + while (i > 0) { + HighsInt parent = (i - 1) / 2; + if (!heapBetter(heap[i], heap[parent])) break; + heapSwap(heap, heapPos, i, parent); + i = parent; + } + }; + + auto heapBubbleDown = [&](std::vector& heap, + std::vector& heapPos, HighsInt i) { + HighsInt heapSize = static_cast(heap.size()); + while (true) { + HighsInt best = i; + HighsInt left = 2 * i + 1; + HighsInt right = 2 * i + 2; + if (left < heapSize && heapBetter(heap[left], heap[best])) best = left; + if (right < heapSize && heapBetter(heap[right], heap[best])) best = right; + if (best == i) break; + heapSwap(heap, heapPos, i, best); + i = best; + } + }; + + auto heapRemove = [&](std::vector& heap, + std::vector& heapPos, HighsInt col) { + HighsInt pos = heapPos[col]; + if (pos == -1) return; + heapPos[col] = -1; + HighsInt last = static_cast(heap.size()) - 1; + if (pos == last) { + heap.pop_back(); + return; + } + heapSwap(heap, heapPos, pos, last); + heap.pop_back(); + heapBubbleUp(heap, heapPos, pos); + heapBubbleDown(heap, heapPos, pos); + }; + + auto heapUpdate = [&](std::vector& heap, + std::vector& heapPos, HighsInt col, + int64_t neRed, int64_t mrRed) { + HighsInt pos = heapPos[col]; + if (pos == -1) return; + heap[pos].neRed = neRed; + heap[pos].mrRed = mrRed; + heapBubbleUp(heap, heapPos, pos); + heapBubbleDown(heap, heapPos, pos); + }; + // collect candidate variables std::vector candidates; computeCandidates(candidates); if (candidates.empty()) return finalise(); - // candidate vector - std::vector scored; - scored.reserve(candidates.size()); - // workspace vectors std::vector iPlus; std::vector iMinus; @@ -7084,25 +7148,32 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector otherCols; otherCols.reserve(model->num_col_); + // indexed max-heap + std::vector heap; + heap.reserve(candidates.size()); + std::vector heapPos(model->num_col_, -1); + std::vector isCandidate(model->num_col_, false); + + // build initial heap for (HighsInt col : candidates) { + isCandidate[col] = true; int64_t neRed; int64_t mrRed; if (!checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, otherCols, neRed, mrRed)) continue; - if (neRed > 0 || (neRed == 0 && mrRed > 0)) - scored.push_back({col, neRed, mrRed}); + if (isReduction(neRed, mrRed)) { + heapPos[col] = static_cast(heap.size()); + heap.push_back({col, neRed, mrRed}); + } } - if (scored.empty()) return finalise(); + if (heap.empty()) return finalise(); - // sort candidates - pdqsort(scored.begin(), scored.end(), - [](const candidate& a, const candidate& b) { - if (a.neRed != b.neRed) return a.neRed > b.neRed; - return a.mrRed > b.mrRed; - }); + // heapify + for (HighsInt i = static_cast(heap.size()) / 2 - 1; i >= 0; --i) + heapBubbleDown(heap, heapPos, i); // vectors for computing new row entries std::vector newRowEntries; @@ -7110,11 +7181,16 @@ HPresolve::Result HPresolve::fourierMotzkin( // vector for storing new rows std::vector newRows; - newRows.reserve(iPlus.size() * iMinus.size()); - // main loop: eliminate variables - for (const candidate& c : scored) { - HighsInt col = c.col; + // workspace for collecting affected candidates + std::vector affectedCols; + std::vector affectedMark(model->num_col_, false); + + // main loop: eliminate variables from heap + while (!heap.empty()) { + HighsInt col = heap[0].col; + heapRemove(heap, heapPos, col); + if (colDeleted[col]) continue; // recompute reduction numbers @@ -7124,9 +7200,33 @@ HPresolve::Result HPresolve::fourierMotzkin( mrRed)) continue; - if (neRed < 0) continue; - if (neRed == 0 && mrRed <= 0) continue; + if (!isReduction(neRed, mrRed)) continue; + + // collect affected candidate columns before modifying the matrix + affectedCols.clear(); + for (HighsInt row : iPlus) { + for (const auto& nz : getRowVector(row)) { + HighsInt k = nz.index(); + if (k == col) continue; + if (!isCandidate[k]) continue; + if (affectedMark[k]) continue; + affectedMark[k] = true; + affectedCols.push_back(k); + } + } + for (HighsInt row : iMinus) { + for (const auto& nz : getRowVector(row)) { + HighsInt k = nz.index(); + if (k == col) continue; + if (!isCandidate[k]) continue; + if (affectedMark[k]) continue; + affectedMark[k] = true; + affectedCols.push_back(k); + } + } + // perform elimination: generate new rows + newRows.clear(); for (HighsInt pRow : iPlus) { HighsInt pPos = findNonzero(pRow, col); assert(pPos != -1); @@ -7214,9 +7314,6 @@ HPresolve::Result HPresolve::fourierMotzkin( rowEntries.push_back(std::move(entries)); } - // clear vector - newRows.clear(); - // add new rows to matrix if (!addToMatrix(postsolve_stack, rowLower, rowUpper, rowEntries)) return finalise(); @@ -7233,8 +7330,25 @@ HPresolve::Result HPresolve::fourierMotzkin( } // mark column as deleted + isCandidate[col] = false; markColDeleted(col); + // update affected candidates in the heap + for (HighsInt k : affectedCols) { + affectedMark[k] = false; + int64_t ne, mr; + if (!checkNonZeros(k, iPlus, iMinus, pPlus, pMinus, otherCols, ne, mr) || + !isReduction(ne, mr)) { + heapRemove(heap, heapPos, k); + } else if (heapPos[k] == -1) { + heapPos[k] = static_cast(heap.size()); + heap.push_back({k, ne, mr}); + heapBubbleUp(heap, heapPos, heapPos[k]); + } else { + heapUpdate(heap, heapPos, k, ne, mr); + } + } + HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } From 6fac31c53c9a8c441fd698a0361e7c9559aa8fb9 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 12:40:09 +0200 Subject: [PATCH 047/196] Clean up --- highs/presolve/HPresolve.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index fd590cdd28e..041ec73e0d5 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7076,6 +7076,7 @@ HPresolve::Result HPresolve::fourierMotzkin( auto heapSwap = [&](std::vector& heap, std::vector& heapPos, HighsInt i, HighsInt j) { + if (i == j) return; std::swap(heap[i], heap[j]); heapPos[heap[i].col] = i; heapPos[heap[j].col] = j; @@ -7110,16 +7111,14 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector& heapPos, HighsInt col) { HighsInt pos = heapPos[col]; if (pos == -1) return; - heapPos[col] = -1; HighsInt last = static_cast(heap.size()) - 1; - if (pos == last) { - heap.pop_back(); - return; - } heapSwap(heap, heapPos, pos, last); + heapPos[col] = -1; heap.pop_back(); - heapBubbleUp(heap, heapPos, pos); - heapBubbleDown(heap, heapPos, pos); + if (pos < static_cast(heap.size())) { + heapBubbleUp(heap, heapPos, pos); + heapBubbleDown(heap, heapPos, pos); + } }; auto heapUpdate = [&](std::vector& heap, From 678b29e89d0bed0712f7bcbd7b607db11818e06b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 12:45:35 +0200 Subject: [PATCH 048/196] Simplify --- highs/presolve/HPresolve.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 041ec73e0d5..85a5c0d6e39 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7084,6 +7084,7 @@ HPresolve::Result HPresolve::fourierMotzkin( auto heapBubbleUp = [&](std::vector& heap, std::vector& heapPos, HighsInt i) { + if (i >= static_cast(heap.size())) return; while (i > 0) { HighsInt parent = (i - 1) / 2; if (!heapBetter(heap[i], heap[parent])) break; @@ -7095,6 +7096,7 @@ HPresolve::Result HPresolve::fourierMotzkin( auto heapBubbleDown = [&](std::vector& heap, std::vector& heapPos, HighsInt i) { HighsInt heapSize = static_cast(heap.size()); + if (i >= heapSize) return; while (true) { HighsInt best = i; HighsInt left = 2 * i + 1; @@ -7115,10 +7117,8 @@ HPresolve::Result HPresolve::fourierMotzkin( heapSwap(heap, heapPos, pos, last); heapPos[col] = -1; heap.pop_back(); - if (pos < static_cast(heap.size())) { - heapBubbleUp(heap, heapPos, pos); - heapBubbleDown(heap, heapPos, pos); - } + heapBubbleUp(heap, heapPos, pos); + heapBubbleDown(heap, heapPos, pos); }; auto heapUpdate = [&](std::vector& heap, From f3dbeb5f2829346d0c6cb1f5dd913edfbab64a2f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 13:53:38 +0200 Subject: [PATCH 049/196] Heap --- highs/presolve/HPresolve.cpp | 52 ++++++++++++------------------------ 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 85a5c0d6e39..27c33438a51 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6987,6 +6987,8 @@ HPresolve::Result HPresolve::fourierMotzkin( checkRows(col, iPlus, iMinus, nePlus, neMinus); if (iPlus.size() == 0 || iMinus.size() == 0) { + // other presolve reductions may handle this case (e.g., implied free + // column substitution) iPlus.clear(); iMinus.clear(); return false; @@ -7017,7 +7019,6 @@ HPresolve::Result HPresolve::fourierMotzkin( pPlus[k] = 0; pMinus[k] = 0; } - otherCols.clear(); int64_t neOld = nePlus + neMinus; int64_t neNew = static_cast(iPlus.size()) * neMinus + @@ -7158,9 +7159,10 @@ HPresolve::Result HPresolve::fourierMotzkin( isCandidate[col] = true; int64_t neRed; int64_t mrRed; - if (!checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, otherCols, neRed, - mrRed)) - continue; + bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, + otherCols, neRed, mrRed); + otherCols.clear(); + if (!elimCandidate) continue; if (isReduction(neRed, mrRed)) { heapPos[col] = static_cast(heap.size()); @@ -7181,9 +7183,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // vector for storing new rows std::vector newRows; - // workspace for collecting affected candidates + // workspace for affected candidates std::vector affectedCols; - std::vector affectedMark(model->num_col_, false); // main loop: eliminate variables from heap while (!heap.empty()) { @@ -7195,33 +7196,11 @@ HPresolve::Result HPresolve::fourierMotzkin( // recompute reduction numbers int64_t neRed; int64_t mrRed; - if (!checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, otherCols, neRed, - mrRed)) + bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, + otherCols, neRed, mrRed); + if (!elimCandidate || !isReduction(neRed, mrRed)) { + otherCols.clear(); continue; - - if (!isReduction(neRed, mrRed)) continue; - - // collect affected candidate columns before modifying the matrix - affectedCols.clear(); - for (HighsInt row : iPlus) { - for (const auto& nz : getRowVector(row)) { - HighsInt k = nz.index(); - if (k == col) continue; - if (!isCandidate[k]) continue; - if (affectedMark[k]) continue; - affectedMark[k] = true; - affectedCols.push_back(k); - } - } - for (HighsInt row : iMinus) { - for (const auto& nz : getRowVector(row)) { - HighsInt k = nz.index(); - if (k == col) continue; - if (!isCandidate[k]) continue; - if (affectedMark[k]) continue; - affectedMark[k] = true; - affectedCols.push_back(k); - } } // perform elimination: generate new rows @@ -7333,11 +7312,13 @@ HPresolve::Result HPresolve::fourierMotzkin( markColDeleted(col); // update affected candidates in the heap + affectedCols.swap(otherCols); for (HighsInt k : affectedCols) { - affectedMark[k] = false; int64_t ne, mr; - if (!checkNonZeros(k, iPlus, iMinus, pPlus, pMinus, otherCols, ne, mr) || - !isReduction(ne, mr)) { + bool elimCandidate = + checkNonZeros(k, iPlus, iMinus, pPlus, pMinus, otherCols, ne, mr); + otherCols.clear(); + if (!elimCandidate || !isReduction(ne, mr)) { heapRemove(heap, heapPos, k); } else if (heapPos[k] == -1) { heapPos[k] = static_cast(heap.size()); @@ -7347,6 +7328,7 @@ HPresolve::Result HPresolve::fourierMotzkin( heapUpdate(heap, heapPos, k, ne, mr); } } + affectedCols.clear(); HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } From 6b4922d42b7a61d0224d23758dfc1a78684efe22 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 14:05:33 +0200 Subject: [PATCH 050/196] Resize singleEquationChecked --- highs/presolve/HPresolve.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 27c33438a51..763fddd5720 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2171,6 +2171,8 @@ bool HPresolve::addToMatrix( // initialise flags if (!okResize(changedRowFlag, model->num_row_, uint8_t{0})) return false; if (!okResize(rowDeleted, model->num_row_, uint8_t{0})) return false; + if (!okResize(singleEquationChecked, model->num_row_, uint8_t{0})) + return false; // initialise row names if (!okResize(model->row_names_, model->num_row_, std::string{})) From 6149f99c521415941e929531b8be92d9588b0c4b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 14:08:34 +0200 Subject: [PATCH 051/196] Simplify again --- highs/presolve/HPresolve.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 763fddd5720..070c76d15c1 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7164,12 +7164,9 @@ HPresolve::Result HPresolve::fourierMotzkin( bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, otherCols, neRed, mrRed); otherCols.clear(); - if (!elimCandidate) continue; - - if (isReduction(neRed, mrRed)) { - heapPos[col] = static_cast(heap.size()); - heap.push_back({col, neRed, mrRed}); - } + if (!elimCandidate || !isReduction(neRed, mrRed)) continue; + heapPos[col] = static_cast(heap.size()); + heap.push_back({col, neRed, mrRed}); } if (heap.empty()) return finalise(); From 1f74f9fc248f183c088e6c20f721d79769234b59 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 14:17:42 +0200 Subject: [PATCH 052/196] Add flag --- highs/presolve/HPresolve.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 27c33438a51..8ab2ab929a2 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5934,6 +5934,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { bool trySparsify = mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif + bool tryFourierMotzkin = + mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; bool tryProbing = mipsolver != nullptr; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; @@ -5963,7 +5965,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { applyConflictGraphSubstitutions(postsolve_stack, numDelCol)); } - if (analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) + if (tryFourierMotzkin && + analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); if (analysis_.allow_rule_[kPresolveRuleAggregator]) From 46bfd28bd5acf11a7d41b565a1273a39039fdadd Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 14:26:41 +0200 Subject: [PATCH 053/196] Add message --- highs/presolve/HPresolve.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b049a01a19a..437543ce5ef 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7188,6 +7188,10 @@ HPresolve::Result HPresolve::fourierMotzkin( // workspace for affected candidates std::vector affectedCols; + HighsInt numColsEliminated = 0; + HighsInt numRowsEliminated = 0; + HighsInt numRowsAdded = 0; + // main loop: eliminate variables from heap while (!heap.empty()) { HighsInt col = heap[0].col; @@ -7297,21 +7301,25 @@ HPresolve::Result HPresolve::fourierMotzkin( // add new rows to matrix if (!addToMatrix(postsolve_stack, rowLower, rowUpper, rowEntries)) return finalise(); + numRowsAdded += static_cast(rowEntries.size()); // remove old rows containing col for (HighsInt rp : iPlus) { postsolve_stack.redundantRow(rp); removeRow(rp); + ++numRowsEliminated; } for (HighsInt rm : iMinus) { if (rowDeleted[rm]) continue; postsolve_stack.redundantRow(rm); removeRow(rm); + ++numRowsEliminated; } // mark column as deleted isCandidate[col] = false; markColDeleted(col); + ++numColsEliminated; // update affected candidates in the heap affectedCols.swap(otherCols); @@ -7335,6 +7343,13 @@ HPresolve::Result HPresolve::fourierMotzkin( HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } + if (numColsEliminated > 0) + highsLogDev(options->log_options, HighsLogType::kInfo, + "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT + " cols and %" HIGHSINT_FORMAT + " rows, and added %" HIGHSINT_FORMAT " rows\n", + numColsEliminated, numRowsEliminated, numRowsAdded); + return finalise(); } From de74be1c2de6a9c6613e5e275d07783cfb74ac6b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 15:55:25 +0200 Subject: [PATCH 054/196] Rename --- highs/presolve/HPresolve.cpp | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b049a01a19a..5dc0c804f55 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6985,7 +6985,7 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector& iMinus, std::vector& pPlus, std::vector& pMinus, - std::vector& otherCols, int64_t& neRed, + std::vector& affectedCols, int64_t& neRed, int64_t& mrRed) { int64_t nePlus; int64_t neMinus; @@ -7004,7 +7004,7 @@ HPresolve::Result HPresolve::fourierMotzkin( for (const auto& nz : getRowVector(row)) { HighsInt k = nz.index(); if (k == col) continue; - if (pPlus[k] == 0 && pMinus[k] == 0) otherCols.push_back(k); + if (pPlus[k] == 0 && pMinus[k] == 0) affectedCols.push_back(k); pPlus[k]++; } } @@ -7012,14 +7012,14 @@ HPresolve::Result HPresolve::fourierMotzkin( for (const auto& nz : getRowVector(row)) { HighsInt k = nz.index(); if (k == col) continue; - if (pPlus[k] == 0 && pMinus[k] == 0) otherCols.push_back(k); + if (pPlus[k] == 0 && pMinus[k] == 0) affectedCols.push_back(k); pMinus[k]++; } } // compute correction term int64_t correction = 0; - for (HighsInt k : otherCols) { + for (HighsInt k : affectedCols) { correction += static_cast(pPlus[k]) * pMinus[k]; pPlus[k] = 0; pMinus[k] = 0; @@ -7150,8 +7150,8 @@ HPresolve::Result HPresolve::fourierMotzkin( iMinus.reserve(model->num_row_); std::vector pPlus(model->num_col_, 0); std::vector pMinus(model->num_col_, 0); - std::vector otherCols; - otherCols.reserve(model->num_col_); + std::vector affectedCols; + affectedCols.reserve(model->num_col_); // indexed max-heap std::vector heap; @@ -7165,8 +7165,8 @@ HPresolve::Result HPresolve::fourierMotzkin( int64_t neRed; int64_t mrRed; bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, - otherCols, neRed, mrRed); - otherCols.clear(); + affectedCols, neRed, mrRed); + affectedCols.clear(); if (!elimCandidate || !isReduction(neRed, mrRed)) continue; heapPos[col] = static_cast(heap.size()); heap.push_back({col, neRed, mrRed}); @@ -7185,8 +7185,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // vector for storing new rows std::vector newRows; - // workspace for affected candidates - std::vector affectedCols; + // vector for saving affected candidates + std::vector saveAffectedCols; // main loop: eliminate variables from heap while (!heap.empty()) { @@ -7199,9 +7199,9 @@ HPresolve::Result HPresolve::fourierMotzkin( int64_t neRed; int64_t mrRed; bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, - otherCols, neRed, mrRed); + affectedCols, neRed, mrRed); if (!elimCandidate || !isReduction(neRed, mrRed)) { - otherCols.clear(); + affectedCols.clear(); continue; } @@ -7314,12 +7314,12 @@ HPresolve::Result HPresolve::fourierMotzkin( markColDeleted(col); // update affected candidates in the heap - affectedCols.swap(otherCols); - for (HighsInt k : affectedCols) { + saveAffectedCols.swap(affectedCols); + for (HighsInt k : saveAffectedCols) { int64_t ne, mr; bool elimCandidate = - checkNonZeros(k, iPlus, iMinus, pPlus, pMinus, otherCols, ne, mr); - otherCols.clear(); + checkNonZeros(k, iPlus, iMinus, pPlus, pMinus, affectedCols, ne, mr); + affectedCols.clear(); if (!elimCandidate || !isReduction(ne, mr)) { heapRemove(heap, heapPos, k); } else if (heapPos[k] == -1) { @@ -7330,7 +7330,7 @@ HPresolve::Result HPresolve::fourierMotzkin( heapUpdate(heap, heapPos, k, ne, mr); } } - affectedCols.clear(); + saveAffectedCols.clear(); HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } From 60fcf05695c589025a2901d098118b62a28e438d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 28 May 2026 20:39:43 +0200 Subject: [PATCH 055/196] Bound handling --- highs/presolve/HPresolve.cpp | 128 +++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 49 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 2774072688e..633344c05d5 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6928,6 +6928,10 @@ HPresolve::Result HPresolve::fourierMotzkin( return Result::kOk; }; + // sentinel row indices for variable bounds + const HighsInt kUpperBoundRow = -2; + const HighsInt kLowerBoundRow = -3; + auto computeCandidates = [&](std::vector& candidates) { for (HighsInt col = 0; col < model->num_col_; col++) { if (colDeleted[col]) continue; @@ -6979,6 +6983,31 @@ HPresolve::Result HPresolve::fourierMotzkin( } } } + + // include finite variable bounds as virtual singleton rows + if (model->col_upper_[col] != kHighsInf) { + iPlus.push_back(kUpperBoundRow); + nePlus += 1; + } + if (model->col_lower_[col] != -kHighsInf) { + iMinus.push_back(kLowerBoundRow); + neMinus += 1; + } + }; + + auto collectAffectedCols = [&](HighsInt col, const std::vector& set, + std::vector& mark, + std::vector& otherMark, + std::vector& affectedCols) { + for (HighsInt row : set) { + if (row < 0) continue; + for (const auto& nz : getRowVector(row)) { + HighsInt k = nz.index(); + if (k == col) continue; + if (mark[k] == 0 && otherMark[k] == 0) affectedCols.push_back(k); + mark[k]++; + } + } }; auto checkNonZeros = [&](HighsInt col, std::vector& iPlus, @@ -7000,22 +7029,8 @@ HPresolve::Result HPresolve::fourierMotzkin( } // take into account other variables present in the rows - for (HighsInt row : iPlus) { - for (const auto& nz : getRowVector(row)) { - HighsInt k = nz.index(); - if (k == col) continue; - if (pPlus[k] == 0 && pMinus[k] == 0) affectedCols.push_back(k); - pPlus[k]++; - } - } - for (HighsInt row : iMinus) { - for (const auto& nz : getRowVector(row)) { - HighsInt k = nz.index(); - if (k == col) continue; - if (pPlus[k] == 0 && pMinus[k] == 0) affectedCols.push_back(k); - pMinus[k]++; - } - } + collectAffectedCols(col, iPlus, pPlus, pMinus, affectedCols); + collectAffectedCols(col, iMinus, pMinus, pPlus, affectedCols); // compute correction term int64_t correction = 0; @@ -7138,6 +7153,38 @@ HPresolve::Result HPresolve::fourierMotzkin( heapBubbleDown(heap, heapPos, pos); }; + auto getRowData = [&](HighsInt row, HighsInt col, HighsInt multiplier, + double& absCoef, HighsInt& direction, double& bound) { + if (row < 0) { + // artificial lower / upper bound row + direction = 1; + absCoef = 1.0; + bound = multiplier > 0 ? model->col_upper_[col] : -model->col_lower_[col]; + } else { + HighsInt pPos = findNonzero(row, col); + assert(pPos != -1); + direction = multiplier * Avalue[pPos] > 0 ? HighsInt{1} : HighsInt{-1}; + absCoef = std::abs(Avalue[pPos]); + bound = direction > 0 ? model->row_upper_[row] : -model->row_lower_[row]; + } + }; + + auto collectRowEntries = [&](HighsInt row, HighsInt col, double scale, + std::vector& newRowEntries, + std::vector& newRowMark) { + if (row < 0) return; + for (const auto& nz : getRowVector(row)) { + if (nz.index() == col) continue; + double val = scale * nz.value(); + if (newRowMark[nz.index()] == -1) { + newRowMark[nz.index()] = static_cast(newRowEntries.size()); + newRowEntries.push_back({nz.index(), val}); + } else { + newRowEntries[newRowMark[nz.index()]].val += val; + } + } + }; + // collect candidate variables std::vector candidates; computeCandidates(candidates); @@ -7212,46 +7259,27 @@ HPresolve::Result HPresolve::fourierMotzkin( // perform elimination: generate new rows newRows.clear(); for (HighsInt pRow : iPlus) { - HighsInt pPos = findNonzero(pRow, col); - assert(pPos != -1); - HighsInt pDirection = Avalue[pPos] > 0 ? HighsInt{1} : HighsInt{-1}; - double pCoefAbs = std::abs(Avalue[pPos]); - double pBound = - pDirection > 0 ? model->row_upper_[pRow] : -model->row_lower_[pRow]; + double pCoefAbs; + double pBound; + HighsInt pDirection; + getRowData(pRow, col, HighsInt{1}, pCoefAbs, pDirection, pBound); for (HighsInt mRow : iMinus) { - HighsInt mPos = findNonzero(mRow, col); - assert(mPos != -1); - HighsInt mDirection = Avalue[mPos] < 0 ? HighsInt{1} : HighsInt{-1}; - double mCoefAbs = std::abs(Avalue[mPos]); - double mBound = - mDirection > 0 ? model->row_upper_[mRow] : -model->row_lower_[mRow]; + double mCoefAbs; + double mBound; + HighsInt mDirection; + getRowData(mRow, col, HighsInt{-1}, mCoefAbs, mDirection, mBound); // scale factor to preserve violation tolerances (see section 4.3): double s = (pCoefAbs * mCoefAbs) / (pCoefAbs + mCoefAbs); double pScale = s / pCoefAbs; double mScale = s / mCoefAbs; - storeRow(pRow); - for (HighsInt rowiter : rowpositions) { - if (Acol[rowiter] == col) continue; - double val = pDirection * pScale * Avalue[rowiter]; - newRowMark[Acol[rowiter]] = - static_cast(newRowEntries.size()); - newRowEntries.push_back({Acol[rowiter], val}); - } - - storeRow(mRow); - for (HighsInt rowiter : rowpositions) { - if (Acol[rowiter] == col) continue; - double val = mDirection * mScale * Avalue[rowiter]; - if (newRowMark[Acol[rowiter]] == -1) { - newRowMark[Acol[rowiter]] = - static_cast(newRowEntries.size()); - newRowEntries.push_back({Acol[rowiter], val}); - } else - newRowEntries[newRowMark[Acol[rowiter]]].val += val; - } + // collect row entries + collectRowEntries(pRow, col, pDirection * pScale, newRowEntries, + newRowMark); + collectRowEntries(mRow, col, mDirection * mScale, newRowEntries, + newRowMark); // reset marker before removing near-zeros for (const auto& e : newRowEntries) newRowMark[e.col] = -1; @@ -7303,13 +7331,15 @@ HPresolve::Result HPresolve::fourierMotzkin( return finalise(); numRowsAdded += static_cast(rowEntries.size()); - // remove old rows containing col + // remove old rows containing col (skip virtual bound rows) for (HighsInt rp : iPlus) { + if (rp < 0) continue; postsolve_stack.redundantRow(rp); removeRow(rp); ++numRowsEliminated; } for (HighsInt rm : iMinus) { + if (rm < 0) continue; if (rowDeleted[rm]) continue; postsolve_stack.redundantRow(rm); removeRow(rm); From 12a096d951b454645f4a4bc1654670ebe9dffc3e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 29 May 2026 08:42:22 +0200 Subject: [PATCH 056/196] Initialise --- highs/presolve/HPresolve.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 633344c05d5..1a6f267ce01 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7016,6 +7016,11 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector& pMinus, std::vector& affectedCols, int64_t& neRed, int64_t& mrRed) { + // initialise + neRed = 0; + mrRed = 0; + + // check rows int64_t nePlus; int64_t neMinus; checkRows(col, iPlus, iMinus, nePlus, neMinus); From 2e6786f12e8df9fa1e5614621df5d2ab662e362e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 29 May 2026 10:00:05 +0200 Subject: [PATCH 057/196] Add another lambda to check if column is a candidate --- highs/presolve/HPresolve.cpp | 178 ++++++++++++++++++----------------- 1 file changed, 92 insertions(+), 86 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 633344c05d5..a52f0e97170 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6932,22 +6932,21 @@ HPresolve::Result HPresolve::fourierMotzkin( const HighsInt kUpperBoundRow = -2; const HighsInt kLowerBoundRow = -3; - auto computeCandidates = [&](std::vector& candidates) { - for (HighsInt col = 0; col < model->num_col_; col++) { - if (colDeleted[col]) continue; - if (colsize[col] == 0) continue; - if (model->integrality_[col] != HighsVarType::kContinuous) continue; - - bool isCandidate = true; - for (const auto& nz : getColumnVector(col)) { - isCandidate = !isEquation(nz.index()); - if (!isCandidate) break; - double absval = std::abs(nz.value()); - isCandidate = absval >= 1.0 / maxCoef && absval <= maxCoef; - if (!isCandidate) break; - } - if (isCandidate) candidates.push_back(col); + auto isCandidate = [&](HighsInt col) { + if (colDeleted[col]) return false; + if (colsize[col] == 0) return false; + if (model->integrality_[col] != HighsVarType::kContinuous) return false; + for (const auto& nz : getColumnVector(col)) { + if (isEquation(nz.index())) return false; + double absval = std::abs(nz.value()); + if (absval < 1.0 / maxCoef || absval > maxCoef) return false; } + return true; + }; + + auto computeCandidates = [&](std::vector& candidates) { + for (HighsInt col = 0; col < model->num_col_; col++) + if (isCandidate(col)) candidates.push_back(col); }; auto checkRows = [&](HighsInt col, std::vector& iPlus, @@ -7010,44 +7009,46 @@ HPresolve::Result HPresolve::fourierMotzkin( } }; - auto checkNonZeros = [&](HighsInt col, std::vector& iPlus, - std::vector& iMinus, - std::vector& pPlus, - std::vector& pMinus, - std::vector& affectedCols, int64_t& neRed, - int64_t& mrRed) { - int64_t nePlus; - int64_t neMinus; - checkRows(col, iPlus, iMinus, nePlus, neMinus); - - if (iPlus.size() == 0 || iMinus.size() == 0) { - // other presolve reductions may handle this case (e.g., implied free - // column substitution) - iPlus.clear(); - iMinus.clear(); - return false; - } + auto checkNonZeros = + [&](HighsInt col, std::vector& iPlus, + std::vector& iMinus, std::vector& pPlus, + std::vector& pMinus, std::vector& affectedCols, + int64_t& neRed, int64_t& mrRed) { + int64_t nePlus; + int64_t neMinus; + checkRows(col, iPlus, iMinus, nePlus, neMinus); + + if (iPlus.size() == 0 || iMinus.size() == 0) { + // other presolve reductions may handle this case (e.g., implied free + // column substitution) + iPlus.clear(); + iMinus.clear(); + return false; + } - // take into account other variables present in the rows - collectAffectedCols(col, iPlus, pPlus, pMinus, affectedCols); - collectAffectedCols(col, iMinus, pMinus, pPlus, affectedCols); + // take into account other variables present in the rows + collectAffectedCols(col, iPlus, pPlus, pMinus, affectedCols); + collectAffectedCols(col, iMinus, pMinus, pPlus, affectedCols); - // compute correction term - int64_t correction = 0; - for (HighsInt k : affectedCols) { - correction += static_cast(pPlus[k]) * pMinus[k]; - pPlus[k] = 0; - pMinus[k] = 0; - } + // compute correction term + int64_t correction = 0; + for (HighsInt k : affectedCols) { + correction += static_cast(pPlus[k]) * pMinus[k]; + pPlus[k] = 0; + pMinus[k] = 0; + } - int64_t neOld = nePlus + neMinus; - int64_t neNew = static_cast(iPlus.size()) * neMinus + - static_cast(iMinus.size()) * nePlus - correction; - neRed = neOld - neNew; - mrRed = iPlus.size() + iMinus.size() - - static_cast(iPlus.size()) * iMinus.size(); - return true; - }; + int64_t mPlus = static_cast(iPlus.size()); + int64_t mMinus = static_cast(iMinus.size()); + int64_t neOld = nePlus + neMinus; + // note that we subtract the entries for column 'col' since these are + // eliminated + int64_t neNew = + mPlus * (neMinus - mMinus) + mMinus * (nePlus - mPlus) - correction; + neRed = neOld - neNew; + mrRed = mPlus + mMinus - mPlus * mMinus; + return true; + }; auto checkNewRow = [&](const newRow& nr, bool& isRedundant) { HighsCDouble impliedLower = 0; @@ -7086,6 +7087,38 @@ HPresolve::Result HPresolve::fourierMotzkin( return Result::kOk; }; + auto getRowData = [&](HighsInt row, HighsInt col, HighsInt multiplier, + double& absCoef, HighsInt& direction, double& bound) { + if (row < 0) { + // artificial lower / upper bound row + direction = 1; + absCoef = 1.0; + bound = multiplier > 0 ? model->col_upper_[col] : -model->col_lower_[col]; + } else { + HighsInt pPos = findNonzero(row, col); + assert(pPos != -1); + direction = multiplier * Avalue[pPos] > 0 ? HighsInt{1} : HighsInt{-1}; + absCoef = std::abs(Avalue[pPos]); + bound = direction > 0 ? model->row_upper_[row] : -model->row_lower_[row]; + } + }; + + auto collectRowEntries = [&](HighsInt row, HighsInt col, double scale, + std::vector& newRowEntries, + std::vector& newRowMark) { + if (row < 0) return; + for (const auto& nz : getRowVector(row)) { + if (nz.index() == col) continue; + double val = scale * nz.value(); + if (newRowMark[nz.index()] == -1) { + newRowMark[nz.index()] = static_cast(newRowEntries.size()); + newRowEntries.push_back({nz.index(), val}); + } else { + newRowEntries[newRowMark[nz.index()]].val += val; + } + } + }; + auto isReduction = [](int64_t neRed, int64_t mrRed) { return neRed > 0 || (neRed == 0 && mrRed > 0); }; @@ -7153,38 +7186,6 @@ HPresolve::Result HPresolve::fourierMotzkin( heapBubbleDown(heap, heapPos, pos); }; - auto getRowData = [&](HighsInt row, HighsInt col, HighsInt multiplier, - double& absCoef, HighsInt& direction, double& bound) { - if (row < 0) { - // artificial lower / upper bound row - direction = 1; - absCoef = 1.0; - bound = multiplier > 0 ? model->col_upper_[col] : -model->col_lower_[col]; - } else { - HighsInt pPos = findNonzero(row, col); - assert(pPos != -1); - direction = multiplier * Avalue[pPos] > 0 ? HighsInt{1} : HighsInt{-1}; - absCoef = std::abs(Avalue[pPos]); - bound = direction > 0 ? model->row_upper_[row] : -model->row_lower_[row]; - } - }; - - auto collectRowEntries = [&](HighsInt row, HighsInt col, double scale, - std::vector& newRowEntries, - std::vector& newRowMark) { - if (row < 0) return; - for (const auto& nz : getRowVector(row)) { - if (nz.index() == col) continue; - double val = scale * nz.value(); - if (newRowMark[nz.index()] == -1) { - newRowMark[nz.index()] = static_cast(newRowEntries.size()); - newRowEntries.push_back({nz.index(), val}); - } else { - newRowEntries[newRowMark[nz.index()]].val += val; - } - } - }; - // collect candidate variables std::vector candidates; computeCandidates(candidates); @@ -7204,11 +7205,8 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector heap; heap.reserve(candidates.size()); std::vector heapPos(model->num_col_, -1); - std::vector isCandidate(model->num_col_, false); - // build initial heap for (HighsInt col : candidates) { - isCandidate[col] = true; int64_t neRed; int64_t mrRed; bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, @@ -7347,24 +7345,32 @@ HPresolve::Result HPresolve::fourierMotzkin( } // mark column as deleted - isCandidate[col] = false; markColDeleted(col); ++numColsEliminated; // update affected candidates in the heap saveAffectedCols.swap(affectedCols); for (HighsInt k : saveAffectedCols) { + // check if variable is a candidate + bool isCandidateCol = isCandidate(k); + // skip variable if it is not on the heap and no candidate + if (heapPos[k] == -1 && !isCandidateCol) continue; + // check column non-zeros int64_t ne, mr; bool elimCandidate = + isCandidateCol && checkNonZeros(k, iPlus, iMinus, pPlus, pMinus, affectedCols, ne, mr); affectedCols.clear(); if (!elimCandidate || !isReduction(ne, mr)) { + // no candidate or not beneficial -> remove from heap heapRemove(heap, heapPos, k); } else if (heapPos[k] == -1) { + // new candidate -> insert into heap heapPos[k] = static_cast(heap.size()); heap.push_back({k, ne, mr}); heapBubbleUp(heap, heapPos, heapPos[k]); } else { + // update heap heapUpdate(heap, heapPos, k, ne, mr); } } From 0045191bd9602e8bd1766c6c3dbe2d2c73b31b06 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 29 May 2026 10:04:51 +0200 Subject: [PATCH 058/196] Format --- highs/presolve/HPresolve.cpp | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index cc53fbed2fb..f6c9d6447e8 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7009,20 +7009,19 @@ HPresolve::Result HPresolve::fourierMotzkin( } }; - auto checkNonZeros = [&](HighsInt col, std::vector& iPlus, - std::vector& iMinus, - std::vector& pPlus, - std::vector& pMinus, - std::vector& affectedCols, int64_t& neRed, - int64_t& mrRed) { - // initialise - neRed = 0; - mrRed = 0; - - // check rows - int64_t nePlus; - int64_t neMinus; - checkRows(col, iPlus, iMinus, nePlus, neMinus); + auto checkNonZeros = + [&](HighsInt col, std::vector& iPlus, + std::vector& iMinus, std::vector& pPlus, + std::vector& pMinus, std::vector& affectedCols, + int64_t& neRed, int64_t& mrRed) { + // initialise + neRed = 0; + mrRed = 0; + + // check rows + int64_t nePlus; + int64_t neMinus; + checkRows(col, iPlus, iMinus, nePlus, neMinus); if (iPlus.size() == 0 || iMinus.size() == 0) { // other presolve reductions may handle this case (e.g., implied free From 59e3a73ff81dbe2ef4b8c2916d1851b0ce1b42b7 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 29 May 2026 10:21:08 +0200 Subject: [PATCH 059/196] Remove check --- highs/presolve/HPresolve.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index f6c9d6447e8..b5d755b903a 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7247,8 +7247,6 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt col = heap[0].col; heapRemove(heap, heapPos, col); - if (colDeleted[col]) continue; - // recompute reduction numbers int64_t neRed; int64_t mrRed; From 7e624e87b2fe6dd4b530a24b95c4c6309637be38 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 29 May 2026 10:42:00 +0200 Subject: [PATCH 060/196] Clean up --- highs/presolve/HPresolve.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b5d755b903a..522a79ff0f8 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7238,6 +7238,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // vector for saving affected candidates std::vector saveAffectedCols; + // counters for numbers of eliminations HighsInt numColsEliminated = 0; HighsInt numRowsEliminated = 0; HighsInt numRowsAdded = 0; @@ -7247,15 +7248,14 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt col = heap[0].col; heapRemove(heap, heapPos, col); - // recompute reduction numbers + // compute affected columns int64_t neRed; int64_t mrRed; bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, affectedCols, neRed, mrRed); - if (!elimCandidate || !isReduction(neRed, mrRed)) { - affectedCols.clear(); - continue; - } + + // heap data should be up-to-date + assert(elimCandidate && isReduction(neRed, mrRed)); // perform elimination: generate new rows newRows.clear(); From 394455bfabef8fc3da93662e1cfe8d23285adfcf Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 29 May 2026 11:13:07 +0200 Subject: [PATCH 061/196] Postolve --- highs/presolve/HPresolve.cpp | 108 ++++++++++++++++---- highs/presolve/HighsPostsolveStack.cpp | 131 +++++++++++++++++++++++++ highs/presolve/HighsPostsolveStack.h | 46 +++++++++ 3 files changed, 268 insertions(+), 17 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 522a79ff0f8..4c37c784726 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7309,40 +7309,114 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector rowUpper; std::vector> rowEntries; - for (const auto& nr : newRows) { - // check whether new row is infeasible or redundant - bool redundant; - HPRESOLVE_CHECKED_CALL(checkNewRow(nr, redundant)); - - // skip redundant rows - if (redundant) continue; + // track which (plusLocalIdx, minusLocalIdx) pair generates each new row + struct NewRowPair { + HighsInt plusLocalIdx; + HighsInt minusLocalIdx; + }; + std::vector newRowPairs; - // add the row - std::vector entries; - entries.reserve(nr.entries.size()); - for (const auto& e : nr.entries) - entries.push_back({e.col, static_cast(e.val)}); - rowLower.push_back(nr.lower); - rowUpper.push_back(nr.upper); - rowEntries.push_back(std::move(entries)); + HighsInt newRowIdx = 0; + HighsInt plusLocalIdx = -1; + for (HighsInt pRow : iPlus) { + if (pRow >= 0) ++plusLocalIdx; + HighsInt minusLocalIdx = -1; + for (HighsInt mRow : iMinus) { + if (mRow >= 0) ++minusLocalIdx; + + // check whether new row is infeasible or redundant + bool redundant; + HPRESOLVE_CHECKED_CALL(checkNewRow(newRows[newRowIdx], redundant)); + + if (!redundant) { + std::vector entries; + entries.reserve(newRows[newRowIdx].entries.size()); + for (const auto& e : newRows[newRowIdx].entries) + entries.push_back({e.col, static_cast(e.val)}); + rowLower.push_back(newRows[newRowIdx].lower); + rowUpper.push_back(newRows[newRowIdx].upper); + rowEntries.push_back(std::move(entries)); + newRowPairs.push_back( + {pRow >= 0 ? plusLocalIdx : HighsInt{-1}, + mRow >= 0 ? minusLocalIdx : HighsInt{-1}}); + } + ++newRowIdx; + } } // add new rows to matrix + HighsInt oldNumRows = model->num_row_; if (!addToMatrix(postsolve_stack, rowLower, rowUpper, rowEntries)) return finalise(); numRowsAdded += static_cast(rowEntries.size()); + // build postsolve data: encode original rows + using Nonzero = HighsPostsolveStack::Nonzero; + using NewRowOrigin = HighsPostsolveStack::NewRowOrigin; + std::vector fmeRowData; + HighsInt numRealPlusRows = 0; + HighsInt numRealMinusRows = 0; + + for (HighsInt pRow : iPlus) { + if (pRow < 0) continue; + HighsInt pPos = findNonzero(pRow, col); + double coefOfCol = Avalue[pPos]; + HighsInt numOther = rowsize[pRow] - 1; + fmeRowData.push_back(Nonzero{numOther, coefOfCol}); + fmeRowData.push_back( + Nonzero{postsolve_stack.getOrigRowIndex()[pRow], + model->row_lower_[pRow]}); + fmeRowData.push_back(Nonzero{0, model->row_upper_[pRow]}); + for (const auto& nz : getRowVector(pRow)) { + if (nz.index() == col) continue; + fmeRowData.push_back( + Nonzero{postsolve_stack.getOrigColIndex()[nz.index()], nz.value()}); + } + ++numRealPlusRows; + } + for (HighsInt mRow : iMinus) { + if (mRow < 0) continue; + HighsInt mPos = findNonzero(mRow, col); + double coefOfCol = Avalue[mPos]; + HighsInt numOther = rowsize[mRow] - 1; + fmeRowData.push_back(Nonzero{numOther, coefOfCol}); + fmeRowData.push_back( + Nonzero{postsolve_stack.getOrigRowIndex()[mRow], + model->row_lower_[mRow]}); + fmeRowData.push_back(Nonzero{0, model->row_upper_[mRow]}); + for (const auto& nz : getRowVector(mRow)) { + if (nz.index() == col) continue; + fmeRowData.push_back( + Nonzero{postsolve_stack.getOrigColIndex()[nz.index()], nz.value()}); + } + ++numRealMinusRows; + } + + // build new row origin mapping + std::vector fmeNewRowOrigins; + HighsInt numNewRowsAdded = static_cast(rowEntries.size()); + fmeNewRowOrigins.reserve(numNewRowsAdded); + for (HighsInt k = 0; k < numNewRowsAdded; ++k) { + fmeNewRowOrigins.push_back( + {postsolve_stack.getOrigRowIndex()[oldNumRows + k], + newRowPairs[k].plusLocalIdx, newRowPairs[k].minusLocalIdx}); + } + + // record postsolve entry + postsolve_stack.fourierMotzkinElimination( + col, model->col_lower_[col], model->col_upper_[col], + model->col_cost_[col], numRealPlusRows, numRealMinusRows, + numNewRowsAdded, fmeRowData, fmeNewRowOrigins); + // remove old rows containing col (skip virtual bound rows) for (HighsInt rp : iPlus) { if (rp < 0) continue; - postsolve_stack.redundantRow(rp); removeRow(rp); ++numRowsEliminated; } for (HighsInt rm : iMinus) { if (rm < 0) continue; if (rowDeleted[rm]) continue; - postsolve_stack.redundantRow(rm); removeRow(rm); ++numRowsEliminated; } diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index f3489208e8d..4c07718de88 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1407,4 +1407,135 @@ void HighsPostsolveStack::SlackColSubstitution::undo( } } +void HighsPostsolveStack::FourierMotzkinElimination::undo( + const HighsPostsolveStack& postsolveStack, const HighsOptions& options, + const std::vector& rowData, + const std::vector& newRowOrigins, HighsSolution& solution, + HighsBasis& basis) const { + // rowData encoding per row (first numPlusRows are I+, then numMinusRows + // are I-): + // [pos+0] Nonzero{numOtherEntries, coefOfCol} + // [pos+1] Nonzero{origRowIndex, rowLower} + // [pos+2] Nonzero{0, rowUpper} + // [pos+3 .. pos+3+numOtherEntries-1] Nonzero{origColIdx, value} + + HighsInt totalRows = numPlusRows + numMinusRows; + + // === PRIMAL POSTSOLVE === + double impliedLower = colLower; + double impliedUpper = colUpper; + + HighsInt pos = 0; + for (HighsInt r = 0; r < totalRows; ++r) { + HighsInt numEntries = rowData[pos].index; + double coefOfCol = rowData[pos].value; + double rowLow = rowData[pos + 1].value; + double rowUp = rowData[pos + 2].value; + HighsInt entryStart = pos + 3; + + HighsCDouble otherSum = 0.0; + for (HighsInt e = 0; e < numEntries; ++e) + otherSum += static_cast(rowData[entryStart + e].value) * + solution.col_value[rowData[entryStart + e].index]; + + if (r < numPlusRows) { + // Plus row: coefOfCol > 0 gives x_j <= (rowUp - otherSum) / coefOfCol + // coefOfCol < 0 gives x_j >= (rowLow - otherSum) / coefOfCol + if (coefOfCol > 0) { + double bound = + static_cast((rowUp - otherSum) / coefOfCol); + impliedUpper = std::min(impliedUpper, bound); + } else { + double bound = + static_cast((rowLow - otherSum) / coefOfCol); + impliedLower = std::max(impliedLower, bound); + } + } else { + // Minus row: coefOfCol < 0 gives x_j <= (rowUp - otherSum) / coefOfCol + // coefOfCol > 0 gives x_j >= (rowLow - otherSum) / coefOfCol + if (coefOfCol < 0) { + double bound = + static_cast((rowUp - otherSum) / coefOfCol); + impliedUpper = std::min(impliedUpper, bound); + } else { + double bound = + static_cast((rowLow - otherSum) / coefOfCol); + impliedLower = std::max(impliedLower, bound); + } + } + + pos = entryStart + numEntries; + } + + if (impliedLower <= 0.0 && impliedUpper >= 0.0) + solution.col_value[col] = 0.0; + else if (impliedLower > 0.0) + solution.col_value[col] = impliedLower; + else + solution.col_value[col] = impliedUpper; + + if (!solution.dual_valid) return; + + // === DUAL POSTSOLVE === + std::vector origRowDuals(totalRows, 0.0); + + for (HighsInt k = 0; k < numNewRows; ++k) { + HighsInt newRow = newRowOrigins[k].newRow; + if (!postsolveStack.isModelRow(newRow)) continue; + double lambda = solution.row_dual[newRow]; + if (newRowOrigins[k].plusRow >= 0) + origRowDuals[newRowOrigins[k].plusRow] += lambda; + if (newRowOrigins[k].minusRow >= 0) + origRowDuals[numPlusRows + newRowOrigins[k].minusRow] += lambda; + solution.row_dual[newRow] = 0.0; + } + + solution.col_dual[col] = colCost; + pos = 0; + for (HighsInt r = 0; r < totalRows; ++r) { + HighsInt numEntries = rowData[pos].index; + double coefOfCol = rowData[pos].value; + HighsInt origRow = rowData[pos + 1].index; + pos += 3 + numEntries; + + if (postsolveStack.isModelRow(origRow)) { + solution.row_dual[origRow] = origRowDuals[r]; + solution.col_dual[col] -= coefOfCol * origRowDuals[r]; + } + } + + // === BASIS POSTSOLVE === + if (!basis.valid) return; + + if (solution.col_value[col] <= + colLower + options.primal_feasibility_tolerance) + basis.col_status[col] = HighsBasisStatus::kLower; + else if (solution.col_value[col] >= + colUpper - options.primal_feasibility_tolerance) + basis.col_status[col] = HighsBasisStatus::kUpper; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + + pos = 0; + for (HighsInt r = 0; r < totalRows; ++r) { + HighsInt numEntries = rowData[pos].index; + HighsInt origRow = rowData[pos + 1].index; + pos += 3 + numEntries; + + if (!postsolveStack.isModelRow(origRow)) continue; + if (std::abs(origRowDuals[r]) > options.dual_feasibility_tolerance) + basis.row_status[origRow] = origRowDuals[r] > 0 + ? HighsBasisStatus::kLower + : HighsBasisStatus::kUpper; + else + basis.row_status[origRow] = HighsBasisStatus::kBasic; + } + + for (HighsInt k = 0; k < numNewRows; ++k) { + HighsInt newRow = newRowOrigins[k].newRow; + if (postsolveStack.isModelRow(newRow)) + basis.row_status[newRow] = HighsBasisStatus::kBasic; + } +} + } // namespace presolve diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index e4bf403f16d..e4b24c5e09c 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -60,6 +60,12 @@ class HighsPostsolveStack { Nonzero() = default; }; + struct NewRowOrigin { + HighsInt newRow; + HighsInt plusRow; + HighsInt minusRow; + }; + size_t debug_prev_numreductions = 0; double debug_prev_col_lower = 0; double debug_prev_col_upper = 0; @@ -241,6 +247,22 @@ class HighsPostsolveStack { HighsBasis& basis); }; + struct FourierMotzkinElimination { + double colLower; + double colUpper; + double colCost; + HighsInt col; + HighsInt numPlusRows; + HighsInt numMinusRows; + HighsInt numNewRows; + + void undo(const HighsPostsolveStack& postsolveStack, + const HighsOptions& options, + const std::vector& rowData, + const std::vector& newRowOrigins, + HighsSolution& solution, HighsBasis& basis) const; + }; + /// tags for reduction enum class ReductionType : uint8_t { kLinearTransform, @@ -257,6 +279,7 @@ class HighsPostsolveStack { kDuplicateRow, kDuplicateColumn, kSlackColSubstitution, + kFourierMotzkinElimination, }; HighsDataStack reductionValues; @@ -543,6 +566,19 @@ class HighsPostsolveStack { reductionAdded(ReductionType::kForcingColumnRemovedRow); } + void fourierMotzkinElimination( + HighsInt col, double colLower, double colUpper, double colCost, + HighsInt numPlusRows, HighsInt numMinusRows, HighsInt numNewRows, + const std::vector& fmeRowData, + const std::vector& fmeNewRowOrigins) { + reductionValues.push(FourierMotzkinElimination{ + colLower, colUpper, colCost, origColIndex[col], numPlusRows, + numMinusRows, numNewRows}); + reductionValues.push(fmeRowData); + reductionValues.push(fmeNewRowOrigins); + reductionAdded(ReductionType::kFourierMotzkinElimination); + } + void duplicateRow(HighsInt row, bool rowUpperTightened, bool rowLowerTightened, HighsInt duplicateRow, double duplicateRowScale) { @@ -784,6 +820,16 @@ class HighsPostsolveStack { reduction.undo(*this, options, rowValues, solution, basis); break; } + case ReductionType::kFourierMotzkinElimination: { + FourierMotzkinElimination reduction; + std::vector fmeNewRowOrigins; + reductionValues.pop(fmeNewRowOrigins); + reductionValues.pop(rowValues); + reductionValues.pop(reduction); + reduction.undo(*this, options, rowValues, fmeNewRowOrigins, solution, + basis); + break; + } default: printf("Reduction case %d not handled\n", int(reductions[i - 1].first)); From badcf5f69b182748bfa3c4c0e82c13e4ffcf5617 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 29 May 2026 15:23:27 +0200 Subject: [PATCH 062/196] WIP --- highs/presolve/HPresolve.cpp | 126 +++++++---------------- highs/presolve/HighsPostsolveStack.cpp | 134 +++++++++++-------------- highs/presolve/HighsPostsolveStack.h | 102 ++++++++++++++++--- 3 files changed, 184 insertions(+), 178 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 4c37c784726..5894429a36a 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6920,6 +6920,8 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector entries; double lower; double upper; + HighsInt plusIndex; + HighsInt minusIndex; }; auto finalise = [&]() { @@ -7297,7 +7299,7 @@ HPresolve::Result HPresolve::fourierMotzkin( double new_upper = static_cast(static_cast(pScale) * pBound + static_cast(mScale) * mBound); - newRows.push_back({newRowEntries, -kHighsInf, new_upper}); + newRows.push_back({newRowEntries, -kHighsInf, new_upper, pRow, mRow}); // clear vector newRowEntries.clear(); @@ -7308,105 +7310,49 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector rowLower; std::vector rowUpper; std::vector> rowEntries; - - // track which (plusLocalIdx, minusLocalIdx) pair generates each new row - struct NewRowPair { - HighsInt plusLocalIdx; - HighsInt minusLocalIdx; - }; - std::vector newRowPairs; - - HighsInt newRowIdx = 0; - HighsInt plusLocalIdx = -1; - for (HighsInt pRow : iPlus) { - if (pRow >= 0) ++plusLocalIdx; - HighsInt minusLocalIdx = -1; - for (HighsInt mRow : iMinus) { - if (mRow >= 0) ++minusLocalIdx; - - // check whether new row is infeasible or redundant - bool redundant; - HPRESOLVE_CHECKED_CALL(checkNewRow(newRows[newRowIdx], redundant)); - - if (!redundant) { - std::vector entries; - entries.reserve(newRows[newRowIdx].entries.size()); - for (const auto& e : newRows[newRowIdx].entries) - entries.push_back({e.col, static_cast(e.val)}); - rowLower.push_back(newRows[newRowIdx].lower); - rowUpper.push_back(newRows[newRowIdx].upper); - rowEntries.push_back(std::move(entries)); - newRowPairs.push_back( - {pRow >= 0 ? plusLocalIdx : HighsInt{-1}, - mRow >= 0 ? minusLocalIdx : HighsInt{-1}}); - } - ++newRowIdx; - } - } - - // add new rows to matrix - HighsInt oldNumRows = model->num_row_; - if (!addToMatrix(postsolve_stack, rowLower, rowUpper, rowEntries)) - return finalise(); - numRowsAdded += static_cast(rowEntries.size()); - - // build postsolve data: encode original rows - using Nonzero = HighsPostsolveStack::Nonzero; - using NewRowOrigin = HighsPostsolveStack::NewRowOrigin; - std::vector fmeRowData; - HighsInt numRealPlusRows = 0; - HighsInt numRealMinusRows = 0; + std::vector> newRowPairs; + + for (const auto& nr : newRows) { + bool redundant; + HPRESOLVE_CHECKED_CALL(checkNewRow(nr, redundant)); + if (redundant) continue; + + std::vector entries; + entries.reserve(nr.entries.size()); + for (const auto& e : nr.entries) + entries.push_back({e.col, static_cast(e.val)}); + rowLower.push_back(nr.lower); + rowUpper.push_back(nr.upper); + rowEntries.push_back(std::move(entries)); + newRowPairs.push_back({nr.plusIndex, nr.minusIndex}); + } + + // record postsolve entry before addToMatrix (which may invalidate + // row slices via reallocation) + using FmeRow = + HighsPostsolveStack::FmeRowData; + std::vector plusRows; + std::vector minusRows; for (HighsInt pRow : iPlus) { if (pRow < 0) continue; - HighsInt pPos = findNonzero(pRow, col); - double coefOfCol = Avalue[pPos]; - HighsInt numOther = rowsize[pRow] - 1; - fmeRowData.push_back(Nonzero{numOther, coefOfCol}); - fmeRowData.push_back( - Nonzero{postsolve_stack.getOrigRowIndex()[pRow], - model->row_lower_[pRow]}); - fmeRowData.push_back(Nonzero{0, model->row_upper_[pRow]}); - for (const auto& nz : getRowVector(pRow)) { - if (nz.index() == col) continue; - fmeRowData.push_back( - Nonzero{postsolve_stack.getOrigColIndex()[nz.index()], nz.value()}); - } - ++numRealPlusRows; + plusRows.push_back({pRow, model->row_lower_[pRow], + model->row_upper_[pRow], getRowVector(pRow)}); } for (HighsInt mRow : iMinus) { if (mRow < 0) continue; - HighsInt mPos = findNonzero(mRow, col); - double coefOfCol = Avalue[mPos]; - HighsInt numOther = rowsize[mRow] - 1; - fmeRowData.push_back(Nonzero{numOther, coefOfCol}); - fmeRowData.push_back( - Nonzero{postsolve_stack.getOrigRowIndex()[mRow], - model->row_lower_[mRow]}); - fmeRowData.push_back(Nonzero{0, model->row_upper_[mRow]}); - for (const auto& nz : getRowVector(mRow)) { - if (nz.index() == col) continue; - fmeRowData.push_back( - Nonzero{postsolve_stack.getOrigColIndex()[nz.index()], nz.value()}); - } - ++numRealMinusRows; - } - - // build new row origin mapping - std::vector fmeNewRowOrigins; - HighsInt numNewRowsAdded = static_cast(rowEntries.size()); - fmeNewRowOrigins.reserve(numNewRowsAdded); - for (HighsInt k = 0; k < numNewRowsAdded; ++k) { - fmeNewRowOrigins.push_back( - {postsolve_stack.getOrigRowIndex()[oldNumRows + k], - newRowPairs[k].plusLocalIdx, newRowPairs[k].minusLocalIdx}); + minusRows.push_back({mRow, model->row_lower_[mRow], + model->row_upper_[mRow], getRowVector(mRow)}); } - // record postsolve entry postsolve_stack.fourierMotzkinElimination( col, model->col_lower_[col], model->col_upper_[col], - model->col_cost_[col], numRealPlusRows, numRealMinusRows, - numNewRowsAdded, fmeRowData, fmeNewRowOrigins); + model->col_cost_[col], plusRows, minusRows, newRowPairs); + + // add new rows to matrix + if (!addToMatrix(postsolve_stack, rowLower, rowUpper, rowEntries)) + return finalise(); + numRowsAdded += static_cast(rowEntries.size()); // remove old rows containing col (skip virtual bound rows) for (HighsInt rp : iPlus) { diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 4c07718de88..61e62a41121 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1409,63 +1409,49 @@ void HighsPostsolveStack::SlackColSubstitution::undo( void HighsPostsolveStack::FourierMotzkinElimination::undo( const HighsPostsolveStack& postsolveStack, const HighsOptions& options, - const std::vector& rowData, + const std::vector& plusHeaders, + const std::vector& plusCoefOfCol, + const std::vector>& plusEntries, + const std::vector& minusHeaders, + const std::vector& minusCoefOfCol, + const std::vector>& minusEntries, const std::vector& newRowOrigins, HighsSolution& solution, HighsBasis& basis) const { - // rowData encoding per row (first numPlusRows are I+, then numMinusRows - // are I-): - // [pos+0] Nonzero{numOtherEntries, coefOfCol} - // [pos+1] Nonzero{origRowIndex, rowLower} - // [pos+2] Nonzero{0, rowUpper} - // [pos+3 .. pos+3+numOtherEntries-1] Nonzero{origColIdx, value} - - HighsInt totalRows = numPlusRows + numMinusRows; + HighsInt numPlus = static_cast(plusHeaders.size()); + HighsInt numMinus = static_cast(minusHeaders.size()); // === PRIMAL POSTSOLVE === double impliedLower = colLower; double impliedUpper = colUpper; - HighsInt pos = 0; - for (HighsInt r = 0; r < totalRows; ++r) { - HighsInt numEntries = rowData[pos].index; - double coefOfCol = rowData[pos].value; - double rowLow = rowData[pos + 1].value; - double rowUp = rowData[pos + 2].value; - HighsInt entryStart = pos + 3; - - HighsCDouble otherSum = 0.0; - for (HighsInt e = 0; e < numEntries; ++e) - otherSum += static_cast(rowData[entryStart + e].value) * - solution.col_value[rowData[entryStart + e].index]; - - if (r < numPlusRows) { - // Plus row: coefOfCol > 0 gives x_j <= (rowUp - otherSum) / coefOfCol - // coefOfCol < 0 gives x_j >= (rowLow - otherSum) / coefOfCol - if (coefOfCol > 0) { - double bound = - static_cast((rowUp - otherSum) / coefOfCol); - impliedUpper = std::min(impliedUpper, bound); - } else { - double bound = - static_cast((rowLow - otherSum) / coefOfCol); - impliedLower = std::max(impliedLower, bound); - } - } else { - // Minus row: coefOfCol < 0 gives x_j <= (rowUp - otherSum) / coefOfCol - // coefOfCol > 0 gives x_j >= (rowLow - otherSum) / coefOfCol - if (coefOfCol < 0) { - double bound = - static_cast((rowUp - otherSum) / coefOfCol); - impliedUpper = std::min(impliedUpper, bound); + auto tightenBounds = [&](const std::vector& headers, + const std::vector& coefs, + const std::vector>& entries) { + for (size_t r = 0; r < headers.size(); ++r) { + const FmeRowHeader& hdr = headers[r]; + double aij = coefs[r]; + + HighsCDouble otherSum = 0.0; + for (const auto& nz : entries[r]) + otherSum += static_cast(nz.value) * + solution.col_value[nz.index]; + + if (aij > 0) { + double ub = static_cast((hdr.rowUpper - otherSum) / aij); + double lb = static_cast((hdr.rowLower - otherSum) / aij); + impliedUpper = std::min(impliedUpper, ub); + impliedLower = std::max(impliedLower, lb); } else { - double bound = - static_cast((rowLow - otherSum) / coefOfCol); - impliedLower = std::max(impliedLower, bound); + double ub = static_cast((hdr.rowLower - otherSum) / aij); + double lb = static_cast((hdr.rowUpper - otherSum) / aij); + impliedUpper = std::min(impliedUpper, ub); + impliedLower = std::max(impliedLower, lb); } } + }; - pos = entryStart + numEntries; - } + tightenBounds(plusHeaders, plusCoefOfCol, plusEntries); + tightenBounds(minusHeaders, minusCoefOfCol, minusEntries); if (impliedLower <= 0.0 && impliedUpper >= 0.0) solution.col_value[col] = 0.0; @@ -1477,32 +1463,29 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (!solution.dual_valid) return; // === DUAL POSTSOLVE === - std::vector origRowDuals(totalRows, 0.0); - + // Distribute new row duals back to original rows for (HighsInt k = 0; k < numNewRows; ++k) { HighsInt newRow = newRowOrigins[k].newRow; if (!postsolveStack.isModelRow(newRow)) continue; double lambda = solution.row_dual[newRow]; if (newRowOrigins[k].plusRow >= 0) - origRowDuals[newRowOrigins[k].plusRow] += lambda; + solution.row_dual[newRowOrigins[k].plusRow] += lambda; if (newRowOrigins[k].minusRow >= 0) - origRowDuals[numPlusRows + newRowOrigins[k].minusRow] += lambda; + solution.row_dual[newRowOrigins[k].minusRow] += lambda; solution.row_dual[newRow] = 0.0; } + // Compute col dual from original row duals solution.col_dual[col] = colCost; - pos = 0; - for (HighsInt r = 0; r < totalRows; ++r) { - HighsInt numEntries = rowData[pos].index; - double coefOfCol = rowData[pos].value; - HighsInt origRow = rowData[pos + 1].index; - pos += 3 + numEntries; - - if (postsolveStack.isModelRow(origRow)) { - solution.row_dual[origRow] = origRowDuals[r]; - solution.col_dual[col] -= coefOfCol * origRowDuals[r]; + auto applyColDual = [&](const std::vector& headers, + const std::vector& coefs) { + for (size_t r = 0; r < headers.size(); ++r) { + if (postsolveStack.isModelRow(headers[r].row)) + solution.col_dual[col] -= coefs[r] * solution.row_dual[headers[r].row]; } - } + }; + applyColDual(plusHeaders, plusCoefOfCol); + applyColDual(minusHeaders, minusCoefOfCol); // === BASIS POSTSOLVE === if (!basis.valid) return; @@ -1516,20 +1499,21 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( else basis.col_status[col] = HighsBasisStatus::kBasic; - pos = 0; - for (HighsInt r = 0; r < totalRows; ++r) { - HighsInt numEntries = rowData[pos].index; - HighsInt origRow = rowData[pos + 1].index; - pos += 3 + numEntries; - - if (!postsolveStack.isModelRow(origRow)) continue; - if (std::abs(origRowDuals[r]) > options.dual_feasibility_tolerance) - basis.row_status[origRow] = origRowDuals[r] > 0 - ? HighsBasisStatus::kLower - : HighsBasisStatus::kUpper; - else - basis.row_status[origRow] = HighsBasisStatus::kBasic; - } + auto applyBasis = [&](const std::vector& headers) { + for (size_t r = 0; r < headers.size(); ++r) { + if (!postsolveStack.isModelRow(headers[r].row)) continue; + double dual = solution.row_dual[headers[r].row]; + if (std::abs(dual) > options.dual_feasibility_tolerance) + basis.row_status[headers[r].row] = dual > 0 + ? HighsBasisStatus::kLower + : HighsBasisStatus::kUpper; + else + basis.row_status[headers[r].row] = HighsBasisStatus::kBasic; + } + }; + + applyBasis(plusHeaders); + applyBasis(minusHeaders); for (HighsInt k = 0; k < numNewRows; ++k) { HighsInt newRow = newRowOrigins[k].newRow; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index e4b24c5e09c..a4a4fa547c0 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -66,6 +66,14 @@ class HighsPostsolveStack { HighsInt minusRow; }; + template + struct FmeRowData { + HighsInt row; + double rowLower; + double rowUpper; + HighsMatrixSlice rowVec; + }; + size_t debug_prev_numreductions = 0; double debug_prev_col_lower = 0; double debug_prev_col_upper = 0; @@ -247,18 +255,27 @@ class HighsPostsolveStack { HighsBasis& basis); }; + struct FmeRowHeader { + HighsInt row; + double rowLower; + double rowUpper; + }; + struct FourierMotzkinElimination { double colLower; double colUpper; double colCost; HighsInt col; - HighsInt numPlusRows; - HighsInt numMinusRows; HighsInt numNewRows; void undo(const HighsPostsolveStack& postsolveStack, const HighsOptions& options, - const std::vector& rowData, + const std::vector& plusHeaders, + const std::vector& plusCoefOfCol, + const std::vector>& plusEntries, + const std::vector& minusHeaders, + const std::vector& minusCoefOfCol, + const std::vector>& minusEntries, const std::vector& newRowOrigins, HighsSolution& solution, HighsBasis& basis) const; }; @@ -566,16 +583,58 @@ class HighsPostsolveStack { reductionAdded(ReductionType::kForcingColumnRemovedRow); } + template void fourierMotzkinElimination( HighsInt col, double colLower, double colUpper, double colCost, - HighsInt numPlusRows, HighsInt numMinusRows, HighsInt numNewRows, - const std::vector& fmeRowData, - const std::vector& fmeNewRowOrigins) { + const std::vector>& plusRows, + const std::vector>& minusRows, + const std::vector>& newRowPairs) { + HighsInt origCol = origColIndex[col]; + + auto translateAndPush = + [&](const std::vector>& rows) { + std::vector headers; + std::vector coefs; + headers.reserve(rows.size()); + coefs.reserve(rows.size()); + for (const auto& rd : rows) { + headers.push_back( + {origRowIndex[rd.row], rd.rowLower, rd.rowUpper}); + double coef = 0.0; + std::vector translated; + for (const HighsSliceNonzero& nz : rd.rowVec) { + if (nz.index() == col) { + coef = nz.value(); + } else { + translated.push_back( + {origColIndex[nz.index()], nz.value()}); + } + } + coefs.push_back(coef); + reductionValues.push(translated); + } + reductionValues.push(coefs); + reductionValues.push(headers); + }; + + // build new row origins: new rows will get indices nextRowIndex, nextRowIndex+1, ... + HighsInt numNewRows = static_cast(newRowPairs.size()); + std::vector translatedOrigins; + translatedOrigins.reserve(numNewRows); + for (HighsInt k = 0; k < numNewRows; ++k) { + HighsInt plusRow = newRowPairs[k].first; + HighsInt minusRow = newRowPairs[k].second; + translatedOrigins.push_back( + {nextRowIndex + k, + plusRow >= 0 ? origRowIndex[plusRow] : HighsInt{-1}, + minusRow >= 0 ? origRowIndex[minusRow] : HighsInt{-1}}); + } + reductionValues.push(FourierMotzkinElimination{ - colLower, colUpper, colCost, origColIndex[col], numPlusRows, - numMinusRows, numNewRows}); - reductionValues.push(fmeRowData); - reductionValues.push(fmeNewRowOrigins); + colLower, colUpper, colCost, origCol, numNewRows}); + translateAndPush(plusRows); + translateAndPush(minusRows); + reductionValues.push(translatedOrigins); reductionAdded(ReductionType::kFourierMotzkinElimination); } @@ -824,10 +883,27 @@ class HighsPostsolveStack { FourierMotzkinElimination reduction; std::vector fmeNewRowOrigins; reductionValues.pop(fmeNewRowOrigins); - reductionValues.pop(rowValues); + + auto popRowData = [&](std::vector& headers, + std::vector& coefs, + std::vector>& entries) { + reductionValues.pop(headers); + reductionValues.pop(coefs); + HighsInt numRows = static_cast(coefs.size()); + entries.resize(numRows); + for (HighsInt r = numRows - 1; r >= 0; --r) + reductionValues.pop(entries[r]); + }; + + std::vector minusHeaders, plusHeaders; + std::vector minusCoefs, plusCoefs; + std::vector> minusEntries, plusEntries; + popRowData(minusHeaders, minusCoefs, minusEntries); + popRowData(plusHeaders, plusCoefs, plusEntries); reductionValues.pop(reduction); - reduction.undo(*this, options, rowValues, fmeNewRowOrigins, solution, - basis); + reduction.undo(*this, options, plusHeaders, plusCoefs, plusEntries, + minusHeaders, minusCoefs, minusEntries, + fmeNewRowOrigins, solution, basis); break; } default: From e2a84b4dcd0d097126bb96e095d8483bdae71357 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 29 May 2026 16:03:41 +0200 Subject: [PATCH 063/196] WIP --- highs/presolve/HighsPostsolveStack.cpp | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 61e62a41121..1ab7f214a1d 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1424,6 +1424,14 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( double impliedLower = colLower; double impliedUpper = colUpper; + auto computeBounds = [&](HighsInt direction, double val, double rhs, + const HighsCDouble& sum, double& impliedBound) { + if (std::abs(rhs) == kHighsInf) return; + double bound = static_cast(rhs - sum) / val; + impliedBound = + direction * std::min(direction * bound, direction * impliedBound); + }; + auto tightenBounds = [&](const std::vector& headers, const std::vector& coefs, const std::vector>& entries) { @@ -1431,22 +1439,15 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( const FmeRowHeader& hdr = headers[r]; double aij = coefs[r]; - HighsCDouble otherSum = 0.0; + HighsCDouble sum = 0.0; for (const auto& nz : entries[r]) - otherSum += static_cast(nz.value) * - solution.col_value[nz.index]; - - if (aij > 0) { - double ub = static_cast((hdr.rowUpper - otherSum) / aij); - double lb = static_cast((hdr.rowLower - otherSum) / aij); - impliedUpper = std::min(impliedUpper, ub); - impliedLower = std::max(impliedLower, lb); - } else { - double ub = static_cast((hdr.rowLower - otherSum) / aij); - double lb = static_cast((hdr.rowUpper - otherSum) / aij); - impliedUpper = std::min(impliedUpper, ub); - impliedLower = std::max(impliedLower, lb); - } + sum += + static_cast(nz.value) * solution.col_value[nz.index]; + + computeBounds(HighsInt{1}, aij, aij > 0 ? hdr.rowUpper : hdr.rowLower, + sum, impliedUpper); + computeBounds(HighsInt{-1}, aij, aij > 0 ? hdr.rowLower : hdr.rowUpper, + sum, impliedLower); } }; @@ -1504,9 +1505,8 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (!postsolveStack.isModelRow(headers[r].row)) continue; double dual = solution.row_dual[headers[r].row]; if (std::abs(dual) > options.dual_feasibility_tolerance) - basis.row_status[headers[r].row] = dual > 0 - ? HighsBasisStatus::kLower - : HighsBasisStatus::kUpper; + basis.row_status[headers[r].row] = + dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; else basis.row_status[headers[r].row] = HighsBasisStatus::kBasic; } From f7d8ab01dd99cf08887642639b507467b1e1be8c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 1 Jun 2026 09:35:17 +0200 Subject: [PATCH 064/196] WIP --- highs/presolve/HPresolve.cpp | 4 ++-- highs/presolve/HighsPostsolveStack.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5894429a36a..238e624c512 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5936,8 +5936,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { bool trySparsify = mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif - bool tryFourierMotzkin = - mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; + bool tryFourierMotzkin = true; + //mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; bool tryProbing = mipsolver != nullptr; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index a4a4fa547c0..23688c6297d 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -920,7 +920,7 @@ class HighsPostsolveStack { if (perform_dual_postsolve) solution.row_dual.resize(origNumRow); if (perform_basis_postsolve) { - assert(numAppendedRows == 0); + //assert(numAppendedRows == 0); basis.row_status.resize(origNumRow); } From 56c00af8b82e1437471dba1eb39b8ebd89da5642 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 1 Jun 2026 10:38:00 +0200 Subject: [PATCH 065/196] WIP --- highs/presolve/HighsPostsolveStack.cpp | 265 +++++++++++++++++++++++-- 1 file changed, 244 insertions(+), 21 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 1ab7f214a1d..8ece82ecea0 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1454,7 +1454,12 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( tightenBounds(plusHeaders, plusCoefOfCol, plusEntries); tightenBounds(minusHeaders, minusCoefOfCol, minusEntries); - if (impliedLower <= 0.0 && impliedUpper >= 0.0) + // Cost-aware primal assignment: push toward the bound favored by the cost + if (colCost < 0 && impliedUpper != kHighsInf) + solution.col_value[col] = impliedUpper; + else if (colCost > 0 && impliedLower != -kHighsInf) + solution.col_value[col] = impliedLower; + else if (impliedLower <= 0.0 && impliedUpper >= 0.0) solution.col_value[col] = 0.0; else if (impliedLower > 0.0) solution.col_value[col] = impliedLower; @@ -1464,15 +1469,41 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (!solution.dual_valid) return; // === DUAL POSTSOLVE === - // Distribute new row duals back to original rows + // Distribute new row duals back to original rows. + // The new row was formed as: (s/pCoefAbs) * row_plus + (s/mCoefAbs) * row_minus + // By LP duality: y_plus += (s/pCoefAbs) * lambda, y_minus += (s/mCoefAbs) * lambda for (HighsInt k = 0; k < numNewRows; ++k) { HighsInt newRow = newRowOrigins[k].newRow; if (!postsolveStack.isModelRow(newRow)) continue; double lambda = solution.row_dual[newRow]; - if (newRowOrigins[k].plusRow >= 0) - solution.row_dual[newRowOrigins[k].plusRow] += lambda; - if (newRowOrigins[k].minusRow >= 0) - solution.row_dual[newRowOrigins[k].minusRow] += lambda; + + HighsInt pOrigRow = newRowOrigins[k].plusRow; + HighsInt mOrigRow = newRowOrigins[k].minusRow; + + double pCoefAbs = 0.0; + double mCoefAbs = 0.0; + if (pOrigRow >= 0) { + for (HighsInt r = 0; r < numPlus; ++r) + if (plusHeaders[r].row == pOrigRow) { + pCoefAbs = std::abs(plusCoefOfCol[r]); + break; + } + } + if (mOrigRow >= 0) { + for (HighsInt r = 0; r < numMinus; ++r) + if (minusHeaders[r].row == mOrigRow) { + mCoefAbs = std::abs(minusCoefOfCol[r]); + break; + } + } + + if (pCoefAbs == 0.0) pCoefAbs = 1.0; + if (mCoefAbs == 0.0) mCoefAbs = 1.0; + double s = (pCoefAbs * mCoefAbs) / (pCoefAbs + mCoefAbs); + if (pOrigRow >= 0) + solution.row_dual[pOrigRow] += lambda * (s / pCoefAbs); + if (mOrigRow >= 0) + solution.row_dual[mOrigRow] += lambda * (s / mCoefAbs); solution.row_dual[newRow] = 0.0; } @@ -1488,37 +1519,229 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( applyColDual(plusHeaders, plusCoefOfCol); applyColDual(minusHeaders, minusCoefOfCol); + // If col is at an interior point (not at a bound), it should be basic + // with col_dual = 0. Absorb the residual into a tight row's dual. + bool atLower = solution.col_value[col] <= + colLower + options.primal_feasibility_tolerance; + bool atUpper = solution.col_value[col] >= + colUpper - options.primal_feasibility_tolerance; + if (!atLower && !atUpper && solution.col_dual[col] != 0.0) { + // Find a tight row to absorb the residual + double residual = solution.col_dual[col]; + bool absorbed = false; + + auto tryAbsorb = [&](const std::vector& headers, + const std::vector& coefs, + const std::vector>& entries) { + if (absorbed) return; + for (size_t r = 0; r < headers.size(); ++r) { + if (!postsolveStack.isModelRow(headers[r].row)) continue; + double aij = coefs[r]; + // Check if this row is tight (activity == bound) + HighsCDouble activity = + static_cast(aij) * solution.col_value[col]; + for (const auto& nz : entries[r]) + activity += static_cast(nz.value) * + solution.col_value[nz.index]; + double act = static_cast(activity); + bool tight = false; + if (headers[r].rowUpper != kHighsInf && + std::abs(act - headers[r].rowUpper) <= + options.primal_feasibility_tolerance) + tight = true; + if (headers[r].rowLower != -kHighsInf && + std::abs(act - headers[r].rowLower) <= + options.primal_feasibility_tolerance) + tight = true; + if (!tight) continue; + // Absorb: adjust row dual so that col_dual becomes 0 + // col_dual -= aij * delta => need delta = residual / aij + double delta = residual / aij; + solution.row_dual[headers[r].row] += delta; + solution.col_dual[col] = 0.0; + absorbed = true; + return; + } + }; + + tryAbsorb(plusHeaders, plusCoefOfCol, plusEntries); + tryAbsorb(minusHeaders, minusCoefOfCol, minusEntries); + } + // === BASIS POSTSOLVE === if (!basis.valid) return; + // Compute row activities (slack = rhs - activity) for original rows + auto computeRowActivity = [&](const std::vector& headers, + const std::vector& coefs, + const std::vector>& entries, + std::vector& slacks) { + slacks.resize(headers.size()); + for (size_t r = 0; r < headers.size(); ++r) { + HighsCDouble activity = + static_cast(coefs[r]) * solution.col_value[col]; + for (const auto& nz : entries[r]) + activity += + static_cast(nz.value) * solution.col_value[nz.index]; + double act = static_cast(activity); + if (headers[r].rowUpper != kHighsInf) + slacks[r] = headers[r].rowUpper - act; + else + slacks[r] = act - headers[r].rowLower; + } + }; + + std::vector plusSlacks, minusSlacks; + computeRowActivity(plusHeaders, plusCoefOfCol, plusEntries, plusSlacks); + computeRowActivity(minusHeaders, minusCoefOfCol, minusEntries, minusSlacks); + + // Initially set col status from primal value + bool colIsBasic = false; if (solution.col_value[col] <= colLower + options.primal_feasibility_tolerance) basis.col_status[col] = HighsBasisStatus::kLower; else if (solution.col_value[col] >= colUpper - options.primal_feasibility_tolerance) basis.col_status[col] = HighsBasisStatus::kUpper; - else + else { basis.col_status[col] = HighsBasisStatus::kBasic; + colIsBasic = true; + } - auto applyBasis = [&](const std::vector& headers) { - for (size_t r = 0; r < headers.size(); ++r) { - if (!postsolveStack.isModelRow(headers[r].row)) continue; - double dual = solution.row_dual[headers[r].row]; - if (std::abs(dual) > options.dual_feasibility_tolerance) - basis.row_status[headers[r].row] = - dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; - else - basis.row_status[headers[r].row] = HighsBasisStatus::kBasic; - } + // Apply paper's propagation rules from new row status to original rows + // plusRow indices refer to origRowIndex space (headers[r].row) + // We need to find which header index corresponds to a given origRow + auto findPlusIndex = [&](HighsInt origRow) -> HighsInt { + for (HighsInt r = 0; r < numPlus; ++r) + if (plusHeaders[r].row == origRow) return r; + return -1; + }; + auto findMinusIndex = [&](HighsInt origRow) -> HighsInt { + for (HighsInt r = 0; r < numMinus; ++r) + if (minusHeaders[r].row == origRow) return r; + return -1; }; - applyBasis(plusHeaders); - applyBasis(minusHeaders); + // Track which original rows have been assigned a status + std::vector plusAssigned(numPlus, 0); + std::vector minusAssigned(numMinus, 0); for (HighsInt k = 0; k < numNewRows; ++k) { HighsInt newRow = newRowOrigins[k].newRow; - if (postsolveStack.isModelRow(newRow)) - basis.row_status[newRow] = HighsBasisStatus::kBasic; + if (!postsolveStack.isModelRow(newRow)) continue; + + HighsBasisStatus newRowStatus = basis.row_status[newRow]; + HighsInt pOrigRow = newRowOrigins[k].plusRow; + HighsInt mOrigRow = newRowOrigins[k].minusRow; + HighsInt pIdx = pOrigRow >= 0 ? findPlusIndex(pOrigRow) : -1; + HighsInt mIdx = mOrigRow >= 0 ? findMinusIndex(mOrigRow) : -1; + + if (newRowStatus != HighsBasisStatus::kBasic) { + // Nonbasic propagation: both parent rows are nonbasic + if (pIdx >= 0 && !plusAssigned[pIdx]) { + double dual = solution.row_dual[plusHeaders[pIdx].row]; + basis.row_status[plusHeaders[pIdx].row] = + dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + plusAssigned[pIdx] = 1; + } + if (mIdx >= 0 && !minusAssigned[mIdx]) { + double dual = solution.row_dual[minusHeaders[mIdx].row]; + basis.row_status[minusHeaders[mIdx].row] = + dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + minusAssigned[mIdx] = 1; + } + } else { + // Basic propagation: one parent row gets the basic status + bool pHasSlack = + pIdx >= 0 && + plusSlacks[pIdx] > options.primal_feasibility_tolerance; + bool mHasSlack = + mIdx >= 0 && + minusSlacks[mIdx] > options.primal_feasibility_tolerance; + + if (pHasSlack && !plusAssigned[pIdx]) { + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + plusAssigned[pIdx] = 1; + if (mIdx >= 0 && !minusAssigned[mIdx]) { + double dual = solution.row_dual[minusHeaders[mIdx].row]; + basis.row_status[minusHeaders[mIdx].row] = + dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + minusAssigned[mIdx] = 1; + } + } else if (mHasSlack && !minusAssigned[mIdx]) { + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + minusAssigned[mIdx] = 1; + if (pIdx >= 0 && !plusAssigned[pIdx]) { + double dual = solution.row_dual[plusHeaders[pIdx].row]; + basis.row_status[plusHeaders[pIdx].row] = + dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + plusAssigned[pIdx] = 1; + } + } else if (!colIsBasic) { + // Both slacks zero: x_j becomes basic (degeneracy) + basis.col_status[col] = HighsBasisStatus::kBasic; + colIsBasic = true; + if (pIdx >= 0 && !plusAssigned[pIdx]) { + double dual = solution.row_dual[plusHeaders[pIdx].row]; + basis.row_status[plusHeaders[pIdx].row] = + dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + plusAssigned[pIdx] = 1; + } + if (mIdx >= 0 && !minusAssigned[mIdx]) { + double dual = solution.row_dual[minusHeaders[mIdx].row]; + basis.row_status[minusHeaders[mIdx].row] = + dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + minusAssigned[mIdx] = 1; + } + } else { + // col already basic, pick one parent as basic + if (pHasSlack || (pIdx >= 0 && !plusAssigned[pIdx])) { + if (pIdx >= 0 && !plusAssigned[pIdx]) { + basis.row_status[plusHeaders[pIdx].row] = + HighsBasisStatus::kBasic; + plusAssigned[pIdx] = 1; + } + if (mIdx >= 0 && !minusAssigned[mIdx]) { + double dual = solution.row_dual[minusHeaders[mIdx].row]; + basis.row_status[minusHeaders[mIdx].row] = + dual > 0 ? HighsBasisStatus::kLower + : HighsBasisStatus::kUpper; + minusAssigned[mIdx] = 1; + } + } else if (mIdx >= 0 && !minusAssigned[mIdx]) { + basis.row_status[minusHeaders[mIdx].row] = + HighsBasisStatus::kBasic; + minusAssigned[mIdx] = 1; + } + } + } + + // New row becomes basic (it's removed from the model) + basis.row_status[newRow] = HighsBasisStatus::kBasic; + } + + // Handle original rows not involved in any new row (vanished constraints) + for (HighsInt r = 0; r < numPlus; ++r) { + if (plusAssigned[r]) continue; + if (!postsolveStack.isModelRow(plusHeaders[r].row)) continue; + if (plusSlacks[r] > options.primal_feasibility_tolerance) + basis.row_status[plusHeaders[r].row] = HighsBasisStatus::kBasic; + else { + double dual = solution.row_dual[plusHeaders[r].row]; + basis.row_status[plusHeaders[r].row] = + dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + } + } + for (HighsInt r = 0; r < numMinus; ++r) { + if (minusAssigned[r]) continue; + if (!postsolveStack.isModelRow(minusHeaders[r].row)) continue; + if (minusSlacks[r] > options.primal_feasibility_tolerance) + basis.row_status[minusHeaders[r].row] = HighsBasisStatus::kBasic; + else { + double dual = solution.row_dual[minusHeaders[r].row]; + basis.row_status[minusHeaders[r].row] = + dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + } } } From d57cbf6d90861df0108b6668bddc190b0a20e113 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 1 Jun 2026 14:12:44 +0200 Subject: [PATCH 066/196] WIP --- highs/presolve/HPresolve.cpp | 5 +- highs/presolve/HighsPostsolveStack.cpp | 108 +++++++++---------------- highs/presolve/HighsPostsolveStack.h | 8 ++ 3 files changed, 50 insertions(+), 71 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 238e624c512..a8921e0c89e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7402,12 +7402,15 @@ HPresolve::Result HPresolve::fourierMotzkin( HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } - if (numColsEliminated > 0) + if (numColsEliminated > 0) { + printf("FME-PRESOLVE: eliminated %d cols %d rows, added %d rows\n", + (int)numColsEliminated, (int)numRowsEliminated, (int)numRowsAdded); highsLogDev(options->log_options, HighsLogType::kInfo, "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT " cols and %" HIGHSINT_FORMAT " rows, and added %" HIGHSINT_FORMAT " rows\n", numColsEliminated, numRowsEliminated, numRowsAdded); + } return finalise(); } diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 8ece82ecea0..cde006547aa 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1454,12 +1454,8 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( tightenBounds(plusHeaders, plusCoefOfCol, plusEntries); tightenBounds(minusHeaders, minusCoefOfCol, minusEntries); - // Cost-aware primal assignment: push toward the bound favored by the cost - if (colCost < 0 && impliedUpper != kHighsInf) - solution.col_value[col] = impliedUpper; - else if (colCost > 0 && impliedLower != -kHighsInf) - solution.col_value[col] = impliedLower; - else if (impliedLower <= 0.0 && impliedUpper >= 0.0) + // Algorithm 3: assign x_j to 0 if feasible, else closest bound to zero + if (impliedLower <= 0.0 && impliedUpper >= 0.0) solution.col_value[col] = 0.0; else if (impliedLower > 0.0) solution.col_value[col] = impliedLower; @@ -1469,6 +1465,12 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (!solution.dual_valid) return; // === DUAL POSTSOLVE === + // Zero parent row duals before assignment (Algorithm 4 uses assignment, not accumulation) + for (HighsInt r = 0; r < numPlus; ++r) + solution.row_dual[plusHeaders[r].row] = 0.0; + for (HighsInt r = 0; r < numMinus; ++r) + solution.row_dual[minusHeaders[r].row] = 0.0; + // Distribute new row duals back to original rows. // The new row was formed as: (s/pCoefAbs) * row_plus + (s/mCoefAbs) * row_minus // By LP duality: y_plus += (s/pCoefAbs) * lambda, y_minus += (s/mCoefAbs) * lambda @@ -1507,71 +1509,23 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( solution.row_dual[newRow] = 0.0; } - // Compute col dual from original row duals + // Compute col dual: c_j - sum(a_ij * y_i) solution.col_dual[col] = colCost; - auto applyColDual = [&](const std::vector& headers, - const std::vector& coefs) { - for (size_t r = 0; r < headers.size(); ++r) { - if (postsolveStack.isModelRow(headers[r].row)) - solution.col_dual[col] -= coefs[r] * solution.row_dual[headers[r].row]; - } - }; - applyColDual(plusHeaders, plusCoefOfCol); - applyColDual(minusHeaders, minusCoefOfCol); - - // If col is at an interior point (not at a bound), it should be basic - // with col_dual = 0. Absorb the residual into a tight row's dual. - bool atLower = solution.col_value[col] <= - colLower + options.primal_feasibility_tolerance; - bool atUpper = solution.col_value[col] >= - colUpper - options.primal_feasibility_tolerance; - if (!atLower && !atUpper && solution.col_dual[col] != 0.0) { - // Find a tight row to absorb the residual - double residual = solution.col_dual[col]; - bool absorbed = false; - - auto tryAbsorb = [&](const std::vector& headers, - const std::vector& coefs, - const std::vector>& entries) { - if (absorbed) return; - for (size_t r = 0; r < headers.size(); ++r) { - if (!postsolveStack.isModelRow(headers[r].row)) continue; - double aij = coefs[r]; - // Check if this row is tight (activity == bound) - HighsCDouble activity = - static_cast(aij) * solution.col_value[col]; - for (const auto& nz : entries[r]) - activity += static_cast(nz.value) * - solution.col_value[nz.index]; - double act = static_cast(activity); - bool tight = false; - if (headers[r].rowUpper != kHighsInf && - std::abs(act - headers[r].rowUpper) <= - options.primal_feasibility_tolerance) - tight = true; - if (headers[r].rowLower != -kHighsInf && - std::abs(act - headers[r].rowLower) <= - options.primal_feasibility_tolerance) - tight = true; - if (!tight) continue; - // Absorb: adjust row dual so that col_dual becomes 0 - // col_dual -= aij * delta => need delta = residual / aij - double delta = residual / aij; - solution.row_dual[headers[r].row] += delta; - solution.col_dual[col] = 0.0; - absorbed = true; - return; - } - }; - - tryAbsorb(plusHeaders, plusCoefOfCol, plusEntries); - tryAbsorb(minusHeaders, minusCoefOfCol, minusEntries); + for (HighsInt r = 0; r < numPlus; ++r) { + if (postsolveStack.isModelRow(plusHeaders[r].row)) + solution.col_dual[col] -= + plusCoefOfCol[r] * solution.row_dual[plusHeaders[r].row]; + } + for (HighsInt r = 0; r < numMinus; ++r) { + if (postsolveStack.isModelRow(minusHeaders[r].row)) + solution.col_dual[col] -= + minusCoefOfCol[r] * solution.row_dual[minusHeaders[r].row]; } // === BASIS POSTSOLVE === if (!basis.valid) return; - // Compute row activities (slack = rhs - activity) for original rows + // Compute normalized row slacks as per paper: s_i / |a_{ij}| auto computeRowActivity = [&](const std::vector& headers, const std::vector& coefs, const std::vector>& entries, @@ -1584,10 +1538,12 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( activity += static_cast(nz.value) * solution.col_value[nz.index]; double act = static_cast(activity); + double rawSlack; if (headers[r].rowUpper != kHighsInf) - slacks[r] = headers[r].rowUpper - act; + rawSlack = headers[r].rowUpper - act; else - slacks[r] = act - headers[r].rowLower; + rawSlack = act - headers[r].rowLower; + slacks[r] = rawSlack / std::abs(coefs[r]); } }; @@ -1716,11 +1672,10 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( } } - // New row becomes basic (it's removed from the model) - basis.row_status[newRow] = HighsBasisStatus::kBasic; } - // Handle original rows not involved in any new row (vanished constraints) + // Vanished constraint rule (paper Section 3.5.3): rows not involved in any + // new row have "vanished". Nonzero slack → basic; zero slack → nonbasic. for (HighsInt r = 0; r < numPlus; ++r) { if (plusAssigned[r]) continue; if (!postsolveStack.isModelRow(plusHeaders[r].row)) continue; @@ -1743,6 +1698,19 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; } } + + // Debug: count basics after FME postsolve + { + HighsInt nBasic = 0; + for (HighsInt i = 0; i < (HighsInt)basis.col_status.size(); ++i) + if (basis.col_status[i] == HighsBasisStatus::kBasic) ++nBasic; + for (HighsInt i = 0; i < (HighsInt)basis.row_status.size(); ++i) + if (basis.row_status[i] == HighsBasisStatus::kBasic) ++nBasic; + HighsInt nRows = (HighsInt)basis.row_status.size(); + if (nBasic != nRows) + printf("FME postsolve RANK: col=%d nBasic=%d nRows=%d (diff=%d)\n", + (int)col, (int)nBasic, (int)nRows, (int)(nBasic - nRows)); + } } } // namespace presolve diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 23688c6297d..fdd6e4b2f23 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -922,6 +922,14 @@ class HighsPostsolveStack { if (perform_basis_postsolve) { //assert(numAppendedRows == 0); basis.row_status.resize(origNumRow); + HighsInt nBasic = 0; + for (HighsInt i = 0; i < (HighsInt)basis.col_status.size(); ++i) + if (basis.col_status[i] == HighsBasisStatus::kBasic) ++nBasic; + for (HighsInt i = 0; i < (HighsInt)basis.row_status.size(); ++i) + if (basis.row_status[i] == HighsBasisStatus::kBasic) ++nBasic; + if (nBasic != origNumRow) + printf("POSTSOLVE BASIS ERROR: nBasic=%d origNumRow=%d (diff=%d)\n", + (int)nBasic, (int)origNumRow, (int)(nBasic - origNumRow)); } #ifdef DEBUG_EXTRA From c4083f03808c13fd3752bd8616c574c9e815e96b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 1 Jun 2026 14:35:08 +0200 Subject: [PATCH 067/196] WIP --- highs/presolve/HighsPostsolveStack.cpp | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index cde006547aa..9fc61a77ac5 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1551,6 +1551,13 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( computeRowActivity(plusHeaders, plusCoefOfCol, plusEntries, plusSlacks); computeRowActivity(minusHeaders, minusCoefOfCol, minusEntries, minusSlacks); + // Debug: print slacks + printf("FME basis postsolve col=%d x_j=%g\n", (int)col, solution.col_value[col]); + for (HighsInt r = 0; r < numPlus; ++r) + printf(" plus[%d] row=%d slack=%g\n", (int)r, (int)plusHeaders[r].row, plusSlacks[r]); + for (HighsInt r = 0; r < numMinus; ++r) + printf(" minus[%d] row=%d slack=%g\n", (int)r, (int)minusHeaders[r].row, minusSlacks[r]); + // Initially set col status from primal value bool colIsBasic = false; if (solution.col_value[col] <= @@ -1583,16 +1590,19 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( std::vector minusAssigned(numMinus, 0); for (HighsInt k = 0; k < numNewRows; ++k) { - HighsInt newRow = newRowOrigins[k].newRow; - if (!postsolveStack.isModelRow(newRow)) continue; - - HighsBasisStatus newRowStatus = basis.row_status[newRow]; HighsInt pOrigRow = newRowOrigins[k].plusRow; HighsInt mOrigRow = newRowOrigins[k].minusRow; HighsInt pIdx = pOrigRow >= 0 ? findPlusIndex(pOrigRow) : -1; HighsInt mIdx = mOrigRow >= 0 ? findMinusIndex(mOrigRow) : -1; - if (newRowStatus != HighsBasisStatus::kBasic) { + // Compute new row slack per paper: s_{i,i'} = s_i/|a_ij| + s_{i'}/|a_{i'j}| + double newRowSlack = 0.0; + if (pIdx >= 0) newRowSlack += plusSlacks[pIdx]; + if (mIdx >= 0) newRowSlack += minusSlacks[mIdx]; + + HighsInt newRow = newRowOrigins[k].newRow; + + if (newRowSlack <= options.primal_feasibility_tolerance) { // Nonbasic propagation: both parent rows are nonbasic if (pIdx >= 0 && !plusAssigned[pIdx]) { double dual = solution.row_dual[plusHeaders[pIdx].row]; @@ -1710,6 +1720,10 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (nBasic != nRows) printf("FME postsolve RANK: col=%d nBasic=%d nRows=%d (diff=%d)\n", (int)col, (int)nBasic, (int)nRows, (int)(nBasic - nRows)); + // Print non-basic original rows + for (HighsInt i = 0; i < std::min((HighsInt)basis.row_status.size(), HighsInt{27}); ++i) + if (basis.row_status[i] != HighsBasisStatus::kBasic) + printf(" row %d nonbasic (status=%d)\n", (int)i, (int)basis.row_status[i]); } } From a86bdff801246217f5cfa28afdcb4f44bc4187db Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 2 Jun 2026 11:14:10 +0200 Subject: [PATCH 068/196] WIP --- highs/presolve/HPresolve.cpp | 2 +- highs/presolve/HighsPostsolveStack.cpp | 285 ++++++++++++------------- highs/presolve/HighsPostsolveStack.h | 37 +++- 3 files changed, 163 insertions(+), 161 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index a8921e0c89e..9ec1557e6ae 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5937,7 +5937,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif bool tryFourierMotzkin = true; - //mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; + // mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; bool tryProbing = mipsolver != nullptr; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 9fc61a77ac5..587227a305b 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1420,7 +1420,9 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( HighsInt numPlus = static_cast(plusHeaders.size()); HighsInt numMinus = static_cast(minusHeaders.size()); - // === PRIMAL POSTSOLVE === + // === PRIMAL POSTSOLVE (Algorithm 3) === + // Compute feasible range for x_j from each parent constraint using current + // solution values of other variables: a_ij * x_j + activity_others in [l, u] double impliedLower = colLower; double impliedUpper = colUpper; @@ -1465,16 +1467,18 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (!solution.dual_valid) return; // === DUAL POSTSOLVE === - // Zero parent row duals before assignment (Algorithm 4 uses assignment, not accumulation) + // Zero parent row duals before assignment (Algorithm 4 uses assignment, not + // accumulation) for (HighsInt r = 0; r < numPlus; ++r) solution.row_dual[plusHeaders[r].row] = 0.0; for (HighsInt r = 0; r < numMinus; ++r) solution.row_dual[minusHeaders[r].row] = 0.0; // Distribute new row duals back to original rows. - // The new row was formed as: (s/pCoefAbs) * row_plus + (s/mCoefAbs) * row_minus - // By LP duality: y_plus += (s/pCoefAbs) * lambda, y_minus += (s/mCoefAbs) * lambda - for (HighsInt k = 0; k < numNewRows; ++k) { + // The new row was formed as: (s/pCoefAbs) * row_plus + (s/mCoefAbs) * + // row_minus By LP duality: y_plus += (s/pCoefAbs) * lambda, y_minus += + // (s/mCoefAbs) * lambda + for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) { HighsInt newRow = newRowOrigins[k].newRow; if (!postsolveStack.isModelRow(newRow)) continue; double lambda = solution.row_dual[newRow]; @@ -1502,10 +1506,8 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (pCoefAbs == 0.0) pCoefAbs = 1.0; if (mCoefAbs == 0.0) mCoefAbs = 1.0; double s = (pCoefAbs * mCoefAbs) / (pCoefAbs + mCoefAbs); - if (pOrigRow >= 0) - solution.row_dual[pOrigRow] += lambda * (s / pCoefAbs); - if (mOrigRow >= 0) - solution.row_dual[mOrigRow] += lambda * (s / mCoefAbs); + if (pOrigRow >= 0) solution.row_dual[pOrigRow] += lambda * (s / pCoefAbs); + if (mOrigRow >= 0) solution.row_dual[mOrigRow] += lambda * (s / mCoefAbs); solution.row_dual[newRow] = 0.0; } @@ -1522,14 +1524,14 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( minusCoefOfCol[r] * solution.row_dual[minusHeaders[r].row]; } - // === BASIS POSTSOLVE === + // === BASIS POSTSOLVE (Algorithm 5) === if (!basis.valid) return; - // Compute normalized row slacks as per paper: s_i / |a_{ij}| - auto computeRowActivity = [&](const std::vector& headers, - const std::vector& coefs, - const std::vector>& entries, - std::vector& slacks) { + // Compute normalized row slacks: s_i / |a_{ij}| + auto computeSlacks = [&](const std::vector& headers, + const std::vector& coefs, + const std::vector>& entries, + std::vector& slacks) { slacks.resize(headers.size()); for (size_t r = 0; r < headers.size(); ++r) { HighsCDouble activity = @@ -1538,42 +1540,51 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( activity += static_cast(nz.value) * solution.col_value[nz.index]; double act = static_cast(activity); - double rawSlack; + double rawSlack = kHighsInf; if (headers[r].rowUpper != kHighsInf) - rawSlack = headers[r].rowUpper - act; - else - rawSlack = act - headers[r].rowLower; + rawSlack = std::min(rawSlack, headers[r].rowUpper - act); + if (headers[r].rowLower != -kHighsInf) + rawSlack = std::min(rawSlack, act - headers[r].rowLower); slacks[r] = rawSlack / std::abs(coefs[r]); } }; std::vector plusSlacks, minusSlacks; - computeRowActivity(plusHeaders, plusCoefOfCol, plusEntries, plusSlacks); - computeRowActivity(minusHeaders, minusCoefOfCol, minusEntries, minusSlacks); - - // Debug: print slacks - printf("FME basis postsolve col=%d x_j=%g\n", (int)col, solution.col_value[col]); + computeSlacks(plusHeaders, plusCoefOfCol, plusEntries, plusSlacks); + computeSlacks(minusHeaders, minusCoefOfCol, minusEntries, minusSlacks); + + printf( + "FME basis postsolve col=%d x_j=%g (origNumRow=%d) col_status_before=%d " + "colLower=%g colUpper=%g numPlus=%d numMinus=%d\n", + (int)col, solution.col_value[col], (int)postsolveStack.origNumRow, + (int)basis.col_status[col], colLower, colUpper, (int)numPlus, + (int)numMinus); + printf(" plusRows:"); for (HighsInt r = 0; r < numPlus; ++r) - printf(" plus[%d] row=%d slack=%g\n", (int)r, (int)plusHeaders[r].row, plusSlacks[r]); + printf(" %d(s=%g)", (int)plusHeaders[r].row, plusSlacks[r]); + printf("\n minusRows:"); for (HighsInt r = 0; r < numMinus; ++r) - printf(" minus[%d] row=%d slack=%g\n", (int)r, (int)minusHeaders[r].row, minusSlacks[r]); + printf(" %d(s=%g)", (int)minusHeaders[r].row, minusSlacks[r]); + printf("\n"); - // Initially set col status from primal value + // Column status bool colIsBasic = false; - if (solution.col_value[col] <= - colLower + options.primal_feasibility_tolerance) + if (newRowOrigins.empty()) { + // Free variable case: x_j is basic + basis.col_status[col] = HighsBasisStatus::kBasic; + colIsBasic = true; + } else if (solution.col_value[col] <= + colLower + options.mip_feasibility_tolerance) basis.col_status[col] = HighsBasisStatus::kLower; else if (solution.col_value[col] >= - colUpper - options.primal_feasibility_tolerance) + colUpper - options.mip_feasibility_tolerance) basis.col_status[col] = HighsBasisStatus::kUpper; else { basis.col_status[col] = HighsBasisStatus::kBasic; colIsBasic = true; } - // Apply paper's propagation rules from new row status to original rows - // plusRow indices refer to origRowIndex space (headers[r].row) - // We need to find which header index corresponds to a given origRow + // Helper to find parent index in plus/minus headers auto findPlusIndex = [&](HighsInt origRow) -> HighsInt { for (HighsInt r = 0; r < numPlus; ++r) if (plusHeaders[r].row == origRow) return r; @@ -1585,146 +1596,118 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( return -1; }; - // Track which original rows have been assigned a status - std::vector plusAssigned(numPlus, 0); - std::vector minusAssigned(numMinus, 0); - - for (HighsInt k = 0; k < numNewRows; ++k) { + // Algorithm 5: iterate over new rows (constraints K) in reverse + // For each new row k with parents (i, i'): + // - β_k basic: parent with nonzero slack becomes basic; + // both slacks zero -> x_j becomes basic + // - β_k nonbasic: no action (parents already have correct status) + for (HighsInt k = (HighsInt)newRowOrigins.size() - 1; k >= 0; --k) { HighsInt pOrigRow = newRowOrigins[k].plusRow; HighsInt mOrigRow = newRowOrigins[k].minusRow; HighsInt pIdx = pOrigRow >= 0 ? findPlusIndex(pOrigRow) : -1; HighsInt mIdx = mOrigRow >= 0 ? findMinusIndex(mOrigRow) : -1; - - // Compute new row slack per paper: s_{i,i'} = s_i/|a_ij| + s_{i'}/|a_{i'j}| - double newRowSlack = 0.0; - if (pIdx >= 0) newRowSlack += plusSlacks[pIdx]; - if (mIdx >= 0) newRowSlack += minusSlacks[mIdx]; - HighsInt newRow = newRowOrigins[k].newRow; - if (newRowSlack <= options.primal_feasibility_tolerance) { - // Nonbasic propagation: both parent rows are nonbasic - if (pIdx >= 0 && !plusAssigned[pIdx]) { - double dual = solution.row_dual[plusHeaders[pIdx].row]; - basis.row_status[plusHeaders[pIdx].row] = - dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; - plusAssigned[pIdx] = 1; - } - if (mIdx >= 0 && !minusAssigned[mIdx]) { - double dual = solution.row_dual[minusHeaders[mIdx].row]; - basis.row_status[minusHeaders[mIdx].row] = - dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; - minusAssigned[mIdx] = 1; - } - } else { - // Basic propagation: one parent row gets the basic status - bool pHasSlack = - pIdx >= 0 && - plusSlacks[pIdx] > options.primal_feasibility_tolerance; - bool mHasSlack = - mIdx >= 0 && - minusSlacks[mIdx] > options.primal_feasibility_tolerance; - - if (pHasSlack && !plusAssigned[pIdx]) { + // Determine β_k status: use combined slack when positive, else trust basis + // status + double pSlack = pIdx >= 0 ? plusSlacks[pIdx] : 0.0; + double mSlack = mIdx >= 0 ? minusSlacks[mIdx] : 0.0; + double combinedSlack = pSlack + mSlack; + bool newRowIsBasic = combinedSlack > options.mip_feasibility_tolerance || + basis.row_status[newRow] == HighsBasisStatus::kBasic; + + printf(" k=%d: newRow=%d beta=%d pOrigRow=%d mOrigRow=%d cSlack=%g\n", + (int)k, (int)newRow, (int)newRowIsBasic, (int)pOrigRow, + (int)mOrigRow, combinedSlack); + + if (!newRowIsBasic) continue; + + // Basic propagation: β_k's basic is transferred to a parent + // After transfer, β_k becomes nonbasic + bool pHasSlack = + pIdx >= 0 && plusSlacks[pIdx] > options.mip_feasibility_tolerance; + bool mHasSlack = + mIdx >= 0 && minusSlacks[mIdx] > options.mip_feasibility_tolerance; + + bool transferred = false; + if (pHasSlack && !mHasSlack) { + bool wasBasic = + basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic; + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + transferred = !wasBasic; + printf(" -> row %d BASIC (plus slack=%g, wasBasic=%d)\n", + (int)plusHeaders[pIdx].row, plusSlacks[pIdx], (int)wasBasic); + } else if (mHasSlack && !pHasSlack) { + bool wasBasic = + basis.row_status[minusHeaders[mIdx].row] == HighsBasisStatus::kBasic; + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + transferred = !wasBasic; + printf(" -> row %d BASIC (minus slack=%g, wasBasic=%d)\n", + (int)minusHeaders[mIdx].row, minusSlacks[mIdx], (int)wasBasic); + } else if (pHasSlack && mHasSlack) { + // Both have slack — pick the one not already basic, prefer larger slack + bool pWasBasic = + basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic; + bool mWasBasic = + basis.row_status[minusHeaders[mIdx].row] == HighsBasisStatus::kBasic; + if (!pWasBasic) { basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - plusAssigned[pIdx] = 1; - if (mIdx >= 0 && !minusAssigned[mIdx]) { - double dual = solution.row_dual[minusHeaders[mIdx].row]; - basis.row_status[minusHeaders[mIdx].row] = - dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; - minusAssigned[mIdx] = 1; - } - } else if (mHasSlack && !minusAssigned[mIdx]) { + transferred = true; + printf(" -> row %d BASIC (plus slack=%g)\n", + (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); + } else if (!mWasBasic) { basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - minusAssigned[mIdx] = 1; - if (pIdx >= 0 && !plusAssigned[pIdx]) { - double dual = solution.row_dual[plusHeaders[pIdx].row]; - basis.row_status[plusHeaders[pIdx].row] = - dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; - plusAssigned[pIdx] = 1; - } - } else if (!colIsBasic) { - // Both slacks zero: x_j becomes basic (degeneracy) - basis.col_status[col] = HighsBasisStatus::kBasic; - colIsBasic = true; - if (pIdx >= 0 && !plusAssigned[pIdx]) { - double dual = solution.row_dual[plusHeaders[pIdx].row]; - basis.row_status[plusHeaders[pIdx].row] = - dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; - plusAssigned[pIdx] = 1; - } - if (mIdx >= 0 && !minusAssigned[mIdx]) { - double dual = solution.row_dual[minusHeaders[mIdx].row]; - basis.row_status[minusHeaders[mIdx].row] = - dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; - minusAssigned[mIdx] = 1; - } + transferred = true; + printf(" -> row %d BASIC (minus slack=%g)\n", + (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); } else { - // col already basic, pick one parent as basic - if (pHasSlack || (pIdx >= 0 && !plusAssigned[pIdx])) { - if (pIdx >= 0 && !plusAssigned[pIdx]) { - basis.row_status[plusHeaders[pIdx].row] = - HighsBasisStatus::kBasic; - plusAssigned[pIdx] = 1; - } - if (mIdx >= 0 && !minusAssigned[mIdx]) { - double dual = solution.row_dual[minusHeaders[mIdx].row]; - basis.row_status[minusHeaders[mIdx].row] = - dual > 0 ? HighsBasisStatus::kLower - : HighsBasisStatus::kUpper; - minusAssigned[mIdx] = 1; - } - } else if (mIdx >= 0 && !minusAssigned[mIdx]) { - basis.row_status[minusHeaders[mIdx].row] = - HighsBasisStatus::kBasic; - minusAssigned[mIdx] = 1; - } + // Both already basic — no actual transfer needed + printf(" -> both parents already basic\n"); } } + if (!transferred && !colIsBasic) { + // Both slacks zero or no new basic gained: x_j becomes basic + basis.col_status[col] = HighsBasisStatus::kBasic; + colIsBasic = true; + printf(" -> col %d BASIC (degenerate)\n", (int)col); + } else if (!transferred) { + // No transfer happened (target was already basic or col already basic) + // Leave β_k basic — don't consume it + printf(" -> no transfer needed, β_k stays\n"); + continue; + } + + // β_k's basic was transferred; mark it nonbasic + basis.row_status[newRow] = HighsBasisStatus::kLower; + printf(" -> newRow %d now NONBASIC\n", (int)newRow); } - // Vanished constraint rule (paper Section 3.5.3): rows not involved in any - // new row have "vanished". Nonzero slack → basic; zero slack → nonbasic. + // Vanished constraint check: rows not involved in any new row + // Nonzero slack -> basic; zero slack -> leave unchanged + auto isInvolved = [&](HighsInt row) { + for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) + if (newRowOrigins[k].plusRow == row || newRowOrigins[k].minusRow == row) + return true; + return false; + }; + for (HighsInt r = 0; r < numPlus; ++r) { - if (plusAssigned[r]) continue; - if (!postsolveStack.isModelRow(plusHeaders[r].row)) continue; - if (plusSlacks[r] > options.primal_feasibility_tolerance) + if (isInvolved(plusHeaders[r].row)) continue; + if (plusSlacks[r] > options.mip_feasibility_tolerance) { basis.row_status[plusHeaders[r].row] = HighsBasisStatus::kBasic; - else { - double dual = solution.row_dual[plusHeaders[r].row]; - basis.row_status[plusHeaders[r].row] = - dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + printf(" vanished row %d BASIC (slack=%g)\n", (int)plusHeaders[r].row, + plusSlacks[r]); } } for (HighsInt r = 0; r < numMinus; ++r) { - if (minusAssigned[r]) continue; - if (!postsolveStack.isModelRow(minusHeaders[r].row)) continue; - if (minusSlacks[r] > options.primal_feasibility_tolerance) + if (isInvolved(minusHeaders[r].row)) continue; + if (minusSlacks[r] > options.mip_feasibility_tolerance) { basis.row_status[minusHeaders[r].row] = HighsBasisStatus::kBasic; - else { - double dual = solution.row_dual[minusHeaders[r].row]; - basis.row_status[minusHeaders[r].row] = - dual > 0 ? HighsBasisStatus::kLower : HighsBasisStatus::kUpper; + printf(" vanished row %d BASIC (slack=%g)\n", (int)minusHeaders[r].row, + minusSlacks[r]); } } - - // Debug: count basics after FME postsolve - { - HighsInt nBasic = 0; - for (HighsInt i = 0; i < (HighsInt)basis.col_status.size(); ++i) - if (basis.col_status[i] == HighsBasisStatus::kBasic) ++nBasic; - for (HighsInt i = 0; i < (HighsInt)basis.row_status.size(); ++i) - if (basis.row_status[i] == HighsBasisStatus::kBasic) ++nBasic; - HighsInt nRows = (HighsInt)basis.row_status.size(); - if (nBasic != nRows) - printf("FME postsolve RANK: col=%d nBasic=%d nRows=%d (diff=%d)\n", - (int)col, (int)nBasic, (int)nRows, (int)(nBasic - nRows)); - // Print non-basic original rows - for (HighsInt i = 0; i < std::min((HighsInt)basis.row_status.size(), HighsInt{27}); ++i) - if (basis.row_status[i] != HighsBasisStatus::kBasic) - printf(" row %d nonbasic (status=%d)\n", (int)i, (int)basis.row_status[i]); - } } } // namespace presolve diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index fdd6e4b2f23..c825fb99d1a 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -266,7 +266,6 @@ class HighsPostsolveStack { double colUpper; double colCost; HighsInt col; - HighsInt numNewRows; void undo(const HighsPostsolveStack& postsolveStack, const HighsOptions& options, @@ -598,16 +597,14 @@ class HighsPostsolveStack { headers.reserve(rows.size()); coefs.reserve(rows.size()); for (const auto& rd : rows) { - headers.push_back( - {origRowIndex[rd.row], rd.rowLower, rd.rowUpper}); + headers.push_back({origRowIndex[rd.row], rd.rowLower, rd.rowUpper}); double coef = 0.0; std::vector translated; for (const HighsSliceNonzero& nz : rd.rowVec) { if (nz.index() == col) { coef = nz.value(); } else { - translated.push_back( - {origColIndex[nz.index()], nz.value()}); + translated.push_back({origColIndex[nz.index()], nz.value()}); } } coefs.push_back(coef); @@ -617,7 +614,8 @@ class HighsPostsolveStack { reductionValues.push(headers); }; - // build new row origins: new rows will get indices nextRowIndex, nextRowIndex+1, ... + // build new row origins: new rows will get indices nextRowIndex, + // nextRowIndex+1, ... HighsInt numNewRows = static_cast(newRowPairs.size()); std::vector translatedOrigins; translatedOrigins.reserve(numNewRows); @@ -630,8 +628,8 @@ class HighsPostsolveStack { minusRow >= 0 ? origRowIndex[minusRow] : HighsInt{-1}}); } - reductionValues.push(FourierMotzkinElimination{ - colLower, colUpper, colCost, origCol, numNewRows}); + reductionValues.push( + FourierMotzkinElimination{colLower, colUpper, colCost, origCol}); translateAndPush(plusRows); translateAndPush(minusRows); reductionValues.push(translatedOrigins); @@ -911,6 +909,27 @@ class HighsPostsolveStack { int(reductions[i - 1].first)); if (kAllowDeveloperAssert) assert(1 == 0); } + if (perform_basis_postsolve) { + HighsInt nBasic = 0; + HighsInt nRows = (HighsInt)basis.row_status.size(); + for (HighsInt j = 0; j < (HighsInt)basis.col_status.size(); ++j) + if (basis.col_status[j] == HighsBasisStatus::kBasic) ++nBasic; + for (HighsInt j = 0; j < nRows; ++j) + if (basis.row_status[j] == HighsBasisStatus::kBasic) ++nBasic; + if (nBasic != nRows) + printf("After reduction %d (type %d): nBasic=%d nRows=%d (diff=%d)\n", + (int)(i - 1), (int)reductions[i - 1].first, (int)nBasic, + (int)nRows, (int)(nBasic - nRows)); + // After last reduction, print which rows are basic + if (i - 1 == numReductions) { + printf("Final basis state (nRows=%d, origNumRow=%d):\n", (int)nRows, + (int)origNumRow); + for (HighsInt j = 0; j < nRows; ++j) + if (basis.row_status[j] == HighsBasisStatus::kBasic) + printf(" row %d basic (orig=%s)\n", (int)j, + j >= origNumRow ? "intermediate" : "original"); + } + } } if (report_col >= 0) printf("After last reduction: col_value[%2d] = %g\n", int(report_col), @@ -920,7 +939,7 @@ class HighsPostsolveStack { if (perform_dual_postsolve) solution.row_dual.resize(origNumRow); if (perform_basis_postsolve) { - //assert(numAppendedRows == 0); + // assert(numAppendedRows == 0); basis.row_status.resize(origNumRow); HighsInt nBasic = 0; for (HighsInt i = 0; i < (HighsInt)basis.col_status.size(); ++i) From d025edf292b1254a18b3a0bf59eab79867ac854f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 2 Jun 2026 11:36:18 +0200 Subject: [PATCH 069/196] WIP --- highs/presolve/HPresolve.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 9ec1557e6ae..01b7b4d1622 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5968,8 +5968,10 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { } if (tryFourierMotzkin && - analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) + analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); + tryFourierMotzkin = false; + } if (analysis_.allow_rule_[kPresolveRuleAggregator]) HPRESOLVE_CHECKED_CALL(aggregator(postsolve_stack)); @@ -7084,8 +7086,11 @@ HPresolve::Result HPresolve::fourierMotzkin( double upper = upperFinite ? static_cast(impliedUpper) : kHighsInf; // check for infeasibility - if (lower > nr.upper + primal_feastol || upper < nr.lower - primal_feastol) + if (lower > nr.upper + primal_feastol || upper < nr.lower - primal_feastol) { + printf("FME infeasibility: implied [%g, %g] vs row [%g, %g]\n", + lower, upper, nr.lower, nr.upper); return Result::kPrimalInfeasible; + } // check for redundancy isRedundant = lower >= nr.lower - primal_feastol && @@ -7245,8 +7250,10 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt numRowsEliminated = 0; HighsInt numRowsAdded = 0; + const HighsInt maxFmeEliminations = 1; + // main loop: eliminate variables from heap - while (!heap.empty()) { + while (!heap.empty() && numColsEliminated < maxFmeEliminations) { HighsInt col = heap[0].col; heapRemove(heap, heapPos, col); @@ -7313,9 +7320,9 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector> newRowPairs; for (const auto& nr : newRows) { - bool redundant; - HPRESOLVE_CHECKED_CALL(checkNewRow(nr, redundant)); - if (redundant) continue; + bool redundant = false; + // HPRESOLVE_CHECKED_CALL(checkNewRow(nr, redundant)); + // if (redundant) continue; std::vector entries; entries.reserve(nr.entries.size()); @@ -7370,6 +7377,9 @@ HPresolve::Result HPresolve::fourierMotzkin( // mark column as deleted markColDeleted(col); ++numColsEliminated; + printf("FME: eliminated col=%d, removed %d plus rows, %d minus rows, added %d new rows\n", + (int)col, (int)iPlus.size(), (int)iMinus.size(), + (int)rowEntries.size()); // update affected candidates in the heap saveAffectedCols.swap(affectedCols); From 77bf8b03e06e0d2bc4d05882c1873eb1f06c451c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 3 Jun 2026 10:06:45 +0200 Subject: [PATCH 070/196] WIP --- highs/presolve/HPresolve.cpp | 10 +- highs/presolve/HighsPostsolveStack.cpp | 290 ++++++++++++++++++------- 2 files changed, 211 insertions(+), 89 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 01b7b4d1622..1b20c11df1d 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -3451,8 +3451,8 @@ HPresolve::Result HPresolve::singletonCol(HighsPostsolveStack& postsolve_stack, static_cast(convertImpliedInteger(col, row))); // dual fixing - HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); - if (colDeleted[col]) return Result::kOk; + // HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); + // if (colDeleted[col]) return Result::kOk; // singleton column stuffing HPRESOLVE_CHECKED_CALL(singletonColStuffing(postsolve_stack, col)); @@ -4575,8 +4575,8 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, } // dual fixing - HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); - if (colDeleted[col]) return Result::kOk; + // HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); + // if (colDeleted[col]) return Result::kOk; // singleton column stuffing HPRESOLVE_CHECKED_CALL(singletonColStuffing(postsolve_stack, col)); @@ -7250,7 +7250,7 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt numRowsEliminated = 0; HighsInt numRowsAdded = 0; - const HighsInt maxFmeEliminations = 1; + const HighsInt maxFmeEliminations = 2; // main loop: eliminate variables from heap while (!heap.empty() && numColsEliminated < maxFmeEliminations) { diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 587227a305b..5b9464d9cde 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1527,6 +1527,8 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( // === BASIS POSTSOLVE (Algorithm 5) === if (!basis.valid) return; + const double tol = options.mip_feasibility_tolerance; + // Compute normalized row slacks: s_i / |a_{ij}| auto computeSlacks = [&](const std::vector& headers, const std::vector& coefs, @@ -1549,6 +1551,21 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( } }; + // Determine nonbasic row status from activity vs bounds + auto nonbasicRowStatus = [&](const FmeRowHeader& hdr, + const std::vector& entries, + double coefOfCol) -> HighsBasisStatus { + HighsCDouble activity = + static_cast(coefOfCol) * solution.col_value[col]; + for (const auto& nz : entries) + activity += + static_cast(nz.value) * solution.col_value[nz.index]; + double act = static_cast(activity); + if (hdr.rowLower != -kHighsInf && act - hdr.rowLower <= tol) + return HighsBasisStatus::kLower; + return HighsBasisStatus::kUpper; + }; + std::vector plusSlacks, minusSlacks; computeSlacks(plusHeaders, plusCoefOfCol, plusEntries, plusSlacks); computeSlacks(minusHeaders, minusCoefOfCol, minusEntries, minusSlacks); @@ -1567,22 +1584,23 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( printf(" %d(s=%g)", (int)minusHeaders[r].row, minusSlacks[r]); printf("\n"); - // Column status + // Free variable case: x_j is basic bool colIsBasic = false; if (newRowOrigins.empty()) { - // Free variable case: x_j is basic basis.col_status[col] = HighsBasisStatus::kBasic; colIsBasic = true; - } else if (solution.col_value[col] <= - colLower + options.mip_feasibility_tolerance) + printf(" free variable -> col %d BASIC\n", (int)col); + } else if (solution.col_value[col] <= colLower + tol) { basis.col_status[col] = HighsBasisStatus::kLower; - else if (solution.col_value[col] >= - colUpper - options.mip_feasibility_tolerance) + } else if (solution.col_value[col] >= colUpper - tol) { basis.col_status[col] = HighsBasisStatus::kUpper; - else { + } else { basis.col_status[col] = HighsBasisStatus::kBasic; colIsBasic = true; } + printf(" col_status=%d colIsBasic=%d newRowOrigins=%d\n", + (int)basis.col_status[col], (int)colIsBasic, + (int)newRowOrigins.size()); // Helper to find parent index in plus/minus headers auto findPlusIndex = [&](HighsInt origRow) -> HighsInt { @@ -1596,11 +1614,17 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( return -1; }; + // Count how many basics we need to distribute: number of new rows that were + // basic in the presolved basis + HighsInt basicsNeeded = 0; + for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) + if (basis.row_status[newRowOrigins[k].newRow] == HighsBasisStatus::kBasic) + basicsNeeded++; + printf(" basicsNeeded=%d (from %d new rows)\n", (int)basicsNeeded, + (int)newRowOrigins.size()); + // Algorithm 5: iterate over new rows (constraints K) in reverse - // For each new row k with parents (i, i'): - // - β_k basic: parent with nonzero slack becomes basic; - // both slacks zero -> x_j becomes basic - // - β_k nonbasic: no action (parents already have correct status) + HighsInt basicsAssigned = colIsBasic ? 1 : 0; for (HighsInt k = (HighsInt)newRowOrigins.size() - 1; k >= 0; --k) { HighsInt pOrigRow = newRowOrigins[k].plusRow; HighsInt mOrigRow = newRowOrigins[k].minusRow; @@ -1608,83 +1632,155 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( HighsInt mIdx = mOrigRow >= 0 ? findMinusIndex(mOrigRow) : -1; HighsInt newRow = newRowOrigins[k].newRow; - // Determine β_k status: use combined slack when positive, else trust basis - // status - double pSlack = pIdx >= 0 ? plusSlacks[pIdx] : 0.0; - double mSlack = mIdx >= 0 ? minusSlacks[mIdx] : 0.0; + // β_k status determined solely from combined slack (paper: s_{i,i'}) + // Virtual bound parents: pOrigRow == -1 means upper bound (slack = u - x_j) + // mOrigRow == -1 means lower bound (slack = x_j - l) + double pSlack = + pIdx >= 0 + ? plusSlacks[pIdx] + : (pOrigRow < 0 + ? std::max(colUpper - solution.col_value[col], 0.0) + : 0.0); + double mSlack = + mIdx >= 0 + ? minusSlacks[mIdx] + : (mOrigRow < 0 + ? std::max(solution.col_value[col] - colLower, 0.0) + : 0.0); double combinedSlack = pSlack + mSlack; - bool newRowIsBasic = combinedSlack > options.mip_feasibility_tolerance || - basis.row_status[newRow] == HighsBasisStatus::kBasic; - - printf(" k=%d: newRow=%d beta=%d pOrigRow=%d mOrigRow=%d cSlack=%g\n", - (int)k, (int)newRow, (int)newRowIsBasic, (int)pOrigRow, - (int)mOrigRow, combinedSlack); - - if (!newRowIsBasic) continue; - - // Basic propagation: β_k's basic is transferred to a parent - // After transfer, β_k becomes nonbasic - bool pHasSlack = - pIdx >= 0 && plusSlacks[pIdx] > options.mip_feasibility_tolerance; - bool mHasSlack = - mIdx >= 0 && minusSlacks[mIdx] > options.mip_feasibility_tolerance; - - bool transferred = false; - if (pHasSlack && !mHasSlack) { - bool wasBasic = - basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic; - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - transferred = !wasBasic; - printf(" -> row %d BASIC (plus slack=%g, wasBasic=%d)\n", - (int)plusHeaders[pIdx].row, plusSlacks[pIdx], (int)wasBasic); - } else if (mHasSlack && !pHasSlack) { - bool wasBasic = - basis.row_status[minusHeaders[mIdx].row] == HighsBasisStatus::kBasic; - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - transferred = !wasBasic; - printf(" -> row %d BASIC (minus slack=%g, wasBasic=%d)\n", - (int)minusHeaders[mIdx].row, minusSlacks[mIdx], (int)wasBasic); - } else if (pHasSlack && mHasSlack) { - // Both have slack — pick the one not already basic, prefer larger slack - bool pWasBasic = - basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic; - bool mWasBasic = - basis.row_status[minusHeaders[mIdx].row] == HighsBasisStatus::kBasic; - if (!pWasBasic) { - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - transferred = true; - printf(" -> row %d BASIC (plus slack=%g)\n", - (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); - } else if (!mWasBasic) { - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - transferred = true; - printf(" -> row %d BASIC (minus slack=%g)\n", - (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); + bool betaIsBasic = combinedSlack > tol; + + printf( + " k=%d: newRow=%d beta=%d pOrigRow=%d mOrigRow=%d pSlack=%g " + "mSlack=%g cSlack=%g\n", + (int)k, (int)newRow, (int)betaIsBasic, (int)pOrigRow, (int)mOrigRow, + pSlack, mSlack, combinedSlack); + + if (!betaIsBasic) { + // Nonbasic propagation: both parents are nonbasic + if (pIdx >= 0) { + basis.row_status[plusHeaders[pIdx].row] = + nonbasicRowStatus(plusHeaders[pIdx], plusEntries[pIdx], + plusCoefOfCol[pIdx]); + printf(" -> row %d NONBASIC (status=%d)\n", + (int)plusHeaders[pIdx].row, + (int)basis.row_status[plusHeaders[pIdx].row]); + } else if (pOrigRow < 0) { + // Virtual upper bound is nonbasic: x_j at upper bound + printf(" -> virtual upper bound NONBASIC\n"); + } + if (mIdx >= 0) { + basis.row_status[minusHeaders[mIdx].row] = + nonbasicRowStatus(minusHeaders[mIdx], minusEntries[mIdx], + minusCoefOfCol[mIdx]); + printf(" -> row %d NONBASIC (status=%d)\n", + (int)minusHeaders[mIdx].row, + (int)basis.row_status[minusHeaders[mIdx].row]); + } else if (mOrigRow < 0) { + // Virtual lower bound is nonbasic: x_j at lower bound + printf(" -> virtual lower bound NONBASIC\n"); + } + // β_k stays nonbasic + basis.row_status[newRow] = HighsBasisStatus::kLower; + } else { + // Basic propagation: transfer β_k's basic to parent with nonzero slack + bool pHasSlack = pSlack > tol; + bool mHasSlack = mSlack > tol; + bool pIsVirtual = pOrigRow < 0; + bool mIsVirtual = mOrigRow < 0; + + if (pHasSlack && !mHasSlack) { + if (pIsVirtual) { + // Virtual upper bound basic → x_j is basic + if (!colIsBasic) { + basis.col_status[col] = HighsBasisStatus::kBasic; + colIsBasic = true; + basicsAssigned++; + printf(" -> col %d BASIC (virtual upper slack=%g)\n", (int)col, + pSlack); + } + } else { + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + basicsAssigned++; + printf(" -> row %d BASIC (plus slack=%g)\n", + (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); + } + } else if (mHasSlack && !pHasSlack) { + if (mIsVirtual) { + // Virtual lower bound basic → x_j is basic + if (!colIsBasic) { + basis.col_status[col] = HighsBasisStatus::kBasic; + colIsBasic = true; + basicsAssigned++; + printf(" -> col %d BASIC (virtual lower slack=%g)\n", (int)col, + mSlack); + } + } else { + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + basicsAssigned++; + printf(" -> row %d BASIC (minus slack=%g)\n", + (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); + } + } else if (pHasSlack && mHasSlack) { + // Both have nonzero slack — one becomes basic + // If one is virtual bound, the other (real row) gets priority + if (pIsVirtual && !mIsVirtual) { + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + basicsAssigned++; + printf(" -> row %d BASIC (minus slack=%g, plus is virtual)\n", + (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); + } else if (mIsVirtual && !pIsVirtual) { + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + basicsAssigned++; + printf(" -> row %d BASIC (plus slack=%g, minus is virtual)\n", + (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); + } else if (pIsVirtual && mIsVirtual) { + // Both virtual bounds have slack → x_j is between bounds → basic + if (!colIsBasic) { + basis.col_status[col] = HighsBasisStatus::kBasic; + colIsBasic = true; + basicsAssigned++; + printf(" -> col %d BASIC (both virtual bounds have slack)\n", + (int)col); + } + } else { + // Both real rows have slack — pick larger + if (plusSlacks[pIdx] >= minusSlacks[mIdx]) { + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + basis.row_status[minusHeaders[mIdx].row] = + nonbasicRowStatus(minusHeaders[mIdx], minusEntries[mIdx], + minusCoefOfCol[mIdx]); + printf(" -> row %d BASIC (plus slack=%g), row %d NONBASIC\n", + (int)plusHeaders[pIdx].row, plusSlacks[pIdx], + (int)minusHeaders[mIdx].row); + } else { + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + basis.row_status[plusHeaders[pIdx].row] = + nonbasicRowStatus(plusHeaders[pIdx], plusEntries[pIdx], + plusCoefOfCol[pIdx]); + printf(" -> row %d BASIC (minus slack=%g), row %d NONBASIC\n", + (int)minusHeaders[mIdx].row, minusSlacks[mIdx], + (int)plusHeaders[pIdx].row); + } + basicsAssigned++; + } } else { - // Both already basic — no actual transfer needed - printf(" -> both parents already basic\n"); + // Degenerate: both slacks zero but combined > tol → x_j basic + if (!colIsBasic) { + basis.col_status[col] = HighsBasisStatus::kBasic; + colIsBasic = true; + basicsAssigned++; + printf(" -> col %d BASIC (degenerate)\n", (int)col); + } } + // β_k becomes nonbasic + basis.row_status[newRow] = HighsBasisStatus::kLower; + printf(" -> newRow %d NONBASIC\n", (int)newRow); } - - if (!transferred && !colIsBasic) { - // Both slacks zero or no new basic gained: x_j becomes basic - basis.col_status[col] = HighsBasisStatus::kBasic; - colIsBasic = true; - printf(" -> col %d BASIC (degenerate)\n", (int)col); - } else if (!transferred) { - // No transfer happened (target was already basic or col already basic) - // Leave β_k basic — don't consume it - printf(" -> no transfer needed, β_k stays\n"); - continue; - } - - // β_k's basic was transferred; mark it nonbasic - basis.row_status[newRow] = HighsBasisStatus::kLower; - printf(" -> newRow %d now NONBASIC\n", (int)newRow); } - // Vanished constraint check: rows not involved in any new row - // Nonzero slack -> basic; zero slack -> leave unchanged + // Vanished constraint check: rows not involved as parent of any new row + // Nonzero slack -> basic; zero slack -> nonbasic auto isInvolved = [&](HighsInt row) { for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) if (newRowOrigins[k].plusRow == row || newRowOrigins[k].minusRow == row) @@ -1694,20 +1790,46 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( for (HighsInt r = 0; r < numPlus; ++r) { if (isInvolved(plusHeaders[r].row)) continue; - if (plusSlacks[r] > options.mip_feasibility_tolerance) { + if (plusSlacks[r] > tol) { basis.row_status[plusHeaders[r].row] = HighsBasisStatus::kBasic; + basicsAssigned++; printf(" vanished row %d BASIC (slack=%g)\n", (int)plusHeaders[r].row, plusSlacks[r]); + } else { + basis.row_status[plusHeaders[r].row] = + nonbasicRowStatus(plusHeaders[r], plusEntries[r], plusCoefOfCol[r]); + printf(" vanished row %d NONBASIC (slack=%g)\n", (int)plusHeaders[r].row, + plusSlacks[r]); } } for (HighsInt r = 0; r < numMinus; ++r) { if (isInvolved(minusHeaders[r].row)) continue; - if (minusSlacks[r] > options.mip_feasibility_tolerance) { + if (minusSlacks[r] > tol) { basis.row_status[minusHeaders[r].row] = HighsBasisStatus::kBasic; + basicsAssigned++; printf(" vanished row %d BASIC (slack=%g)\n", (int)minusHeaders[r].row, minusSlacks[r]); + } else { + basis.row_status[minusHeaders[r].row] = nonbasicRowStatus( + minusHeaders[r], minusEntries[r], minusCoefOfCol[r]); + printf(" vanished row %d NONBASIC (slack=%g)\n", + (int)minusHeaders[r].row, minusSlacks[r]); } } + + // Repair: if we assigned fewer basics than needed, promote the nonbasic + // parent rows with the largest slack (sub-tolerance but numerically closest + // to basic) + printf(" basicsAssigned=%d basicsNeeded=%d\n", (int)basicsAssigned, + (int)basicsNeeded); + if (basicsAssigned < basicsNeeded) { + printf(" repair needed: basicsAssigned=%d basicsNeeded=%d\n", + (int)basicsAssigned, (int)basicsNeeded); + // Degenerate case: presolved basis had basic new rows but all slacks + // are below tolerance. Leave the deficit for the simplex factorization + // to handle via rank repair — it makes better numerical choices than + // we can with near-zero slacks. + } } } // namespace presolve From a833703053e7d6bfe1969c626100f7c425a8e47d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 3 Jun 2026 10:47:24 +0200 Subject: [PATCH 071/196] WIP --- highs/presolve/HPresolve.cpp | 3 +- highs/presolve/HighsPostsolveStack.cpp | 78 ++++++++++++++++++-------- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 1b20c11df1d..ec873466b7c 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5970,7 +5970,6 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { if (tryFourierMotzkin && analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); - tryFourierMotzkin = false; } if (analysis_.allow_rule_[kPresolveRuleAggregator]) @@ -7250,7 +7249,7 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt numRowsEliminated = 0; HighsInt numRowsAdded = 0; - const HighsInt maxFmeEliminations = 2; + const HighsInt maxFmeEliminations = kHighsIInf; // main loop: eliminate variables from heap while (!heap.empty() && numColsEliminated < maxFmeEliminations) { diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 5b9464d9cde..eb86beb28a8 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1638,17 +1638,15 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( double pSlack = pIdx >= 0 ? plusSlacks[pIdx] - : (pOrigRow < 0 - ? std::max(colUpper - solution.col_value[col], 0.0) - : 0.0); + : (pOrigRow < 0 ? std::max(colUpper - solution.col_value[col], 0.0) + : 0.0); double mSlack = mIdx >= 0 ? minusSlacks[mIdx] - : (mOrigRow < 0 - ? std::max(solution.col_value[col] - colLower, 0.0) - : 0.0); + : (mOrigRow < 0 ? std::max(solution.col_value[col] - colLower, 0.0) + : 0.0); double combinedSlack = pSlack + mSlack; - bool betaIsBasic = combinedSlack > tol; + bool betaIsBasic = basis.row_status[newRow] == HighsBasisStatus::kBasic; printf( " k=%d: newRow=%d beta=%d pOrigRow=%d mOrigRow=%d pSlack=%g " @@ -1659,9 +1657,8 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (!betaIsBasic) { // Nonbasic propagation: both parents are nonbasic if (pIdx >= 0) { - basis.row_status[plusHeaders[pIdx].row] = - nonbasicRowStatus(plusHeaders[pIdx], plusEntries[pIdx], - plusCoefOfCol[pIdx]); + basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( + plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); printf(" -> row %d NONBASIC (status=%d)\n", (int)plusHeaders[pIdx].row, (int)basis.row_status[plusHeaders[pIdx].row]); @@ -1670,9 +1667,8 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( printf(" -> virtual upper bound NONBASIC\n"); } if (mIdx >= 0) { - basis.row_status[minusHeaders[mIdx].row] = - nonbasicRowStatus(minusHeaders[mIdx], minusEntries[mIdx], - minusCoefOfCol[mIdx]); + basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( + minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); printf(" -> row %d NONBASIC (status=%d)\n", (int)minusHeaders[mIdx].row, (int)basis.row_status[minusHeaders[mIdx].row]); @@ -1691,13 +1687,21 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (pHasSlack && !mHasSlack) { if (pIsVirtual) { - // Virtual upper bound basic → x_j is basic if (!colIsBasic) { basis.col_status[col] = HighsBasisStatus::kBasic; colIsBasic = true; basicsAssigned++; printf(" -> col %d BASIC (virtual upper slack=%g)\n", (int)col, pSlack); + } else if (mIdx >= 0 && basis.row_status[minusHeaders[mIdx].row] != + HighsBasisStatus::kBasic) { + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + basicsAssigned++; + printf(" -> row %d BASIC (degenerate minus, virtual upper)\n", + (int)minusHeaders[mIdx].row); + } else { + basicsAssigned++; + printf(" -> col already basic, other parent already basic\n"); } } else { basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; @@ -1707,13 +1711,21 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( } } else if (mHasSlack && !pHasSlack) { if (mIsVirtual) { - // Virtual lower bound basic → x_j is basic if (!colIsBasic) { basis.col_status[col] = HighsBasisStatus::kBasic; colIsBasic = true; basicsAssigned++; printf(" -> col %d BASIC (virtual lower slack=%g)\n", (int)col, mSlack); + } else if (pIdx >= 0 && basis.row_status[plusHeaders[pIdx].row] != + HighsBasisStatus::kBasic) { + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + basicsAssigned++; + printf(" -> row %d BASIC (degenerate plus, virtual lower)\n", + (int)plusHeaders[pIdx].row); + } else { + basicsAssigned++; + printf(" -> col already basic, other parent already basic\n"); } } else { basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; @@ -1747,17 +1759,15 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( // Both real rows have slack — pick larger if (plusSlacks[pIdx] >= minusSlacks[mIdx]) { basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - basis.row_status[minusHeaders[mIdx].row] = - nonbasicRowStatus(minusHeaders[mIdx], minusEntries[mIdx], - minusCoefOfCol[mIdx]); + basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( + minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); printf(" -> row %d BASIC (plus slack=%g), row %d NONBASIC\n", (int)plusHeaders[pIdx].row, plusSlacks[pIdx], (int)minusHeaders[mIdx].row); } else { basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - basis.row_status[plusHeaders[pIdx].row] = - nonbasicRowStatus(plusHeaders[pIdx], plusEntries[pIdx], - plusCoefOfCol[pIdx]); + basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( + plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); printf(" -> row %d BASIC (minus slack=%g), row %d NONBASIC\n", (int)minusHeaders[mIdx].row, minusSlacks[mIdx], (int)plusHeaders[pIdx].row); @@ -1765,12 +1775,34 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( basicsAssigned++; } } else { - // Degenerate: both slacks zero but combined > tol → x_j basic - if (!colIsBasic) { + // Degenerate: both slacks zero (or below tol) + // Make x_j basic first; if already basic, make a parent row basic + // If parent is already basic, the transfer is implicit + bool pAlreadyBasic = + pIdx >= 0 && + basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic; + bool mAlreadyBasic = + mIdx >= 0 && basis.row_status[minusHeaders[mIdx].row] == + HighsBasisStatus::kBasic; + if (pAlreadyBasic || mAlreadyBasic) { + basicsAssigned++; + printf(" -> parent already basic (plus=%d minus=%d)\n", + (int)pAlreadyBasic, (int)mAlreadyBasic); + } else if (!colIsBasic) { basis.col_status[col] = HighsBasisStatus::kBasic; colIsBasic = true; basicsAssigned++; printf(" -> col %d BASIC (degenerate)\n", (int)col); + } else if (pIdx >= 0) { + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + basicsAssigned++; + printf(" -> row %d BASIC (degenerate, plus)\n", + (int)plusHeaders[pIdx].row); + } else if (mIdx >= 0) { + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + basicsAssigned++; + printf(" -> row %d BASIC (degenerate, minus)\n", + (int)minusHeaders[mIdx].row); } } // β_k becomes nonbasic From e16f90c1ae8d0cee607cfd849dcf61c28c750275 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 3 Jun 2026 11:13:21 +0200 Subject: [PATCH 072/196] WIP --- highs/presolve/HPresolve.cpp | 1 + highs/presolve/HighsPostsolveStack.cpp | 85 +++++++++++++++----------- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ec873466b7c..85d5e4fa0b3 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5970,6 +5970,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { if (tryFourierMotzkin && analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); + tryFourierMotzkin = false; } if (analysis_.allow_rule_[kPresolveRuleAggregator]) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index eb86beb28a8..303dd3d1ea0 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1624,7 +1624,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( (int)newRowOrigins.size()); // Algorithm 5: iterate over new rows (constraints K) in reverse - HighsInt basicsAssigned = colIsBasic ? 1 : 0; + HighsInt basicsAssigned = 0; for (HighsInt k = (HighsInt)newRowOrigins.size() - 1; k >= 0; --k) { HighsInt pOrigRow = newRowOrigins[k].plusRow; HighsInt mOrigRow = newRowOrigins[k].minusRow; @@ -1693,21 +1693,21 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( basicsAssigned++; printf(" -> col %d BASIC (virtual upper slack=%g)\n", (int)col, pSlack); - } else if (mIdx >= 0 && basis.row_status[minusHeaders[mIdx].row] != - HighsBasisStatus::kBasic) { - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - basicsAssigned++; - printf(" -> row %d BASIC (degenerate minus, virtual upper)\n", - (int)minusHeaders[mIdx].row); } else { + // col already basic = virtual bound already "basic" basicsAssigned++; - printf(" -> col already basic, other parent already basic\n"); + printf(" -> col already basic (virtual upper satisfied)\n"); } } else { - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + if (basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic) { + printf(" -> row %d already basic (plus slack=%g)\n", + (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); + } else { + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + printf(" -> row %d BASIC (plus slack=%g)\n", + (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); + } basicsAssigned++; - printf(" -> row %d BASIC (plus slack=%g)\n", - (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); } } else if (mHasSlack && !pHasSlack) { if (mIsVirtual) { @@ -1717,57 +1717,72 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( basicsAssigned++; printf(" -> col %d BASIC (virtual lower slack=%g)\n", (int)col, mSlack); - } else if (pIdx >= 0 && basis.row_status[plusHeaders[pIdx].row] != - HighsBasisStatus::kBasic) { - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - basicsAssigned++; - printf(" -> row %d BASIC (degenerate plus, virtual lower)\n", - (int)plusHeaders[pIdx].row); } else { + // col already basic = virtual bound already "basic" basicsAssigned++; - printf(" -> col already basic, other parent already basic\n"); + printf(" -> col already basic (virtual lower satisfied)\n"); } } else { - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + if (basis.row_status[minusHeaders[mIdx].row] == HighsBasisStatus::kBasic) { + printf(" -> row %d already basic (minus slack=%g)\n", + (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); + } else { + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + printf(" -> row %d BASIC (minus slack=%g)\n", + (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); + } basicsAssigned++; - printf(" -> row %d BASIC (minus slack=%g)\n", - (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); } } else if (pHasSlack && mHasSlack) { // Both have nonzero slack — one becomes basic // If one is virtual bound, the other (real row) gets priority if (pIsVirtual && !mIsVirtual) { - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + if (basis.row_status[minusHeaders[mIdx].row] == HighsBasisStatus::kBasic) { + printf(" -> row %d already basic (minus slack=%g, plus is virtual)\n", + (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); + } else { + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + printf(" -> row %d BASIC (minus slack=%g, plus is virtual)\n", + (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); + } basicsAssigned++; - printf(" -> row %d BASIC (minus slack=%g, plus is virtual)\n", - (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); } else if (mIsVirtual && !pIsVirtual) { - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - basicsAssigned++; - printf(" -> row %d BASIC (plus slack=%g, minus is virtual)\n", - (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); + if (basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic) { + printf(" -> row %d already basic (plus slack=%g, minus is virtual)\n", + (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); + } else { + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + printf(" -> row %d BASIC (plus slack=%g, minus is virtual)\n", + (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); + } } else if (pIsVirtual && mIsVirtual) { // Both virtual bounds have slack → x_j is between bounds → basic if (!colIsBasic) { basis.col_status[col] = HighsBasisStatus::kBasic; colIsBasic = true; - basicsAssigned++; printf(" -> col %d BASIC (both virtual bounds have slack)\n", (int)col); + } else { + printf(" -> col already basic (both virtual satisfied)\n"); } + basicsAssigned++; } else { - // Both real rows have slack — pick larger - if (plusSlacks[pIdx] >= minusSlacks[mIdx]) { + // Both real rows have slack — pick the one not already basic + bool pAlready = + basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic; + bool mAlready = basis.row_status[minusHeaders[mIdx].row] == + HighsBasisStatus::kBasic; + if (pAlready || mAlready) { + // One parent already basic — transfer satisfied + printf(" -> parent already basic (plus=%d minus=%d)\n", + (int)pAlready, (int)mAlready); + } else if (plusSlacks[pIdx] >= minusSlacks[mIdx]) { basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( - minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); printf(" -> row %d BASIC (plus slack=%g), row %d NONBASIC\n", (int)plusHeaders[pIdx].row, plusSlacks[pIdx], (int)minusHeaders[mIdx].row); } else { basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( - plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); printf(" -> row %d BASIC (minus slack=%g), row %d NONBASIC\n", (int)minusHeaders[mIdx].row, minusSlacks[mIdx], (int)plusHeaders[pIdx].row); From 80d6a770af903dc6b9630addf7443b9f8358beff Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 3 Jun 2026 16:43:45 +0200 Subject: [PATCH 073/196] WIP --- highs/presolve/HPresolve.cpp | 2 +- highs/presolve/HighsPostsolveStack.cpp | 261 ++++++------------------- 2 files changed, 63 insertions(+), 200 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 85d5e4fa0b3..26d24fe1b11 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5970,7 +5970,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { if (tryFourierMotzkin && analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); - tryFourierMotzkin = false; + // tryFourierMotzkin = false; } if (analysis_.allow_rule_[kPresolveRuleAggregator]) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 303dd3d1ea0..b794aab4956 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1584,23 +1584,30 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( printf(" %d(s=%g)", (int)minusHeaders[r].row, minusSlacks[r]); printf("\n"); + // Compute expected number of basics this undo must produce + HighsInt numNewRows = (HighsInt)newRowOrigins.size(); + HighsInt basicNewRows = 0; + for (HighsInt k = 0; k < numNewRows; ++k) + if (basis.row_status[newRowOrigins[k].newRow] == HighsBasisStatus::kBasic) + basicNewRows++; + HighsInt basicsNeeded = (numPlus + numMinus) - (numNewRows - basicNewRows); + printf( + " basicsNeeded=%d (numPlus=%d numMinus=%d numNewRows=%d " + "basicNewRows=%d)\n", + (int)basicsNeeded, (int)numPlus, (int)numMinus, (int)numNewRows, + (int)basicNewRows); + // Free variable case: x_j is basic - bool colIsBasic = false; if (newRowOrigins.empty()) { basis.col_status[col] = HighsBasisStatus::kBasic; - colIsBasic = true; - printf(" free variable -> col %d BASIC\n", (int)col); - } else if (solution.col_value[col] <= colLower + tol) { - basis.col_status[col] = HighsBasisStatus::kLower; - } else if (solution.col_value[col] >= colUpper - tol) { - basis.col_status[col] = HighsBasisStatus::kUpper; } else { - basis.col_status[col] = HighsBasisStatus::kBasic; - colIsBasic = true; + if (solution.col_value[col] <= colLower + tol) + basis.col_status[col] = HighsBasisStatus::kLower; + else if (solution.col_value[col] >= colUpper - tol) + basis.col_status[col] = HighsBasisStatus::kUpper; + else + basis.col_status[col] = HighsBasisStatus::kBasic; } - printf(" col_status=%d colIsBasic=%d newRowOrigins=%d\n", - (int)basis.col_status[col], (int)colIsBasic, - (int)newRowOrigins.size()); // Helper to find parent index in plus/minus headers auto findPlusIndex = [&](HighsInt origRow) -> HighsInt { @@ -1614,17 +1621,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( return -1; }; - // Count how many basics we need to distribute: number of new rows that were - // basic in the presolved basis - HighsInt basicsNeeded = 0; - for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) - if (basis.row_status[newRowOrigins[k].newRow] == HighsBasisStatus::kBasic) - basicsNeeded++; - printf(" basicsNeeded=%d (from %d new rows)\n", (int)basicsNeeded, - (int)newRowOrigins.size()); - // Algorithm 5: iterate over new rows (constraints K) in reverse - HighsInt basicsAssigned = 0; for (HighsInt k = (HighsInt)newRowOrigins.size() - 1; k >= 0; --k) { HighsInt pOrigRow = newRowOrigins[k].plusRow; HighsInt mOrigRow = newRowOrigins[k].minusRow; @@ -1632,9 +1629,6 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( HighsInt mIdx = mOrigRow >= 0 ? findMinusIndex(mOrigRow) : -1; HighsInt newRow = newRowOrigins[k].newRow; - // β_k status determined solely from combined slack (paper: s_{i,i'}) - // Virtual bound parents: pOrigRow == -1 means upper bound (slack = u - x_j) - // mOrigRow == -1 means lower bound (slack = x_j - l) double pSlack = pIdx >= 0 ? plusSlacks[pIdx] @@ -1645,184 +1639,57 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( ? minusSlacks[mIdx] : (mOrigRow < 0 ? std::max(solution.col_value[col] - colLower, 0.0) : 0.0); - double combinedSlack = pSlack + mSlack; bool betaIsBasic = basis.row_status[newRow] == HighsBasisStatus::kBasic; printf( " k=%d: newRow=%d beta=%d pOrigRow=%d mOrigRow=%d pSlack=%g " - "mSlack=%g cSlack=%g\n", + "mSlack=%g\n", (int)k, (int)newRow, (int)betaIsBasic, (int)pOrigRow, (int)mOrigRow, - pSlack, mSlack, combinedSlack); + pSlack, mSlack); if (!betaIsBasic) { // Nonbasic propagation: both parents are nonbasic - if (pIdx >= 0) { + if (pIdx >= 0) basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); - printf(" -> row %d NONBASIC (status=%d)\n", - (int)plusHeaders[pIdx].row, - (int)basis.row_status[plusHeaders[pIdx].row]); - } else if (pOrigRow < 0) { - // Virtual upper bound is nonbasic: x_j at upper bound - printf(" -> virtual upper bound NONBASIC\n"); - } - if (mIdx >= 0) { + if (mIdx >= 0) basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); - printf(" -> row %d NONBASIC (status=%d)\n", - (int)minusHeaders[mIdx].row, - (int)basis.row_status[minusHeaders[mIdx].row]); - } else if (mOrigRow < 0) { - // Virtual lower bound is nonbasic: x_j at lower bound - printf(" -> virtual lower bound NONBASIC\n"); - } - // β_k stays nonbasic - basis.row_status[newRow] = HighsBasisStatus::kLower; } else { - // Basic propagation: transfer β_k's basic to parent with nonzero slack - bool pHasSlack = pSlack > tol; - bool mHasSlack = mSlack > tol; - bool pIsVirtual = pOrigRow < 0; - bool mIsVirtual = mOrigRow < 0; - - if (pHasSlack && !mHasSlack) { - if (pIsVirtual) { - if (!colIsBasic) { - basis.col_status[col] = HighsBasisStatus::kBasic; - colIsBasic = true; - basicsAssigned++; - printf(" -> col %d BASIC (virtual upper slack=%g)\n", (int)col, - pSlack); - } else { - // col already basic = virtual bound already "basic" - basicsAssigned++; - printf(" -> col already basic (virtual upper satisfied)\n"); - } - } else { - if (basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic) { - printf(" -> row %d already basic (plus slack=%g)\n", - (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); - } else { - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - printf(" -> row %d BASIC (plus slack=%g)\n", - (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); - } - basicsAssigned++; - } - } else if (mHasSlack && !pHasSlack) { - if (mIsVirtual) { - if (!colIsBasic) { - basis.col_status[col] = HighsBasisStatus::kBasic; - colIsBasic = true; - basicsAssigned++; - printf(" -> col %d BASIC (virtual lower slack=%g)\n", (int)col, - mSlack); - } else { - // col already basic = virtual bound already "basic" - basicsAssigned++; - printf(" -> col already basic (virtual lower satisfied)\n"); - } - } else { - if (basis.row_status[minusHeaders[mIdx].row] == HighsBasisStatus::kBasic) { - printf(" -> row %d already basic (minus slack=%g)\n", - (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); - } else { - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - printf(" -> row %d BASIC (minus slack=%g)\n", - (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); - } - basicsAssigned++; - } - } else if (pHasSlack && mHasSlack) { - // Both have nonzero slack — one becomes basic - // If one is virtual bound, the other (real row) gets priority - if (pIsVirtual && !mIsVirtual) { - if (basis.row_status[minusHeaders[mIdx].row] == HighsBasisStatus::kBasic) { - printf(" -> row %d already basic (minus slack=%g, plus is virtual)\n", - (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); - } else { - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - printf(" -> row %d BASIC (minus slack=%g, plus is virtual)\n", - (int)minusHeaders[mIdx].row, minusSlacks[mIdx]); - } - basicsAssigned++; - } else if (mIsVirtual && !pIsVirtual) { - if (basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic) { - printf(" -> row %d already basic (plus slack=%g, minus is virtual)\n", - (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); - } else { - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - printf(" -> row %d BASIC (plus slack=%g, minus is virtual)\n", - (int)plusHeaders[pIdx].row, plusSlacks[pIdx]); - } - } else if (pIsVirtual && mIsVirtual) { - // Both virtual bounds have slack → x_j is between bounds → basic - if (!colIsBasic) { - basis.col_status[col] = HighsBasisStatus::kBasic; - colIsBasic = true; - printf(" -> col %d BASIC (both virtual bounds have slack)\n", - (int)col); - } else { - printf(" -> col already basic (both virtual satisfied)\n"); - } - basicsAssigned++; - } else { - // Both real rows have slack — pick the one not already basic - bool pAlready = - basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic; - bool mAlready = basis.row_status[minusHeaders[mIdx].row] == - HighsBasisStatus::kBasic; - if (pAlready || mAlready) { - // One parent already basic — transfer satisfied - printf(" -> parent already basic (plus=%d minus=%d)\n", - (int)pAlready, (int)mAlready); - } else if (plusSlacks[pIdx] >= minusSlacks[mIdx]) { - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - printf(" -> row %d BASIC (plus slack=%g), row %d NONBASIC\n", - (int)plusHeaders[pIdx].row, plusSlacks[pIdx], - (int)minusHeaders[mIdx].row); - } else { - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - printf(" -> row %d BASIC (minus slack=%g), row %d NONBASIC\n", - (int)minusHeaders[mIdx].row, minusSlacks[mIdx], - (int)plusHeaders[pIdx].row); - } - basicsAssigned++; + // Basic propagation: parent with nonzero slack becomes basic. + // If both slacks zero, x_j becomes basic (degenerate). + // Make one variable basic: try primary row, then col, then fallback row + auto makeBasic = [&](HighsInt primaryIdx, bool primaryIsPlus, + HighsInt fallbackIdx, bool fallbackIsPlus) { + if (primaryIdx >= 0) { + auto& hdr = primaryIsPlus ? plusHeaders[primaryIdx] + : minusHeaders[primaryIdx]; + basis.row_status[hdr.row] = HighsBasisStatus::kBasic; + } else if (basis.col_status[col] != HighsBasisStatus::kBasic) { + basis.col_status[col] = HighsBasisStatus::kBasic; + } else if (fallbackIdx >= 0) { + auto& hdr = fallbackIsPlus ? plusHeaders[fallbackIdx] + : minusHeaders[fallbackIdx]; + basis.row_status[hdr.row] = HighsBasisStatus::kBasic; } + }; + + if (pSlack > tol && mSlack <= tol) { + makeBasic(pIdx, true, mIdx, false); + } else if (mSlack > tol && pSlack <= tol) { + makeBasic(mIdx, false, pIdx, true); + } else if (pSlack > tol && mSlack > tol) { + makeBasic(pIdx, true, mIdx, false); } else { - // Degenerate: both slacks zero (or below tol) - // Make x_j basic first; if already basic, make a parent row basic - // If parent is already basic, the transfer is implicit - bool pAlreadyBasic = - pIdx >= 0 && - basis.row_status[plusHeaders[pIdx].row] == HighsBasisStatus::kBasic; - bool mAlreadyBasic = - mIdx >= 0 && basis.row_status[minusHeaders[mIdx].row] == - HighsBasisStatus::kBasic; - if (pAlreadyBasic || mAlreadyBasic) { - basicsAssigned++; - printf(" -> parent already basic (plus=%d minus=%d)\n", - (int)pAlreadyBasic, (int)mAlreadyBasic); - } else if (!colIsBasic) { + // Both slacks zero: x_j becomes basic (degenerate) + // Primary is col; fallback to real parent row + if (basis.col_status[col] != HighsBasisStatus::kBasic) basis.col_status[col] = HighsBasisStatus::kBasic; - colIsBasic = true; - basicsAssigned++; - printf(" -> col %d BASIC (degenerate)\n", (int)col); - } else if (pIdx >= 0) { + else if (pIdx >= 0) basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - basicsAssigned++; - printf(" -> row %d BASIC (degenerate, plus)\n", - (int)plusHeaders[pIdx].row); - } else if (mIdx >= 0) { + else if (mIdx >= 0) basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - basicsAssigned++; - printf(" -> row %d BASIC (degenerate, minus)\n", - (int)minusHeaders[mIdx].row); - } } - // β_k becomes nonbasic - basis.row_status[newRow] = HighsBasisStatus::kLower; - printf(" -> newRow %d NONBASIC\n", (int)newRow); } } @@ -1839,7 +1706,6 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (isInvolved(plusHeaders[r].row)) continue; if (plusSlacks[r] > tol) { basis.row_status[plusHeaders[r].row] = HighsBasisStatus::kBasic; - basicsAssigned++; printf(" vanished row %d BASIC (slack=%g)\n", (int)plusHeaders[r].row, plusSlacks[r]); } else { @@ -1853,7 +1719,6 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (isInvolved(minusHeaders[r].row)) continue; if (minusSlacks[r] > tol) { basis.row_status[minusHeaders[r].row] = HighsBasisStatus::kBasic; - basicsAssigned++; printf(" vanished row %d BASIC (slack=%g)\n", (int)minusHeaders[r].row, minusSlacks[r]); } else { @@ -1864,19 +1729,17 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( } } - // Repair: if we assigned fewer basics than needed, promote the nonbasic - // parent rows with the largest slack (sub-tolerance but numerically closest - // to basic) - printf(" basicsAssigned=%d basicsNeeded=%d\n", (int)basicsAssigned, - (int)basicsNeeded); - if (basicsAssigned < basicsNeeded) { - printf(" repair needed: basicsAssigned=%d basicsNeeded=%d\n", - (int)basicsAssigned, (int)basicsNeeded); - // Degenerate case: presolved basis had basic new rows but all slacks - // are below tolerance. Leave the deficit for the simplex factorization - // to handle via rank repair — it makes better numerical choices than - // we can with near-zero slacks. - } + // Count basics produced: col + parent rows that are now basic + HighsInt basicsProduced = 0; + if (basis.col_status[col] == HighsBasisStatus::kBasic) basicsProduced++; + for (HighsInt r = 0; r < numPlus; ++r) + if (basis.row_status[plusHeaders[r].row] == HighsBasisStatus::kBasic) + basicsProduced++; + for (HighsInt r = 0; r < numMinus; ++r) + if (basis.row_status[minusHeaders[r].row] == HighsBasisStatus::kBasic) + basicsProduced++; + printf(" basicsProduced=%d basicsNeeded=%d (diff=%d)\n", (int)basicsProduced, + (int)basicsNeeded, (int)(basicsProduced - basicsNeeded)); } } // namespace presolve From fe3ec59a42ace7879f423ad489ec74a58fb07c77 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 3 Jun 2026 21:47:52 +0200 Subject: [PATCH 074/196] WIP --- highs/presolve/HPresolve.cpp | 29 ++-- highs/presolve/HighsPostsolveStack.cpp | 209 ++++++++++--------------- 2 files changed, 98 insertions(+), 140 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 26d24fe1b11..3e61eb42796 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5936,8 +5936,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { bool trySparsify = mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif - bool tryFourierMotzkin = true; - // mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; + bool tryFourierMotzkin = + mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; bool tryProbing = mipsolver != nullptr; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; @@ -5970,7 +5970,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { if (tryFourierMotzkin && analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); - // tryFourierMotzkin = false; + tryFourierMotzkin = false; } if (analysis_.allow_rule_[kPresolveRuleAggregator]) @@ -7086,9 +7086,10 @@ HPresolve::Result HPresolve::fourierMotzkin( double upper = upperFinite ? static_cast(impliedUpper) : kHighsInf; // check for infeasibility - if (lower > nr.upper + primal_feastol || upper < nr.lower - primal_feastol) { - printf("FME infeasibility: implied [%g, %g] vs row [%g, %g]\n", - lower, upper, nr.lower, nr.upper); + if (lower > nr.upper + primal_feastol || + upper < nr.lower - primal_feastol) { + printf("FME infeasibility: implied [%g, %g] vs row [%g, %g]\n", lower, + upper, nr.lower, nr.upper); return Result::kPrimalInfeasible; } @@ -7250,10 +7251,8 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt numRowsEliminated = 0; HighsInt numRowsAdded = 0; - const HighsInt maxFmeEliminations = kHighsIInf; - // main loop: eliminate variables from heap - while (!heap.empty() && numColsEliminated < maxFmeEliminations) { + while (!heap.empty()) { HighsInt col = heap[0].col; heapRemove(heap, heapPos, col); @@ -7321,8 +7320,8 @@ HPresolve::Result HPresolve::fourierMotzkin( for (const auto& nr : newRows) { bool redundant = false; - // HPRESOLVE_CHECKED_CALL(checkNewRow(nr, redundant)); - // if (redundant) continue; + HPRESOLVE_CHECKED_CALL(checkNewRow(nr, redundant)); + if (redundant) continue; std::vector entries; entries.reserve(nr.entries.size()); @@ -7377,9 +7376,6 @@ HPresolve::Result HPresolve::fourierMotzkin( // mark column as deleted markColDeleted(col); ++numColsEliminated; - printf("FME: eliminated col=%d, removed %d plus rows, %d minus rows, added %d new rows\n", - (int)col, (int)iPlus.size(), (int)iMinus.size(), - (int)rowEntries.size()); // update affected candidates in the heap saveAffectedCols.swap(affectedCols); @@ -7412,15 +7408,12 @@ HPresolve::Result HPresolve::fourierMotzkin( HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } - if (numColsEliminated > 0) { - printf("FME-PRESOLVE: eliminated %d cols %d rows, added %d rows\n", - (int)numColsEliminated, (int)numRowsEliminated, (int)numRowsAdded); + if (numColsEliminated > 0) highsLogDev(options->log_options, HighsLogType::kInfo, "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT " cols and %" HIGHSINT_FORMAT " rows, and added %" HIGHSINT_FORMAT " rows\n", numColsEliminated, numRowsEliminated, numRowsAdded); - } return finalise(); } diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index b794aab4956..444de375906 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1420,12 +1420,11 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( HighsInt numPlus = static_cast(plusHeaders.size()); HighsInt numMinus = static_cast(minusHeaders.size()); - // === PRIMAL POSTSOLVE (Algorithm 3) === - // Compute feasible range for x_j from each parent constraint using current - // solution values of other variables: a_ij * x_j + activity_others in [l, u] + // primal postsolve (algorithm 3) double impliedLower = colLower; double impliedUpper = colUpper; + // lambda for computing a bound auto computeBounds = [&](HighsInt direction, double val, double rhs, const HighsCDouble& sum, double& impliedBound) { if (std::abs(rhs) == kHighsInf) return; @@ -1434,6 +1433,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( direction * std::min(direction * bound, direction * impliedBound); }; + // lambda for computing lower/upper bound auto tightenBounds = [&](const std::vector& headers, const std::vector& coefs, const std::vector>& entries) { @@ -1453,10 +1453,11 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( } }; + // compute bounds tightenBounds(plusHeaders, plusCoefOfCol, plusEntries); tightenBounds(minusHeaders, minusCoefOfCol, minusEntries); - // Algorithm 3: assign x_j to 0 if feasible, else closest bound to zero + // algorithm 3: assign x_j to 0 if feasible, else closest bound to zero if (impliedLower <= 0.0 && impliedUpper >= 0.0) solution.col_value[col] = 0.0; else if (impliedLower > 0.0) @@ -1466,18 +1467,14 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( if (!solution.dual_valid) return; - // === DUAL POSTSOLVE === - // Zero parent row duals before assignment (Algorithm 4 uses assignment, not - // accumulation) + // dual postsolve + // zero parent row duals for (HighsInt r = 0; r < numPlus; ++r) solution.row_dual[plusHeaders[r].row] = 0.0; for (HighsInt r = 0; r < numMinus; ++r) solution.row_dual[minusHeaders[r].row] = 0.0; - // Distribute new row duals back to original rows. - // The new row was formed as: (s/pCoefAbs) * row_plus + (s/mCoefAbs) * - // row_minus By LP duality: y_plus += (s/pCoefAbs) * lambda, y_minus += - // (s/mCoefAbs) * lambda + // distribute new row duals back to original rows for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) { HighsInt newRow = newRowOrigins[k].newRow; if (!postsolveStack.isModelRow(newRow)) continue; @@ -1511,7 +1508,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( solution.row_dual[newRow] = 0.0; } - // Compute col dual: c_j - sum(a_ij * y_i) + // compute col dual solution.col_dual[col] = colCost; for (HighsInt r = 0; r < numPlus; ++r) { if (postsolveStack.isModelRow(plusHeaders[r].row)) @@ -1524,12 +1521,12 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( minusCoefOfCol[r] * solution.row_dual[minusHeaders[r].row]; } - // === BASIS POSTSOLVE (Algorithm 5) === + // basis postsolve (algorithm 5) if (!basis.valid) return; const double tol = options.mip_feasibility_tolerance; - // Compute normalized row slacks: s_i / |a_{ij}| + // Compute row slacks: s_i / |a_{ij}| auto computeSlacks = [&](const std::vector& headers, const std::vector& coefs, const std::vector>& entries, @@ -1551,13 +1548,12 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( } }; - // Determine nonbasic row status from activity vs bounds auto nonbasicRowStatus = [&](const FmeRowHeader& hdr, - const std::vector& entries, + const std::vector& rowEntries, double coefOfCol) -> HighsBasisStatus { HighsCDouble activity = static_cast(coefOfCol) * solution.col_value[col]; - for (const auto& nz : entries) + for (const auto& nz : rowEntries) activity += static_cast(nz.value) * solution.col_value[nz.index]; double act = static_cast(activity); @@ -1570,46 +1566,6 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( computeSlacks(plusHeaders, plusCoefOfCol, plusEntries, plusSlacks); computeSlacks(minusHeaders, minusCoefOfCol, minusEntries, minusSlacks); - printf( - "FME basis postsolve col=%d x_j=%g (origNumRow=%d) col_status_before=%d " - "colLower=%g colUpper=%g numPlus=%d numMinus=%d\n", - (int)col, solution.col_value[col], (int)postsolveStack.origNumRow, - (int)basis.col_status[col], colLower, colUpper, (int)numPlus, - (int)numMinus); - printf(" plusRows:"); - for (HighsInt r = 0; r < numPlus; ++r) - printf(" %d(s=%g)", (int)plusHeaders[r].row, plusSlacks[r]); - printf("\n minusRows:"); - for (HighsInt r = 0; r < numMinus; ++r) - printf(" %d(s=%g)", (int)minusHeaders[r].row, minusSlacks[r]); - printf("\n"); - - // Compute expected number of basics this undo must produce - HighsInt numNewRows = (HighsInt)newRowOrigins.size(); - HighsInt basicNewRows = 0; - for (HighsInt k = 0; k < numNewRows; ++k) - if (basis.row_status[newRowOrigins[k].newRow] == HighsBasisStatus::kBasic) - basicNewRows++; - HighsInt basicsNeeded = (numPlus + numMinus) - (numNewRows - basicNewRows); - printf( - " basicsNeeded=%d (numPlus=%d numMinus=%d numNewRows=%d " - "basicNewRows=%d)\n", - (int)basicsNeeded, (int)numPlus, (int)numMinus, (int)numNewRows, - (int)basicNewRows); - - // Free variable case: x_j is basic - if (newRowOrigins.empty()) { - basis.col_status[col] = HighsBasisStatus::kBasic; - } else { - if (solution.col_value[col] <= colLower + tol) - basis.col_status[col] = HighsBasisStatus::kLower; - else if (solution.col_value[col] >= colUpper - tol) - basis.col_status[col] = HighsBasisStatus::kUpper; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } - - // Helper to find parent index in plus/minus headers auto findPlusIndex = [&](HighsInt origRow) -> HighsInt { for (HighsInt r = 0; r < numPlus; ++r) if (plusHeaders[r].row == origRow) return r; @@ -1621,7 +1577,41 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( return -1; }; - // Algorithm 5: iterate over new rows (constraints K) in reverse + // x_j is always at a bound: nonbasic + if (impliedLower >= -tol && impliedUpper <= tol) { + if (colLower == 0.0) + basis.col_status[col] = HighsBasisStatus::kLower; + else if (colUpper == 0.0) + basis.col_status[col] = HighsBasisStatus::kUpper; + else + basis.col_status[col] = HighsBasisStatus::kZero; + } else if (impliedLower > tol) { + basis.col_status[col] = HighsBasisStatus::kLower; + } else { + basis.col_status[col] = HighsBasisStatus::kUpper; + } + + // free variable case: no constraints generated, x_j is basic + if (newRowOrigins.empty()) { + basis.col_status[col] = HighsBasisStatus::kBasic; + for (HighsInt r = 0; r < numPlus; ++r) { + if (plusSlacks[r] > tol) + basis.row_status[plusHeaders[r].row] = HighsBasisStatus::kBasic; + else + basis.row_status[plusHeaders[r].row] = + nonbasicRowStatus(plusHeaders[r], plusEntries[r], plusCoefOfCol[r]); + } + for (HighsInt r = 0; r < numMinus; ++r) { + if (minusSlacks[r] > tol) + basis.row_status[minusHeaders[r].row] = HighsBasisStatus::kBasic; + else + basis.row_status[minusHeaders[r].row] = nonbasicRowStatus( + minusHeaders[r], minusEntries[r], minusCoefOfCol[r]); + } + return; + } + + // algorithm 5: iterate over constraints K in reverse for (HighsInt k = (HighsInt)newRowOrigins.size() - 1; k >= 0; --k) { HighsInt pOrigRow = newRowOrigins[k].plusRow; HighsInt mOrigRow = newRowOrigins[k].minusRow; @@ -1641,14 +1631,8 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( : 0.0); bool betaIsBasic = basis.row_status[newRow] == HighsBasisStatus::kBasic; - printf( - " k=%d: newRow=%d beta=%d pOrigRow=%d mOrigRow=%d pSlack=%g " - "mSlack=%g\n", - (int)k, (int)newRow, (int)betaIsBasic, (int)pOrigRow, (int)mOrigRow, - pSlack, mSlack); - if (!betaIsBasic) { - // Nonbasic propagation: both parents are nonbasic + // non-basic propagation: both parent slacks are zero, both non-basic if (pIdx >= 0) basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); @@ -1656,45 +1640,48 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); } else { - // Basic propagation: parent with nonzero slack becomes basic. - // If both slacks zero, x_j becomes basic (degenerate). - // Make one variable basic: try primary row, then col, then fallback row - auto makeBasic = [&](HighsInt primaryIdx, bool primaryIsPlus, - HighsInt fallbackIdx, bool fallbackIsPlus) { - if (primaryIdx >= 0) { - auto& hdr = primaryIsPlus ? plusHeaders[primaryIdx] - : minusHeaders[primaryIdx]; - basis.row_status[hdr.row] = HighsBasisStatus::kBasic; - } else if (basis.col_status[col] != HighsBasisStatus::kBasic) { - basis.col_status[col] = HighsBasisStatus::kBasic; - } else if (fallbackIdx >= 0) { - auto& hdr = fallbackIsPlus ? plusHeaders[fallbackIdx] - : minusHeaders[fallbackIdx]; - basis.row_status[hdr.row] = HighsBasisStatus::kBasic; - } - }; - + // basic propagation if (pSlack > tol && mSlack <= tol) { - makeBasic(pIdx, true, mIdx, false); + // plus parent has slack: it becomes basic + if (pIdx >= 0) + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + if (mIdx >= 0) + basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( + minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); } else if (mSlack > tol && pSlack <= tol) { - makeBasic(mIdx, false, pIdx, true); - } else if (pSlack > tol && mSlack > tol) { - makeBasic(pIdx, true, mIdx, false); - } else { - // Both slacks zero: x_j becomes basic (degenerate) - // Primary is col; fallback to real parent row - if (basis.col_status[col] != HighsBasisStatus::kBasic) + // minus parent has slack: it becomes basic + if (mIdx >= 0) + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + else basis.col_status[col] = HighsBasisStatus::kBasic; - else if (pIdx >= 0) + if (pIdx >= 0) + basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( + plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); + } else if (pSlack > tol && mSlack > tol) { + // both have slack: plus parent becomes basic + if (pIdx >= 0) basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - else if (mIdx >= 0) - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + if (mIdx >= 0) + basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( + minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); + } else { + // both slacks zero (degenerate): x_j becomes basic + basis.col_status[col] = HighsBasisStatus::kBasic; + if (pIdx >= 0) + basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( + plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); + if (mIdx >= 0) + basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( + minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); } } } - // Vanished constraint check: rows not involved as parent of any new row - // Nonzero slack -> basic; zero slack -> nonbasic + // vanished constraint check: parent rows not involved in any new row auto isInvolved = [&](HighsInt row) { for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) if (newRowOrigins[k].plusRow == row || newRowOrigins[k].minusRow == row) @@ -1704,42 +1691,20 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( for (HighsInt r = 0; r < numPlus; ++r) { if (isInvolved(plusHeaders[r].row)) continue; - if (plusSlacks[r] > tol) { + if (plusSlacks[r] > tol) basis.row_status[plusHeaders[r].row] = HighsBasisStatus::kBasic; - printf(" vanished row %d BASIC (slack=%g)\n", (int)plusHeaders[r].row, - plusSlacks[r]); - } else { + else basis.row_status[plusHeaders[r].row] = nonbasicRowStatus(plusHeaders[r], plusEntries[r], plusCoefOfCol[r]); - printf(" vanished row %d NONBASIC (slack=%g)\n", (int)plusHeaders[r].row, - plusSlacks[r]); - } } for (HighsInt r = 0; r < numMinus; ++r) { if (isInvolved(minusHeaders[r].row)) continue; - if (minusSlacks[r] > tol) { + if (minusSlacks[r] > tol) basis.row_status[minusHeaders[r].row] = HighsBasisStatus::kBasic; - printf(" vanished row %d BASIC (slack=%g)\n", (int)minusHeaders[r].row, - minusSlacks[r]); - } else { + else basis.row_status[minusHeaders[r].row] = nonbasicRowStatus( minusHeaders[r], minusEntries[r], minusCoefOfCol[r]); - printf(" vanished row %d NONBASIC (slack=%g)\n", - (int)minusHeaders[r].row, minusSlacks[r]); - } } - - // Count basics produced: col + parent rows that are now basic - HighsInt basicsProduced = 0; - if (basis.col_status[col] == HighsBasisStatus::kBasic) basicsProduced++; - for (HighsInt r = 0; r < numPlus; ++r) - if (basis.row_status[plusHeaders[r].row] == HighsBasisStatus::kBasic) - basicsProduced++; - for (HighsInt r = 0; r < numMinus; ++r) - if (basis.row_status[minusHeaders[r].row] == HighsBasisStatus::kBasic) - basicsProduced++; - printf(" basicsProduced=%d basicsNeeded=%d (diff=%d)\n", (int)basicsProduced, - (int)basicsNeeded, (int)(basicsProduced - basicsNeeded)); } } // namespace presolve From 7973649045be803fcb2f79ef130cd5f9f77da37d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 3 Jun 2026 21:56:33 +0200 Subject: [PATCH 075/196] Remove debugging code --- highs/presolve/HPresolve.cpp | 2 +- highs/presolve/HighsPostsolveStack.h | 35 +--------------------------- 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 3e61eb42796..cdc62b7e617 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5970,7 +5970,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { if (tryFourierMotzkin && analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); - tryFourierMotzkin = false; + //tryFourierMotzkin = false; } if (analysis_.allow_rule_[kPresolveRuleAggregator]) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index c825fb99d1a..aa8f567eeb0 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -909,27 +909,6 @@ class HighsPostsolveStack { int(reductions[i - 1].first)); if (kAllowDeveloperAssert) assert(1 == 0); } - if (perform_basis_postsolve) { - HighsInt nBasic = 0; - HighsInt nRows = (HighsInt)basis.row_status.size(); - for (HighsInt j = 0; j < (HighsInt)basis.col_status.size(); ++j) - if (basis.col_status[j] == HighsBasisStatus::kBasic) ++nBasic; - for (HighsInt j = 0; j < nRows; ++j) - if (basis.row_status[j] == HighsBasisStatus::kBasic) ++nBasic; - if (nBasic != nRows) - printf("After reduction %d (type %d): nBasic=%d nRows=%d (diff=%d)\n", - (int)(i - 1), (int)reductions[i - 1].first, (int)nBasic, - (int)nRows, (int)(nBasic - nRows)); - // After last reduction, print which rows are basic - if (i - 1 == numReductions) { - printf("Final basis state (nRows=%d, origNumRow=%d):\n", (int)nRows, - (int)origNumRow); - for (HighsInt j = 0; j < nRows; ++j) - if (basis.row_status[j] == HighsBasisStatus::kBasic) - printf(" row %d basic (orig=%s)\n", (int)j, - j >= origNumRow ? "intermediate" : "original"); - } - } } if (report_col >= 0) printf("After last reduction: col_value[%2d] = %g\n", int(report_col), @@ -937,19 +916,7 @@ class HighsPostsolveStack { solution.row_value.resize(origNumRow); if (perform_dual_postsolve) solution.row_dual.resize(origNumRow); - - if (perform_basis_postsolve) { - // assert(numAppendedRows == 0); - basis.row_status.resize(origNumRow); - HighsInt nBasic = 0; - for (HighsInt i = 0; i < (HighsInt)basis.col_status.size(); ++i) - if (basis.col_status[i] == HighsBasisStatus::kBasic) ++nBasic; - for (HighsInt i = 0; i < (HighsInt)basis.row_status.size(); ++i) - if (basis.row_status[i] == HighsBasisStatus::kBasic) ++nBasic; - if (nBasic != origNumRow) - printf("POSTSOLVE BASIS ERROR: nBasic=%d origNumRow=%d (diff=%d)\n", - (int)nBasic, (int)origNumRow, (int)(nBasic - origNumRow)); - } + if (perform_basis_postsolve) basis.row_status.resize(origNumRow); #ifdef DEBUG_EXTRA // solution should not contain NaN or Inf From 56881a66dfa4b5509cd90da86ca96d435fbfc290 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 3 Jun 2026 22:00:26 +0200 Subject: [PATCH 076/196] Apply only once --- highs/presolve/HPresolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index cdc62b7e617..3e61eb42796 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5970,7 +5970,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { if (tryFourierMotzkin && analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); - //tryFourierMotzkin = false; + tryFourierMotzkin = false; } if (analysis_.allow_rule_[kPresolveRuleAggregator]) From 3825291cc9ecfb024aa9f0147b316a31fc94f17e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 3 Jun 2026 22:06:44 +0200 Subject: [PATCH 077/196] Revert change for debugging --- highs/presolve/HPresolve.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 3e61eb42796..e7e614d56f0 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -3451,8 +3451,8 @@ HPresolve::Result HPresolve::singletonCol(HighsPostsolveStack& postsolve_stack, static_cast(convertImpliedInteger(col, row))); // dual fixing - // HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); - // if (colDeleted[col]) return Result::kOk; + HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); + if (colDeleted[col]) return Result::kOk; // singleton column stuffing HPRESOLVE_CHECKED_CALL(singletonColStuffing(postsolve_stack, col)); @@ -4575,8 +4575,8 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, } // dual fixing - // HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); - // if (colDeleted[col]) return Result::kOk; + HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); + if (colDeleted[col]) return Result::kOk; // singleton column stuffing HPRESOLVE_CHECKED_CALL(singletonColStuffing(postsolve_stack, col)); From dcd8dd66eafa0a0affbd7bc469d3421f7d9de9e5 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 5 Jun 2026 08:10:00 +0200 Subject: [PATCH 078/196] Use implied bounds and remove debug print --- highs/presolve/HPresolve.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index e7e614d56f0..42ca06f537b 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7066,8 +7066,8 @@ HPresolve::Result HPresolve::fourierMotzkin( bool upperFinite = true; isRedundant = false; for (const auto& e : nr.entries) { - double lb = model->col_lower_[e.col]; - double ub = model->col_upper_[e.col]; + double lb = implColLower[e.col]; + double ub = implColUpper[e.col]; if (e.val > 0) { lowerFinite = lowerFinite && lb != -kHighsInf; if (lowerFinite) impliedLower += e.val * lb; @@ -7086,12 +7086,8 @@ HPresolve::Result HPresolve::fourierMotzkin( double upper = upperFinite ? static_cast(impliedUpper) : kHighsInf; // check for infeasibility - if (lower > nr.upper + primal_feastol || - upper < nr.lower - primal_feastol) { - printf("FME infeasibility: implied [%g, %g] vs row [%g, %g]\n", lower, - upper, nr.lower, nr.upper); + if (lower > nr.upper + primal_feastol || upper < nr.lower - primal_feastol) return Result::kPrimalInfeasible; - } // check for redundancy isRedundant = lower >= nr.lower - primal_feastol && From 21ef5a377bee8bc9f3a5e0dabb116d0b4b2874cc Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 5 Jun 2026 08:21:02 +0200 Subject: [PATCH 079/196] Fix build error --- highs/presolve/HPresolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 42ca06f537b..9dea6b437b7 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7292,7 +7292,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // remove near-zero entries newRowEntries.erase( std::remove_if(newRowEntries.begin(), newRowEntries.end(), - [&](const auto& e) { + [&](const newRowEntry& e) { return abs(e.val) <= options->small_matrix_value; }), newRowEntries.end()); From abaa8c8b51b7c4fb2fbed51eafd84b88bb665ba3 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 5 Jun 2026 11:41:32 +0200 Subject: [PATCH 080/196] Skip cols with non-zero objective coefficient --- highs/presolve/HPresolve.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 9dea6b437b7..b84552e5a99 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6940,6 +6940,7 @@ HPresolve::Result HPresolve::fourierMotzkin( if (colDeleted[col]) return false; if (colsize[col] == 0) return false; if (model->integrality_[col] != HighsVarType::kContinuous) return false; + if (model->col_cost_[col] != 0.0) return false; for (const auto& nz : getColumnVector(col)) { if (isEquation(nz.index())) return false; double absval = std::abs(nz.value()); From 9945207a915f6d9f72ff93fb30e52a9588e21352 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 5 Jun 2026 16:08:04 +0200 Subject: [PATCH 081/196] Add artificial variable for objective --- highs/presolve/HPresolve.cpp | 72 ++++++++++++++++++++++++++ highs/presolve/HPresolve.h | 2 + highs/presolve/HighsPostsolveStack.cpp | 1 + highs/presolve/HighsPostsolveStack.h | 20 +++++-- 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b84552e5a99..7ba2680b314 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6936,6 +6936,78 @@ HPresolve::Result HPresolve::fourierMotzkin( const HighsInt kUpperBoundRow = -2; const HighsInt kLowerBoundRow = -3; + // Add z column to reformulate objective as a constraint: + // min c^T x becomes min -z with c^T x - z <= 0 + // This allows FME to eliminate columns with nonzero cost. + if (fourierMotzkinObjCol == -1) { + bool hasNonzeroCost = false; + for (HighsInt j = 0; j < model->num_col_; ++j) { + if (!colDeleted[j] && model->col_cost_[j] != 0.0) { + hasNonzeroCost = true; + break; + } + } + if (hasNonzeroCost) { + HighsInt zCol = model->num_col_; + model->num_col_++; + model->a_matrix_.num_col_++; + + // extend model vectors + model->col_cost_.push_back(-1.0); + model->col_lower_.push_back(-kHighsInf); + model->col_upper_.push_back(kHighsInf); + model->integrality_.push_back(HighsVarType::kContinuous); + if (model->col_names_.size() > 0) + model->col_names_.push_back("fme_obj_z"); + + // extend presolve vectors + colhead.push_back(-1); + colsize.push_back(0); + colDeleted.push_back(0); + implColLower.push_back(-kHighsInf); + implColUpper.push_back(kHighsInf); + colLowerSource.push_back(-1); + colUpperSource.push_back(-1); + implRowDualSourceByCol.push_back({}); + changedColFlag.push_back(1); + + // update implied bound structures (pointers may be invalidated by + // reallocation of column vectors above) + impliedRowBounds.setBoundArrays( + model->col_lower_.data(), model->col_upper_.data(), + implColLower.data(), implColUpper.data(), colLowerSource.data(), + colUpperSource.data()); + impliedDualRowBounds.setNumSums(model->num_col_); + impliedDualRowBounds.setBoundArrays( + rowDualLower.data(), rowDualUpper.data(), implRowDualLower.data(), + implRowDualUpper.data(), rowDualLowerSource.data(), + rowDualUpperSource.data()); + + // register in postsolve stack + postsolve_stack.appendColToModel(); + + // build the objective constraint row: z - c^T x <= offset + // (z <= c^T x + offset, with min -z maximizing z) + std::vector objRowLower = {-kHighsInf}; + std::vector objRowUpper = {model->offset_}; + std::vector> objRowEntries(1); + for (HighsInt j = 0; j < zCol; ++j) { + if (!colDeleted[j] && model->col_cost_[j] != 0.0) + objRowEntries[0].push_back({j, -model->col_cost_[j]}); + } + objRowEntries[0].push_back({zCol, 1.0}); + + // zero out original costs and offset + for (HighsInt j = 0; j < zCol; ++j) model->col_cost_[j] = 0.0; + model->offset_ = 0.0; + + // add the constraint row to the matrix + addToMatrix(postsolve_stack, objRowLower, objRowUpper, objRowEntries); + + fourierMotzkinObjCol = zCol; + } + } + auto isCandidate = [&](HighsInt col) { if (colDeleted[col]) return false; if (colsize[col] == 0) return false; diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 483aaf0de2d..68e3dcb3b7b 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -106,6 +106,8 @@ class HPresolve { std::set> equations; std::vector>::iterator> eqiters; + HighsInt fourierMotzkinObjCol = -1; + bool shrinkProblemEnabled; size_t reductionLimit; diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 444de375906..c78188b1eef 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -22,6 +22,7 @@ void HighsPostsolveStack::initializeIndexMaps(HighsInt numRow, origNumRow = numRow; origNumCol = numCol; nextRowIndex = numRow; + nextColIndex = numCol; origRowIndex.resize(numRow); std::iota(origRowIndex.begin(), origRowIndex.end(), 0); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index aa8f567eeb0..8b6a92f282c 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -311,6 +311,7 @@ class HighsPostsolveStack { HighsInt origNumRow = -1; HighsInt numAppendedRows = 0; HighsInt nextRowIndex = -1; + HighsInt nextColIndex = -1; void reductionAdded(ReductionType type) { size_t position = reductionValues.getCurrentDataSize(); @@ -381,6 +382,13 @@ class HighsPostsolveStack { HighsInt getNextRowIndex() const { return nextRowIndex; } + HighsInt getNextColIndex() const { return nextColIndex; } + + void appendColToModel() { + origColIndex.push_back(nextColIndex++); + linearlyTransformable.push_back(false); + } + void initializeIndexMaps(HighsInt numRow, HighsInt numCol); void compressIndexMaps(const std::vector& newRowIndex, @@ -754,8 +762,8 @@ class HighsPostsolveStack { bool perform_basis_postsolve = basis.valid; // expand solution to original index space - assert(origNumCol > 0); - undoIterateBackwards(solution.col_value, origColIndex, origNumCol); + assert(nextColIndex > 0); + undoIterateBackwards(solution.col_value, origColIndex, nextColIndex); assert(nextRowIndex >= 0); undoIterateBackwards(solution.row_value, origRowIndex, nextRowIndex); @@ -763,14 +771,14 @@ class HighsPostsolveStack { if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space - undoIterateBackwards(solution.col_dual, origColIndex, origNumCol); + undoIterateBackwards(solution.col_dual, origColIndex, nextColIndex); undoIterateBackwards(solution.row_dual, origRowIndex, nextRowIndex); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space - undoIterateBackwards(basis.col_status, origColIndex, origNumCol); + undoIterateBackwards(basis.col_status, origColIndex, nextColIndex); undoIterateBackwards(basis.row_status, origRowIndex, nextRowIndex); } @@ -914,6 +922,10 @@ class HighsPostsolveStack { printf("After last reduction: col_value[%2d] = %g\n", int(report_col), solution.col_value[report_col]); + solution.col_value.resize(origNumCol); + if (perform_dual_postsolve) solution.col_dual.resize(origNumCol); + if (perform_basis_postsolve) basis.col_status.resize(origNumCol); + solution.row_value.resize(origNumRow); if (perform_dual_postsolve) solution.row_dual.resize(origNumRow); if (perform_basis_postsolve) basis.row_status.resize(origNumRow); From 8edc1c385ac90c97c05bc1083da6c1993d1b0770 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 09:03:48 +0200 Subject: [PATCH 082/196] WIP --- highs/presolve/HPresolve.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 7ba2680b314..b485ed89085 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6957,6 +6957,7 @@ HPresolve::Result HPresolve::fourierMotzkin( model->col_lower_.push_back(-kHighsInf); model->col_upper_.push_back(kHighsInf); model->integrality_.push_back(HighsVarType::kContinuous); + model->a_matrix_.start_.push_back(model->a_matrix_.start_.back()); if (model->col_names_.size() > 0) model->col_names_.push_back("fme_obj_z"); @@ -6970,6 +6971,7 @@ HPresolve::Result HPresolve::fourierMotzkin( colUpperSource.push_back(-1); implRowDualSourceByCol.push_back({}); changedColFlag.push_back(1); + numProbes.push_back(0); // update implied bound structures (pointers may be invalidated by // reallocation of column vectors above) @@ -6978,10 +6980,6 @@ HPresolve::Result HPresolve::fourierMotzkin( implColLower.data(), implColUpper.data(), colLowerSource.data(), colUpperSource.data()); impliedDualRowBounds.setNumSums(model->num_col_); - impliedDualRowBounds.setBoundArrays( - rowDualLower.data(), rowDualUpper.data(), implRowDualLower.data(), - implRowDualUpper.data(), rowDualLowerSource.data(), - rowDualUpperSource.data()); // register in postsolve stack postsolve_stack.appendColToModel(); From 6eb1cb2c86d37e2663aed229b91ca3c403edb9c4 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 09:42:21 +0200 Subject: [PATCH 083/196] Resize --- highs/mip/HighsCliqueTable.h | 14 +++++++++----- highs/mip/HighsImplications.h | 23 ++++++++++------------- highs/presolve/HPresolve.cpp | 2 ++ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/highs/mip/HighsCliqueTable.h b/highs/mip/HighsCliqueTable.h index 55d46126a08..53c05e0383f 100644 --- a/highs/mip/HighsCliqueTable.h +++ b/highs/mip/HighsCliqueTable.h @@ -163,11 +163,7 @@ class HighsCliqueTable { int64_t numNeighbourhoodQueries; HighsCliqueTable(HighsInt ncols) { - invertedHashList.resize(2 * static_cast(ncols)); - invertedHashListSizeTwo.resize(2 * static_cast(ncols)); - numcliquesvar.resize(2 * static_cast(ncols), 0); - colsubstituted.resize(ncols); - colDeleted.resize(ncols, false); + resize(static_cast(ncols)); nfixings = 0; numNeighbourhoodQueries = 0; numEntries = 0; @@ -176,6 +172,14 @@ class HighsCliqueTable { inPresolve = false; } + void resize(size_t ncols) { + invertedHashList.resize(2 * ncols); + invertedHashListSizeTwo.resize(2 * ncols); + numcliquesvar.resize(2 * ncols, 0); + colsubstituted.resize(ncols); + colDeleted.resize(ncols, false); + } + void setPresolveFlag(bool inPresolve) { this->inPresolve = inPresolve; } bool getPresolveFlag() const { return inPresolve; } diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 6a7c4121538..8c390da8243 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -58,15 +58,10 @@ class HighsImplications { std::vector substitutions; std::vector colsubstituted; HighsImplications(const HighsMipSolver& mipsolver) : mipsolver(mipsolver) { - HighsInt numcol = mipsolver.numCol(); - implications.resize(2 * static_cast(numcol)); - colsubstituted.resize(numcol); - vubs.resize(numcol); - vlbs.resize(numcol); nextCleanupCall = mipsolver.numNonzero(); numImplications = 0; numVarBounds = 0; - maxVarBounds = calcMaxVarBounds(numcol); + resize(mipsolver.numCol()); } std::function @@ -78,22 +73,24 @@ class HighsImplications { implications.clear(); implications.shrink_to_fit(); - HighsInt numcol = mipsolver.numCol(); - implications.resize(2 * static_cast(numcol)); - colsubstituted.resize(numcol); numImplications = 0; vubs.clear(); vubs.shrink_to_fit(); - vubs.resize(numcol); vlbs.clear(); vlbs.shrink_to_fit(); - vlbs.resize(numcol); + resize(mipsolver.numCol()); numVarBounds = 0; - maxVarBounds = calcMaxVarBounds(numcol); - nextCleanupCall = mipsolver.numNonzero(); } + void resize(HighsInt ncols) { + implications.resize(2 * static_cast(ncols)); + colsubstituted.resize(ncols); + vubs.resize(ncols); + vlbs.resize(ncols); + maxVarBounds = calcMaxVarBounds(ncols); + } + constexpr static int64_t calcMaxVarBounds(HighsInt numcol) { return int64_t{5000000} + 10 * static_cast(numcol); }; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b485ed89085..c2ad04e1489 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7003,6 +7003,8 @@ HPresolve::Result HPresolve::fourierMotzkin( addToMatrix(postsolve_stack, objRowLower, objRowUpper, objRowEntries); fourierMotzkinObjCol = zCol; + + shrinkProblem(postsolve_stack); } } From 00fc8f016295c1dd6c3f363b4a931ba81d3ebbef Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 10:09:50 +0200 Subject: [PATCH 084/196] Fix reformulation --- highs/presolve/HPresolve.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index c2ad04e1489..91bc7a43b7b 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6953,7 +6953,7 @@ HPresolve::Result HPresolve::fourierMotzkin( model->a_matrix_.num_col_++; // extend model vectors - model->col_cost_.push_back(-1.0); + model->col_cost_.push_back(1.0); model->col_lower_.push_back(-kHighsInf); model->col_upper_.push_back(kHighsInf); model->integrality_.push_back(HighsVarType::kContinuous); @@ -6984,16 +6984,16 @@ HPresolve::Result HPresolve::fourierMotzkin( // register in postsolve stack postsolve_stack.appendColToModel(); - // build the objective constraint row: z - c^T x <= offset - // (z <= c^T x + offset, with min -z maximizing z) + // build the objective constraint row: c^T x - z <= -offset + // (z >= c^T x + offset, with min z minimizing original objective) std::vector objRowLower = {-kHighsInf}; - std::vector objRowUpper = {model->offset_}; + std::vector objRowUpper = {-model->offset_}; std::vector> objRowEntries(1); for (HighsInt j = 0; j < zCol; ++j) { if (!colDeleted[j] && model->col_cost_[j] != 0.0) - objRowEntries[0].push_back({j, -model->col_cost_[j]}); + objRowEntries[0].push_back({j, model->col_cost_[j]}); } - objRowEntries[0].push_back({zCol, 1.0}); + objRowEntries[0].push_back({zCol, -1.0}); // zero out original costs and offset for (HighsInt j = 0; j < zCol; ++j) model->col_cost_[j] = 0.0; From 2dab50a5e1b470ef3e7ca04dbb913a1b9c1a6a57 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 10:21:34 +0200 Subject: [PATCH 085/196] Fix pseudo cost --- highs/mip/HighsPseudocost.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/highs/mip/HighsPseudocost.cpp b/highs/mip/HighsPseudocost.cpp index 07dcb78501d..fd7be963936 100644 --- a/highs/mip/HighsPseudocost.cpp +++ b/highs/mip/HighsPseudocost.cpp @@ -40,9 +40,12 @@ HighsPseudocost::HighsPseudocost(const HighsMipSolver& mipsolver) conflict_avg_score = mipsolver.pscostinit->conflict_avg_score * mipsolver.numCol(); + HighsInt numOrigCol = + static_cast(mipsolver.pscostinit->pseudocostup.size()); for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { HighsInt origCol = mipsolver.mipdata_->postSolveStack.getOrigColIndex()[i]; + if (origCol >= numOrigCol) continue; pseudocostup[i] = mipsolver.pscostinit->pseudocostup[origCol]; nsamplesup[i] = mipsolver.pscostinit->nsamplesup[origCol]; From f071113b8482d358841f3452bb1331ac2e4790a0 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 10:31:07 +0200 Subject: [PATCH 086/196] Fix basis --- highs/mip/HighsMipSolverData.cpp | 5 ++++- highs/presolve/HighsPostsolveStack.h | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index a0068a0d0fb..2790ce4120b 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1418,7 +1418,10 @@ void HighsMipSolverData::basisTransfer() { firstrootbasis.row_status[i] = status; } - for (HighsInt i = 0; i < numCol; ++i) { + for (HighsInt i = 0; + i < static_cast(postSolveStack.getOrigColIndex().size()); + ++i) { + if (!postSolveStack.isOrigCol(i)) break; HighsBasisStatus status = mipsolver.rootbasis->col_status[postSolveStack.getOrigColIndex()[i]]; firstrootbasis.col_status[i] = status; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 8b6a92f282c..dfb1fa045ed 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -325,6 +325,10 @@ class HighsPostsolveStack { const std::vector& getOrigRowIndex() const { return origRowIndex; } + bool isOrigCol(HighsInt col) const { + return origColIndex[col] < origNumCol; + } + bool isOrigRow(HighsInt row) const { return origRowType[row] == OrigRowType::kOriginal; } From 9875e49cc89d59d1095185086e7e920a6df76f60 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 10:41:11 +0200 Subject: [PATCH 087/196] WIP --- highs/mip/HighsPseudocost.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/highs/mip/HighsPseudocost.cpp b/highs/mip/HighsPseudocost.cpp index fd7be963936..19897913fef 100644 --- a/highs/mip/HighsPseudocost.cpp +++ b/highs/mip/HighsPseudocost.cpp @@ -40,12 +40,10 @@ HighsPseudocost::HighsPseudocost(const HighsMipSolver& mipsolver) conflict_avg_score = mipsolver.pscostinit->conflict_avg_score * mipsolver.numCol(); - HighsInt numOrigCol = - static_cast(mipsolver.pscostinit->pseudocostup.size()); for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { + if (!mipsolver.mipdata_->postSolveStack.isOrigCol(i)) continue; HighsInt origCol = mipsolver.mipdata_->postSolveStack.getOrigColIndex()[i]; - if (origCol >= numOrigCol) continue; pseudocostup[i] = mipsolver.pscostinit->pseudocostup[origCol]; nsamplesup[i] = mipsolver.pscostinit->nsamplesup[origCol]; @@ -113,6 +111,7 @@ HighsPseudocostInitialization::HighsPseudocostInitialization( conflict_avg_score /= ncols * pscost.conflict_weight; for (HighsInt i = 0; i != ncols; ++i) { + if (!postsolveStack.isOrigCol(i)) continue; pseudocostup[postsolveStack.getOrigColIndex()[i]] = pscost.pseudocostup[i]; pseudocostdown[postsolveStack.getOrigColIndex()[i]] = pscost.pseudocostdown[i]; From bc1e4746d4f07840b06d27a87bf2bb4c450f6b91 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 10:50:06 +0200 Subject: [PATCH 088/196] WIP --- highs/mip/HighsMipSolverData.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 2790ce4120b..244993f368a 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1289,13 +1289,14 @@ void HighsMipSolverData::performRestart() { // if we have a basis after solving the root LP, we expand it to the // original space so that it can be used for constructing a starting basis // for the presolved model after the restart - root_basis.col_status.resize(postSolveStack.getOrigNumCol()); + root_basis.col_status.resize(postSolveStack.getNextColIndex()); root_basis.row_status.resize(postSolveStack.getNextRowIndex(), HighsBasisStatus::kBasic); root_basis.valid = true; root_basis.useful = true; - for (HighsInt i = 0; i < mipsolver.numCol(); ++i) + HighsInt numCol = basis.col_status.size(); + for (HighsInt i = 0; i < numCol; ++i) root_basis.col_status[postSolveStack.getOrigColIndex()[i]] = basis.col_status[i]; From 5dafacdc5984df8648f61f00c703bdb2ed56eb05 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 11:23:34 +0200 Subject: [PATCH 089/196] More postsolve changes --- highs/presolve/HPresolve.cpp | 27 ++++++++++++++---- highs/presolve/HPresolve.h | 3 ++ highs/presolve/HighsPostsolveStack.cpp | 9 ++++++ highs/presolve/HighsPostsolveStack.h | 38 ++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 6 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 91bc7a43b7b..d06f83bc12c 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2209,6 +2209,16 @@ bool HPresolve::addToMatrix( return true; } +bool HPresolve::addToMatrix(HighsPostsolveStack& postsolve_stack, + double row_lower, double row_upper, + std::vector row_entries) { + std::vector rl = {row_lower}; + std::vector ru = {row_upper}; + std::vector> re; + re.push_back(std::move(row_entries)); + return addToMatrix(postsolve_stack, rl, ru, re); +} + HighsTripletListSlice HPresolve::getColumnVector(HighsInt col) const { return HighsTripletListSlice(Arow.data(), Avalue.data(), Anext.data(), colhead[col]); @@ -6986,21 +6996,26 @@ HPresolve::Result HPresolve::fourierMotzkin( // build the objective constraint row: c^T x - z <= -offset // (z >= c^T x + offset, with min z minimizing original objective) - std::vector objRowLower = {-kHighsInf}; - std::vector objRowUpper = {-model->offset_}; - std::vector> objRowEntries(1); + double offset = model->offset_; + std::vector objRow; for (HighsInt j = 0; j < zCol; ++j) { if (!colDeleted[j] && model->col_cost_[j] != 0.0) - objRowEntries[0].push_back({j, model->col_cost_[j]}); + objRow.push_back({j, model->col_cost_[j]}); } - objRowEntries[0].push_back({zCol, -1.0}); + objRow.push_back({zCol, -1.0}); // zero out original costs and offset for (HighsInt j = 0; j < zCol; ++j) model->col_cost_[j] = 0.0; model->offset_ = 0.0; // add the constraint row to the matrix - addToMatrix(postsolve_stack, objRowLower, objRowUpper, objRowEntries); + addToMatrix(postsolve_stack, -kHighsInf, -offset, objRow); + + // register reduction so getReducedPrimalSolution can compute z + std::vector costEntries; + for (const auto& entry : objRow) + costEntries.emplace_back(entry.col, entry.val); + postsolve_stack.fourierMotzkinObjCol(zCol, offset, costEntries); fourierMotzkinObjCol = zCol; diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 68e3dcb3b7b..2a7b066fe6c 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -390,6 +390,9 @@ class HPresolve { const std::vector& row_upper, const std::vector>& row_entries); + bool addToMatrix(HighsPostsolveStack& postsolve_stack, double row_lower, + double row_upper, std::vector row_entries); + Result prepareProbing(HighsPostsolveStack& postsolve_stack, bool& firstCall); Result finaliseProbing(HighsPostsolveStack& postsolve_stack, bool firstCall, diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index c78188b1eef..c987ec84d57 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -78,6 +78,15 @@ void HighsPostsolveStack::LinearTransform::transformToPresolvedSpace( primalSol[col] /= scale; } +void HighsPostsolveStack::FourierMotzkinObjCol::transformToPresolvedSpace( + const std::vector& costEntries, + std::vector& primalSol) const { + double val = offset; + for (const Nonzero& entry : costEntries) + val += entry.value * primalSol[entry.index]; + primalSol[col] = val; +} + static HighsBasisStatus computeRowStatus(double dual, HighsPostsolveStack::RowType rowType) { if (rowType == HighsPostsolveStack::RowType::kEq) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index dfb1fa045ed..5283ea70f28 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -93,6 +93,14 @@ class HighsPostsolveStack { void transformToPresolvedSpace(std::vector& primalSol) const; }; + struct FourierMotzkinObjCol { + double offset; + HighsInt col; + + void transformToPresolvedSpace(const std::vector& costEntries, + std::vector& primalSol) const; + }; + struct FreeColSubstitution { double rhs; double colCost; @@ -296,6 +304,7 @@ class HighsPostsolveStack { kDuplicateColumn, kSlackColSubstitution, kFourierMotzkinElimination, + kFourierMotzkinObjCol, }; HighsDataStack reductionValues; @@ -648,6 +657,18 @@ class HighsPostsolveStack { reductionAdded(ReductionType::kFourierMotzkinElimination); } + void fourierMotzkinObjCol(HighsInt col, double offset, + const std::vector& costEntries) { + reductionValues.push(FourierMotzkinObjCol{offset, origColIndex[col]}); + std::vector translatedEntries; + translatedEntries.reserve(costEntries.size()); + for (const Nonzero& entry : costEntries) + if (entry.index != col) + translatedEntries.emplace_back(origColIndex[entry.index], entry.value); + reductionValues.push(translatedEntries); + reductionAdded(ReductionType::kFourierMotzkinObjCol); + } + void duplicateRow(HighsInt row, bool rowUpperTightened, bool rowLowerTightened, HighsInt duplicateRow, double duplicateRowScale) { @@ -688,6 +709,7 @@ class HighsPostsolveStack { std::vector getReducedPrimalSolution( const std::vector& origPrimalSolution) { std::vector reducedSolution = origPrimalSolution; + reducedSolution.resize(nextColIndex, 0.0); for (const std::pair& primalColTransformation : reductions) { @@ -706,6 +728,15 @@ class HighsPostsolveStack { linearTransform.transformToPresolvedSpace(reducedSolution); break; } + case ReductionType::kFourierMotzkinObjCol: { + reductionValues.setPosition(primalColTransformation.second); + FourierMotzkinObjCol fmObjCol; + reductionValues.pop(fmObjCol); + std::vector costEntries; + reductionValues.pop(costEntries); + fmObjCol.transformToPresolvedSpace(costEntries, reducedSolution); + break; + } default: continue; } @@ -916,6 +947,13 @@ class HighsPostsolveStack { fmeNewRowOrigins, solution, basis); break; } + case ReductionType::kFourierMotzkinObjCol: { + std::vector costEntries; + reductionValues.pop(costEntries); + FourierMotzkinObjCol reduction; + reductionValues.pop(reduction); + break; + } default: printf("Reduction case %d not handled\n", int(reductions[i - 1].first)); From 34d4b2926e90128d475c1b24c0d1da839b1802ac Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 11:35:29 +0200 Subject: [PATCH 090/196] Fix postsolve --- highs/presolve/HighsPostsolveStack.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 5283ea70f28..3ea97fbb5d6 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -334,9 +334,7 @@ class HighsPostsolveStack { const std::vector& getOrigRowIndex() const { return origRowIndex; } - bool isOrigCol(HighsInt col) const { - return origColIndex[col] < origNumCol; - } + bool isOrigCol(HighsInt col) const { return origColIndex[col] < origNumCol; } bool isOrigRow(HighsInt row) const { return origRowType[row] == OrigRowType::kOriginal; @@ -730,10 +728,10 @@ class HighsPostsolveStack { } case ReductionType::kFourierMotzkinObjCol: { reductionValues.setPosition(primalColTransformation.second); - FourierMotzkinObjCol fmObjCol; - reductionValues.pop(fmObjCol); std::vector costEntries; reductionValues.pop(costEntries); + FourierMotzkinObjCol fmObjCol; + reductionValues.pop(fmObjCol); fmObjCol.transformToPresolvedSpace(costEntries, reducedSolution); break; } From 5bda1d12a520b7ee8a2cdbabb993eafff7965034 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 12:09:05 +0200 Subject: [PATCH 091/196] New lambda --- highs/presolve/HPresolve.cpp | 163 ++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 79 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index d06f83bc12c..0dfa2e20389 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6946,92 +6946,19 @@ HPresolve::Result HPresolve::fourierMotzkin( const HighsInt kUpperBoundRow = -2; const HighsInt kLowerBoundRow = -3; - // Add z column to reformulate objective as a constraint: - // min c^T x becomes min -z with c^T x - z <= 0 - // This allows FME to eliminate columns with nonzero cost. - if (fourierMotzkinObjCol == -1) { - bool hasNonzeroCost = false; - for (HighsInt j = 0; j < model->num_col_; ++j) { - if (!colDeleted[j] && model->col_cost_[j] != 0.0) { - hasNonzeroCost = true; - break; - } - } - if (hasNonzeroCost) { - HighsInt zCol = model->num_col_; - model->num_col_++; - model->a_matrix_.num_col_++; - - // extend model vectors - model->col_cost_.push_back(1.0); - model->col_lower_.push_back(-kHighsInf); - model->col_upper_.push_back(kHighsInf); - model->integrality_.push_back(HighsVarType::kContinuous); - model->a_matrix_.start_.push_back(model->a_matrix_.start_.back()); - if (model->col_names_.size() > 0) - model->col_names_.push_back("fme_obj_z"); - - // extend presolve vectors - colhead.push_back(-1); - colsize.push_back(0); - colDeleted.push_back(0); - implColLower.push_back(-kHighsInf); - implColUpper.push_back(kHighsInf); - colLowerSource.push_back(-1); - colUpperSource.push_back(-1); - implRowDualSourceByCol.push_back({}); - changedColFlag.push_back(1); - numProbes.push_back(0); - - // update implied bound structures (pointers may be invalidated by - // reallocation of column vectors above) - impliedRowBounds.setBoundArrays( - model->col_lower_.data(), model->col_upper_.data(), - implColLower.data(), implColUpper.data(), colLowerSource.data(), - colUpperSource.data()); - impliedDualRowBounds.setNumSums(model->num_col_); - - // register in postsolve stack - postsolve_stack.appendColToModel(); - - // build the objective constraint row: c^T x - z <= -offset - // (z >= c^T x + offset, with min z minimizing original objective) - double offset = model->offset_; - std::vector objRow; - for (HighsInt j = 0; j < zCol; ++j) { - if (!colDeleted[j] && model->col_cost_[j] != 0.0) - objRow.push_back({j, model->col_cost_[j]}); - } - objRow.push_back({zCol, -1.0}); - - // zero out original costs and offset - for (HighsInt j = 0; j < zCol; ++j) model->col_cost_[j] = 0.0; - model->offset_ = 0.0; - - // add the constraint row to the matrix - addToMatrix(postsolve_stack, -kHighsInf, -offset, objRow); - - // register reduction so getReducedPrimalSolution can compute z - std::vector costEntries; - for (const auto& entry : objRow) - costEntries.emplace_back(entry.col, entry.val); - postsolve_stack.fourierMotzkinObjCol(zCol, offset, costEntries); - - fourierMotzkinObjCol = zCol; - - shrinkProblem(postsolve_stack); - } - } + auto acceptCoef = [&](double val) { + double absval = std::abs(val); + return absval == 0.0 || (absval >= 1.0 / maxCoef && absval <= maxCoef); + }; auto isCandidate = [&](HighsInt col) { if (colDeleted[col]) return false; if (colsize[col] == 0) return false; if (model->integrality_[col] != HighsVarType::kContinuous) return false; - if (model->col_cost_[col] != 0.0) return false; + if (!acceptCoef(model->col_cost_[col])) return false; for (const auto& nz : getColumnVector(col)) { if (isEquation(nz.index())) return false; - double absval = std::abs(nz.value()); - if (absval < 1.0 / maxCoef || absval > maxCoef) return false; + if (!acceptCoef(nz.value())) return false; } return true; }; @@ -7220,6 +7147,81 @@ HPresolve::Result HPresolve::fourierMotzkin( return neRed > 0 || (neRed == 0 && mrRed > 0); }; + // Reformulate objective as a constraint: min c^T x + offset becomes + // min z with c^T x - z <= -offset. This allows FME to eliminate + // continuous columns with nonzero cost. Only applied if at least one + // continuous column with nonzero cost would be an FME candidate. + auto reformulateObjective = [&]() { + if (fourierMotzkinObjCol != -1) return; + + bool needsReformulation = false; + for (HighsInt j = 0; j < model->num_col_; ++j) { + needsReformulation = model->col_cost_[j] != 0.0 && isCandidate(j); + if (needsReformulation) break; + } + if (!needsReformulation) return; + + HighsInt zCol = model->num_col_; + model->num_col_++; + model->a_matrix_.num_col_++; + + // extend model vectors + model->col_cost_.push_back(1.0); + model->col_lower_.push_back(-kHighsInf); + model->col_upper_.push_back(kHighsInf); + model->integrality_.push_back(HighsVarType::kContinuous); + model->a_matrix_.start_.push_back(model->a_matrix_.start_.back()); + if (model->col_names_.size() > 0) model->col_names_.push_back("fme_obj_z"); + + // extend presolve vectors + colhead.push_back(-1); + colsize.push_back(0); + colDeleted.push_back(0); + implColLower.push_back(-kHighsInf); + implColUpper.push_back(kHighsInf); + colLowerSource.push_back(-1); + colUpperSource.push_back(-1); + implRowDualSourceByCol.push_back({}); + changedColFlag.push_back(1); + numProbes.push_back(0); + + // update implied bound structures (pointers may be invalidated by + // reallocation of column vectors above) + impliedRowBounds.setBoundArrays( + model->col_lower_.data(), model->col_upper_.data(), implColLower.data(), + implColUpper.data(), colLowerSource.data(), colUpperSource.data()); + impliedDualRowBounds.setNumSums(model->num_col_); + + // register in postsolve stack + postsolve_stack.appendColToModel(); + + // build the objective constraint row: c^T x - z <= -offset + double offset = model->offset_; + std::vector objRow; + for (HighsInt j = 0; j < zCol; ++j) { + if (!colDeleted[j] && model->col_cost_[j] != 0.0) + objRow.push_back({j, model->col_cost_[j]}); + } + objRow.push_back({zCol, -1.0}); + + // zero out original costs and offset + for (HighsInt j = 0; j < zCol; ++j) model->col_cost_[j] = 0.0; + model->offset_ = 0.0; + + // add the constraint row to the matrix + addToMatrix(postsolve_stack, -kHighsInf, -offset, objRow); + + // register reduction so getReducedPrimalSolution can compute z + std::vector costEntries; + for (const auto& entry : objRow) + costEntries.emplace_back(entry.col, entry.val); + postsolve_stack.fourierMotzkinObjCol(zCol, offset, costEntries); + + fourierMotzkinObjCol = zCol; + + shrinkProblem(postsolve_stack); + }; + auto heapBetter = [](const candidate& a, const candidate& b) { if (a.neRed != b.neRed) return a.neRed > b.neRed; return a.mrRed > b.mrRed; @@ -7283,6 +7285,9 @@ HPresolve::Result HPresolve::fourierMotzkin( heapBubbleDown(heap, heapPos, pos); }; + // reformulate objective if beneficial + reformulateObjective(); + // collect candidate variables std::vector candidates; computeCandidates(candidates); From c46f1de17850e9a23f63002e963399ccf6f1cd3d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 13:58:14 +0200 Subject: [PATCH 092/196] Simplify --- highs/presolve/HPresolve.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 0dfa2e20389..a184c2d000c 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6956,10 +6956,8 @@ HPresolve::Result HPresolve::fourierMotzkin( if (colsize[col] == 0) return false; if (model->integrality_[col] != HighsVarType::kContinuous) return false; if (!acceptCoef(model->col_cost_[col])) return false; - for (const auto& nz : getColumnVector(col)) { - if (isEquation(nz.index())) return false; - if (!acceptCoef(nz.value())) return false; - } + for (const auto& nz : getColumnVector(col)) + if (isEquation(nz.index()) || !acceptCoef(nz.value())) return false; return true; }; From cea2bfe8582ba3431a1976e49bf7f103becf22c1 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 16:12:20 +0200 Subject: [PATCH 093/196] Reformulate only if needed --- highs/presolve/HPresolve.cpp | 79 ++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index a184c2d000c..1b6123ae784 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6964,6 +6964,7 @@ HPresolve::Result HPresolve::fourierMotzkin( auto computeCandidates = [&](std::vector& candidates) { for (HighsInt col = 0; col < model->num_col_; col++) if (isCandidate(col)) candidates.push_back(col); + return !candidates.empty(); }; auto checkRows = [&](HighsInt col, std::vector& iPlus, @@ -7145,10 +7146,9 @@ HPresolve::Result HPresolve::fourierMotzkin( return neRed > 0 || (neRed == 0 && mrRed > 0); }; - // Reformulate objective as a constraint: min c^T x + offset becomes - // min z with c^T x - z <= -offset. This allows FME to eliminate - // continuous columns with nonzero cost. Only applied if at least one - // continuous column with nonzero cost would be an FME candidate. + // reformulate objective as a constraint: min c^T x + offset becomes + // min z with c^T x - z <= -offset. this allows FME to eliminate + // continuous columns with nonzero cost. auto reformulateObjective = [&]() { if (fourierMotzkinObjCol != -1) return; @@ -7283,42 +7283,69 @@ HPresolve::Result HPresolve::fourierMotzkin( heapBubbleDown(heap, heapPos, pos); }; - // reformulate objective if beneficial - reformulateObjective(); + auto buildHeap = + [&](const std::vector& candidates, std::vector& heap, + std::vector& heapPos, std::vector& iPlus, + std::vector& iMinus, std::vector& pPlus, + std::vector& pMinus, std::vector& affectedCols) { + heap.clear(); + heap.reserve(candidates.size()); + heapPos.assign(model->num_col_, -1); + pPlus.assign(model->num_col_, 0); + pMinus.assign(model->num_col_, 0); + iPlus.reserve(model->num_row_); + iMinus.reserve(model->num_row_); + affectedCols.reserve(model->num_col_); + for (HighsInt col : candidates) { + int64_t neRed; + int64_t mrRed; + bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, + affectedCols, neRed, mrRed); + affectedCols.clear(); + if (!elimCandidate || !isReduction(neRed, mrRed)) continue; + heapPos[col] = static_cast(heap.size()); + heap.push_back({col, neRed, mrRed}); + } + return !heap.empty(); + }; // collect candidate variables std::vector candidates; - computeCandidates(candidates); - if (candidates.empty()) return finalise(); + if (!computeCandidates(candidates)) return finalise(); // workspace vectors std::vector iPlus; std::vector iMinus; - iPlus.reserve(model->num_row_); - iMinus.reserve(model->num_row_); - std::vector pPlus(model->num_col_, 0); - std::vector pMinus(model->num_col_, 0); + std::vector pPlus; + std::vector pMinus; std::vector affectedCols; - affectedCols.reserve(model->num_col_); // indexed max-heap std::vector heap; - heap.reserve(candidates.size()); - std::vector heapPos(model->num_col_, -1); + std::vector heapPos; + // build initial heap - for (HighsInt col : candidates) { - int64_t neRed; - int64_t mrRed; - bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, - affectedCols, neRed, mrRed); - affectedCols.clear(); - if (!elimCandidate || !isReduction(neRed, mrRed)) continue; - heapPos[col] = static_cast(heap.size()); - heap.push_back({col, neRed, mrRed}); + if (!buildHeap(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, + affectedCols)) + return finalise(); + + // reformulate objective only if at least one heap candidate has nonzero + // cost + for (const auto& c : heap) { + if (model->col_cost_[c.col] != 0.0) { + // reformulate + reformulateObjective(); + // re-compute candidates (shrinkProblem invalidates indices) + candidates.clear(); + if (!computeCandidates(candidates)) return finalise(); + // re-build heap + if (!buildHeap(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, + affectedCols)) + return finalise(); + break; + } } - if (heap.empty()) return finalise(); - // heapify for (HighsInt i = static_cast(heap.size()) / 2 - 1; i >= 0; --i) heapBubbleDown(heap, heapPos, i); From d4d40ed659dd821c9724aa3360525615ff367f2b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 16:28:15 +0200 Subject: [PATCH 094/196] Fix output alignment --- highs/presolve/HPresolveAnalysis.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HPresolveAnalysis.cpp b/highs/presolve/HPresolveAnalysis.cpp index f4724ebd056..a08d278cdef 100644 --- a/highs/presolve/HPresolveAnalysis.cpp +++ b/highs/presolve/HPresolveAnalysis.cpp @@ -48,7 +48,7 @@ void HPresolveAnalysis::setup(const HighsLp* model_, if (!allow || (!options->presolve_rule_off && options_->log_dev_level)) highsLogUser(options->log_options, HighsLogType::kInfo, - " Rule %2d (set bit %2d = %5d): %s\n", + " Rule %2d (set bit %2d = %6d): %s\n", int(rule_type), int(rule_type), int(bit), utilPresolveRuleTypeToString(rule_type).c_str()); } else if (!allow && !silent) { From a6afedbdf5cf777a2cd6a8c72e6355e62d6f2a93 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 8 Jun 2026 16:40:15 +0200 Subject: [PATCH 095/196] Remove extra check --- highs/presolve/HPresolve.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 1b6123ae784..aaac48fb55e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7152,13 +7152,6 @@ HPresolve::Result HPresolve::fourierMotzkin( auto reformulateObjective = [&]() { if (fourierMotzkinObjCol != -1) return; - bool needsReformulation = false; - for (HighsInt j = 0; j < model->num_col_; ++j) { - needsReformulation = model->col_cost_[j] != 0.0 && isCandidate(j); - if (needsReformulation) break; - } - if (!needsReformulation) return; - HighsInt zCol = model->num_col_; model->num_col_++; model->a_matrix_.num_col_++; From 4c32837cbb48dcfb24bbb2c1a1c6fe85c273f877 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 9 Jun 2026 08:34:04 +0200 Subject: [PATCH 096/196] Enable on LPs --- highs/presolve/HPresolve.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index aaac48fb55e..ed7bab5de0d 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5947,7 +5947,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif bool tryFourierMotzkin = - mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; + true; // mipsolver != nullptr || + // !options->lp_presolve_requires_basis_postsolve; bool tryProbing = mipsolver != nullptr; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; From 734ff6a786313ec728f746a8de346fbe8a2b9857 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 9 Jun 2026 20:21:13 +0200 Subject: [PATCH 097/196] Clean up --- highs/presolve/HighsPostsolveStack.cpp | 231 ++++++++++++------------- 1 file changed, 106 insertions(+), 125 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index c987ec84d57..4babdc67158 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1558,20 +1558,6 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( } }; - auto nonbasicRowStatus = [&](const FmeRowHeader& hdr, - const std::vector& rowEntries, - double coefOfCol) -> HighsBasisStatus { - HighsCDouble activity = - static_cast(coefOfCol) * solution.col_value[col]; - for (const auto& nz : rowEntries) - activity += - static_cast(nz.value) * solution.col_value[nz.index]; - double act = static_cast(activity); - if (hdr.rowLower != -kHighsInf && act - hdr.rowLower <= tol) - return HighsBasisStatus::kLower; - return HighsBasisStatus::kUpper; - }; - std::vector plusSlacks, minusSlacks; computeSlacks(plusHeaders, plusCoefOfCol, plusEntries, plusSlacks); computeSlacks(minusHeaders, minusCoefOfCol, minusEntries, minusSlacks); @@ -1588,110 +1574,49 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( }; // x_j is always at a bound: nonbasic - if (impliedLower >= -tol && impliedUpper <= tol) { - if (colLower == 0.0) - basis.col_status[col] = HighsBasisStatus::kLower; - else if (colUpper == 0.0) - basis.col_status[col] = HighsBasisStatus::kUpper; - else - basis.col_status[col] = HighsBasisStatus::kZero; - } else if (impliedLower > tol) { + if (impliedLower <= 0.0 && impliedUpper >= 0.0) + basis.col_status[col] = HighsBasisStatus::kZero; + else if (impliedLower > 0.0) basis.col_status[col] = HighsBasisStatus::kLower; - } else { + else basis.col_status[col] = HighsBasisStatus::kUpper; - } - // free variable case: no constraints generated, x_j is basic - if (newRowOrigins.empty()) { - basis.col_status[col] = HighsBasisStatus::kBasic; - for (HighsInt r = 0; r < numPlus; ++r) { - if (plusSlacks[r] > tol) - basis.row_status[plusHeaders[r].row] = HighsBasisStatus::kBasic; - else - basis.row_status[plusHeaders[r].row] = - nonbasicRowStatus(plusHeaders[r], plusEntries[r], plusCoefOfCol[r]); - } - for (HighsInt r = 0; r < numMinus; ++r) { - if (minusSlacks[r] > tol) - basis.row_status[minusHeaders[r].row] = HighsBasisStatus::kBasic; - else - basis.row_status[minusHeaders[r].row] = nonbasicRowStatus( - minusHeaders[r], minusEntries[r], minusCoefOfCol[r]); - } - return; - } + // Algorithm 5: build set K of all rows (new + parent, deduplicated) and + // iterate backward by decreasing row index. + struct KEntry { + HighsInt rowIdx; + HighsInt type; // 0 = new row, 1 = parent row + HighsInt idx; // index into newRowOrigins (type=0) or parent index (type=1) + }; - // algorithm 5: iterate over constraints K in reverse - for (HighsInt k = (HighsInt)newRowOrigins.size() - 1; k >= 0; --k) { - HighsInt pOrigRow = newRowOrigins[k].plusRow; - HighsInt mOrigRow = newRowOrigins[k].minusRow; - HighsInt pIdx = pOrigRow >= 0 ? findPlusIndex(pOrigRow) : -1; - HighsInt mIdx = mOrigRow >= 0 ? findMinusIndex(mOrigRow) : -1; - HighsInt newRow = newRowOrigins[k].newRow; + std::vector allK; + allK.reserve(newRowOrigins.size() + numPlus + numMinus); + for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) + allK.push_back({newRowOrigins[k].newRow, 0, k}); - double pSlack = - pIdx >= 0 - ? plusSlacks[pIdx] - : (pOrigRow < 0 ? std::max(colUpper - solution.col_value[col], 0.0) - : 0.0); - double mSlack = - mIdx >= 0 - ? minusSlacks[mIdx] - : (mOrigRow < 0 ? std::max(solution.col_value[col] - colLower, 0.0) - : 0.0); - bool betaIsBasic = basis.row_status[newRow] == HighsBasisStatus::kBasic; - - if (!betaIsBasic) { - // non-basic propagation: both parent slacks are zero, both non-basic - if (pIdx >= 0) - basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( - plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); - if (mIdx >= 0) - basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( - minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); - } else { - // basic propagation - if (pSlack > tol && mSlack <= tol) { - // plus parent has slack: it becomes basic - if (pIdx >= 0) - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - if (mIdx >= 0) - basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( - minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); - } else if (mSlack > tol && pSlack <= tol) { - // minus parent has slack: it becomes basic - if (mIdx >= 0) - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - if (pIdx >= 0) - basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( - plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); - } else if (pSlack > tol && mSlack > tol) { - // both have slack: plus parent becomes basic - if (pIdx >= 0) - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - if (mIdx >= 0) - basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( - minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); - } else { - // both slacks zero (degenerate): x_j becomes basic - basis.col_status[col] = HighsBasisStatus::kBasic; - if (pIdx >= 0) - basis.row_status[plusHeaders[pIdx].row] = nonbasicRowStatus( - plusHeaders[pIdx], plusEntries[pIdx], plusCoefOfCol[pIdx]); - if (mIdx >= 0) - basis.row_status[minusHeaders[mIdx].row] = nonbasicRowStatus( - minusHeaders[mIdx], minusEntries[mIdx], minusCoefOfCol[mIdx]); - } + // Add parent rows, deduplicating by row index (ranged rows appear in both) + std::vector parentAdded(basis.row_status.size(), false); + for (HighsInt r = 0; r < numPlus; ++r) { + HighsInt row = plusHeaders[r].row; + if (!parentAdded[row]) { + allK.push_back({row, 1, r}); + parentAdded[row] = true; + } + } + for (HighsInt r = 0; r < numMinus; ++r) { + HighsInt row = minusHeaders[r].row; + if (!parentAdded[row]) { + allK.push_back({row, 1, r}); + parentAdded[row] = true; } } - // vanished constraint check: parent rows not involved in any new row + // Sort by decreasing row index + std::sort(allK.begin(), allK.end(), [](const KEntry& a, const KEntry& b) { + return a.rowIdx > b.rowIdx; + }); + + // Determine which parent rows are involved in at least one new row auto isInvolved = [&](HighsInt row) { for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) if (newRowOrigins[k].plusRow == row || newRowOrigins[k].minusRow == row) @@ -1699,21 +1624,77 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( return false; }; - for (HighsInt r = 0; r < numPlus; ++r) { - if (isInvolved(plusHeaders[r].row)) continue; - if (plusSlacks[r] > tol) - basis.row_status[plusHeaders[r].row] = HighsBasisStatus::kBasic; - else - basis.row_status[plusHeaders[r].row] = - nonbasicRowStatus(plusHeaders[r], plusEntries[r], plusCoefOfCol[r]); - } - for (HighsInt r = 0; r < numMinus; ++r) { - if (isInvolved(minusHeaders[r].row)) continue; - if (minusSlacks[r] > tol) - basis.row_status[minusHeaders[r].row] = HighsBasisStatus::kBasic; - else - basis.row_status[minusHeaders[r].row] = nonbasicRowStatus( - minusHeaders[r], minusEntries[r], minusCoefOfCol[r]); + for (const auto& entry : allK) { + if (entry.type == 0) { + // New row: apply propagation rules + HighsInt k = entry.idx; + HighsInt pOrigRow = newRowOrigins[k].plusRow; + HighsInt mOrigRow = newRowOrigins[k].minusRow; + HighsInt pIdx = pOrigRow >= 0 ? findPlusIndex(pOrigRow) : -1; + HighsInt mIdx = mOrigRow >= 0 ? findMinusIndex(mOrigRow) : -1; + + double pSlack = + pIdx >= 0 ? plusSlacks[pIdx] + : (pOrigRow < 0 + ? std::max(colUpper - solution.col_value[col], 0.0) + : 0.0); + double mSlack = + mIdx >= 0 ? minusSlacks[mIdx] + : (mOrigRow < 0 + ? std::max(solution.col_value[col] - colLower, 0.0) + : 0.0); + bool betaIsBasic = + basis.row_status[entry.rowIdx] == HighsBasisStatus::kBasic; + + if (!betaIsBasic) { + // non-basic propagation + if (pIdx >= 0) + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kNonbasic; + else + basis.col_status[col] = HighsBasisStatus::kNonbasic; + if (mIdx >= 0) + basis.row_status[minusHeaders[mIdx].row] = + HighsBasisStatus::kNonbasic; + else + basis.col_status[col] = HighsBasisStatus::kNonbasic; + } else { + // basic propagation + if (pSlack > tol && mSlack <= tol) { + if (pIdx >= 0) + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + } else if (mSlack > tol && pSlack <= tol) { + if (mIdx >= 0) + basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + } else if (pSlack > tol && mSlack > tol) { + if (pIdx >= 0) + basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + } else { + basis.col_status[col] = HighsBasisStatus::kBasic; + } + } + } else { + // Parent row: Pk = empty check (vanished constraint / free variable) + if (isInvolved(entry.rowIdx)) continue; + // Free variable rule: if no new rows exist, x_j is basic + if (newRowOrigins.empty()) + basis.col_status[col] = HighsBasisStatus::kBasic; + // Vanished constraint rule: nonzero slack → basic, else nonbasic + HighsInt pIdx = findPlusIndex(entry.rowIdx); + HighsInt mIdx = findMinusIndex(entry.rowIdx); + double slack = 0.0; + if (pIdx >= 0) slack += plusSlacks[pIdx]; + if (mIdx >= 0) slack += minusSlacks[mIdx]; + if (slack > tol) + basis.row_status[entry.rowIdx] = HighsBasisStatus::kBasic; + else + basis.row_status[entry.rowIdx] = HighsBasisStatus::kNonbasic; + } } } From bb2faca15d3c94627c971fa2eaedda28336de651 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 9 Jun 2026 20:38:12 +0200 Subject: [PATCH 098/196] Use static_cast --- highs/presolve/HighsPostsolveStack.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 4babdc67158..ec5eed3f149 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1485,7 +1485,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( solution.row_dual[minusHeaders[r].row] = 0.0; // distribute new row duals back to original rows - for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) { + for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) { HighsInt newRow = newRowOrigins[k].newRow; if (!postsolveStack.isModelRow(newRow)) continue; double lambda = solution.row_dual[newRow]; @@ -1591,7 +1591,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( std::vector allK; allK.reserve(newRowOrigins.size() + numPlus + numMinus); - for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) + for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) allK.push_back({newRowOrigins[k].newRow, 0, k}); // Add parent rows, deduplicating by row index (ranged rows appear in both) @@ -1618,7 +1618,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( // Determine which parent rows are involved in at least one new row auto isInvolved = [&](HighsInt row) { - for (HighsInt k = 0; k < (HighsInt)newRowOrigins.size(); ++k) + for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) if (newRowOrigins[k].plusRow == row || newRowOrigins[k].minusRow == row) return true; return false; From 58ec85c911d05532d02925ab4d4ba87dde499b87 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 9 Jun 2026 20:51:34 +0200 Subject: [PATCH 099/196] Allow multiple rounds of FM presolve --- highs/presolve/HPresolve.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ed7bab5de0d..0dcac4d98f8 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5981,7 +5981,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { if (tryFourierMotzkin && analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); - tryFourierMotzkin = false; + // tryFourierMotzkin = false; + if (problemSizeReduction() > 0.05) continue; } if (analysis_.allow_rule_[kPresolveRuleAggregator]) From 4234afabf19447d5958425b84695174a877a489b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 9 Jun 2026 21:19:49 +0200 Subject: [PATCH 100/196] Minor changes --- highs/presolve/HighsPostsolveStack.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index ec5eed3f149..d371315ee43 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1581,8 +1581,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( else basis.col_status[col] = HighsBasisStatus::kUpper; - // Algorithm 5: build set K of all rows (new + parent, deduplicated) and - // iterate backward by decreasing row index. + // build set K of all rows struct KEntry { HighsInt rowIdx; HighsInt type; // 0 = new row, 1 = parent row @@ -1594,7 +1593,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) allK.push_back({newRowOrigins[k].newRow, 0, k}); - // Add parent rows, deduplicating by row index (ranged rows appear in both) + // add parent rows, deduplicating by row index (ranged rows appear in both) std::vector parentAdded(basis.row_status.size(), false); for (HighsInt r = 0; r < numPlus; ++r) { HighsInt row = plusHeaders[r].row; @@ -1611,12 +1610,12 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( } } - // Sort by decreasing row index + // sort by decreasing row index std::sort(allK.begin(), allK.end(), [](const KEntry& a, const KEntry& b) { return a.rowIdx > b.rowIdx; }); - // Determine which parent rows are involved in at least one new row + // determine which parent rows are involved in at least one new row auto isInvolved = [&](HighsInt row) { for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) if (newRowOrigins[k].plusRow == row || newRowOrigins[k].minusRow == row) @@ -1626,7 +1625,7 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( for (const auto& entry : allK) { if (entry.type == 0) { - // New row: apply propagation rules + // new row: apply propagation rules HighsInt k = entry.idx; HighsInt pOrigRow = newRowOrigins[k].plusRow; HighsInt mOrigRow = newRowOrigins[k].minusRow; @@ -1679,12 +1678,12 @@ void HighsPostsolveStack::FourierMotzkinElimination::undo( } } } else { - // Parent row: Pk = empty check (vanished constraint / free variable) + // parent row: Pk = empty check (vanished constraint / free variable) if (isInvolved(entry.rowIdx)) continue; - // Free variable rule: if no new rows exist, x_j is basic + // free variable rule: if no new rows exist, x_j is basic if (newRowOrigins.empty()) basis.col_status[col] = HighsBasisStatus::kBasic; - // Vanished constraint rule: nonzero slack → basic, else nonbasic + // vanished constraint rule: nonzero slack → basic, else nonbasic HighsInt pIdx = findPlusIndex(entry.rowIdx); HighsInt mIdx = findMinusIndex(entry.rowIdx); double slack = 0.0; From a842f2475aa9711fa91799c6a35dd5e933eb0977 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 9 Jun 2026 21:23:36 +0200 Subject: [PATCH 101/196] Still issues with basis postsolve --- highs/presolve/HPresolve.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 0dcac4d98f8..25aa067e98c 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5947,8 +5947,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif bool tryFourierMotzkin = - true; // mipsolver != nullptr || - // !options->lp_presolve_requires_basis_postsolve; + mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; bool tryProbing = mipsolver != nullptr; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; @@ -5979,11 +5978,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { } if (tryFourierMotzkin && - analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { + analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); - // tryFourierMotzkin = false; - if (problemSizeReduction() > 0.05) continue; - } if (analysis_.allow_rule_[kPresolveRuleAggregator]) HPRESOLVE_CHECKED_CALL(aggregator(postsolve_stack)); From be608135ace66516cdfec47ed19740294a063e64 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 13:58:38 +0200 Subject: [PATCH 102/196] Rework postsolve --- highs/presolve/HPresolve.cpp | 148 +++++++++- highs/presolve/HighsPostsolveStack.cpp | 375 ++++++++----------------- highs/presolve/HighsPostsolveStack.h | 230 ++++++++------- 3 files changed, 382 insertions(+), 371 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 25aa067e98c..639008d8953 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7356,6 +7356,29 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt numRowsEliminated = 0; HighsInt numRowsAdded = 0; + // FM block data for postsolve + using FmeRow = + HighsPostsolveStack::FmeRowData; + using FmeDescendant = HighsPostsolveStack::FmeDescendant; + std::vector blockCols; + std::vector blockColLowers; + std::vector blockColUppers; + std::vector blockColCosts; + std::vector blockNumPlus; + std::vector blockNumMinus; + std::vector>> blockDescendants; + + // Ancestry tracking: for each model row, which (step, parentLocalIdx) + // pairs contributed to it, with cumulative scale factor. + // parentLocalIdx: 0..numPlus-1 for plus parents, numPlus..numPlus+numMinus-1 + // for minus parents. + struct AncestryEntry { + HighsInt step; + HighsInt parentLocalIdx; + double scale; + }; + std::unordered_map> rowAncestry; + // main loop: eliminate variables from heap while (!heap.empty()) { HighsInt col = heap[0].col; @@ -7370,6 +7393,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // heap data should be up-to-date assert(elimCandidate && isReduction(neRed, mrRed)); + HighsInt stepIdx = static_cast(blockCols.size()); + // perform elimination: generate new rows newRows.clear(); for (HighsInt pRow : iPlus) { @@ -7438,10 +7463,7 @@ HPresolve::Result HPresolve::fourierMotzkin( newRowPairs.push_back({nr.plusIndex, nr.minusIndex}); } - // record postsolve entry before addToMatrix (which may invalidate - // row slices via reallocation) - using FmeRow = - HighsPostsolveStack::FmeRowData; + // Serialize row data for postsolve before addToMatrix invalidates slices std::vector plusRows; std::vector minusRows; @@ -7456,15 +7478,101 @@ HPresolve::Result HPresolve::fourierMotzkin( model->row_upper_[mRow], getRowVector(mRow)}); } - postsolve_stack.fourierMotzkinElimination( - col, model->col_lower_[col], model->col_upper_[col], - model->col_cost_[col], plusRows, minusRows, newRowPairs); + std::pair storedCounts = + postsolve_stack.fourierMotzkinBlockPushStep(col, plusRows, minusRows); + HighsInt numPlusStored = storedCounts.first; + HighsInt numMinusStored = storedCounts.second; + + // save block metadata + blockCols.push_back(col); + blockColLowers.push_back(model->col_lower_[col]); + blockColUppers.push_back(model->col_upper_[col]); + blockColCosts.push_back(model->col_cost_[col]); + blockNumPlus.push_back(numPlusStored); + blockNumMinus.push_back(numMinusStored); // add new rows to matrix + HighsInt firstNewRow = model->num_row_; if (!addToMatrix(postsolve_stack, rowLower, rowUpper, rowEntries)) return finalise(); numRowsAdded += static_cast(rowEntries.size()); + // Build ancestry for new rows + // Map from iPlus/iMinus index to parentLocalIdx in this step + // plusRows indices: 0..numPlusStored-1, minusRows: numPlusStored..end + auto findPlusLocalIdx = [&](HighsInt row) -> HighsInt { + HighsInt idx = 0; + for (HighsInt pRow : iPlus) { + if (pRow < 0) continue; + if (pRow == row) return idx; + idx++; + } + return -1; + }; + auto findMinusLocalIdx = [&](HighsInt row) -> HighsInt { + HighsInt idx = 0; + for (HighsInt mRow : iMinus) { + if (mRow < 0) continue; + if (mRow == row) return numPlusStored + idx; + idx++; + } + return -1; + }; + + for (HighsInt k = 0; k < static_cast(newRowPairs.size()); ++k) { + HighsInt newModelRow = firstNewRow + k; + HighsInt pRow = newRowPairs[k].first; + HighsInt mRow = newRowPairs[k].second; + + std::vector& newAnc = rowAncestry[newModelRow]; + + // Compute scale factors for this (pRow, mRow) pair + double pCoefAbs = 1.0, mCoefAbs = 1.0; + if (pRow >= 0) { + HighsInt pPos = findNonzero(pRow, col); + if (pPos != -1) pCoefAbs = std::abs(Avalue[pPos]); + } + if (mRow >= 0) { + HighsInt mPos = findNonzero(mRow, col); + if (mPos != -1) mCoefAbs = std::abs(Avalue[mPos]); + } + double s = (pCoefAbs * mCoefAbs) / (pCoefAbs + mCoefAbs); + double pScaleFactor = s / pCoefAbs; + double mScaleFactor = s / mCoefAbs; + + // Inherit ancestry from plus parent + if (pRow >= 0) { + auto it = rowAncestry.find(pRow); + if (it != rowAncestry.end()) { + for (const auto& a : it->second) + newAnc.push_back({a.step, a.parentLocalIdx, a.scale * pScaleFactor}); + } + HighsInt pLocalIdx = findPlusLocalIdx(pRow); + if (pLocalIdx >= 0) + newAnc.push_back({stepIdx, pLocalIdx, pScaleFactor}); + } + + // Inherit ancestry from minus parent + if (mRow >= 0) { + auto it = rowAncestry.find(mRow); + if (it != rowAncestry.end()) { + for (const auto& a : it->second) + newAnc.push_back({a.step, a.parentLocalIdx, a.scale * mScaleFactor}); + } + HighsInt mLocalIdx = findMinusLocalIdx(mRow); + if (mLocalIdx >= 0) + newAnc.push_back({stepIdx, mLocalIdx, mScaleFactor}); + } + } + + // Remove ancestry entries for deleted parent rows + for (HighsInt rp : iPlus) { + if (rp >= 0) rowAncestry.erase(rp); + } + for (HighsInt rm : iMinus) { + if (rm >= 0) rowAncestry.erase(rm); + } + // remove old rows containing col (skip virtual bound rows) for (HighsInt rp : iPlus) { if (rp < 0) continue; @@ -7513,12 +7621,36 @@ HPresolve::Result HPresolve::fourierMotzkin( HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } - if (numColsEliminated > 0) + // Build K^j_i mapping from ancestry and finalize the FM block + if (numColsEliminated > 0) { + HighsInt numSteps = static_cast(blockCols.size()); + + // Collect all surviving rows with ancestry + // For each (step, parentLocalIdx), gather the final descendants + blockDescendants.resize(numSteps); + for (HighsInt s = 0; s < numSteps; ++s) { + HighsInt numParents = blockNumPlus[s] + blockNumMinus[s]; + blockDescendants[s].resize(numParents); + } + + for (const auto& entry : rowAncestry) { + HighsInt row = entry.first; + HighsInt origRow = postsolve_stack.getOrigRowIndex()[row]; + for (const auto& a : entry.second) + blockDescendants[a.step][a.parentLocalIdx].push_back( + {origRow, a.scale}); + } + + postsolve_stack.fourierMotzkinBlockFinalize( + blockCols, blockColLowers, blockColUppers, blockColCosts, + blockNumPlus, blockNumMinus, blockDescendants); + highsLogDev(options->log_options, HighsLogType::kInfo, "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT " cols and %" HIGHSINT_FORMAT " rows, and added %" HIGHSINT_FORMAT " rows\n", numColsEliminated, numRowsEliminated, numRowsAdded); + } return finalise(); } diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index d371315ee43..c648deaf53b 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -87,6 +87,15 @@ void HighsPostsolveStack::FourierMotzkinObjCol::transformToPresolvedSpace( primalSol[col] = val; } +void HighsPostsolveStack::FourierMotzkinObjCol::undo( + const std::vector& costEntries, HighsSolution& solution) const { + if (!solution.dual_valid) return; + double zDual = solution.col_dual[col]; + for (const Nonzero& entry : costEntries) + solution.col_dual[entry.index] += entry.value * zDual; + solution.col_dual[col] = 0.0; +} + static HighsBasisStatus computeRowStatus(double dual, HighsPostsolveStack::RowType rowType) { if (rowType == HighsPostsolveStack::RowType::kEq) @@ -1417,283 +1426,129 @@ void HighsPostsolveStack::SlackColSubstitution::undo( } } -void HighsPostsolveStack::FourierMotzkinElimination::undo( - const HighsPostsolveStack& postsolveStack, const HighsOptions& options, - const std::vector& plusHeaders, - const std::vector& plusCoefOfCol, - const std::vector>& plusEntries, - const std::vector& minusHeaders, - const std::vector& minusCoefOfCol, - const std::vector>& minusEntries, - const std::vector& newRowOrigins, HighsSolution& solution, - HighsBasis& basis) const { - HighsInt numPlus = static_cast(plusHeaders.size()); - HighsInt numMinus = static_cast(minusHeaders.size()); - - // primal postsolve (algorithm 3) - double impliedLower = colLower; - double impliedUpper = colUpper; - - // lambda for computing a bound - auto computeBounds = [&](HighsInt direction, double val, double rhs, - const HighsCDouble& sum, double& impliedBound) { - if (std::abs(rhs) == kHighsInf) return; - double bound = static_cast(rhs - sum) / val; - impliedBound = - direction * std::min(direction * bound, direction * impliedBound); +void HighsPostsolveStack::undoFourierMotzkinBlock( + HighsDataStack& stack, const HighsOptions& options, + HighsSolution& solution, HighsBasis& basis) { + // Pop numSteps + HighsInt numSteps; + stack.pop(numSteps); + + struct StepData { + FmeStepHeader header; + std::vector plusHeaders; + std::vector plusCoefs; + std::vector> plusEntries; + std::vector minusHeaders; + std::vector minusCoefs; + std::vector> minusEntries; + std::vector> descendants; }; - // lambda for computing lower/upper bound - auto tightenBounds = [&](const std::vector& headers, - const std::vector& coefs, - const std::vector>& entries) { - for (size_t r = 0; r < headers.size(); ++r) { - const FmeRowHeader& hdr = headers[r]; - double aij = coefs[r]; - - HighsCDouble sum = 0.0; - for (const auto& nz : entries[r]) - sum += - static_cast(nz.value) * solution.col_value[nz.index]; - - computeBounds(HighsInt{1}, aij, aij > 0 ? hdr.rowUpper : hdr.rowLower, - sum, impliedUpper); - computeBounds(HighsInt{-1}, aij, aij > 0 ? hdr.rowLower : hdr.rowUpper, - sum, impliedLower); - } - }; + std::vector steps(numSteps); - // compute bounds - tightenBounds(plusHeaders, plusCoefOfCol, plusEntries); - tightenBounds(minusHeaders, minusCoefOfCol, minusEntries); + // Pop step headers (pushed last-to-first, so pop first-to-last... no: + // pushed in order s=0..N-1, so pop in reverse s=N-1..0) + for (HighsInt s = numSteps - 1; s >= 0; --s) + stack.pop(steps[s].header); - // algorithm 3: assign x_j to 0 if feasible, else closest bound to zero - if (impliedLower <= 0.0 && impliedUpper >= 0.0) - solution.col_value[col] = 0.0; - else if (impliedLower > 0.0) - solution.col_value[col] = impliedLower; - else - solution.col_value[col] = impliedUpper; + // Pop descendants (pushed in order s=0..N-1, p=0..numParents-1) + for (HighsInt s = numSteps - 1; s >= 0; --s) { + HighsInt numParents = steps[s].header.numPlus + steps[s].header.numMinus; + steps[s].descendants.resize(numParents); + for (HighsInt p = numParents - 1; p >= 0; --p) + stack.pop(steps[s].descendants[p]); + } - if (!solution.dual_valid) return; + // Pop row data (pushed in order s=0..N-1, so pop s=N-1..0) + for (HighsInt s = numSteps - 1; s >= 0; --s) { + // pop minus row data + stack.pop(steps[s].minusHeaders); + stack.pop(steps[s].minusCoefs); + HighsInt numMinus = static_cast(steps[s].minusCoefs.size()); + steps[s].minusEntries.resize(numMinus); + for (HighsInt r = numMinus - 1; r >= 0; --r) + stack.pop(steps[s].minusEntries[r]); + + // pop plus row data + stack.pop(steps[s].plusHeaders); + stack.pop(steps[s].plusCoefs); + HighsInt numPlus = static_cast(steps[s].plusCoefs.size()); + steps[s].plusEntries.resize(numPlus); + for (HighsInt r = numPlus - 1; r >= 0; --r) + stack.pop(steps[s].plusEntries[r]); + } - // dual postsolve - // zero parent row duals - for (HighsInt r = 0; r < numPlus; ++r) - solution.row_dual[plusHeaders[r].row] = 0.0; - for (HighsInt r = 0; r < numMinus; ++r) - solution.row_dual[minusHeaders[r].row] = 0.0; - - // distribute new row duals back to original rows - for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) { - HighsInt newRow = newRowOrigins[k].newRow; - if (!postsolveStack.isModelRow(newRow)) continue; - double lambda = solution.row_dual[newRow]; - - HighsInt pOrigRow = newRowOrigins[k].plusRow; - HighsInt mOrigRow = newRowOrigins[k].minusRow; - - double pCoefAbs = 0.0; - double mCoefAbs = 0.0; - if (pOrigRow >= 0) { - for (HighsInt r = 0; r < numPlus; ++r) - if (plusHeaders[r].row == pOrigRow) { - pCoefAbs = std::abs(plusCoefOfCol[r]); - break; + // Primal postsolve (Algorithm 3): process in reverse elimination order + for (HighsInt s = numSteps - 1; s >= 0; --s) { + const auto& step = steps[s]; + HighsInt col = step.header.col; + double impliedLower = step.header.colLower; + double impliedUpper = step.header.colUpper; + + auto tightenBounds = [&](const std::vector& headers, + const std::vector& coefs, + const std::vector>& entries) { + for (size_t r = 0; r < headers.size(); ++r) { + double aij = coefs[r]; + HighsCDouble sum = 0.0; + for (const auto& nz : entries[r]) + sum += static_cast(nz.value) * + solution.col_value[nz.index]; + double rhs_upper = aij > 0 ? headers[r].rowUpper : headers[r].rowLower; + double rhs_lower = aij > 0 ? headers[r].rowLower : headers[r].rowUpper; + if (rhs_upper != kHighsInf) { + double bound = static_cast(rhs_upper - sum) / aij; + impliedUpper = std::min(impliedUpper, bound); } - } - if (mOrigRow >= 0) { - for (HighsInt r = 0; r < numMinus; ++r) - if (minusHeaders[r].row == mOrigRow) { - mCoefAbs = std::abs(minusCoefOfCol[r]); - break; + if (rhs_lower != -kHighsInf) { + double bound = static_cast(rhs_lower - sum) / aij; + impliedLower = std::max(impliedLower, bound); } - } + } + }; - if (pCoefAbs == 0.0) pCoefAbs = 1.0; - if (mCoefAbs == 0.0) mCoefAbs = 1.0; - double s = (pCoefAbs * mCoefAbs) / (pCoefAbs + mCoefAbs); - if (pOrigRow >= 0) solution.row_dual[pOrigRow] += lambda * (s / pCoefAbs); - if (mOrigRow >= 0) solution.row_dual[mOrigRow] += lambda * (s / mCoefAbs); - solution.row_dual[newRow] = 0.0; - } + tightenBounds(step.plusHeaders, step.plusCoefs, step.plusEntries); + tightenBounds(step.minusHeaders, step.minusCoefs, step.minusEntries); - // compute col dual - solution.col_dual[col] = colCost; - for (HighsInt r = 0; r < numPlus; ++r) { - if (postsolveStack.isModelRow(plusHeaders[r].row)) - solution.col_dual[col] -= - plusCoefOfCol[r] * solution.row_dual[plusHeaders[r].row]; - } - for (HighsInt r = 0; r < numMinus; ++r) { - if (postsolveStack.isModelRow(minusHeaders[r].row)) - solution.col_dual[col] -= - minusCoefOfCol[r] * solution.row_dual[minusHeaders[r].row]; + if (impliedLower <= 0.0 && impliedUpper >= 0.0) + solution.col_value[col] = 0.0; + else if (impliedLower > 0.0) + solution.col_value[col] = impliedLower; + else + solution.col_value[col] = impliedUpper; } - // basis postsolve (algorithm 5) - if (!basis.valid) return; + if (!solution.dual_valid) return; - const double tol = options.mip_feasibility_tolerance; - - // Compute row slacks: s_i / |a_{ij}| - auto computeSlacks = [&](const std::vector& headers, - const std::vector& coefs, - const std::vector>& entries, - std::vector& slacks) { - slacks.resize(headers.size()); - for (size_t r = 0; r < headers.size(); ++r) { - HighsCDouble activity = - static_cast(coefs[r]) * solution.col_value[col]; - for (const auto& nz : entries[r]) - activity += - static_cast(nz.value) * solution.col_value[nz.index]; - double act = static_cast(activity); - double rawSlack = kHighsInf; - if (headers[r].rowUpper != kHighsInf) - rawSlack = std::min(rawSlack, headers[r].rowUpper - act); - if (headers[r].rowLower != -kHighsInf) - rawSlack = std::min(rawSlack, act - headers[r].rowLower); - slacks[r] = rawSlack / std::abs(coefs[r]); + // Dual postsolve (Algorithm 4): process in reverse elimination order + for (HighsInt s = numSteps - 1; s >= 0; --s) { + const auto& step = steps[s]; + HighsInt col = step.header.col; + HighsInt numPlus = step.header.numPlus; + HighsInt numMinus = step.header.numMinus; + + // u_i = Σ_{k ∈ K^j_i} λ_k * scaleFactor + for (HighsInt p = 0; p < numPlus; ++p) { + double ui = 0.0; + for (const auto& desc : step.descendants[p]) + ui += solution.row_dual[desc.row] * desc.scaleFactor; + solution.row_dual[step.plusHeaders[p].row] = ui; + } + for (HighsInt m = 0; m < numMinus; ++m) { + double vi = 0.0; + for (const auto& desc : step.descendants[numPlus + m]) + vi += solution.row_dual[desc.row] * desc.scaleFactor; + solution.row_dual[step.minusHeaders[m].row] = vi; } - }; - - std::vector plusSlacks, minusSlacks; - computeSlacks(plusHeaders, plusCoefOfCol, plusEntries, plusSlacks); - computeSlacks(minusHeaders, minusCoefOfCol, minusEntries, minusSlacks); - auto findPlusIndex = [&](HighsInt origRow) -> HighsInt { + // col_dual = cost - Σ a_{ij} * row_dual[i] + solution.col_dual[col] = step.header.colCost; for (HighsInt r = 0; r < numPlus; ++r) - if (plusHeaders[r].row == origRow) return r; - return -1; - }; - auto findMinusIndex = [&](HighsInt origRow) -> HighsInt { + solution.col_dual[col] -= + step.plusCoefs[r] * solution.row_dual[step.plusHeaders[r].row]; for (HighsInt r = 0; r < numMinus; ++r) - if (minusHeaders[r].row == origRow) return r; - return -1; - }; - - // x_j is always at a bound: nonbasic - if (impliedLower <= 0.0 && impliedUpper >= 0.0) - basis.col_status[col] = HighsBasisStatus::kZero; - else if (impliedLower > 0.0) - basis.col_status[col] = HighsBasisStatus::kLower; - else - basis.col_status[col] = HighsBasisStatus::kUpper; - - // build set K of all rows - struct KEntry { - HighsInt rowIdx; - HighsInt type; // 0 = new row, 1 = parent row - HighsInt idx; // index into newRowOrigins (type=0) or parent index (type=1) - }; - - std::vector allK; - allK.reserve(newRowOrigins.size() + numPlus + numMinus); - for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) - allK.push_back({newRowOrigins[k].newRow, 0, k}); - - // add parent rows, deduplicating by row index (ranged rows appear in both) - std::vector parentAdded(basis.row_status.size(), false); - for (HighsInt r = 0; r < numPlus; ++r) { - HighsInt row = plusHeaders[r].row; - if (!parentAdded[row]) { - allK.push_back({row, 1, r}); - parentAdded[row] = true; - } - } - for (HighsInt r = 0; r < numMinus; ++r) { - HighsInt row = minusHeaders[r].row; - if (!parentAdded[row]) { - allK.push_back({row, 1, r}); - parentAdded[row] = true; - } - } - - // sort by decreasing row index - std::sort(allK.begin(), allK.end(), [](const KEntry& a, const KEntry& b) { - return a.rowIdx > b.rowIdx; - }); - - // determine which parent rows are involved in at least one new row - auto isInvolved = [&](HighsInt row) { - for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) - if (newRowOrigins[k].plusRow == row || newRowOrigins[k].minusRow == row) - return true; - return false; - }; - - for (const auto& entry : allK) { - if (entry.type == 0) { - // new row: apply propagation rules - HighsInt k = entry.idx; - HighsInt pOrigRow = newRowOrigins[k].plusRow; - HighsInt mOrigRow = newRowOrigins[k].minusRow; - HighsInt pIdx = pOrigRow >= 0 ? findPlusIndex(pOrigRow) : -1; - HighsInt mIdx = mOrigRow >= 0 ? findMinusIndex(mOrigRow) : -1; - - double pSlack = - pIdx >= 0 ? plusSlacks[pIdx] - : (pOrigRow < 0 - ? std::max(colUpper - solution.col_value[col], 0.0) - : 0.0); - double mSlack = - mIdx >= 0 ? minusSlacks[mIdx] - : (mOrigRow < 0 - ? std::max(solution.col_value[col] - colLower, 0.0) - : 0.0); - bool betaIsBasic = - basis.row_status[entry.rowIdx] == HighsBasisStatus::kBasic; - - if (!betaIsBasic) { - // non-basic propagation - if (pIdx >= 0) - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kNonbasic; - else - basis.col_status[col] = HighsBasisStatus::kNonbasic; - if (mIdx >= 0) - basis.row_status[minusHeaders[mIdx].row] = - HighsBasisStatus::kNonbasic; - else - basis.col_status[col] = HighsBasisStatus::kNonbasic; - } else { - // basic propagation - if (pSlack > tol && mSlack <= tol) { - if (pIdx >= 0) - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } else if (mSlack > tol && pSlack <= tol) { - if (mIdx >= 0) - basis.row_status[minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } else if (pSlack > tol && mSlack > tol) { - if (pIdx >= 0) - basis.row_status[plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } else { - basis.col_status[col] = HighsBasisStatus::kBasic; - } - } - } else { - // parent row: Pk = empty check (vanished constraint / free variable) - if (isInvolved(entry.rowIdx)) continue; - // free variable rule: if no new rows exist, x_j is basic - if (newRowOrigins.empty()) - basis.col_status[col] = HighsBasisStatus::kBasic; - // vanished constraint rule: nonzero slack → basic, else nonbasic - HighsInt pIdx = findPlusIndex(entry.rowIdx); - HighsInt mIdx = findMinusIndex(entry.rowIdx); - double slack = 0.0; - if (pIdx >= 0) slack += plusSlacks[pIdx]; - if (mIdx >= 0) slack += minusSlacks[mIdx]; - if (slack > tol) - basis.row_status[entry.rowIdx] = HighsBasisStatus::kBasic; - else - basis.row_status[entry.rowIdx] = HighsBasisStatus::kNonbasic; - } + solution.col_dual[col] -= + step.minusCoefs[r] * solution.row_dual[step.minusHeaders[r].row]; } } diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 3ea97fbb5d6..2f19fa7e91a 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -60,12 +60,6 @@ class HighsPostsolveStack { Nonzero() = default; }; - struct NewRowOrigin { - HighsInt newRow; - HighsInt plusRow; - HighsInt minusRow; - }; - template struct FmeRowData { HighsInt row; @@ -74,6 +68,26 @@ class HighsPostsolveStack { HighsMatrixSlice rowVec; }; + struct FmeRowHeader { + HighsInt row; + double rowLower; + double rowUpper; + }; + + struct FmeStepHeader { + double colLower; + double colUpper; + double colCost; + HighsInt col; + HighsInt numPlus; + HighsInt numMinus; + }; + + struct FmeDescendant { + HighsInt row; + double scaleFactor; + }; + size_t debug_prev_numreductions = 0; double debug_prev_col_lower = 0; double debug_prev_col_upper = 0; @@ -99,6 +113,9 @@ class HighsPostsolveStack { void transformToPresolvedSpace(const std::vector& costEntries, std::vector& primalSol) const; + + void undo(const std::vector& costEntries, + HighsSolution& solution) const; }; struct FreeColSubstitution { @@ -263,29 +280,10 @@ class HighsPostsolveStack { HighsBasis& basis); }; - struct FmeRowHeader { - HighsInt row; - double rowLower; - double rowUpper; - }; - - struct FourierMotzkinElimination { - double colLower; - double colUpper; - double colCost; - HighsInt col; - - void undo(const HighsPostsolveStack& postsolveStack, - const HighsOptions& options, - const std::vector& plusHeaders, - const std::vector& plusCoefOfCol, - const std::vector>& plusEntries, - const std::vector& minusHeaders, - const std::vector& minusCoefOfCol, - const std::vector>& minusEntries, - const std::vector& newRowOrigins, - HighsSolution& solution, HighsBasis& basis) const; - }; + static void undoFourierMotzkinBlock(HighsDataStack& stack, + const HighsOptions& options, + HighsSolution& solution, + HighsBasis& basis); /// tags for reduction enum class ReductionType : uint8_t { @@ -303,7 +301,7 @@ class HighsPostsolveStack { kDuplicateRow, kDuplicateColumn, kSlackColSubstitution, - kFourierMotzkinElimination, + kFourierMotzkinBlock, kFourierMotzkinObjCol, }; @@ -601,58 +599,106 @@ class HighsPostsolveStack { reductionAdded(ReductionType::kForcingColumnRemovedRow); } + // Serialization layout for FM block (push order, so pop is reversed): + // For each step (first eliminated to last): + // For each plus row: vector entries + // vector plusCoefs + // vector plusHeaders + // For each minus row: vector entries + // vector minusCoefs + // vector minusHeaders + // Then (after all steps): + // For each step, for each parent: vector + // For each step: FmeStepHeader + // numSteps (HighsInt) + + // Push one step's row data. Must be called before addToMatrix invalidates + // the row slices. Returns (numPlus, numMinus) for later use. template - void fourierMotzkinElimination( - HighsInt col, double colLower, double colUpper, double colCost, + std::pair fourierMotzkinBlockPushStep( + HighsInt col, const std::vector>& plusRows, - const std::vector>& minusRows, - const std::vector>& newRowPairs) { - HighsInt origCol = origColIndex[col]; - - auto translateAndPush = - [&](const std::vector>& rows) { - std::vector headers; - std::vector coefs; - headers.reserve(rows.size()); - coefs.reserve(rows.size()); - for (const auto& rd : rows) { - headers.push_back({origRowIndex[rd.row], rd.rowLower, rd.rowUpper}); - double coef = 0.0; - std::vector translated; - for (const HighsSliceNonzero& nz : rd.rowVec) { - if (nz.index() == col) { - coef = nz.value(); - } else { - translated.push_back({origColIndex[nz.index()], nz.value()}); - } - } - coefs.push_back(coef); - reductionValues.push(translated); - } - reductionValues.push(coefs); - reductionValues.push(headers); - }; - - // build new row origins: new rows will get indices nextRowIndex, - // nextRowIndex+1, ... - HighsInt numNewRows = static_cast(newRowPairs.size()); - std::vector translatedOrigins; - translatedOrigins.reserve(numNewRows); - for (HighsInt k = 0; k < numNewRows; ++k) { - HighsInt plusRow = newRowPairs[k].first; - HighsInt minusRow = newRowPairs[k].second; - translatedOrigins.push_back( - {nextRowIndex + k, - plusRow >= 0 ? origRowIndex[plusRow] : HighsInt{-1}, - minusRow >= 0 ? origRowIndex[minusRow] : HighsInt{-1}}); + const std::vector>& minusRows) { + // push plus row entries + std::vector plusHeaders; + std::vector plusCoefs; + plusHeaders.reserve(plusRows.size()); + plusCoefs.reserve(plusRows.size()); + for (const auto& rd : plusRows) { + std::vector translated; + double coef = 0.0; + for (const HighsSliceNonzero& nz : rd.rowVec) { + if (nz.index() == col) + coef = nz.value(); + else + translated.push_back({origColIndex[nz.index()], nz.value()}); + } + reductionValues.push(translated); + plusCoefs.push_back(coef); + plusHeaders.push_back({origRowIndex[rd.row], rd.rowLower, rd.rowUpper}); } + reductionValues.push(plusCoefs); + reductionValues.push(plusHeaders); + + // push minus row entries + std::vector minusHeaders; + std::vector minusCoefs; + minusHeaders.reserve(minusRows.size()); + minusCoefs.reserve(minusRows.size()); + for (const auto& rd : minusRows) { + std::vector translated; + double coef = 0.0; + for (const HighsSliceNonzero& nz : rd.rowVec) { + if (nz.index() == col) + coef = nz.value(); + else + translated.push_back({origColIndex[nz.index()], nz.value()}); + } + reductionValues.push(translated); + minusCoefs.push_back(coef); + minusHeaders.push_back({origRowIndex[rd.row], rd.rowLower, rd.rowUpper}); + } + reductionValues.push(minusCoefs); + reductionValues.push(minusHeaders); - reductionValues.push( - FourierMotzkinElimination{colLower, colUpper, colCost, origCol}); - translateAndPush(plusRows); - translateAndPush(minusRows); - reductionValues.push(translatedOrigins); - reductionAdded(ReductionType::kFourierMotzkinElimination); + return {static_cast(plusRows.size()), + static_cast(minusRows.size())}; + } + + // Finalize the FM block: push descendants mapping and step headers. + // Called once after all elimination steps are complete. + void fourierMotzkinBlockFinalize( + const std::vector& eliminatedCols, + const std::vector& colLowers, + const std::vector& colUppers, + const std::vector& colCosts, + const std::vector& numPlusPerStep, + const std::vector& numMinusPerStep, + const std::vector>>& + descendantsAll) { + HighsInt numSteps = static_cast(eliminatedCols.size()); + + // push descendants for each step's parents + for (HighsInt s = 0; s < numSteps; ++s) { + HighsInt numParents = numPlusPerStep[s] + numMinusPerStep[s]; + assert(static_cast(descendantsAll[s].size()) == numParents); + for (HighsInt p = 0; p < numParents; ++p) + reductionValues.push(descendantsAll[s][p]); + } + + // push step headers + for (HighsInt s = 0; s < numSteps; ++s) { + FmeStepHeader header{colLowers[s], + colUppers[s], + colCosts[s], + origColIndex[eliminatedCols[s]], + numPlusPerStep[s], + numMinusPerStep[s]}; + reductionValues.push(header); + } + + reductionValues.push(numSteps); + reductionAdded(ReductionType::kFourierMotzkinBlock); } void fourierMotzkinObjCol(HighsInt col, double offset, @@ -918,31 +964,8 @@ class HighsPostsolveStack { reduction.undo(*this, options, rowValues, solution, basis); break; } - case ReductionType::kFourierMotzkinElimination: { - FourierMotzkinElimination reduction; - std::vector fmeNewRowOrigins; - reductionValues.pop(fmeNewRowOrigins); - - auto popRowData = [&](std::vector& headers, - std::vector& coefs, - std::vector>& entries) { - reductionValues.pop(headers); - reductionValues.pop(coefs); - HighsInt numRows = static_cast(coefs.size()); - entries.resize(numRows); - for (HighsInt r = numRows - 1; r >= 0; --r) - reductionValues.pop(entries[r]); - }; - - std::vector minusHeaders, plusHeaders; - std::vector minusCoefs, plusCoefs; - std::vector> minusEntries, plusEntries; - popRowData(minusHeaders, minusCoefs, minusEntries); - popRowData(plusHeaders, plusCoefs, plusEntries); - reductionValues.pop(reduction); - reduction.undo(*this, options, plusHeaders, plusCoefs, plusEntries, - minusHeaders, minusCoefs, minusEntries, - fmeNewRowOrigins, solution, basis); + case ReductionType::kFourierMotzkinBlock: { + undoFourierMotzkinBlock(reductionValues, options, solution, basis); break; } case ReductionType::kFourierMotzkinObjCol: { @@ -950,6 +973,7 @@ class HighsPostsolveStack { reductionValues.pop(costEntries); FourierMotzkinObjCol reduction; reductionValues.pop(reduction); + reduction.undo(costEntries, solution); break; } default: From 39c9d79a32ba0667862d18b480e0cca6ebfe1d8e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 14:18:24 +0200 Subject: [PATCH 103/196] Formatted --- highs/presolve/HPresolve.cpp | 30 +++-- highs/presolve/HighsPostsolveStack.cpp | 149 +++++++++++++++++++++++-- highs/presolve/HighsPostsolveStack.h | 33 ++++-- 3 files changed, 187 insertions(+), 25 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 639008d8953..be42cb1e08e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7357,9 +7357,9 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt numRowsAdded = 0; // FM block data for postsolve - using FmeRow = - HighsPostsolveStack::FmeRowData; + using FmeRow = HighsPostsolveStack::FmeRowData; using FmeDescendant = HighsPostsolveStack::FmeDescendant; + using FmeNewRow = HighsPostsolveStack::FmeNewRow; std::vector blockCols; std::vector blockColLowers; std::vector blockColUppers; @@ -7367,6 +7367,7 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector blockNumPlus; std::vector blockNumMinus; std::vector>> blockDescendants; + std::vector> blockNewRows; // Ancestry tracking: for each model row, which (step, parentLocalIdx) // pairs contributed to it, with cumulative scale factor. @@ -7513,7 +7514,7 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt idx = 0; for (HighsInt mRow : iMinus) { if (mRow < 0) continue; - if (mRow == row) return numPlusStored + idx; + if (mRow == row) return idx; idx++; } return -1; @@ -7545,7 +7546,8 @@ HPresolve::Result HPresolve::fourierMotzkin( auto it = rowAncestry.find(pRow); if (it != rowAncestry.end()) { for (const auto& a : it->second) - newAnc.push_back({a.step, a.parentLocalIdx, a.scale * pScaleFactor}); + newAnc.push_back( + {a.step, a.parentLocalIdx, a.scale * pScaleFactor}); } HighsInt pLocalIdx = findPlusLocalIdx(pRow); if (pLocalIdx >= 0) @@ -7557,14 +7559,26 @@ HPresolve::Result HPresolve::fourierMotzkin( auto it = rowAncestry.find(mRow); if (it != rowAncestry.end()) { for (const auto& a : it->second) - newAnc.push_back({a.step, a.parentLocalIdx, a.scale * mScaleFactor}); + newAnc.push_back( + {a.step, a.parentLocalIdx, a.scale * mScaleFactor}); } HighsInt mLocalIdx = findMinusLocalIdx(mRow); if (mLocalIdx >= 0) - newAnc.push_back({stepIdx, mLocalIdx, mScaleFactor}); + newAnc.push_back({stepIdx, numPlusStored + mLocalIdx, mScaleFactor}); } } + // Build FmeNewRow data for this step (basis postsolve) + std::vector stepNewRows; + stepNewRows.reserve(newRowPairs.size()); + for (HighsInt k = 0; k < static_cast(newRowPairs.size()); ++k) { + HighsInt newModelRow = firstNewRow + k; + HighsInt pIdx = findPlusLocalIdx(newRowPairs[k].first); + HighsInt mIdx = findMinusLocalIdx(newRowPairs[k].second); + stepNewRows.push_back({newModelRow, pIdx, mIdx}); + } + blockNewRows.push_back(std::move(stepNewRows)); + // Remove ancestry entries for deleted parent rows for (HighsInt rp : iPlus) { if (rp >= 0) rowAncestry.erase(rp); @@ -7642,8 +7656,8 @@ HPresolve::Result HPresolve::fourierMotzkin( } postsolve_stack.fourierMotzkinBlockFinalize( - blockCols, blockColLowers, blockColUppers, blockColCosts, - blockNumPlus, blockNumMinus, blockDescendants); + blockCols, blockColLowers, blockColUppers, blockColCosts, blockNumPlus, + blockNumMinus, blockDescendants, blockNewRows); highsLogDev(options->log_options, HighsLogType::kInfo, "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index c648deaf53b..04e5d86a71e 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1426,9 +1426,10 @@ void HighsPostsolveStack::SlackColSubstitution::undo( } } -void HighsPostsolveStack::undoFourierMotzkinBlock( - HighsDataStack& stack, const HighsOptions& options, - HighsSolution& solution, HighsBasis& basis) { +void HighsPostsolveStack::undoFourierMotzkinBlock(HighsDataStack& stack, + const HighsOptions& options, + HighsSolution& solution, + HighsBasis& basis) { // Pop numSteps HighsInt numSteps; stack.pop(numSteps); @@ -1442,16 +1443,18 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( std::vector minusCoefs; std::vector> minusEntries; std::vector> descendants; + std::vector newRows; }; std::vector steps(numSteps); - // Pop step headers (pushed last-to-first, so pop first-to-last... no: - // pushed in order s=0..N-1, so pop in reverse s=N-1..0) - for (HighsInt s = numSteps - 1; s >= 0; --s) - stack.pop(steps[s].header); + // Pop step headers + for (HighsInt s = numSteps - 1; s >= 0; --s) stack.pop(steps[s].header); - // Pop descendants (pushed in order s=0..N-1, p=0..numParents-1) + // Pop new row origins + for (HighsInt s = numSteps - 1; s >= 0; --s) stack.pop(steps[s].newRows); + + // Pop descendants for (HighsInt s = numSteps - 1; s >= 0; --s) { HighsInt numParents = steps[s].header.numPlus + steps[s].header.numMinus; steps[s].descendants.resize(numParents); @@ -1459,7 +1462,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( stack.pop(steps[s].descendants[p]); } - // Pop row data (pushed in order s=0..N-1, so pop s=N-1..0) + // Pop row data for (HighsInt s = numSteps - 1; s >= 0; --s) { // pop minus row data stack.pop(steps[s].minusHeaders); @@ -1550,6 +1553,134 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( solution.col_dual[col] -= step.minusCoefs[r] * solution.row_dual[step.minusHeaders[r].row]; } + + // Basis postsolve (Algorithm 5): process in reverse elimination order + if (!basis.valid) return; + + const double tol = options.primal_feasibility_tolerance; + + for (HighsInt s = numSteps - 1; s >= 0; --s) { + const auto& step = steps[s]; + HighsInt col = step.header.col; + HighsInt numPlus = step.header.numPlus; + HighsInt numMinus = step.header.numMinus; + + // Compute row slacks for parent rows: slack_i = min(u - act, act - l) + // divided by |a_ij| for normalization + auto computeSlack = [&](const std::vector& headers, + const std::vector& coefs, + const std::vector>& entries, + HighsInt r) -> double { + HighsCDouble activity = + static_cast(coefs[r]) * solution.col_value[col]; + for (const auto& nz : entries[r]) + activity += + static_cast(nz.value) * solution.col_value[nz.index]; + double act = static_cast(activity); + double rawSlack = kHighsInf; + if (headers[r].rowUpper != kHighsInf) + rawSlack = std::min(rawSlack, headers[r].rowUpper - act); + if (headers[r].rowLower != -kHighsInf) + rawSlack = std::min(rawSlack, act - headers[r].rowLower); + return rawSlack / std::abs(coefs[r]); + }; + + // Determine which parent rows are involved in at least one new row + std::vector plusInvolved(numPlus, false); + std::vector minusInvolved(numMinus, false); + for (const auto& nr : step.newRows) { + if (nr.plusParentIdx >= 0) plusInvolved[nr.plusParentIdx] = true; + if (nr.minusParentIdx >= 0) minusInvolved[nr.minusParentIdx] = true; + } + + // Default: x_j is nonbasic at the value assigned by primal postsolve + if (solution.col_value[col] == 0.0 && step.header.colLower <= 0.0 && + step.header.colUpper >= 0.0) + basis.col_status[col] = HighsBasisStatus::kZero; + else if (solution.col_value[col] == step.header.colLower) + basis.col_status[col] = HighsBasisStatus::kLower; + else if (solution.col_value[col] == step.header.colUpper) + basis.col_status[col] = HighsBasisStatus::kUpper; + else + basis.col_status[col] = HighsBasisStatus::kNonbasic; + + // Process new rows in reverse order (highest index first = Algorithm 5) + for (HighsInt k = static_cast(step.newRows.size()) - 1; k >= 0; + --k) { + const auto& nr = step.newRows[k]; + HighsInt pIdx = nr.plusParentIdx; + HighsInt mIdx = nr.minusParentIdx; + + bool newRowBasic = basis.row_status[nr.row] == HighsBasisStatus::kBasic; + + if (!newRowBasic) { + // Nonbasic propagation: both parent rows become nonbasic + if (pIdx >= 0) + basis.row_status[step.plusHeaders[pIdx].row] = + HighsBasisStatus::kNonbasic; + if (mIdx >= 0) + basis.row_status[step.minusHeaders[mIdx].row] = + HighsBasisStatus::kNonbasic; + } else { + // Basic propagation: determine which parent gets the basic status + double pSlack = + pIdx >= 0 + ? computeSlack(step.plusHeaders, step.plusCoefs, + step.plusEntries, pIdx) + : std::max(step.header.colUpper - solution.col_value[col], 0.0); + double mSlack = + mIdx >= 0 + ? computeSlack(step.minusHeaders, step.minusCoefs, + step.minusEntries, mIdx) + : std::max(solution.col_value[col] - step.header.colLower, 0.0); + + if (pSlack > tol && mSlack <= tol) { + if (pIdx >= 0) + basis.row_status[step.plusHeaders[pIdx].row] = + HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + } else if (mSlack > tol && pSlack <= tol) { + if (mIdx >= 0) + basis.row_status[step.minusHeaders[mIdx].row] = + HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + } else if (pSlack > tol) { + if (pIdx >= 0) + basis.row_status[step.plusHeaders[pIdx].row] = + HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + } else { + basis.col_status[col] = HighsBasisStatus::kBasic; + } + } + } + + // Vanished constraint check: parent rows not involved in any new row + if (step.newRows.empty()) { + // Free variable case: no new rows at all + basis.col_status[col] = HighsBasisStatus::kBasic; + } else { + for (HighsInt p = 0; p < numPlus; ++p) { + if (plusInvolved[p]) continue; + double slack = + computeSlack(step.plusHeaders, step.plusCoefs, step.plusEntries, p); + basis.row_status[step.plusHeaders[p].row] = + slack > tol ? HighsBasisStatus::kBasic + : HighsBasisStatus::kNonbasic; + } + for (HighsInt m = 0; m < numMinus; ++m) { + if (minusInvolved[m]) continue; + double slack = computeSlack(step.minusHeaders, step.minusCoefs, + step.minusEntries, m); + basis.row_status[step.minusHeaders[m].row] = + slack > tol ? HighsBasisStatus::kBasic + : HighsBasisStatus::kNonbasic; + } + } + } } } // namespace presolve diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 2f19fa7e91a..86eaf925565 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -81,6 +81,7 @@ class HighsPostsolveStack { HighsInt col; HighsInt numPlus; HighsInt numMinus; + HighsInt numNewRows; }; struct FmeDescendant { @@ -88,6 +89,12 @@ class HighsPostsolveStack { double scaleFactor; }; + struct FmeNewRow { + HighsInt row; + HighsInt plusParentIdx; // index into plus parents (-1 if bound row) + HighsInt minusParentIdx; // index into minus parents (-1 if bound row) + }; + size_t debug_prev_numreductions = 0; double debug_prev_col_lower = 0; double debug_prev_col_upper = 0; @@ -616,8 +623,7 @@ class HighsPostsolveStack { // the row slices. Returns (numPlus, numMinus) for later use. template std::pair fourierMotzkinBlockPushStep( - HighsInt col, - const std::vector>& plusRows, + HighsInt col, const std::vector>& plusRows, const std::vector>& minusRows) { // push plus row entries std::vector plusHeaders; @@ -665,17 +671,17 @@ class HighsPostsolveStack { static_cast(minusRows.size())}; } - // Finalize the FM block: push descendants mapping and step headers. - // Called once after all elimination steps are complete. + // Finalize the FM block: push descendants mapping, new row origins, + // and step headers. Called once after all elimination steps are complete. void fourierMotzkinBlockFinalize( const std::vector& eliminatedCols, const std::vector& colLowers, - const std::vector& colUppers, - const std::vector& colCosts, + const std::vector& colUppers, const std::vector& colCosts, const std::vector& numPlusPerStep, const std::vector& numMinusPerStep, const std::vector>>& - descendantsAll) { + descendantsAll, + const std::vector>& newRowsAll) { HighsInt numSteps = static_cast(eliminatedCols.size()); // push descendants for each step's parents @@ -686,6 +692,16 @@ class HighsPostsolveStack { reductionValues.push(descendantsAll[s][p]); } + // push new row origins for each step (translate row to orig space) + for (HighsInt s = 0; s < numSteps; ++s) { + std::vector translated; + translated.reserve(newRowsAll[s].size()); + for (const auto& nr : newRowsAll[s]) + translated.push_back( + {origRowIndex[nr.row], nr.plusParentIdx, nr.minusParentIdx}); + reductionValues.push(translated); + } + // push step headers for (HighsInt s = 0; s < numSteps; ++s) { FmeStepHeader header{colLowers[s], @@ -693,7 +709,8 @@ class HighsPostsolveStack { colCosts[s], origColIndex[eliminatedCols[s]], numPlusPerStep[s], - numMinusPerStep[s]}; + numMinusPerStep[s], + static_cast(newRowsAll[s].size())}; reductionValues.push(header); } From e5321468165af1d5bb137f413aa9820b8423fb17 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 14:49:49 +0200 Subject: [PATCH 104/196] Simplify --- highs/presolve/HPresolve.cpp | 169 +++++++++++---------------- highs/presolve/HighsPostsolveStack.h | 25 ++-- 2 files changed, 85 insertions(+), 109 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index be42cb1e08e..5b197091c27 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6932,6 +6932,8 @@ HPresolve::Result HPresolve::fourierMotzkin( double upper; HighsInt plusIndex; HighsInt minusIndex; + double plusScale; + double minusScale; }; auto finalise = [&]() { @@ -7366,17 +7368,15 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector blockColCosts; std::vector blockNumPlus; std::vector blockNumMinus; - std::vector>> blockDescendants; + std::vector>> blockPlusDescendants; + std::vector>> blockMinusDescendants; std::vector> blockNewRows; - // Ancestry tracking: for each model row, which (step, parentLocalIdx) - // pairs contributed to it, with cumulative scale factor. - // parentLocalIdx: 0..numPlus-1 for plus parents, numPlus..numPlus+numMinus-1 - // for minus parents. struct AncestryEntry { HighsInt step; HighsInt parentLocalIdx; double scale; + bool isMinus; }; std::unordered_map> rowAncestry; @@ -7436,7 +7436,8 @@ HPresolve::Result HPresolve::fourierMotzkin( double new_upper = static_cast(static_cast(pScale) * pBound + static_cast(mScale) * mBound); - newRows.push_back({newRowEntries, -kHighsInf, new_upper, pRow, mRow}); + newRows.push_back( + {newRowEntries, -kHighsInf, new_upper, pRow, mRow, pScale, mScale}); // clear vector newRowEntries.clear(); @@ -7444,10 +7445,16 @@ HPresolve::Result HPresolve::fourierMotzkin( } // add new rows, filtering out redundant ones + struct NewRowOrigin { + HighsInt plusRow; + HighsInt minusRow; + double plusScale; + double minusScale; + }; std::vector rowLower; std::vector rowUpper; std::vector> rowEntries; - std::vector> newRowPairs; + std::vector newRowOrigins; for (const auto& nr : newRows) { bool redundant = false; @@ -7461,36 +7468,32 @@ HPresolve::Result HPresolve::fourierMotzkin( rowLower.push_back(nr.lower); rowUpper.push_back(nr.upper); rowEntries.push_back(std::move(entries)); - newRowPairs.push_back({nr.plusIndex, nr.minusIndex}); + newRowOrigins.push_back( + {nr.plusIndex, nr.minusIndex, nr.plusScale, nr.minusScale}); } // Serialize row data for postsolve before addToMatrix invalidates slices - std::vector plusRows; - std::vector minusRows; - - for (HighsInt pRow : iPlus) { - if (pRow < 0) continue; - plusRows.push_back({pRow, model->row_lower_[pRow], - model->row_upper_[pRow], getRowVector(pRow)}); - } - for (HighsInt mRow : iMinus) { - if (mRow < 0) continue; - minusRows.push_back({mRow, model->row_lower_[mRow], - model->row_upper_[mRow], getRowVector(mRow)}); - } + auto collectRows = [&](const std::vector& rows) { + std::vector result; + for (HighsInt r : rows) { + if (r < 0) continue; + result.push_back( + {r, model->row_lower_[r], model->row_upper_[r], getRowVector(r)}); + } + return result; + }; + std::vector plusRows = collectRows(iPlus); + std::vector minusRows = collectRows(iMinus); - std::pair storedCounts = - postsolve_stack.fourierMotzkinBlockPushStep(col, plusRows, minusRows); - HighsInt numPlusStored = storedCounts.first; - HighsInt numMinusStored = storedCounts.second; + postsolve_stack.fourierMotzkinBlockPushStep(col, plusRows, minusRows); // save block metadata blockCols.push_back(col); blockColLowers.push_back(model->col_lower_[col]); blockColUppers.push_back(model->col_upper_[col]); blockColCosts.push_back(model->col_cost_[col]); - blockNumPlus.push_back(numPlusStored); - blockNumMinus.push_back(numMinusStored); + blockNumPlus.push_back(static_cast(plusRows.size())); + blockNumMinus.push_back(static_cast(minusRows.size())); // add new rows to matrix HighsInt firstNewRow = model->num_row_; @@ -7498,83 +7501,48 @@ HPresolve::Result HPresolve::fourierMotzkin( return finalise(); numRowsAdded += static_cast(rowEntries.size()); - // Build ancestry for new rows - // Map from iPlus/iMinus index to parentLocalIdx in this step - // plusRows indices: 0..numPlusStored-1, minusRows: numPlusStored..end - auto findPlusLocalIdx = [&](HighsInt row) -> HighsInt { + // Find 0-based local index of a row within a list (skipping negatives) + auto findLocalIdx = [](HighsInt row, + const std::vector& rows) -> HighsInt { HighsInt idx = 0; - for (HighsInt pRow : iPlus) { - if (pRow < 0) continue; - if (pRow == row) return idx; + for (HighsInt r : rows) { + if (r < 0) continue; + if (r == row) return idx; idx++; } return -1; }; - auto findMinusLocalIdx = [&](HighsInt row) -> HighsInt { - HighsInt idx = 0; - for (HighsInt mRow : iMinus) { - if (mRow < 0) continue; - if (mRow == row) return idx; - idx++; + + auto inheritAncestry = [&](HighsInt parentRow, double scale, + const std::vector& parentList, + bool isMinus, + std::vector& newAnc) { + if (parentRow < 0) return; + auto it = rowAncestry.find(parentRow); + if (it != rowAncestry.end()) { + for (const auto& a : it->second) + newAnc.push_back( + {a.step, a.parentLocalIdx, a.scale * scale, a.isMinus}); } - return -1; + HighsInt localIdx = findLocalIdx(parentRow, parentList); + if (localIdx >= 0) newAnc.push_back({stepIdx, localIdx, scale, isMinus}); }; - for (HighsInt k = 0; k < static_cast(newRowPairs.size()); ++k) { + for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) { HighsInt newModelRow = firstNewRow + k; - HighsInt pRow = newRowPairs[k].first; - HighsInt mRow = newRowPairs[k].second; - + const auto& origin = newRowOrigins[k]; std::vector& newAnc = rowAncestry[newModelRow]; - - // Compute scale factors for this (pRow, mRow) pair - double pCoefAbs = 1.0, mCoefAbs = 1.0; - if (pRow >= 0) { - HighsInt pPos = findNonzero(pRow, col); - if (pPos != -1) pCoefAbs = std::abs(Avalue[pPos]); - } - if (mRow >= 0) { - HighsInt mPos = findNonzero(mRow, col); - if (mPos != -1) mCoefAbs = std::abs(Avalue[mPos]); - } - double s = (pCoefAbs * mCoefAbs) / (pCoefAbs + mCoefAbs); - double pScaleFactor = s / pCoefAbs; - double mScaleFactor = s / mCoefAbs; - - // Inherit ancestry from plus parent - if (pRow >= 0) { - auto it = rowAncestry.find(pRow); - if (it != rowAncestry.end()) { - for (const auto& a : it->second) - newAnc.push_back( - {a.step, a.parentLocalIdx, a.scale * pScaleFactor}); - } - HighsInt pLocalIdx = findPlusLocalIdx(pRow); - if (pLocalIdx >= 0) - newAnc.push_back({stepIdx, pLocalIdx, pScaleFactor}); - } - - // Inherit ancestry from minus parent - if (mRow >= 0) { - auto it = rowAncestry.find(mRow); - if (it != rowAncestry.end()) { - for (const auto& a : it->second) - newAnc.push_back( - {a.step, a.parentLocalIdx, a.scale * mScaleFactor}); - } - HighsInt mLocalIdx = findMinusLocalIdx(mRow); - if (mLocalIdx >= 0) - newAnc.push_back({stepIdx, numPlusStored + mLocalIdx, mScaleFactor}); - } + inheritAncestry(origin.plusRow, origin.plusScale, iPlus, false, newAnc); + inheritAncestry(origin.minusRow, origin.minusScale, iMinus, true, newAnc); } // Build FmeNewRow data for this step (basis postsolve) std::vector stepNewRows; - stepNewRows.reserve(newRowPairs.size()); - for (HighsInt k = 0; k < static_cast(newRowPairs.size()); ++k) { + stepNewRows.reserve(newRowOrigins.size()); + for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) { HighsInt newModelRow = firstNewRow + k; - HighsInt pIdx = findPlusLocalIdx(newRowPairs[k].first); - HighsInt mIdx = findMinusLocalIdx(newRowPairs[k].second); + HighsInt pIdx = findLocalIdx(newRowOrigins[k].plusRow, iPlus); + HighsInt mIdx = findLocalIdx(newRowOrigins[k].minusRow, iMinus); stepNewRows.push_back({newModelRow, pIdx, mIdx}); } blockNewRows.push_back(std::move(stepNewRows)); @@ -7639,25 +7607,30 @@ HPresolve::Result HPresolve::fourierMotzkin( if (numColsEliminated > 0) { HighsInt numSteps = static_cast(blockCols.size()); - // Collect all surviving rows with ancestry - // For each (step, parentLocalIdx), gather the final descendants - blockDescendants.resize(numSteps); + blockPlusDescendants.resize(numSteps); + blockMinusDescendants.resize(numSteps); for (HighsInt s = 0; s < numSteps; ++s) { - HighsInt numParents = blockNumPlus[s] + blockNumMinus[s]; - blockDescendants[s].resize(numParents); + blockPlusDescendants[s].resize(blockNumPlus[s]); + blockMinusDescendants[s].resize(blockNumMinus[s]); } for (const auto& entry : rowAncestry) { HighsInt row = entry.first; HighsInt origRow = postsolve_stack.getOrigRowIndex()[row]; - for (const auto& a : entry.second) - blockDescendants[a.step][a.parentLocalIdx].push_back( - {origRow, a.scale}); + for (const auto& a : entry.second) { + if (a.isMinus) + blockMinusDescendants[a.step][a.parentLocalIdx].push_back( + {origRow, a.scale}); + else + blockPlusDescendants[a.step][a.parentLocalIdx].push_back( + {origRow, a.scale}); + } } postsolve_stack.fourierMotzkinBlockFinalize( blockCols, blockColLowers, blockColUppers, blockColCosts, blockNumPlus, - blockNumMinus, blockDescendants, blockNewRows); + blockNumMinus, blockPlusDescendants, blockMinusDescendants, + blockNewRows); highsLogDev(options->log_options, HighsLogType::kInfo, "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 86eaf925565..2e69ff240bd 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -620,9 +620,9 @@ class HighsPostsolveStack { // numSteps (HighsInt) // Push one step's row data. Must be called before addToMatrix invalidates - // the row slices. Returns (numPlus, numMinus) for later use. + // the row slices. template - std::pair fourierMotzkinBlockPushStep( + void fourierMotzkinBlockPushStep( HighsInt col, const std::vector>& plusRows, const std::vector>& minusRows) { // push plus row entries @@ -666,9 +666,6 @@ class HighsPostsolveStack { } reductionValues.push(minusCoefs); reductionValues.push(minusHeaders); - - return {static_cast(plusRows.size()), - static_cast(minusRows.size())}; } // Finalize the FM block: push descendants mapping, new row origins, @@ -680,16 +677,22 @@ class HighsPostsolveStack { const std::vector& numPlusPerStep, const std::vector& numMinusPerStep, const std::vector>>& - descendantsAll, + plusDescendantsAll, + const std::vector>>& + minusDescendantsAll, const std::vector>& newRowsAll) { HighsInt numSteps = static_cast(eliminatedCols.size()); - // push descendants for each step's parents + // push descendants for each step's parents (plus then minus) for (HighsInt s = 0; s < numSteps; ++s) { - HighsInt numParents = numPlusPerStep[s] + numMinusPerStep[s]; - assert(static_cast(descendantsAll[s].size()) == numParents); - for (HighsInt p = 0; p < numParents; ++p) - reductionValues.push(descendantsAll[s][p]); + assert(static_cast(plusDescendantsAll[s].size()) == + numPlusPerStep[s]); + for (HighsInt p = 0; p < numPlusPerStep[s]; ++p) + reductionValues.push(plusDescendantsAll[s][p]); + assert(static_cast(minusDescendantsAll[s].size()) == + numMinusPerStep[s]); + for (HighsInt m = 0; m < numMinusPerStep[s]; ++m) + reductionValues.push(minusDescendantsAll[s][m]); } // push new row origins for each step (translate row to orig space) From 6a27df15f1e44153cca5a4b44efcb8a03cffdfc7 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 14:56:09 +0200 Subject: [PATCH 105/196] Simplify again --- highs/presolve/HPresolve.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5b197091c27..c7df6e4904e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7547,22 +7547,16 @@ HPresolve::Result HPresolve::fourierMotzkin( } blockNewRows.push_back(std::move(stepNewRows)); - // Remove ancestry entries for deleted parent rows - for (HighsInt rp : iPlus) { - if (rp >= 0) rowAncestry.erase(rp); - } - for (HighsInt rm : iMinus) { - if (rm >= 0) rowAncestry.erase(rm); - } - - // remove old rows containing col (skip virtual bound rows) + // Remove old rows containing col (skip virtual bound rows) for (HighsInt rp : iPlus) { if (rp < 0) continue; + rowAncestry.erase(rp); removeRow(rp); ++numRowsEliminated; } for (HighsInt rm : iMinus) { if (rm < 0) continue; + rowAncestry.erase(rm); if (rowDeleted[rm]) continue; removeRow(rm); ++numRowsEliminated; From f42e203446a7adc77f1322bf5e14874a70ad084b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 14:57:57 +0200 Subject: [PATCH 106/196] Comments --- highs/presolve/HPresolve.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index c7df6e4904e..3950da9b67c 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7472,7 +7472,7 @@ HPresolve::Result HPresolve::fourierMotzkin( {nr.plusIndex, nr.minusIndex, nr.plusScale, nr.minusScale}); } - // Serialize row data for postsolve before addToMatrix invalidates slices + // serialize row data for postsolve before addToMatrix invalidates slices auto collectRows = [&](const std::vector& rows) { std::vector result; for (HighsInt r : rows) { @@ -7501,7 +7501,7 @@ HPresolve::Result HPresolve::fourierMotzkin( return finalise(); numRowsAdded += static_cast(rowEntries.size()); - // Find 0-based local index of a row within a list (skipping negatives) + // find local index of a row within a list (skipping negatives) auto findLocalIdx = [](HighsInt row, const std::vector& rows) -> HighsInt { HighsInt idx = 0; @@ -7536,7 +7536,7 @@ HPresolve::Result HPresolve::fourierMotzkin( inheritAncestry(origin.minusRow, origin.minusScale, iMinus, true, newAnc); } - // Build FmeNewRow data for this step (basis postsolve) + // build FmeNewRow data for this step (basis postsolve) std::vector stepNewRows; stepNewRows.reserve(newRowOrigins.size()); for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) { @@ -7547,7 +7547,7 @@ HPresolve::Result HPresolve::fourierMotzkin( } blockNewRows.push_back(std::move(stepNewRows)); - // Remove old rows containing col (skip virtual bound rows) + // remove old rows containing col (skip virtual bound rows) for (HighsInt rp : iPlus) { if (rp < 0) continue; rowAncestry.erase(rp); @@ -7597,7 +7597,7 @@ HPresolve::Result HPresolve::fourierMotzkin( HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } - // Build K^j_i mapping from ancestry and finalize the FM block + // build K^j_i mapping from ancestry and finalize the FM block if (numColsEliminated > 0) { HighsInt numSteps = static_cast(blockCols.size()); From 9533f1d059a2ae01d54c67f021d39e601d1b0ef0 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 15:05:55 +0200 Subject: [PATCH 107/196] Simplify some more --- highs/presolve/HPresolve.cpp | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index c7df6e4904e..ff13e1c0213 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7513,9 +7513,8 @@ HPresolve::Result HPresolve::fourierMotzkin( return -1; }; - auto inheritAncestry = [&](HighsInt parentRow, double scale, - const std::vector& parentList, - bool isMinus, + auto inheritAncestry = [&](HighsInt parentRow, HighsInt localIdx, + double scale, bool isMinus, std::vector& newAnc) { if (parentRow < 0) return; auto it = rowAncestry.find(parentRow); @@ -7524,25 +7523,19 @@ HPresolve::Result HPresolve::fourierMotzkin( newAnc.push_back( {a.step, a.parentLocalIdx, a.scale * scale, a.isMinus}); } - HighsInt localIdx = findLocalIdx(parentRow, parentList); if (localIdx >= 0) newAnc.push_back({stepIdx, localIdx, scale, isMinus}); }; - for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) { - HighsInt newModelRow = firstNewRow + k; - const auto& origin = newRowOrigins[k]; - std::vector& newAnc = rowAncestry[newModelRow]; - inheritAncestry(origin.plusRow, origin.plusScale, iPlus, false, newAnc); - inheritAncestry(origin.minusRow, origin.minusScale, iMinus, true, newAnc); - } - - // Build FmeNewRow data for this step (basis postsolve) std::vector stepNewRows; stepNewRows.reserve(newRowOrigins.size()); for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) { HighsInt newModelRow = firstNewRow + k; - HighsInt pIdx = findLocalIdx(newRowOrigins[k].plusRow, iPlus); - HighsInt mIdx = findLocalIdx(newRowOrigins[k].minusRow, iMinus); + const auto& origin = newRowOrigins[k]; + HighsInt pIdx = findLocalIdx(origin.plusRow, iPlus); + HighsInt mIdx = findLocalIdx(origin.minusRow, iMinus); + std::vector& newAnc = rowAncestry[newModelRow]; + inheritAncestry(origin.plusRow, pIdx, origin.plusScale, false, newAnc); + inheritAncestry(origin.minusRow, mIdx, origin.minusScale, true, newAnc); stepNewRows.push_back({newModelRow, pIdx, mIdx}); } blockNewRows.push_back(std::move(stepNewRows)); From 52516a15e9d7057fda112b07a4bc727f70277d41 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 15:18:01 +0200 Subject: [PATCH 108/196] Try on LPs again --- highs/presolve/HPresolve.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5df5c397919..273106e351b 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5946,8 +5946,6 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { bool trySparsify = mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif - bool tryFourierMotzkin = - mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; bool tryProbing = mipsolver != nullptr; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; @@ -5977,8 +5975,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { applyConflictGraphSubstitutions(postsolve_stack, numDelCol)); } - if (tryFourierMotzkin && - analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) + if (analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); if (analysis_.allow_rule_[kPresolveRuleAggregator]) From 7064915318db785d1865e0e37f9aa679f914f9ed Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 15:31:07 +0200 Subject: [PATCH 109/196] Fix test --- check/TestPresolve.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index 8af6618f340..cbbad8fdd07 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -71,6 +71,8 @@ TEST_CASE("postsolve-no-basis", "[highs_test_presolve]") { "Col Primal Col Primal\n"); for (HighsInt iCol = 0; iCol < presolved_lp.num_col_; iCol++) { HighsInt original_iCol = original_col_indices[iCol]; + // Skip columns added by presolve (e.g. FME objective reformulation) + if (original_iCol >= highs.getNumCol()) continue; if (dev_run) printf("%3d %11.5g %3d %11.5g\n", int(iCol), solution.col_value[iCol], int(original_iCol), postsolve_solution.col_value[original_iCol]); From 741af63c0e665c29ad8e34f1690de4ba4bf62b6f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 15:40:39 +0200 Subject: [PATCH 110/196] Clean up postsolve --- highs/presolve/HighsPostsolveStack.cpp | 29 ++++++++++---------------- highs/presolve/HighsPostsolveStack.h | 18 ++++++++++++++-- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 04e5d86a71e..310ded222eb 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1426,27 +1426,12 @@ void HighsPostsolveStack::SlackColSubstitution::undo( } } -void HighsPostsolveStack::undoFourierMotzkinBlock(HighsDataStack& stack, - const HighsOptions& options, - HighsSolution& solution, - HighsBasis& basis) { - // Pop numSteps +std::vector +HighsPostsolveStack::popFourierMotzkinBlock(HighsDataStack& stack) { HighsInt numSteps; stack.pop(numSteps); - struct StepData { - FmeStepHeader header; - std::vector plusHeaders; - std::vector plusCoefs; - std::vector> plusEntries; - std::vector minusHeaders; - std::vector minusCoefs; - std::vector> minusEntries; - std::vector> descendants; - std::vector newRows; - }; - - std::vector steps(numSteps); + std::vector steps(numSteps); // Pop step headers for (HighsInt s = numSteps - 1; s >= 0; --s) stack.pop(steps[s].header); @@ -1481,6 +1466,14 @@ void HighsPostsolveStack::undoFourierMotzkinBlock(HighsDataStack& stack, stack.pop(steps[s].plusEntries[r]); } + return steps; +} + +void HighsPostsolveStack::undoFourierMotzkinBlock( + const std::vector& steps, const HighsOptions& options, + HighsSolution& solution, HighsBasis& basis) { + HighsInt numSteps = static_cast(steps.size()); + // Primal postsolve (Algorithm 3): process in reverse elimination order for (HighsInt s = numSteps - 1; s >= 0; --s) { const auto& step = steps[s]; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 2e69ff240bd..00933bd09f9 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -287,7 +287,20 @@ class HighsPostsolveStack { HighsBasis& basis); }; - static void undoFourierMotzkinBlock(HighsDataStack& stack, + struct FmeStepData { + FmeStepHeader header; + std::vector plusHeaders; + std::vector plusCoefs; + std::vector> plusEntries; + std::vector minusHeaders; + std::vector minusCoefs; + std::vector> minusEntries; + std::vector> descendants; + std::vector newRows; + }; + + static std::vector popFourierMotzkinBlock(HighsDataStack& stack); + static void undoFourierMotzkinBlock(const std::vector& steps, const HighsOptions& options, HighsSolution& solution, HighsBasis& basis); @@ -985,7 +998,8 @@ class HighsPostsolveStack { break; } case ReductionType::kFourierMotzkinBlock: { - undoFourierMotzkinBlock(reductionValues, options, solution, basis); + auto steps = popFourierMotzkinBlock(reductionValues); + undoFourierMotzkinBlock(steps, options, solution, basis); break; } case ReductionType::kFourierMotzkinObjCol: { From 2f5bd414367de5969fb80d6f4748c8b5a73a323c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 15:44:52 +0200 Subject: [PATCH 111/196] Comments --- highs/presolve/HighsPostsolveStack.cpp | 34 +++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 310ded222eb..c5f89ec0d19 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1433,13 +1433,13 @@ HighsPostsolveStack::popFourierMotzkinBlock(HighsDataStack& stack) { std::vector steps(numSteps); - // Pop step headers + // step headers for (HighsInt s = numSteps - 1; s >= 0; --s) stack.pop(steps[s].header); - // Pop new row origins + // new row origins for (HighsInt s = numSteps - 1; s >= 0; --s) stack.pop(steps[s].newRows); - // Pop descendants + // descendants for (HighsInt s = numSteps - 1; s >= 0; --s) { HighsInt numParents = steps[s].header.numPlus + steps[s].header.numMinus; steps[s].descendants.resize(numParents); @@ -1447,9 +1447,9 @@ HighsPostsolveStack::popFourierMotzkinBlock(HighsDataStack& stack) { stack.pop(steps[s].descendants[p]); } - // Pop row data + // row data for (HighsInt s = numSteps - 1; s >= 0; --s) { - // pop minus row data + // minus row data stack.pop(steps[s].minusHeaders); stack.pop(steps[s].minusCoefs); HighsInt numMinus = static_cast(steps[s].minusCoefs.size()); @@ -1457,7 +1457,7 @@ HighsPostsolveStack::popFourierMotzkinBlock(HighsDataStack& stack) { for (HighsInt r = numMinus - 1; r >= 0; --r) stack.pop(steps[s].minusEntries[r]); - // pop plus row data + // plus row data stack.pop(steps[s].plusHeaders); stack.pop(steps[s].plusCoefs); HighsInt numPlus = static_cast(steps[s].plusCoefs.size()); @@ -1474,7 +1474,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( HighsSolution& solution, HighsBasis& basis) { HighsInt numSteps = static_cast(steps.size()); - // Primal postsolve (Algorithm 3): process in reverse elimination order + // primal postsolve (Algorithm 3): process in reverse elimination order for (HighsInt s = numSteps - 1; s >= 0; --s) { const auto& step = steps[s]; HighsInt col = step.header.col; @@ -1516,7 +1516,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( if (!solution.dual_valid) return; - // Dual postsolve (Algorithm 4): process in reverse elimination order + // dual postsolve (Algorithm 4): process in reverse elimination order for (HighsInt s = numSteps - 1; s >= 0; --s) { const auto& step = steps[s]; HighsInt col = step.header.col; @@ -1547,7 +1547,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( step.minusCoefs[r] * solution.row_dual[step.minusHeaders[r].row]; } - // Basis postsolve (Algorithm 5): process in reverse elimination order + // basis postsolve (Algorithm 5): process in reverse elimination order if (!basis.valid) return; const double tol = options.primal_feasibility_tolerance; @@ -1558,7 +1558,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( HighsInt numPlus = step.header.numPlus; HighsInt numMinus = step.header.numMinus; - // Compute row slacks for parent rows: slack_i = min(u - act, act - l) + // compute row slacks for parent rows: slack_i = min(u - act, act - l) // divided by |a_ij| for normalization auto computeSlack = [&](const std::vector& headers, const std::vector& coefs, @@ -1578,7 +1578,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( return rawSlack / std::abs(coefs[r]); }; - // Determine which parent rows are involved in at least one new row + // determine which parent rows are involved in at least one new row std::vector plusInvolved(numPlus, false); std::vector minusInvolved(numMinus, false); for (const auto& nr : step.newRows) { @@ -1586,7 +1586,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( if (nr.minusParentIdx >= 0) minusInvolved[nr.minusParentIdx] = true; } - // Default: x_j is nonbasic at the value assigned by primal postsolve + // default: x_j is nonbasic at the value assigned by primal postsolve if (solution.col_value[col] == 0.0 && step.header.colLower <= 0.0 && step.header.colUpper >= 0.0) basis.col_status[col] = HighsBasisStatus::kZero; @@ -1597,7 +1597,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( else basis.col_status[col] = HighsBasisStatus::kNonbasic; - // Process new rows in reverse order (highest index first = Algorithm 5) + // process new rows in reverse order (highest index first = Algorithm 5) for (HighsInt k = static_cast(step.newRows.size()) - 1; k >= 0; --k) { const auto& nr = step.newRows[k]; @@ -1607,7 +1607,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( bool newRowBasic = basis.row_status[nr.row] == HighsBasisStatus::kBasic; if (!newRowBasic) { - // Nonbasic propagation: both parent rows become nonbasic + // non-basic propagation: both parent rows become nonbasic if (pIdx >= 0) basis.row_status[step.plusHeaders[pIdx].row] = HighsBasisStatus::kNonbasic; @@ -1615,7 +1615,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( basis.row_status[step.minusHeaders[mIdx].row] = HighsBasisStatus::kNonbasic; } else { - // Basic propagation: determine which parent gets the basic status + // basic propagation: determine which parent gets the basic status double pSlack = pIdx >= 0 ? computeSlack(step.plusHeaders, step.plusCoefs, @@ -1651,9 +1651,9 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } } - // Vanished constraint check: parent rows not involved in any new row + // vanished constraint check: parent rows not involved in any new row if (step.newRows.empty()) { - // Free variable case: no new rows at all + // free variable case: no new rows at all basis.col_status[col] = HighsBasisStatus::kBasic; } else { for (HighsInt p = 0; p < numPlus; ++p) { From c89ad9f1bfc02065eeb12c602530a333e4cb787d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 16:23:52 +0200 Subject: [PATCH 112/196] Simplify postsolve and use HighsCDouble if needed --- highs/presolve/HighsPostsolveStack.cpp | 50 ++++++++++++++------------ highs/presolve/HighsPostsolveStack.h | 3 +- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 310ded222eb..4ca60dea143 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1439,12 +1439,16 @@ HighsPostsolveStack::popFourierMotzkinBlock(HighsDataStack& stack) { // Pop new row origins for (HighsInt s = numSteps - 1; s >= 0; --s) stack.pop(steps[s].newRows); - // Pop descendants + // Pop descendants (minus then plus, reverse of push order) for (HighsInt s = numSteps - 1; s >= 0; --s) { - HighsInt numParents = steps[s].header.numPlus + steps[s].header.numMinus; - steps[s].descendants.resize(numParents); - for (HighsInt p = numParents - 1; p >= 0; --p) - stack.pop(steps[s].descendants[p]); + HighsInt numMinus = steps[s].header.numMinus; + steps[s].minusDescendants.resize(numMinus); + for (HighsInt m = numMinus - 1; m >= 0; --m) + stack.pop(steps[s].minusDescendants[m]); + HighsInt numPlus = steps[s].header.numPlus; + steps[s].plusDescendants.resize(numPlus); + for (HighsInt p = numPlus - 1; p >= 0; --p) + stack.pop(steps[s].plusDescendants[p]); } // Pop row data @@ -1524,27 +1528,29 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( HighsInt numMinus = step.header.numMinus; // u_i = Σ_{k ∈ K^j_i} λ_k * scaleFactor - for (HighsInt p = 0; p < numPlus; ++p) { - double ui = 0.0; - for (const auto& desc : step.descendants[p]) - ui += solution.row_dual[desc.row] * desc.scaleFactor; - solution.row_dual[step.plusHeaders[p].row] = ui; - } - for (HighsInt m = 0; m < numMinus; ++m) { - double vi = 0.0; - for (const auto& desc : step.descendants[numPlus + m]) - vi += solution.row_dual[desc.row] * desc.scaleFactor; - solution.row_dual[step.minusHeaders[m].row] = vi; - } + auto recoverDual = + [&](const std::vector& headers, + const std::vector>& descendants) { + for (size_t r = 0; r < headers.size(); ++r) { + HighsCDouble dual = 0.0; + for (const auto& desc : descendants[r]) + dual += static_cast(solution.row_dual[desc.row]) * + desc.scaleFactor; + solution.row_dual[headers[r].row] = static_cast(dual); + } + }; + recoverDual(step.plusHeaders, step.plusDescendants); + recoverDual(step.minusHeaders, step.minusDescendants); // col_dual = cost - Σ a_{ij} * row_dual[i] - solution.col_dual[col] = step.header.colCost; + HighsCDouble colDual = step.header.colCost; for (HighsInt r = 0; r < numPlus; ++r) - solution.col_dual[col] -= - step.plusCoefs[r] * solution.row_dual[step.plusHeaders[r].row]; + colDual -= static_cast(step.plusCoefs[r]) * + solution.row_dual[step.plusHeaders[r].row]; for (HighsInt r = 0; r < numMinus; ++r) - solution.col_dual[col] -= - step.minusCoefs[r] * solution.row_dual[step.minusHeaders[r].row]; + colDual -= static_cast(step.minusCoefs[r]) * + solution.row_dual[step.minusHeaders[r].row]; + solution.col_dual[col] = static_cast(colDual); } // Basis postsolve (Algorithm 5): process in reverse elimination order diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 00933bd09f9..6b31a75adce 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -292,10 +292,11 @@ class HighsPostsolveStack { std::vector plusHeaders; std::vector plusCoefs; std::vector> plusEntries; + std::vector> plusDescendants; std::vector minusHeaders; std::vector minusCoefs; std::vector> minusEntries; - std::vector> descendants; + std::vector> minusDescendants; std::vector newRows; }; From 39e7cdd1c96e469daba2ad862c1162439b98891e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 10 Jun 2026 16:58:10 +0200 Subject: [PATCH 113/196] Store correct scaling --- highs/presolve/HPresolve.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 273106e351b..9e360d79cbf 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7433,8 +7433,8 @@ HPresolve::Result HPresolve::fourierMotzkin( double new_upper = static_cast(static_cast(pScale) * pBound + static_cast(mScale) * mBound); - newRows.push_back( - {newRowEntries, -kHighsInf, new_upper, pRow, mRow, pScale, mScale}); + newRows.push_back({newRowEntries, -kHighsInf, new_upper, pRow, mRow, + pDirection * pScale, mDirection * mScale}); // clear vector newRowEntries.clear(); From e7f9686fe87533a06d8d3dbe48ac9d14a43256b8 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 08:07:23 +0200 Subject: [PATCH 114/196] Primal postsolve --- highs/presolve/HighsPostsolveStack.cpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index e4f0938e297..bb1607652eb 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1510,9 +1510,9 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( tightenBounds(step.plusHeaders, step.plusCoefs, step.plusEntries); tightenBounds(step.minusHeaders, step.minusCoefs, step.minusEntries); - if (impliedLower <= 0.0 && impliedUpper >= 0.0) + if (impliedLower == -kHighsInf && impliedUpper == kHighsInf) solution.col_value[col] = 0.0; - else if (impliedLower > 0.0) + else if (impliedLower != -kHighsInf) solution.col_value[col] = impliedLower; else solution.col_value[col] = impliedUpper; @@ -1556,7 +1556,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( // basis postsolve (Algorithm 5): process in reverse elimination order if (!basis.valid) return; - const double tol = options.primal_feasibility_tolerance; + const double tol = options.mip_feasibility_tolerance; for (HighsInt s = numSteps - 1; s >= 0; --s) { const auto& step = steps[s]; @@ -1592,16 +1592,15 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( if (nr.minusParentIdx >= 0) minusInvolved[nr.minusParentIdx] = true; } - // default: x_j is nonbasic at the value assigned by primal postsolve - if (solution.col_value[col] == 0.0 && step.header.colLower <= 0.0 && - step.header.colUpper >= 0.0) + // default: x_j is non-basic at the value assigned by primal postsolve + if (step.header.colLower == -kHighsInf && step.header.colUpper == kHighsInf) basis.col_status[col] = HighsBasisStatus::kZero; - else if (solution.col_value[col] == step.header.colLower) + else if (solution.col_value[col] <= step.header.colLower + tol) basis.col_status[col] = HighsBasisStatus::kLower; - else if (solution.col_value[col] == step.header.colUpper) + else if (solution.col_value[col] >= step.header.colUpper - tol) basis.col_status[col] = HighsBasisStatus::kUpper; else - basis.col_status[col] = HighsBasisStatus::kNonbasic; + basis.col_status[col] = HighsBasisStatus::kBasic; // process new rows in reverse order (highest index first = Algorithm 5) for (HighsInt k = static_cast(step.newRows.size()) - 1; k >= 0; @@ -1613,7 +1612,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( bool newRowBasic = basis.row_status[nr.row] == HighsBasisStatus::kBasic; if (!newRowBasic) { - // non-basic propagation: both parent rows become nonbasic + // non-basic propagation: both parent rows become non-basic if (pIdx >= 0) basis.row_status[step.plusHeaders[pIdx].row] = HighsBasisStatus::kNonbasic; From 6e1baee5679d0ceb19f739770139f5ac7f0c37e0 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 08:23:42 +0200 Subject: [PATCH 115/196] Clean up some more --- highs/presolve/HPresolve.cpp | 37 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 9e360d79cbf..a8e1fce6520 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7371,7 +7371,7 @@ HPresolve::Result HPresolve::fourierMotzkin( struct AncestryEntry { HighsInt step; - HighsInt parentLocalIdx; + HighsInt parentRowIndex; double scale; bool isMinus; }; @@ -7499,28 +7499,25 @@ HPresolve::Result HPresolve::fourierMotzkin( numRowsAdded += static_cast(rowEntries.size()); // find local index of a row within a list (skipping negatives) - auto findLocalIdx = [](HighsInt row, - const std::vector& rows) -> HighsInt { - HighsInt idx = 0; - for (HighsInt r : rows) { - if (r < 0) continue; - if (r == row) return idx; - idx++; - } + auto findRowIndex = [](HighsInt row, + const std::vector& rows) -> HighsInt { + for (HighsInt i = 0; i < static_cast(rows.size()); ++i) + if (rows[i].row == row) return i; return -1; }; - auto inheritAncestry = [&](HighsInt parentRow, HighsInt localIdx, - double scale, bool isMinus, + auto inheritAncestry = [&](HighsInt parentRow, HighsInt parentRowIndex, + HighsInt stepIndex, double scale, bool isMinus, std::vector& newAnc) { if (parentRow < 0) return; auto it = rowAncestry.find(parentRow); if (it != rowAncestry.end()) { for (const auto& a : it->second) newAnc.push_back( - {a.step, a.parentLocalIdx, a.scale * scale, a.isMinus}); + {a.step, a.parentRowIndex, a.scale * scale, a.isMinus}); } - if (localIdx >= 0) newAnc.push_back({stepIdx, localIdx, scale, isMinus}); + if (parentRowIndex >= 0) + newAnc.push_back({stepIndex, parentRowIndex, scale, isMinus}); }; // build FmeNewRow data and ancestry for this step @@ -7529,11 +7526,13 @@ HPresolve::Result HPresolve::fourierMotzkin( for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) { HighsInt newModelRow = firstNewRow + k; const auto& origin = newRowOrigins[k]; - HighsInt pIdx = findLocalIdx(origin.plusRow, iPlus); - HighsInt mIdx = findLocalIdx(origin.minusRow, iMinus); + HighsInt pIdx = findRowIndex(origin.plusRow, plusRows); + HighsInt mIdx = findRowIndex(origin.minusRow, minusRows); std::vector& newAnc = rowAncestry[newModelRow]; - inheritAncestry(origin.plusRow, pIdx, origin.plusScale, false, newAnc); - inheritAncestry(origin.minusRow, mIdx, origin.minusScale, true, newAnc); + inheritAncestry(origin.plusRow, pIdx, stepIdx, origin.plusScale, false, + newAnc); + inheritAncestry(origin.minusRow, mIdx, stepIdx, origin.minusScale, true, + newAnc); stepNewRows.push_back({newModelRow, pIdx, mIdx}); } blockNewRows.push_back(std::move(stepNewRows)); @@ -7604,10 +7603,10 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt origRow = postsolve_stack.getOrigRowIndex()[row]; for (const auto& a : entry.second) { if (a.isMinus) - blockMinusDescendants[a.step][a.parentLocalIdx].push_back( + blockMinusDescendants[a.step][a.parentRowIndex].push_back( {origRow, a.scale}); else - blockPlusDescendants[a.step][a.parentLocalIdx].push_back( + blockPlusDescendants[a.step][a.parentRowIndex].push_back( {origRow, a.scale}); } } From bb1d4717f217ca759e45008486a56b213644b68c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 08:25:14 +0200 Subject: [PATCH 116/196] Fix comment --- highs/presolve/HPresolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index a8e1fce6520..0d3d5af4e68 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7498,7 +7498,7 @@ HPresolve::Result HPresolve::fourierMotzkin( return finalise(); numRowsAdded += static_cast(rowEntries.size()); - // find local index of a row within a list (skipping negatives) + // find index of a row within a list auto findRowIndex = [](HighsInt row, const std::vector& rows) -> HighsInt { for (HighsInt i = 0; i < static_cast(rows.size()); ++i) From a35437bf6739f312037c951e708a212baa418edf Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 08:37:23 +0200 Subject: [PATCH 117/196] Clean up --- highs/presolve/HPresolve.cpp | 117 +++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 54 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 0d3d5af4e68..0508fe097f3 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6908,6 +6908,10 @@ HPresolve::Result HPresolve::fourierMotzkin( const bool logging_on = analysis_.logging_on_; if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleFourierMotzkin); + using FmeRow = HighsPostsolveStack::FmeRowData; + using FmeDescendant = HighsPostsolveStack::FmeDescendant; + using FmeNewRow = HighsPostsolveStack::FmeNewRow; + // max. absolute coefficient const double maxCoef = 1e3; @@ -7223,8 +7227,8 @@ HPresolve::Result HPresolve::fourierMotzkin( heapPos[heap[j].col] = j; }; - auto heapBubbleUp = [&](std::vector& heap, - std::vector& heapPos, HighsInt i) { + auto heapSiftUp = [&](std::vector& heap, + std::vector& heapPos, HighsInt i) { if (i >= static_cast(heap.size())) return; while (i > 0) { HighsInt parent = (i - 1) / 2; @@ -7234,8 +7238,8 @@ HPresolve::Result HPresolve::fourierMotzkin( } }; - auto heapBubbleDown = [&](std::vector& heap, - std::vector& heapPos, HighsInt i) { + auto heapSiftDown = [&](std::vector& heap, + std::vector& heapPos, HighsInt i) { HighsInt heapSize = static_cast(heap.size()); if (i >= heapSize) return; while (true) { @@ -7258,8 +7262,8 @@ HPresolve::Result HPresolve::fourierMotzkin( heapSwap(heap, heapPos, pos, last); heapPos[col] = -1; heap.pop_back(); - heapBubbleUp(heap, heapPos, pos); - heapBubbleDown(heap, heapPos, pos); + heapSiftUp(heap, heapPos, pos); + heapSiftDown(heap, heapPos, pos); }; auto heapUpdate = [&](std::vector& heap, @@ -7269,8 +7273,8 @@ HPresolve::Result HPresolve::fourierMotzkin( if (pos == -1) return; heap[pos].neRed = neRed; heap[pos].mrRed = mrRed; - heapBubbleUp(heap, heapPos, pos); - heapBubbleDown(heap, heapPos, pos); + heapSiftUp(heap, heapPos, pos); + heapSiftDown(heap, heapPos, pos); }; auto buildHeap = @@ -7299,6 +7303,24 @@ HPresolve::Result HPresolve::fourierMotzkin( return !heap.empty(); }; + // find index of a row within a list + auto findRowIndex = [](HighsInt row, + const std::vector& rows) -> HighsInt { + for (HighsInt i = 0; i < static_cast(rows.size()); ++i) + if (rows[i].row == row) return i; + return -1; + }; + + auto collectRows = [&](const std::vector& rows) { + std::vector result; + for (HighsInt r : rows) { + if (r < 0) continue; + result.push_back( + {r, model->row_lower_[r], model->row_upper_[r], getRowVector(r)}); + } + return result; + }; + // collect candidate variables std::vector candidates; if (!computeCandidates(candidates)) return finalise(); @@ -7338,7 +7360,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // heapify for (HighsInt i = static_cast(heap.size()) / 2 - 1; i >= 0; --i) - heapBubbleDown(heap, heapPos, i); + heapSiftDown(heap, heapPos, i); // vectors for computing new row entries std::vector newRowEntries; @@ -7347,6 +7369,18 @@ HPresolve::Result HPresolve::fourierMotzkin( // vector for storing new rows std::vector newRows; + // workspace for filtering new rows + struct NewRowOrigin { + HighsInt plusRow; + HighsInt minusRow; + double plusScale; + double minusScale; + }; + std::vector rowLower; + std::vector rowUpper; + std::vector> rowEntries; + std::vector newRowOrigins; + // vector for saving affected candidates std::vector saveAffectedCols; @@ -7356,9 +7390,6 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt numRowsAdded = 0; // FM block data for postsolve - using FmeRow = HighsPostsolveStack::FmeRowData; - using FmeDescendant = HighsPostsolveStack::FmeDescendant; - using FmeNewRow = HighsPostsolveStack::FmeNewRow; std::vector blockCols; std::vector blockColLowers; std::vector blockColUppers; @@ -7377,6 +7408,20 @@ HPresolve::Result HPresolve::fourierMotzkin( }; std::unordered_map> rowAncestry; + auto inheritAncestry = [&](HighsInt parentRow, HighsInt parentRowIndex, + HighsInt stepIndex, double scale, bool isMinus, + std::vector& newAnc) { + if (parentRow < 0) return; + auto it = rowAncestry.find(parentRow); + if (it != rowAncestry.end()) { + for (const auto& a : it->second) + newAnc.push_back( + {a.step, a.parentRowIndex, a.scale * scale, a.isMinus}); + } + if (parentRowIndex >= 0) + newAnc.push_back({stepIndex, parentRowIndex, scale, isMinus}); + }; + // main loop: eliminate variables from heap while (!heap.empty()) { HighsInt col = heap[0].col; @@ -7442,16 +7487,10 @@ HPresolve::Result HPresolve::fourierMotzkin( } // add new rows, filtering out redundant ones - struct NewRowOrigin { - HighsInt plusRow; - HighsInt minusRow; - double plusScale; - double minusScale; - }; - std::vector rowLower; - std::vector rowUpper; - std::vector> rowEntries; - std::vector newRowOrigins; + rowLower.clear(); + rowUpper.clear(); + rowEntries.clear(); + newRowOrigins.clear(); for (const auto& nr : newRows) { bool redundant = false; @@ -7470,15 +7509,7 @@ HPresolve::Result HPresolve::fourierMotzkin( } // serialize row data for postsolve before addToMatrix invalidates slices - auto collectRows = [&](const std::vector& rows) { - std::vector result; - for (HighsInt r : rows) { - if (r < 0) continue; - result.push_back( - {r, model->row_lower_[r], model->row_upper_[r], getRowVector(r)}); - } - return result; - }; + std::vector plusRows = collectRows(iPlus); std::vector minusRows = collectRows(iMinus); @@ -7498,28 +7529,6 @@ HPresolve::Result HPresolve::fourierMotzkin( return finalise(); numRowsAdded += static_cast(rowEntries.size()); - // find index of a row within a list - auto findRowIndex = [](HighsInt row, - const std::vector& rows) -> HighsInt { - for (HighsInt i = 0; i < static_cast(rows.size()); ++i) - if (rows[i].row == row) return i; - return -1; - }; - - auto inheritAncestry = [&](HighsInt parentRow, HighsInt parentRowIndex, - HighsInt stepIndex, double scale, bool isMinus, - std::vector& newAnc) { - if (parentRow < 0) return; - auto it = rowAncestry.find(parentRow); - if (it != rowAncestry.end()) { - for (const auto& a : it->second) - newAnc.push_back( - {a.step, a.parentRowIndex, a.scale * scale, a.isMinus}); - } - if (parentRowIndex >= 0) - newAnc.push_back({stepIndex, parentRowIndex, scale, isMinus}); - }; - // build FmeNewRow data and ancestry for this step std::vector stepNewRows; stepNewRows.reserve(newRowOrigins.size()); @@ -7576,7 +7585,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // new candidate -> insert into heap heapPos[k] = static_cast(heap.size()); heap.push_back({k, ne, mr}); - heapBubbleUp(heap, heapPos, heapPos[k]); + heapSiftUp(heap, heapPos, heapPos[k]); } else { // update heap heapUpdate(heap, heapPos, k, ne, mr); From d89b6ac0c49c73745999f50e59987f03187cbc10 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 08:40:11 +0200 Subject: [PATCH 118/196] Comment --- highs/presolve/HPresolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 0508fe097f3..bef9c48a753 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7509,10 +7509,10 @@ HPresolve::Result HPresolve::fourierMotzkin( } // serialize row data for postsolve before addToMatrix invalidates slices - std::vector plusRows = collectRows(iPlus); std::vector minusRows = collectRows(iMinus); + // push row data for this elimination step onto the postsolve stack postsolve_stack.fourierMotzkinBlockPushStep(col, plusRows, minusRows); // save block metadata From 166834358fcf44a45ad6784e3538179e13473e26 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 08:46:21 +0200 Subject: [PATCH 119/196] Clean up --- highs/presolve/HPresolve.cpp | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index bef9c48a753..f915f29d491 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6937,6 +6937,20 @@ HPresolve::Result HPresolve::fourierMotzkin( double minusScale; }; + struct NewRowOrigin { + HighsInt plusRow; + HighsInt minusRow; + double plusScale; + double minusScale; + }; + + struct AncestryEntry { + HighsInt step; + HighsInt parentRowIndex; + double scale; + bool isMinus; + }; + auto finalise = [&]() { analysis_.logging_on_ = logging_on; if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleFourierMotzkin); @@ -7370,12 +7384,6 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector newRows; // workspace for filtering new rows - struct NewRowOrigin { - HighsInt plusRow; - HighsInt minusRow; - double plusScale; - double minusScale; - }; std::vector rowLower; std::vector rowUpper; std::vector> rowEntries; @@ -7400,12 +7408,6 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector>> blockMinusDescendants; std::vector> blockNewRows; - struct AncestryEntry { - HighsInt step; - HighsInt parentRowIndex; - double scale; - bool isMinus; - }; std::unordered_map> rowAncestry; auto inheritAncestry = [&](HighsInt parentRow, HighsInt parentRowIndex, From a5fffb93ad4ebf381ac6a59a7b27ce121795fc0c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 08:50:24 +0200 Subject: [PATCH 120/196] Clean up --- highs/presolve/HPresolve.cpp | 4 +++- highs/presolve/HighsPostsolveStack.h | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index f915f29d491..6c7b24e52ff 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7408,6 +7408,7 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector>> blockMinusDescendants; std::vector> blockNewRows; + // maps surviving row to its ancestry (which parent rows it descends from) std::unordered_map> rowAncestry; auto inheritAncestry = [&](HighsInt parentRow, HighsInt parentRowIndex, @@ -7622,7 +7623,8 @@ HPresolve::Result HPresolve::fourierMotzkin( } } - postsolve_stack.fourierMotzkinBlockFinalize( + // finalise the block: push descendants, new row origins, and step headers + postsolve_stack.fourierMotzkinBlockFinalise( blockCols, blockColLowers, blockColUppers, blockColCosts, blockNumPlus, blockNumMinus, blockPlusDescendants, blockMinusDescendants, blockNewRows); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 6b31a75adce..ff9fc0604aa 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -682,9 +682,9 @@ class HighsPostsolveStack { reductionValues.push(minusHeaders); } - // Finalize the FM block: push descendants mapping, new row origins, + // Finalise the FM block: push descendants mapping, new row origins, // and step headers. Called once after all elimination steps are complete. - void fourierMotzkinBlockFinalize( + void fourierMotzkinBlockFinalise( const std::vector& eliminatedCols, const std::vector& colLowers, const std::vector& colUppers, const std::vector& colCosts, From 80bca3f5136c5a9a98e942b86b073473ccf863a2 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 08:59:11 +0200 Subject: [PATCH 121/196] Primal postsolve as in paper --- highs/presolve/HPresolve.cpp | 6 +++--- highs/presolve/HighsPostsolveStack.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 6c7b24e52ff..6cdc52477ac 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7291,7 +7291,7 @@ HPresolve::Result HPresolve::fourierMotzkin( heapSiftDown(heap, heapPos, pos); }; - auto buildHeap = + auto heapBuild = [&](const std::vector& candidates, std::vector& heap, std::vector& heapPos, std::vector& iPlus, std::vector& iMinus, std::vector& pPlus, @@ -7351,7 +7351,7 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector heapPos; // build initial heap - if (!buildHeap(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, + if (!heapBuild(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, affectedCols)) return finalise(); @@ -7365,7 +7365,7 @@ HPresolve::Result HPresolve::fourierMotzkin( candidates.clear(); if (!computeCandidates(candidates)) return finalise(); // re-build heap - if (!buildHeap(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, + if (!heapBuild(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, affectedCols)) return finalise(); break; diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index bb1607652eb..6a84e5c1677 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1510,9 +1510,9 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( tightenBounds(step.plusHeaders, step.plusCoefs, step.plusEntries); tightenBounds(step.minusHeaders, step.minusCoefs, step.minusEntries); - if (impliedLower == -kHighsInf && impliedUpper == kHighsInf) + if (impliedLower <= 0.0 && impliedUpper >= 0.0) solution.col_value[col] = 0.0; - else if (impliedLower != -kHighsInf) + else if (impliedLower > 0.0) solution.col_value[col] = impliedLower; else solution.col_value[col] = impliedUpper; From 6b7ebd861fd2b6aed98e91760eb4ffffed0b8dd8 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 09:12:12 +0200 Subject: [PATCH 122/196] Dual postsolve fix for ranged rows --- highs/presolve/HighsPostsolveStack.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 6a84e5c1677..772dd4a1562 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1536,7 +1536,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( for (const auto& desc : descendants[r]) dual += static_cast(solution.row_dual[desc.row]) * desc.scaleFactor; - solution.row_dual[headers[r].row] = static_cast(dual); + solution.row_dual[headers[r].row] += static_cast(dual); } }; recoverDual(step.plusHeaders, step.plusDescendants); From e9cc2a4fdc97c95612c4638e81fc65e2ea323385 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 09:37:43 +0200 Subject: [PATCH 123/196] Fix col dual --- highs/presolve/HighsPostsolveStack.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 772dd4a1562..cde5f0438c8 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1542,14 +1542,21 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( recoverDual(step.plusHeaders, step.plusDescendants); recoverDual(step.minusHeaders, step.minusDescendants); - // col_dual = cost - Σ a_{ij} * row_dual[i] + // col_dual = cost - Σ a_{ij} * row_dual[i] (each row counted once) HighsCDouble colDual = step.header.colCost; - for (HighsInt r = 0; r < numPlus; ++r) - colDual -= static_cast(step.plusCoefs[r]) * - solution.row_dual[step.plusHeaders[r].row]; - for (HighsInt r = 0; r < numMinus; ++r) + std::vector visited(solution.row_dual.size(), false); + for (HighsInt r = 0; r < numPlus; ++r) { + HighsInt row = step.plusHeaders[r].row; + colDual -= + static_cast(step.plusCoefs[r]) * solution.row_dual[row]; + visited[row] = true; + } + for (HighsInt r = 0; r < numMinus; ++r) { + HighsInt row = step.minusHeaders[r].row; + if (visited[row]) continue; colDual -= static_cast(step.minusCoefs[r]) * - solution.row_dual[step.minusHeaders[r].row]; + solution.row_dual[row]; + } solution.col_dual[col] = static_cast(colDual); } From 324c0b8f4803735a560dbf161c3638a07409ed65 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 09:58:04 +0200 Subject: [PATCH 124/196] Basis postsolve again --- highs/presolve/HighsPostsolveStack.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index cde5f0438c8..ea280434152 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1601,7 +1601,9 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( // default: x_j is non-basic at the value assigned by primal postsolve if (step.header.colLower == -kHighsInf && step.header.colUpper == kHighsInf) - basis.col_status[col] = HighsBasisStatus::kZero; + basis.col_status[col] = std::abs(solution.col_value[col]) <= tol + ? HighsBasisStatus::kZero + : HighsBasisStatus::kBasic; else if (solution.col_value[col] <= step.header.colLower + tol) basis.col_status[col] = HighsBasisStatus::kLower; else if (solution.col_value[col] >= step.header.colUpper - tol) From 242c9899ab2435329d0646a938eb176825008c96 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 10:20:38 +0200 Subject: [PATCH 125/196] Basis postsolve again --- highs/presolve/HighsPostsolveStack.cpp | 68 +++++++++++--------------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index ea280434152..16c4f3d2b95 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1618,50 +1618,40 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( HighsInt pIdx = nr.plusParentIdx; HighsInt mIdx = nr.minusParentIdx; - bool newRowBasic = basis.row_status[nr.row] == HighsBasisStatus::kBasic; - - if (!newRowBasic) { - // non-basic propagation: both parent rows become non-basic + if (basis.row_status[nr.row] != HighsBasisStatus::kBasic) continue; + + // basic propagation: determine which parent gets the basic status + double pSlack = + pIdx >= 0 + ? computeSlack(step.plusHeaders, step.plusCoefs, step.plusEntries, + pIdx) + : std::max(step.header.colUpper - solution.col_value[col], 0.0); + double mSlack = + mIdx >= 0 + ? computeSlack(step.minusHeaders, step.minusCoefs, + step.minusEntries, mIdx) + : std::max(solution.col_value[col] - step.header.colLower, 0.0); + + if (pSlack > tol && mSlack <= tol) { if (pIdx >= 0) basis.row_status[step.plusHeaders[pIdx].row] = - HighsBasisStatus::kNonbasic; + HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + } else if (mSlack > tol && pSlack <= tol) { if (mIdx >= 0) basis.row_status[step.minusHeaders[mIdx].row] = - HighsBasisStatus::kNonbasic; - } else { - // basic propagation: determine which parent gets the basic status - double pSlack = - pIdx >= 0 - ? computeSlack(step.plusHeaders, step.plusCoefs, - step.plusEntries, pIdx) - : std::max(step.header.colUpper - solution.col_value[col], 0.0); - double mSlack = - mIdx >= 0 - ? computeSlack(step.minusHeaders, step.minusCoefs, - step.minusEntries, mIdx) - : std::max(solution.col_value[col] - step.header.colLower, 0.0); - - if (pSlack > tol && mSlack <= tol) { - if (pIdx >= 0) - basis.row_status[step.plusHeaders[pIdx].row] = - HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } else if (mSlack > tol && pSlack <= tol) { - if (mIdx >= 0) - basis.row_status[step.minusHeaders[mIdx].row] = - HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } else if (pSlack > tol) { - if (pIdx >= 0) - basis.row_status[step.plusHeaders[pIdx].row] = - HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } else { + HighsBasisStatus::kBasic; + else basis.col_status[col] = HighsBasisStatus::kBasic; - } + } else if (pSlack > tol) { + if (pIdx >= 0) + basis.row_status[step.plusHeaders[pIdx].row] = + HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; + } else { + basis.col_status[col] = HighsBasisStatus::kBasic; } } From f34c37c3e029bbb5a30c93496005103eb4e061b6 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 11:53:59 +0200 Subject: [PATCH 126/196] Primal postsolve and FME column index changes --- highs/lp_data/HighsLp.cpp | 1 + highs/lp_data/HighsLp.h | 1 + highs/presolve/HPresolve.cpp | 14 +++++++-- highs/presolve/HPresolve.h | 2 -- highs/presolve/HighsPostsolveStack.cpp | 43 ++++++++++++++++++-------- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/highs/lp_data/HighsLp.cpp b/highs/lp_data/HighsLp.cpp index ceb4de3e51c..aef964ac2c0 100644 --- a/highs/lp_data/HighsLp.cpp +++ b/highs/lp_data/HighsLp.cpp @@ -226,6 +226,7 @@ void HighsLp::clear() { this->is_moved_ = false; this->cost_row_location_ = -1; this->has_infinite_cost_ = false; + this->fme_obj_col_ = -1; this->mods_.clear(); } diff --git a/highs/lp_data/HighsLp.h b/highs/lp_data/HighsLp.h index e3dc241593e..73b48b77d00 100644 --- a/highs/lp_data/HighsLp.h +++ b/highs/lp_data/HighsLp.h @@ -55,6 +55,7 @@ class HighsLp { bool is_moved_; HighsInt cost_row_location_; bool has_infinite_cost_; + HighsInt fme_obj_col_ = -1; HighsLpMods mods_; bool operator==(const HighsLp& lp) const; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 6cdc52477ac..692c40441e4 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -855,6 +855,8 @@ void HPresolve::shrinkProblem(HighsPostsolveStack& postsolve_stack) { } } } + if (model->fme_obj_col_ >= 0) + model->fme_obj_col_ = newColIndex[model->fme_obj_col_]; colDeleted.assign(model->num_col_, false); model->col_cost_.resize(model->num_col_); model->col_lower_.resize(model->num_col_); @@ -6969,6 +6971,7 @@ HPresolve::Result HPresolve::fourierMotzkin( auto isCandidate = [&](HighsInt col) { if (colDeleted[col]) return false; if (colsize[col] == 0) return false; + if (col == model->fme_obj_col_) return false; if (model->integrality_[col] != HighsVarType::kContinuous) return false; if (!acceptCoef(model->col_cost_[col])) return false; for (const auto& nz : getColumnVector(col)) @@ -7165,7 +7168,14 @@ HPresolve::Result HPresolve::fourierMotzkin( // min z with c^T x - z <= -offset. this allows FME to eliminate // continuous columns with nonzero cost. auto reformulateObjective = [&]() { - if (fourierMotzkinObjCol != -1) return; + if (model->fme_obj_col_ != -1) { + assert(model->fme_obj_col_ < model->num_col_); + assert(!colDeleted[model->fme_obj_col_]); + assert(model->col_cost_[model->fme_obj_col_] == 1.0); + for (HighsInt j = 0; j < model->num_col_; ++j) + if (j != model->fme_obj_col_) assert(model->col_cost_[j] == 0.0); + return; + } HighsInt zCol = model->num_col_; model->num_col_++; @@ -7223,7 +7233,7 @@ HPresolve::Result HPresolve::fourierMotzkin( costEntries.emplace_back(entry.col, entry.val); postsolve_stack.fourierMotzkinObjCol(zCol, offset, costEntries); - fourierMotzkinObjCol = zCol; + model->fme_obj_col_ = zCol; shrinkProblem(postsolve_stack); }; diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 2a7b066fe6c..abd7ac90423 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -106,8 +106,6 @@ class HPresolve { std::set> equations; std::vector>::iterator> eqiters; - HighsInt fourierMotzkinObjCol = -1; - bool shrinkProblemEnabled; size_t reductionLimit; diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 16c4f3d2b95..ec7ea3ddc4a 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1496,11 +1496,11 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( solution.col_value[nz.index]; double rhs_upper = aij > 0 ? headers[r].rowUpper : headers[r].rowLower; double rhs_lower = aij > 0 ? headers[r].rowLower : headers[r].rowUpper; - if (rhs_upper != kHighsInf) { + if (std::abs(rhs_upper) != kHighsInf) { double bound = static_cast(rhs_upper - sum) / aij; impliedUpper = std::min(impliedUpper, bound); } - if (rhs_lower != -kHighsInf) { + if (std::abs(rhs_lower) != kHighsInf) { double bound = static_cast(rhs_lower - sum) / aij; impliedLower = std::max(impliedLower, bound); } @@ -1611,6 +1611,12 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( else basis.col_status[col] = HighsBasisStatus::kBasic; + auto parentAvailable = [&](const std::vector& headers, + HighsInt idx) { + return idx >= 0 && + basis.row_status[headers[idx].row] != HighsBasisStatus::kBasic; + }; + // process new rows in reverse order (highest index first = Algorithm 5) for (HighsInt k = static_cast(step.newRows.size()) - 1; k >= 0; --k) { @@ -1621,35 +1627,46 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( if (basis.row_status[nr.row] != HighsBasisStatus::kBasic) continue; // basic propagation: determine which parent gets the basic status + bool pAvail = parentAvailable(step.plusHeaders, pIdx); + bool mAvail = parentAvailable(step.minusHeaders, mIdx); + double pSlack = - pIdx >= 0 - ? computeSlack(step.plusHeaders, step.plusCoefs, step.plusEntries, - pIdx) - : std::max(step.header.colUpper - solution.col_value[col], 0.0); + pAvail ? computeSlack(step.plusHeaders, step.plusCoefs, + step.plusEntries, pIdx) + : pIdx < 0 + ? std::max(step.header.colUpper - solution.col_value[col], 0.0) + : 0.0; double mSlack = - mIdx >= 0 - ? computeSlack(step.minusHeaders, step.minusCoefs, - step.minusEntries, mIdx) - : std::max(solution.col_value[col] - step.header.colLower, 0.0); + mAvail ? computeSlack(step.minusHeaders, step.minusCoefs, + step.minusEntries, mIdx) + : mIdx < 0 + ? std::max(solution.col_value[col] - step.header.colLower, 0.0) + : 0.0; if (pSlack > tol && mSlack <= tol) { - if (pIdx >= 0) + if (pAvail) basis.row_status[step.plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; else basis.col_status[col] = HighsBasisStatus::kBasic; } else if (mSlack > tol && pSlack <= tol) { - if (mIdx >= 0) + if (mAvail) basis.row_status[step.minusHeaders[mIdx].row] = HighsBasisStatus::kBasic; else basis.col_status[col] = HighsBasisStatus::kBasic; } else if (pSlack > tol) { - if (pIdx >= 0) + if (pAvail) basis.row_status[step.plusHeaders[pIdx].row] = HighsBasisStatus::kBasic; else basis.col_status[col] = HighsBasisStatus::kBasic; + } else if (mSlack > tol) { + if (mAvail) + basis.row_status[step.minusHeaders[mIdx].row] = + HighsBasisStatus::kBasic; + else + basis.col_status[col] = HighsBasisStatus::kBasic; } else { basis.col_status[col] = HighsBasisStatus::kBasic; } From 7bf2792455a359f09de8718cde746da4bce844ec Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 14:35:06 +0200 Subject: [PATCH 127/196] Modified reformulation logic --- highs/presolve/HPresolve.cpp | 195 +++++++++++++++++++++-------------- 1 file changed, 120 insertions(+), 75 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 692c40441e4..88528d4dc40 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6959,9 +6959,10 @@ HPresolve::Result HPresolve::fourierMotzkin( return Result::kOk; }; - // sentinel row indices for variable bounds + // sentinel row indices for variable bounds and virtual objective row const HighsInt kUpperBoundRow = -2; const HighsInt kLowerBoundRow = -3; + const HighsInt kVirtualObjRow = -4; auto acceptCoef = [&](double val) { double absval = std::abs(val); @@ -6987,7 +6988,8 @@ HPresolve::Result HPresolve::fourierMotzkin( auto checkRows = [&](HighsInt col, std::vector& iPlus, std::vector& iMinus, int64_t& nePlus, - int64_t& neMinus) { + int64_t& neMinus, + const std::vector& objRowCols) { nePlus = 0; neMinus = 0; iPlus.clear(); @@ -7028,13 +7030,35 @@ HPresolve::Result HPresolve::fourierMotzkin( iMinus.push_back(kLowerBoundRow); neMinus += 1; } + + // simulate the objective constraint row for candidates with nonzero + // cost when the reformulation has not yet been performed + if (!objRowCols.empty() && model->col_cost_[col] != 0.0) { + int64_t objRowSize = static_cast(objRowCols.size()); + if (model->col_cost_[col] > 0.0) { + iPlus.push_back(kVirtualObjRow); + nePlus += objRowSize; + } else { + iMinus.push_back(kVirtualObjRow); + neMinus += objRowSize; + } + } }; auto collectAffectedCols = [&](HighsInt col, const std::vector& set, std::vector& mark, std::vector& otherMark, - std::vector& affectedCols) { + std::vector& affectedCols, + const std::vector& objRowCols) { for (HighsInt row : set) { + if (row == kVirtualObjRow) { + for (HighsInt k : objRowCols) { + if (k == col) continue; + if (mark[k] == 0 && otherMark[k] == 0) affectedCols.push_back(k); + mark[k]++; + } + continue; + } if (row < 0) continue; for (const auto& nz : getRowVector(row)) { HighsInt k = nz.index(); @@ -7045,51 +7069,53 @@ HPresolve::Result HPresolve::fourierMotzkin( } }; - auto checkNonZeros = - [&](HighsInt col, std::vector& iPlus, - std::vector& iMinus, std::vector& pPlus, - std::vector& pMinus, std::vector& affectedCols, - int64_t& neRed, int64_t& mrRed) { - // initialise - neRed = 0; - mrRed = 0; - - // check rows - int64_t nePlus; - int64_t neMinus; - checkRows(col, iPlus, iMinus, nePlus, neMinus); - - if (iPlus.size() == 0 || iMinus.size() == 0) { - // other presolve reductions may handle this case (e.g., implied free - // column substitution) - iPlus.clear(); - iMinus.clear(); - return false; - } + auto checkNonZeros = [&](HighsInt col, std::vector& iPlus, + std::vector& iMinus, + std::vector& pPlus, + std::vector& pMinus, + std::vector& affectedCols, int64_t& neRed, + int64_t& mrRed, + const std::vector& objRowCols) { + // initialise + neRed = 0; + mrRed = 0; + + // check rows + int64_t nePlus; + int64_t neMinus; + checkRows(col, iPlus, iMinus, nePlus, neMinus, objRowCols); + + if (iPlus.size() == 0 || iMinus.size() == 0) { + // other presolve reductions may handle this case (e.g., implied free + // column substitution) + iPlus.clear(); + iMinus.clear(); + return false; + } - // take into account other variables present in the rows - collectAffectedCols(col, iPlus, pPlus, pMinus, affectedCols); - collectAffectedCols(col, iMinus, pMinus, pPlus, affectedCols); + // take into account other variables present in the rows + collectAffectedCols(col, iPlus, pPlus, pMinus, affectedCols, objRowCols); + collectAffectedCols(col, iMinus, pMinus, pPlus, affectedCols, objRowCols); - // compute correction term - int64_t correction = 0; - for (HighsInt k : affectedCols) { - correction += static_cast(pPlus[k]) * pMinus[k]; - pPlus[k] = 0; - pMinus[k] = 0; - } + // compute correction term + int64_t correction = 0; + for (HighsInt k : affectedCols) { + correction += static_cast(pPlus[k]) * pMinus[k]; + pPlus[k] = 0; + pMinus[k] = 0; + } - int64_t mPlus = static_cast(iPlus.size()); - int64_t mMinus = static_cast(iMinus.size()); - int64_t neOld = nePlus + neMinus; - // note that we subtract the entries for column 'col' since these are - // eliminated - int64_t neNew = - mPlus * (neMinus - mMinus) + mMinus * (nePlus - mPlus) - correction; - neRed = neOld - neNew; - mrRed = mPlus + mMinus - mPlus * mMinus; - return true; - }; + int64_t mPlus = static_cast(iPlus.size()); + int64_t mMinus = static_cast(iMinus.size()); + int64_t neOld = nePlus + neMinus; + // note that we subtract the entries for column 'col' since these are + // eliminated + int64_t neNew = + mPlus * (neMinus - mMinus) + mMinus * (nePlus - mPlus) - correction; + neRed = neOld - neNew; + mrRed = mPlus + mMinus - mPlus * mMinus; + return true; + }; auto checkNewRow = [&](const newRow& nr, bool& isRedundant) { HighsCDouble impliedLower = 0; @@ -7169,12 +7195,21 @@ HPresolve::Result HPresolve::fourierMotzkin( // continuous columns with nonzero cost. auto reformulateObjective = [&]() { if (model->fme_obj_col_ != -1) { - assert(model->fme_obj_col_ < model->num_col_); - assert(!colDeleted[model->fme_obj_col_]); - assert(model->col_cost_[model->fme_obj_col_] == 1.0); - for (HighsInt j = 0; j < model->num_col_; ++j) - if (j != model->fme_obj_col_) assert(model->col_cost_[j] == 0.0); - return; + printf( + "reformulateObjective: fme_obj_col_=%d, num_col=%d, " + "colDeleted=%d, cost=%.6g\n", + (int)model->fme_obj_col_, (int)model->num_col_, + model->fme_obj_col_ < model->num_col_ + ? (int)colDeleted[model->fme_obj_col_] + : -1, + model->fme_obj_col_ < model->num_col_ + ? model->col_cost_[model->fme_obj_col_] + : -999.0); + if (model->fme_obj_col_ < model->num_col_ && + !colDeleted[model->fme_obj_col_]) { + return; + } + model->fme_obj_col_ = -1; } HighsInt zCol = model->num_col_; @@ -7305,7 +7340,8 @@ HPresolve::Result HPresolve::fourierMotzkin( [&](const std::vector& candidates, std::vector& heap, std::vector& heapPos, std::vector& iPlus, std::vector& iMinus, std::vector& pPlus, - std::vector& pMinus, std::vector& affectedCols) { + std::vector& pMinus, std::vector& affectedCols, + const std::vector& objRowCols) { heap.clear(); heap.reserve(candidates.size()); heapPos.assign(model->num_col_, -1); @@ -7317,8 +7353,9 @@ HPresolve::Result HPresolve::fourierMotzkin( for (HighsInt col : candidates) { int64_t neRed; int64_t mrRed; - bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, - affectedCols, neRed, mrRed); + bool elimCandidate = + checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, affectedCols, + neRed, mrRed, objRowCols); affectedCols.clear(); if (!elimCandidate || !isReduction(neRed, mrRed)) continue; heapPos[col] = static_cast(heap.size()); @@ -7349,6 +7386,16 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector candidates; if (!computeCandidates(candidates)) return finalise(); + // precompute the virtual objective row: columns with nonzero cost + // used to simulate the objective constraint in checkRows before + // reformulation actually happens + std::vector objRowCols; + if (model->fme_obj_col_ == -1) { + for (HighsInt j = 0; j < model->num_col_; ++j) { + if (!colDeleted[j] && model->col_cost_[j] != 0.0) objRowCols.push_back(j); + } + } + // workspace vectors std::vector iPlus; std::vector iMinus; @@ -7362,26 +7409,9 @@ HPresolve::Result HPresolve::fourierMotzkin( // build initial heap if (!heapBuild(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, - affectedCols)) + affectedCols, objRowCols)) return finalise(); - // reformulate objective only if at least one heap candidate has nonzero - // cost - for (const auto& c : heap) { - if (model->col_cost_[c.col] != 0.0) { - // reformulate - reformulateObjective(); - // re-compute candidates (shrinkProblem invalidates indices) - candidates.clear(); - if (!computeCandidates(candidates)) return finalise(); - // re-build heap - if (!heapBuild(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, - affectedCols)) - return finalise(); - break; - } - } - // heapify for (HighsInt i = static_cast(heap.size()) / 2 - 1; i >= 0; --i) heapSiftDown(heap, heapPos, i); @@ -7440,11 +7470,26 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt col = heap[0].col; heapRemove(heap, heapPos, col); + // if this candidate has nonzero cost and objective has not yet been + // reformulated, perform the reformulation now and rebuild the heap + if (model->fme_obj_col_ == -1 && model->col_cost_[col] != 0.0) { + reformulateObjective(); + objRowCols.clear(); + candidates.clear(); + if (!computeCandidates(candidates)) return finalise(); + if (!heapBuild(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, + affectedCols, objRowCols)) + return finalise(); + for (HighsInt i = static_cast(heap.size()) / 2 - 1; i >= 0; --i) + heapSiftDown(heap, heapPos, i); + continue; + } + // compute affected columns int64_t neRed; int64_t mrRed; bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, - affectedCols, neRed, mrRed); + affectedCols, neRed, mrRed, objRowCols); // heap data should be up-to-date assert(elimCandidate && isReduction(neRed, mrRed)); @@ -7588,8 +7633,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // check column non-zeros int64_t ne, mr; bool elimCandidate = - isCandidateCol && - checkNonZeros(k, iPlus, iMinus, pPlus, pMinus, affectedCols, ne, mr); + isCandidateCol && checkNonZeros(k, iPlus, iMinus, pPlus, pMinus, + affectedCols, ne, mr, objRowCols); affectedCols.clear(); if (!elimCandidate || !isReduction(ne, mr)) { // no candidate or not beneficial -> remove from heap From ec5d2b1fa6405580124216681af3ce761ae606bc Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 14:45:01 +0200 Subject: [PATCH 128/196] Clean up --- highs/presolve/HPresolve.cpp | 59 ++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 88528d4dc40..7244b03e0d0 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6962,7 +6962,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // sentinel row indices for variable bounds and virtual objective row const HighsInt kUpperBoundRow = -2; const HighsInt kLowerBoundRow = -3; - const HighsInt kVirtualObjRow = -4; + const HighsInt kObjectiveRow = -4; auto acceptCoef = [&](double val) { double absval = std::abs(val); @@ -6986,10 +6986,10 @@ HPresolve::Result HPresolve::fourierMotzkin( return !candidates.empty(); }; - auto checkRows = [&](HighsInt col, std::vector& iPlus, + auto checkRows = [&](HighsInt col, const std::vector& objRowCols, + std::vector& iPlus, std::vector& iMinus, int64_t& nePlus, - int64_t& neMinus, - const std::vector& objRowCols) { + int64_t& neMinus) { nePlus = 0; neMinus = 0; iPlus.clear(); @@ -7036,46 +7036,47 @@ HPresolve::Result HPresolve::fourierMotzkin( if (!objRowCols.empty() && model->col_cost_[col] != 0.0) { int64_t objRowSize = static_cast(objRowCols.size()); if (model->col_cost_[col] > 0.0) { - iPlus.push_back(kVirtualObjRow); + iPlus.push_back(kObjectiveRow); nePlus += objRowSize; } else { - iMinus.push_back(kVirtualObjRow); + iMinus.push_back(kObjectiveRow); neMinus += objRowSize; } } }; auto collectAffectedCols = [&](HighsInt col, const std::vector& set, + const std::vector& objRowCols, std::vector& mark, std::vector& otherMark, - std::vector& affectedCols, - const std::vector& objRowCols) { + std::vector& affectedCols) { for (HighsInt row : set) { - if (row == kVirtualObjRow) { + if (row == kObjectiveRow) { for (HighsInt k : objRowCols) { if (k == col) continue; if (mark[k] == 0 && otherMark[k] == 0) affectedCols.push_back(k); mark[k]++; } - continue; - } - if (row < 0) continue; - for (const auto& nz : getRowVector(row)) { - HighsInt k = nz.index(); - if (k == col) continue; - if (mark[k] == 0 && otherMark[k] == 0) affectedCols.push_back(k); - mark[k]++; + } else { + if (row < 0) continue; + for (const auto& nz : getRowVector(row)) { + HighsInt k = nz.index(); + if (k == col) continue; + if (mark[k] == 0 && otherMark[k] == 0) affectedCols.push_back(k); + mark[k]++; + } } } }; - auto checkNonZeros = [&](HighsInt col, std::vector& iPlus, + auto checkNonZeros = [&](HighsInt col, + const std::vector& objRowCols, + std::vector& iPlus, std::vector& iMinus, std::vector& pPlus, std::vector& pMinus, std::vector& affectedCols, int64_t& neRed, - int64_t& mrRed, - const std::vector& objRowCols) { + int64_t& mrRed) { // initialise neRed = 0; mrRed = 0; @@ -7083,7 +7084,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // check rows int64_t nePlus; int64_t neMinus; - checkRows(col, iPlus, iMinus, nePlus, neMinus, objRowCols); + checkRows(col, objRowCols, iPlus, iMinus, nePlus, neMinus); if (iPlus.size() == 0 || iMinus.size() == 0) { // other presolve reductions may handle this case (e.g., implied free @@ -7094,8 +7095,8 @@ HPresolve::Result HPresolve::fourierMotzkin( } // take into account other variables present in the rows - collectAffectedCols(col, iPlus, pPlus, pMinus, affectedCols, objRowCols); - collectAffectedCols(col, iMinus, pMinus, pPlus, affectedCols, objRowCols); + collectAffectedCols(col, iPlus, objRowCols, pPlus, pMinus, affectedCols); + collectAffectedCols(col, iMinus, objRowCols, pMinus, pPlus, affectedCols); // compute correction term int64_t correction = 0; @@ -7354,8 +7355,8 @@ HPresolve::Result HPresolve::fourierMotzkin( int64_t neRed; int64_t mrRed; bool elimCandidate = - checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, affectedCols, - neRed, mrRed, objRowCols); + checkNonZeros(col, objRowCols, iPlus, iMinus, pPlus, pMinus, + affectedCols, neRed, mrRed); affectedCols.clear(); if (!elimCandidate || !isReduction(neRed, mrRed)) continue; heapPos[col] = static_cast(heap.size()); @@ -7488,8 +7489,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // compute affected columns int64_t neRed; int64_t mrRed; - bool elimCandidate = checkNonZeros(col, iPlus, iMinus, pPlus, pMinus, - affectedCols, neRed, mrRed, objRowCols); + bool elimCandidate = checkNonZeros(col, objRowCols, iPlus, iMinus, pPlus, + pMinus, affectedCols, neRed, mrRed); // heap data should be up-to-date assert(elimCandidate && isReduction(neRed, mrRed)); @@ -7633,8 +7634,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // check column non-zeros int64_t ne, mr; bool elimCandidate = - isCandidateCol && checkNonZeros(k, iPlus, iMinus, pPlus, pMinus, - affectedCols, ne, mr, objRowCols); + isCandidateCol && checkNonZeros(k, objRowCols, iPlus, iMinus, pPlus, + pMinus, affectedCols, ne, mr); affectedCols.clear(); if (!elimCandidate || !isReduction(ne, mr)) { // no candidate or not beneficial -> remove from heap From 66d5f9b718192f8f70122fcd9c7a3ee5fc60b551 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 11 Jun 2026 15:00:54 +0200 Subject: [PATCH 129/196] Move lambda --- highs/presolve/HPresolve.cpp | 39 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 7244b03e0d0..366f7505385 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7383,6 +7383,22 @@ HPresolve::Result HPresolve::fourierMotzkin( return result; }; + auto inheritAncestry = + [&](std::unordered_map>& rowAncestry, + HighsInt newModelRow, HighsInt parentRow, HighsInt parentRowIndex, + HighsInt stepIndex, double scale, bool isMinus) { + if (parentRow < 0) return; + auto it = rowAncestry.find(parentRow); + if (it != rowAncestry.end()) { + for (const auto& a : it->second) + rowAncestry[newModelRow].push_back( + {a.step, a.parentRowIndex, a.scale * scale, a.isMinus}); + } + if (parentRowIndex >= 0) + rowAncestry[newModelRow].push_back( + {stepIndex, parentRowIndex, scale, isMinus}); + }; + // collect candidate variables std::vector candidates; if (!computeCandidates(candidates)) return finalise(); @@ -7452,20 +7468,6 @@ HPresolve::Result HPresolve::fourierMotzkin( // maps surviving row to its ancestry (which parent rows it descends from) std::unordered_map> rowAncestry; - auto inheritAncestry = [&](HighsInt parentRow, HighsInt parentRowIndex, - HighsInt stepIndex, double scale, bool isMinus, - std::vector& newAnc) { - if (parentRow < 0) return; - auto it = rowAncestry.find(parentRow); - if (it != rowAncestry.end()) { - for (const auto& a : it->second) - newAnc.push_back( - {a.step, a.parentRowIndex, a.scale * scale, a.isMinus}); - } - if (parentRowIndex >= 0) - newAnc.push_back({stepIndex, parentRowIndex, scale, isMinus}); - }; - // main loop: eliminate variables from heap while (!heap.empty()) { HighsInt col = heap[0].col; @@ -7596,11 +7598,10 @@ HPresolve::Result HPresolve::fourierMotzkin( const auto& origin = newRowOrigins[k]; HighsInt pIdx = findRowIndex(origin.plusRow, plusRows); HighsInt mIdx = findRowIndex(origin.minusRow, minusRows); - std::vector& newAnc = rowAncestry[newModelRow]; - inheritAncestry(origin.plusRow, pIdx, stepIdx, origin.plusScale, false, - newAnc); - inheritAncestry(origin.minusRow, mIdx, stepIdx, origin.minusScale, true, - newAnc); + inheritAncestry(rowAncestry, newModelRow, origin.plusRow, pIdx, stepIdx, + origin.plusScale, false); + inheritAncestry(rowAncestry, newModelRow, origin.minusRow, mIdx, stepIdx, + origin.minusScale, true); stepNewRows.push_back({newModelRow, pIdx, mIdx}); } blockNewRows.push_back(std::move(stepNewRows)); From f8a6fa4c63ca9200d22aae4184c2aada44ddf73d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 12 Jun 2026 09:48:50 +0200 Subject: [PATCH 130/196] Basis postsolve still not working --- highs/presolve/HPresolve.cpp | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 366f7505385..52885650b68 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5949,6 +5949,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif bool tryProbing = mipsolver != nullptr; + bool tryFourierMotzkin = + mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; bool dependentEquationsCalled = mipsolver != nullptr; @@ -5977,7 +5979,8 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { applyConflictGraphSubstitutions(postsolve_stack, numDelCol)); } - if (analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) + if (tryFourierMotzkin && + analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); if (analysis_.allow_rule_[kPresolveRuleAggregator]) @@ -6959,7 +6962,7 @@ HPresolve::Result HPresolve::fourierMotzkin( return Result::kOk; }; - // sentinel row indices for variable bounds and virtual objective row + // sentinel row indices for variable bounds and objective row const HighsInt kUpperBoundRow = -2; const HighsInt kLowerBoundRow = -3; const HighsInt kObjectiveRow = -4; @@ -7021,7 +7024,7 @@ HPresolve::Result HPresolve::fourierMotzkin( } } - // include finite variable bounds as virtual singleton rows + // include finite variable bounds as singleton rows if (model->col_upper_[col] != kHighsInf) { iPlus.push_back(kUpperBoundRow); nePlus += 1; @@ -7196,21 +7199,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // continuous columns with nonzero cost. auto reformulateObjective = [&]() { if (model->fme_obj_col_ != -1) { - printf( - "reformulateObjective: fme_obj_col_=%d, num_col=%d, " - "colDeleted=%d, cost=%.6g\n", - (int)model->fme_obj_col_, (int)model->num_col_, - model->fme_obj_col_ < model->num_col_ - ? (int)colDeleted[model->fme_obj_col_] - : -1, - model->fme_obj_col_ < model->num_col_ - ? model->col_cost_[model->fme_obj_col_] - : -999.0); - if (model->fme_obj_col_ < model->num_col_ && - !colDeleted[model->fme_obj_col_]) { - return; - } - model->fme_obj_col_ = -1; + assert(!colDeleted[model->fme_obj_col_]); + return; } HighsInt zCol = model->num_col_; @@ -7403,7 +7393,7 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector candidates; if (!computeCandidates(candidates)) return finalise(); - // precompute the virtual objective row: columns with nonzero cost + // precompute the objective row: columns with nonzero cost // used to simulate the objective constraint in checkRows before // reformulation actually happens std::vector objRowCols; @@ -7606,7 +7596,7 @@ HPresolve::Result HPresolve::fourierMotzkin( } blockNewRows.push_back(std::move(stepNewRows)); - // remove old rows containing col (skip virtual bound rows) + // remove old rows containing col (skip bound rows) for (HighsInt rp : iPlus) { if (rp < 0) continue; rowAncestry.erase(rp); From 35568f8488f07febfc84538cf6be02aa0d77e27c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 12 Jun 2026 10:05:14 +0200 Subject: [PATCH 131/196] Minor change --- highs/presolve/HighsPostsolveStack.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index ec7ea3ddc4a..fc3a840a919 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1494,14 +1494,17 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( for (const auto& nz : entries[r]) sum += static_cast(nz.value) * solution.col_value[nz.index]; - double rhs_upper = aij > 0 ? headers[r].rowUpper : headers[r].rowLower; - double rhs_lower = aij > 0 ? headers[r].rowLower : headers[r].rowUpper; - if (std::abs(rhs_upper) != kHighsInf) { - double bound = static_cast(rhs_upper - sum) / aij; + HighsInt direction = aij > 0 ? HighsInt{1} : HighsInt{-1}; + double rhs_upper = + direction > 0 ? headers[r].rowUpper : headers[r].rowLower; + double rhs_lower = + direction > 0 ? headers[r].rowLower : headers[r].rowUpper; + if (direction * rhs_upper != kHighsInf) { + double bound = static_cast((rhs_upper - sum) / aij); impliedUpper = std::min(impliedUpper, bound); } - if (std::abs(rhs_lower) != kHighsInf) { - double bound = static_cast(rhs_lower - sum) / aij; + if (direction * rhs_lower != -kHighsInf) { + double bound = static_cast((rhs_lower - sum) / aij); impliedLower = std::max(impliedLower, bound); } } From 335f9d22fcf69091cd82c3220b524c10b31d2ae4 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 12 Jun 2026 10:27:10 +0200 Subject: [PATCH 132/196] Resize vector --- highs/presolve/HPresolve.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 52885650b68..c6794b8e922 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7469,6 +7469,7 @@ HPresolve::Result HPresolve::fourierMotzkin( reformulateObjective(); objRowCols.clear(); candidates.clear(); + newRowMark.resize(model->num_col_, -1); if (!computeCandidates(candidates)) return finalise(); if (!heapBuild(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, affectedCols, objRowCols)) From 06cb5ec1168df35c958bf37fbf9fecefd662875f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 12 Jun 2026 10:52:58 +0200 Subject: [PATCH 133/196] Simplify some more --- highs/presolve/HPresolve.cpp | 66 ++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index c6794b8e922..4b7f6c8aa36 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6984,6 +6984,7 @@ HPresolve::Result HPresolve::fourierMotzkin( }; auto computeCandidates = [&](std::vector& candidates) { + candidates.clear(); for (HighsInt col = 0; col < model->num_col_; col++) if (isCandidate(col)) candidates.push_back(col); return !candidates.empty(); @@ -7327,12 +7328,21 @@ HPresolve::Result HPresolve::fourierMotzkin( heapSiftDown(heap, heapPos, pos); }; - auto heapBuild = - [&](const std::vector& candidates, std::vector& heap, + auto heapify = [&](std::vector& heap, + std::vector& heapPos) { + for (HighsInt i = static_cast(heap.size()) / 2 - 1; i >= 0; --i) + heapSiftDown(heap, heapPos, i); + }; + + auto collectCandidatesAndBuildHeap = + [&](std::vector& candidates, std::vector& heap, std::vector& heapPos, std::vector& iPlus, std::vector& iMinus, std::vector& pPlus, std::vector& pMinus, std::vector& affectedCols, const std::vector& objRowCols) { + // compute candidates + if (!computeCandidates(candidates)) return false; + // set up data structures for heap heap.clear(); heap.reserve(candidates.size()); heapPos.assign(model->num_col_, -1); @@ -7341,6 +7351,7 @@ HPresolve::Result HPresolve::fourierMotzkin( iPlus.reserve(model->num_row_); iMinus.reserve(model->num_row_); affectedCols.reserve(model->num_col_); + // inspect candidates for (HighsInt col : candidates) { int64_t neRed; int64_t mrRed; @@ -7349,10 +7360,14 @@ HPresolve::Result HPresolve::fourierMotzkin( affectedCols, neRed, mrRed); affectedCols.clear(); if (!elimCandidate || !isReduction(neRed, mrRed)) continue; + // add to heap heapPos[col] = static_cast(heap.size()); heap.push_back({col, neRed, mrRed}); } - return !heap.empty(); + if (heap.empty()) return false; + // heapify + heapify(heap, heapPos); + return true; }; // find index of a row within a list @@ -7389,9 +7404,17 @@ HPresolve::Result HPresolve::fourierMotzkin( {stepIndex, parentRowIndex, scale, isMinus}); }; - // collect candidate variables + // workspace vectors std::vector candidates; - if (!computeCandidates(candidates)) return finalise(); + std::vector iPlus; + std::vector iMinus; + std::vector pPlus; + std::vector pMinus; + std::vector affectedCols; + + // indexed max-heap + std::vector heap; + std::vector heapPos; // precompute the objective row: columns with nonzero cost // used to simulate the objective constraint in checkRows before @@ -7403,26 +7426,11 @@ HPresolve::Result HPresolve::fourierMotzkin( } } - // workspace vectors - std::vector iPlus; - std::vector iMinus; - std::vector pPlus; - std::vector pMinus; - std::vector affectedCols; - - // indexed max-heap - std::vector heap; - std::vector heapPos; - - // build initial heap - if (!heapBuild(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, - affectedCols, objRowCols)) + // compute candidates and build initial heap + if (!collectCandidatesAndBuildHeap(candidates, heap, heapPos, iPlus, iMinus, + pPlus, pMinus, affectedCols, objRowCols)) return finalise(); - // heapify - for (HighsInt i = static_cast(heap.size()) / 2 - 1; i >= 0; --i) - heapSiftDown(heap, heapPos, i); - // vectors for computing new row entries std::vector newRowEntries; std::vector newRowMark(model->num_col_, -1); @@ -7466,16 +7474,16 @@ HPresolve::Result HPresolve::fourierMotzkin( // if this candidate has nonzero cost and objective has not yet been // reformulated, perform the reformulation now and rebuild the heap if (model->fme_obj_col_ == -1 && model->col_cost_[col] != 0.0) { + // reformulate objective reformulateObjective(); + // clear vector for objective and resize marker objRowCols.clear(); - candidates.clear(); newRowMark.resize(model->num_col_, -1); - if (!computeCandidates(candidates)) return finalise(); - if (!heapBuild(candidates, heap, heapPos, iPlus, iMinus, pPlus, pMinus, - affectedCols, objRowCols)) + // re-compute candidates and re-build heap + if (!collectCandidatesAndBuildHeap(candidates, heap, heapPos, iPlus, + iMinus, pPlus, pMinus, affectedCols, + objRowCols)) return finalise(); - for (HighsInt i = static_cast(heap.size()) / 2 - 1; i >= 0; --i) - heapSiftDown(heap, heapPos, i); continue; } From 5bbd61c08f068405e54d2bd950137b92767764e7 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 22 Jun 2026 08:21:05 +0200 Subject: [PATCH 134/196] Move logic to postsolve --- check/TestSemiVariables.cpp | 3 ++ highs/presolve/HPresolve.cpp | 43 ++++------------------------ highs/presolve/HighsPostsolveStack.h | 36 ++++++++++++++++++++--- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/check/TestSemiVariables.cpp b/check/TestSemiVariables.cpp index 5d2db970f5f..ae04b6d06b3 100644 --- a/check/TestSemiVariables.cpp +++ b/check/TestSemiVariables.cpp @@ -334,6 +334,9 @@ TEST_CASE("3015", "[highs_test_semi_variables]") { double optimal_objective_value = -1407973.679417; Highs highs; highs.setOptionValue("output_flag", dev_run); + // Disable Fourier-Motzkin presolve so that the semi-variable + // infeasibility is still triggered with default mip_feasibility_tolerance + highs.setOptionValue("presolve_rule_off", 131072); highs.readModel(filename); HighsStatus status = highs.run(); REQUIRE(status == HighsStatus::kError); diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 4b7f6c8aa36..c68d5876825 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6916,6 +6916,7 @@ HPresolve::Result HPresolve::fourierMotzkin( using FmeRow = HighsPostsolveStack::FmeRowData; using FmeDescendant = HighsPostsolveStack::FmeDescendant; using FmeNewRow = HighsPostsolveStack::FmeNewRow; + using FmeAncestryEntry = HighsPostsolveStack::FmeAncestryEntry; // max. absolute coefficient const double maxCoef = 1e3; @@ -6949,13 +6950,6 @@ HPresolve::Result HPresolve::fourierMotzkin( double minusScale; }; - struct AncestryEntry { - HighsInt step; - HighsInt parentRowIndex; - double scale; - bool isMinus; - }; - auto finalise = [&]() { analysis_.logging_on_ = logging_on; if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleFourierMotzkin); @@ -7389,7 +7383,8 @@ HPresolve::Result HPresolve::fourierMotzkin( }; auto inheritAncestry = - [&](std::unordered_map>& rowAncestry, + [&](std::unordered_map>& + rowAncestry, HighsInt newModelRow, HighsInt parentRow, HighsInt parentRowIndex, HighsInt stepIndex, double scale, bool isMinus) { if (parentRow < 0) return; @@ -7459,12 +7454,10 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector blockColCosts; std::vector blockNumPlus; std::vector blockNumMinus; - std::vector>> blockPlusDescendants; - std::vector>> blockMinusDescendants; std::vector> blockNewRows; // maps surviving row to its ancestry (which parent rows it descends from) - std::unordered_map> rowAncestry; + std::unordered_map> rowAncestry; // main loop: eliminate variables from heap while (!heap.empty()) { @@ -7655,35 +7648,11 @@ HPresolve::Result HPresolve::fourierMotzkin( HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } - // build K^j_i mapping from ancestry and finalize the FM block + // finalize the FM block if (numColsEliminated > 0) { - HighsInt numSteps = static_cast(blockCols.size()); - - blockPlusDescendants.resize(numSteps); - blockMinusDescendants.resize(numSteps); - for (HighsInt s = 0; s < numSteps; ++s) { - blockPlusDescendants[s].resize(blockNumPlus[s]); - blockMinusDescendants[s].resize(blockNumMinus[s]); - } - - for (const auto& entry : rowAncestry) { - HighsInt row = entry.first; - HighsInt origRow = postsolve_stack.getOrigRowIndex()[row]; - for (const auto& a : entry.second) { - if (a.isMinus) - blockMinusDescendants[a.step][a.parentRowIndex].push_back( - {origRow, a.scale}); - else - blockPlusDescendants[a.step][a.parentRowIndex].push_back( - {origRow, a.scale}); - } - } - - // finalise the block: push descendants, new row origins, and step headers postsolve_stack.fourierMotzkinBlockFinalise( blockCols, blockColLowers, blockColUppers, blockColCosts, blockNumPlus, - blockNumMinus, blockPlusDescendants, blockMinusDescendants, - blockNewRows); + blockNumMinus, rowAncestry, blockNewRows); highsLogDev(options->log_options, HighsLogType::kInfo, "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index ff9fc0604aa..0957b64462e 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "lp_data/HConst.h" @@ -95,6 +96,13 @@ class HighsPostsolveStack { HighsInt minusParentIdx; // index into minus parents (-1 if bound row) }; + struct FmeAncestryEntry { + HighsInt step; + HighsInt parentRowIndex; + double scale; + bool isMinus; + }; + size_t debug_prev_numreductions = 0; double debug_prev_col_lower = 0; double debug_prev_col_upper = 0; @@ -690,13 +698,33 @@ class HighsPostsolveStack { const std::vector& colUppers, const std::vector& colCosts, const std::vector& numPlusPerStep, const std::vector& numMinusPerStep, - const std::vector>>& - plusDescendantsAll, - const std::vector>>& - minusDescendantsAll, + const std::unordered_map>& + rowAncestry, const std::vector>& newRowsAll) { HighsInt numSteps = static_cast(eliminatedCols.size()); + // build K^j_i mapping from ancestry + std::vector>> plusDescendantsAll( + numSteps); + std::vector>> minusDescendantsAll( + numSteps); + for (HighsInt s = 0; s < numSteps; ++s) { + plusDescendantsAll[s].resize(numPlusPerStep[s]); + minusDescendantsAll[s].resize(numMinusPerStep[s]); + } + for (const auto& entry : rowAncestry) { + HighsInt row = entry.first; + HighsInt origRow = origRowIndex[row]; + for (const auto& a : entry.second) { + if (a.isMinus) + minusDescendantsAll[a.step][a.parentRowIndex].push_back( + {origRow, a.scale}); + else + plusDescendantsAll[a.step][a.parentRowIndex].push_back( + {origRow, a.scale}); + } + } + // push descendants for each step's parents (plus then minus) for (HighsInt s = 0; s < numSteps; ++s) { assert(static_cast(plusDescendantsAll[s].size()) == From 5a71bfcd27d011a1dd6f694f557c77cd90fd03b9 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 22 Jun 2026 08:22:29 +0200 Subject: [PATCH 135/196] Comment --- highs/presolve/HPresolve.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index c68d5876825..6e889e61017 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7648,12 +7648,13 @@ HPresolve::Result HPresolve::fourierMotzkin( HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); } - // finalize the FM block if (numColsEliminated > 0) { + // finalize the FM block postsolve_stack.fourierMotzkinBlockFinalise( blockCols, blockColLowers, blockColUppers, blockColCosts, blockNumPlus, blockNumMinus, rowAncestry, blockNewRows); + // log message highsLogDev(options->log_options, HighsLogType::kInfo, "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT " cols and %" HIGHSINT_FORMAT From 65c046be8c070c311382da9b6b7de89d8a894548 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 22 Jun 2026 08:45:26 +0200 Subject: [PATCH 136/196] Try to simplify a little more --- highs/presolve/HPresolve.cpp | 31 +++++++---------- highs/presolve/HighsPostsolveStack.h | 51 +++++++++++++++------------- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 6e889e61017..f1ccecfd2b5 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6917,6 +6917,7 @@ HPresolve::Result HPresolve::fourierMotzkin( using FmeDescendant = HighsPostsolveStack::FmeDescendant; using FmeNewRow = HighsPostsolveStack::FmeNewRow; using FmeAncestryEntry = HighsPostsolveStack::FmeAncestryEntry; + using FmeBlockStep = HighsPostsolveStack::FmeBlockStep; // max. absolute coefficient const double maxCoef = 1e3; @@ -7448,13 +7449,7 @@ HPresolve::Result HPresolve::fourierMotzkin( HighsInt numRowsAdded = 0; // FM block data for postsolve - std::vector blockCols; - std::vector blockColLowers; - std::vector blockColUppers; - std::vector blockColCosts; - std::vector blockNumPlus; - std::vector blockNumMinus; - std::vector> blockNewRows; + std::vector blockSteps; // maps surviving row to its ancestry (which parent rows it descends from) std::unordered_map> rowAncestry; @@ -7489,7 +7484,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // heap data should be up-to-date assert(elimCandidate && isReduction(neRed, mrRed)); - HighsInt stepIdx = static_cast(blockCols.size()); + HighsInt stepIdx = static_cast(blockSteps.size()); // perform elimination: generate new rows newRows.clear(); @@ -7569,12 +7564,13 @@ HPresolve::Result HPresolve::fourierMotzkin( postsolve_stack.fourierMotzkinBlockPushStep(col, plusRows, minusRows); // save block metadata - blockCols.push_back(col); - blockColLowers.push_back(model->col_lower_[col]); - blockColUppers.push_back(model->col_upper_[col]); - blockColCosts.push_back(model->col_cost_[col]); - blockNumPlus.push_back(static_cast(plusRows.size())); - blockNumMinus.push_back(static_cast(minusRows.size())); + blockSteps.push_back({col, + model->col_lower_[col], + model->col_upper_[col], + model->col_cost_[col], + static_cast(plusRows.size()), + static_cast(minusRows.size()), + {}}); // add new rows to matrix HighsInt firstNewRow = model->num_row_; @@ -7583,7 +7579,7 @@ HPresolve::Result HPresolve::fourierMotzkin( numRowsAdded += static_cast(rowEntries.size()); // build FmeNewRow data and ancestry for this step - std::vector stepNewRows; + auto& stepNewRows = blockSteps.back().newRows; stepNewRows.reserve(newRowOrigins.size()); for (HighsInt k = 0; k < static_cast(newRowOrigins.size()); ++k) { HighsInt newModelRow = firstNewRow + k; @@ -7596,7 +7592,6 @@ HPresolve::Result HPresolve::fourierMotzkin( origin.minusScale, true); stepNewRows.push_back({newModelRow, pIdx, mIdx}); } - blockNewRows.push_back(std::move(stepNewRows)); // remove old rows containing col (skip bound rows) for (HighsInt rp : iPlus) { @@ -7650,9 +7645,7 @@ HPresolve::Result HPresolve::fourierMotzkin( if (numColsEliminated > 0) { // finalize the FM block - postsolve_stack.fourierMotzkinBlockFinalise( - blockCols, blockColLowers, blockColUppers, blockColCosts, blockNumPlus, - blockNumMinus, rowAncestry, blockNewRows); + postsolve_stack.fourierMotzkinBlockFinalise(blockSteps, rowAncestry); // log message highsLogDev(options->log_options, HighsLogType::kInfo, diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 0957b64462e..6bd29dd84f3 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -103,6 +103,16 @@ class HighsPostsolveStack { bool isMinus; }; + struct FmeBlockStep { + HighsInt col; + double colLower; + double colUpper; + double colCost; + HighsInt numPlus; + HighsInt numMinus; + std::vector newRows; + }; + size_t debug_prev_numreductions = 0; double debug_prev_col_lower = 0; double debug_prev_col_upper = 0; @@ -693,15 +703,10 @@ class HighsPostsolveStack { // Finalise the FM block: push descendants mapping, new row origins, // and step headers. Called once after all elimination steps are complete. void fourierMotzkinBlockFinalise( - const std::vector& eliminatedCols, - const std::vector& colLowers, - const std::vector& colUppers, const std::vector& colCosts, - const std::vector& numPlusPerStep, - const std::vector& numMinusPerStep, + const std::vector& blockSteps, const std::unordered_map>& - rowAncestry, - const std::vector>& newRowsAll) { - HighsInt numSteps = static_cast(eliminatedCols.size()); + rowAncestry) { + HighsInt numSteps = static_cast(blockSteps.size()); // build K^j_i mapping from ancestry std::vector>> plusDescendantsAll( @@ -709,8 +714,8 @@ class HighsPostsolveStack { std::vector>> minusDescendantsAll( numSteps); for (HighsInt s = 0; s < numSteps; ++s) { - plusDescendantsAll[s].resize(numPlusPerStep[s]); - minusDescendantsAll[s].resize(numMinusPerStep[s]); + plusDescendantsAll[s].resize(blockSteps[s].numPlus); + minusDescendantsAll[s].resize(blockSteps[s].numMinus); } for (const auto& entry : rowAncestry) { HighsInt row = entry.first; @@ -728,20 +733,20 @@ class HighsPostsolveStack { // push descendants for each step's parents (plus then minus) for (HighsInt s = 0; s < numSteps; ++s) { assert(static_cast(plusDescendantsAll[s].size()) == - numPlusPerStep[s]); - for (HighsInt p = 0; p < numPlusPerStep[s]; ++p) + blockSteps[s].numPlus); + for (HighsInt p = 0; p < blockSteps[s].numPlus; ++p) reductionValues.push(plusDescendantsAll[s][p]); assert(static_cast(minusDescendantsAll[s].size()) == - numMinusPerStep[s]); - for (HighsInt m = 0; m < numMinusPerStep[s]; ++m) + blockSteps[s].numMinus); + for (HighsInt m = 0; m < blockSteps[s].numMinus; ++m) reductionValues.push(minusDescendantsAll[s][m]); } // push new row origins for each step (translate row to orig space) for (HighsInt s = 0; s < numSteps; ++s) { std::vector translated; - translated.reserve(newRowsAll[s].size()); - for (const auto& nr : newRowsAll[s]) + translated.reserve(blockSteps[s].newRows.size()); + for (const auto& nr : blockSteps[s].newRows) translated.push_back( {origRowIndex[nr.row], nr.plusParentIdx, nr.minusParentIdx}); reductionValues.push(translated); @@ -749,13 +754,13 @@ class HighsPostsolveStack { // push step headers for (HighsInt s = 0; s < numSteps; ++s) { - FmeStepHeader header{colLowers[s], - colUppers[s], - colCosts[s], - origColIndex[eliminatedCols[s]], - numPlusPerStep[s], - numMinusPerStep[s], - static_cast(newRowsAll[s].size())}; + FmeStepHeader header{blockSteps[s].colLower, + blockSteps[s].colUpper, + blockSteps[s].colCost, + origColIndex[blockSteps[s].col], + blockSteps[s].numPlus, + blockSteps[s].numMinus, + static_cast(blockSteps[s].newRows.size())}; reductionValues.push(header); } From fc0aec0f07ce54b7fa04a7ab7dd8cd1a6dbb72d2 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 22 Jun 2026 08:49:57 +0200 Subject: [PATCH 137/196] Remove unused code --- highs/presolve/HighsPostsolveStack.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 6bd29dd84f3..42da1aaec24 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -82,7 +82,6 @@ class HighsPostsolveStack { HighsInt col; HighsInt numPlus; HighsInt numMinus; - HighsInt numNewRows; }; struct FmeDescendant { @@ -754,13 +753,10 @@ class HighsPostsolveStack { // push step headers for (HighsInt s = 0; s < numSteps; ++s) { - FmeStepHeader header{blockSteps[s].colLower, - blockSteps[s].colUpper, - blockSteps[s].colCost, - origColIndex[blockSteps[s].col], - blockSteps[s].numPlus, - blockSteps[s].numMinus, - static_cast(blockSteps[s].newRows.size())}; + FmeStepHeader header{ + blockSteps[s].colLower, blockSteps[s].colUpper, + blockSteps[s].colCost, origColIndex[blockSteps[s].col], + blockSteps[s].numPlus, blockSteps[s].numMinus}; reductionValues.push(header); } From e6f2874b32df330bc3a1337719a7e8448d8bad8c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 22 Jun 2026 11:34:20 +0200 Subject: [PATCH 138/196] Use checkLimits() correctly --- highs/presolve/HPresolve.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 6e889e61017..53747e6e961 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6953,7 +6953,7 @@ HPresolve::Result HPresolve::fourierMotzkin( auto finalise = [&]() { analysis_.logging_on_ = logging_on; if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleFourierMotzkin); - return Result::kOk; + return checkLimits(postsolve_stack); }; // sentinel row indices for variable bounds and objective row @@ -7645,7 +7645,7 @@ HPresolve::Result HPresolve::fourierMotzkin( } saveAffectedCols.clear(); - HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); + if (checkLimits(postsolve_stack) != Result::kOk) break; } if (numColsEliminated > 0) { From ee118a43ee27fd311125c2f4b0526110a8ac32bb Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 22 Jun 2026 13:19:58 +0200 Subject: [PATCH 139/196] Use tolerance in primal postsolve --- highs/presolve/HighsPostsolveStack.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index fc3a840a919..cc09a980073 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1476,6 +1476,8 @@ HighsPostsolveStack::popFourierMotzkinBlock(HighsDataStack& stack) { void HighsPostsolveStack::undoFourierMotzkinBlock( const std::vector& steps, const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) { + const double tol = options.mip_feasibility_tolerance; + HighsInt numSteps = static_cast(steps.size()); // primal postsolve (Algorithm 3): process in reverse elimination order @@ -1513,7 +1515,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( tightenBounds(step.plusHeaders, step.plusCoefs, step.plusEntries); tightenBounds(step.minusHeaders, step.minusCoefs, step.minusEntries); - if (impliedLower <= 0.0 && impliedUpper >= 0.0) + if (impliedLower <= tol && impliedUpper >= -tol) solution.col_value[col] = 0.0; else if (impliedLower > 0.0) solution.col_value[col] = impliedLower; @@ -1566,8 +1568,6 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( // basis postsolve (Algorithm 5): process in reverse elimination order if (!basis.valid) return; - const double tol = options.mip_feasibility_tolerance; - for (HighsInt s = numSteps - 1; s >= 0; --s) { const auto& step = steps[s]; HighsInt col = step.header.col; From b2da68c727382ee0a700a4c6beed58ac52e1f08b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 22 Jun 2026 14:18:42 +0200 Subject: [PATCH 140/196] Clean up --- highs/presolve/HighsPostsolveStack.h | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 0957b64462e..558f0c741a7 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -628,21 +628,6 @@ class HighsPostsolveStack { reductionAdded(ReductionType::kForcingColumnRemovedRow); } - // Serialization layout for FM block (push order, so pop is reversed): - // For each step (first eliminated to last): - // For each plus row: vector entries - // vector plusCoefs - // vector plusHeaders - // For each minus row: vector entries - // vector minusCoefs - // vector minusHeaders - // Then (after all steps): - // For each step, for each parent: vector - // For each step: FmeStepHeader - // numSteps (HighsInt) - - // Push one step's row data. Must be called before addToMatrix invalidates - // the row slices. template void fourierMotzkinBlockPushStep( HighsInt col, const std::vector>& plusRows, @@ -690,8 +675,6 @@ class HighsPostsolveStack { reductionValues.push(minusHeaders); } - // Finalise the FM block: push descendants mapping, new row origins, - // and step headers. Called once after all elimination steps are complete. void fourierMotzkinBlockFinalise( const std::vector& eliminatedCols, const std::vector& colLowers, From bdcc554d1bf9bc57836500d60edb4747b5dbeffc Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 22 Jun 2026 16:14:14 +0200 Subject: [PATCH 141/196] Reset stale values --- highs/presolve/HighsPostsolveStack.h | 1 + 1 file changed, 1 insertion(+) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index d9f2ca6e2f4..14f921d1242 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -864,6 +864,7 @@ class HighsPostsolveStack { for (size_t i = index.size(); i > 0; --i) { assert(static_cast(index[i - 1]) >= i - 1); values[index[i - 1]] = values[i - 1]; + if (index[i - 1] != static_cast(i - 1)) values[i - 1] = T{}; } #endif } From 3f0aa9b4828598ba2eebbf89b6f942234b26b6b6 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 23 Jun 2026 14:06:32 +0200 Subject: [PATCH 142/196] Fix sc50b --- highs/presolve/HPresolve.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 66f5e6aab6e..3649ef3bd23 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7462,6 +7462,21 @@ HPresolve::Result HPresolve::fourierMotzkin( // if this candidate has nonzero cost and objective has not yet been // reformulated, perform the reformulation now and rebuild the heap if (model->fme_obj_col_ == -1 && model->col_cost_[col] != 0.0) { + // finalise any in-progress FM block before reformulating, since + // reformulateObjective pushes other reductions onto the data stack + if (!blockSteps.empty()) { + postsolve_stack.fourierMotzkinBlockFinalise(blockSteps, rowAncestry); + highsLogDev(options->log_options, HighsLogType::kInfo, + "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT + " cols and %" HIGHSINT_FORMAT + " rows, and added %" HIGHSINT_FORMAT " rows\n", + numColsEliminated, numRowsEliminated, numRowsAdded); + blockSteps.clear(); + rowAncestry.clear(); + numColsEliminated = 0; + numRowsEliminated = 0; + numRowsAdded = 0; + } // reformulate objective reformulateObjective(); // clear vector for objective and resize marker From 3a5163b835b9eee011f20807158ab24144a28458 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 23 Jun 2026 16:25:23 +0200 Subject: [PATCH 143/196] Use global bounds --- highs/presolve/HPresolve.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 3649ef3bd23..5d55e92e531 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7124,8 +7124,8 @@ HPresolve::Result HPresolve::fourierMotzkin( bool upperFinite = true; isRedundant = false; for (const auto& e : nr.entries) { - double lb = implColLower[e.col]; - double ub = implColUpper[e.col]; + double lb = model->col_lower_[e.col]; + double ub = model->col_upper_[e.col]; if (e.val > 0) { lowerFinite = lowerFinite && lb != -kHighsInf; if (lowerFinite) impliedLower += e.val * lb; From a35484a5589b5a3ff40bc1d91d1f76b01c51d617 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 23 Jun 2026 22:36:32 +0200 Subject: [PATCH 144/196] Add postsolve handling for rows that were converted to equations --- highs/presolve/HPresolve.cpp | 9 +++++---- highs/presolve/HighsPostsolveStack.cpp | 13 ++++++++++++ highs/presolve/HighsPostsolveStack.h | 28 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5d55e92e531..389d71f3d92 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2254,6 +2254,8 @@ void HPresolve::markRowDeleted(HighsInt row) { void HPresolve::markColDeleted(HighsInt col) { assert(!colDeleted[col]); + if (col == model->fme_obj_col_) model->fme_obj_col_ = -1; + // prevents col from being added to change vector changedColFlag[col] = true; colDeleted[col] = true; @@ -3654,11 +3656,11 @@ HPresolve::Result HPresolve::rowPresolve(HighsPostsolveStack& postsolve_stack, double origRowUpper = model->row_upper_[row]; double origRowLower = model->row_lower_[row]; + // Convert to equality constraint and record for dual postsolve if (!isEquation(row)) { if (isImpliedEquationAtLower(row)) { - // Convert to equality constraint (note that currently postsolve will not - // know about this conversion) model->row_upper_[row] = model->row_lower_[row]; + postsolve_stack.impliedEquation(row, true, getRowVector(row)); // Since row upper bound is now finite, lower bound on row dual is // -kHighsInf changeRowDualLower(row, -kHighsInf); @@ -3666,9 +3668,8 @@ HPresolve::Result HPresolve::rowPresolve(HighsPostsolveStack& postsolve_stack, HPRESOLVE_CHECKED_CALL( checkRedundantBounds(rowDualLowerSource[row], row)); } else if (isImpliedEquationAtUpper(row)) { - // Convert to equality constraint (note that currently postsolve will not - // know about this conversion) model->row_lower_[row] = model->row_upper_[row]; + postsolve_stack.impliedEquation(row, false, getRowVector(row)); // Since row lower bound is now finite, upper bound on row dual is // kHighsInf changeRowDualUpper(row, kHighsInf); diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index cc09a980073..a97c8b83930 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -493,6 +493,19 @@ void HighsPostsolveStack::RedundantRow::undo( if (basis.valid) basis.row_status[row] = HighsBasisStatus::kBasic; } +void HighsPostsolveStack::ImpliedEquation::undo( + const HighsPostsolveStack& postsolveStack, + const std::vector& rowValues, HighsSolution& solution) const { + if (!solution.dual_valid) return; + if (!postsolveStack.isModelRow(row)) return; + double oldDual = solution.row_dual[row]; + if (atLower ? (oldDual < 0) : (oldDual > 0)) { + solution.row_dual[row] = 0; + for (const auto& nz : rowValues) + solution.col_dual[nz.index] += nz.value * oldDual; + } +} + void HighsPostsolveStack::ForcingRow::undo( const HighsPostsolveStack& postsolveStack, const HighsOptions& options, const std::vector& rowValues, HighsSolution& solution, diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 14f921d1242..962e66f0182 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -229,6 +229,15 @@ class HighsPostsolveStack { HighsBasis& basis) const; }; + struct ImpliedEquation { + HighsInt row; + bool atLower; + + void undo(const HighsPostsolveStack& postsolveStack, + const std::vector& rowValues, + HighsSolution& solution) const; + }; + struct ForcingRow { double side; HighsInt row; @@ -333,6 +342,7 @@ class HighsPostsolveStack { kSingletonRow, kFixedCol, kRedundantRow, + kImpliedEquation, kForcingRow, kForcingColumn, kForcingColumnRemovedRow, @@ -595,6 +605,17 @@ class HighsPostsolveStack { reductionAdded(ReductionType::kRedundantRow); } + template + void impliedEquation(HighsInt row, bool atLower, + const HighsMatrixSlice& rowVec) { + rowValues.clear(); + for (const HighsSliceNonzero& rowVal : rowVec) + rowValues.emplace_back(origColIndex[rowVal.index()], rowVal.value()); + reductionValues.push(ImpliedEquation{origRowIndex[row], atLower}); + reductionValues.push(rowValues); + reductionAdded(ReductionType::kImpliedEquation); + } + template void forcingRow(HighsInt row, const HighsMatrixSlice& rowVec, double side, @@ -971,6 +992,13 @@ class HighsPostsolveStack { reduction.undo(*this, options, solution, basis); break; } + case ReductionType::kImpliedEquation: { + ImpliedEquation reduction; + reductionValues.pop(rowValues); + reductionValues.pop(reduction); + reduction.undo(*this, rowValues, solution); + break; + } case ReductionType::kForcingRow: { ForcingRow reduction; reductionValues.pop(rowValues); From 6b9c3ace3d0c925ac4726085226687558f338902 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 08:26:40 +0200 Subject: [PATCH 145/196] Cost should be zero --- highs/presolve/HPresolve.cpp | 2 +- highs/presolve/HighsPostsolveStack.cpp | 4 ++-- highs/presolve/HighsPostsolveStack.h | 9 +++------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 389d71f3d92..c58918146d2 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7580,10 +7580,10 @@ HPresolve::Result HPresolve::fourierMotzkin( postsolve_stack.fourierMotzkinBlockPushStep(col, plusRows, minusRows); // save block metadata + assert(model->col_cost_[col] == 0.0); blockSteps.push_back({col, model->col_lower_[col], model->col_upper_[col], - model->col_cost_[col], static_cast(plusRows.size()), static_cast(minusRows.size()), {}}); diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index a97c8b83930..d5e38b6fd66 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1560,8 +1560,8 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( recoverDual(step.plusHeaders, step.plusDescendants); recoverDual(step.minusHeaders, step.minusDescendants); - // col_dual = cost - Σ a_{ij} * row_dual[i] (each row counted once) - HighsCDouble colDual = step.header.colCost; + // col_dual = -Σ a_{ij} * row_dual[i] (cost is zero after reformulation) + HighsCDouble colDual = 0.0; std::vector visited(solution.row_dual.size(), false); for (HighsInt r = 0; r < numPlus; ++r) { HighsInt row = step.plusHeaders[r].row; diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 962e66f0182..00f4f68e9cf 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -78,7 +78,6 @@ class HighsPostsolveStack { struct FmeStepHeader { double colLower; double colUpper; - double colCost; HighsInt col; HighsInt numPlus; HighsInt numMinus; @@ -106,7 +105,6 @@ class HighsPostsolveStack { HighsInt col; double colLower; double colUpper; - double colCost; HighsInt numPlus; HighsInt numMinus; std::vector newRows; @@ -757,10 +755,9 @@ class HighsPostsolveStack { // push step headers for (HighsInt s = 0; s < numSteps; ++s) { - FmeStepHeader header{ - blockSteps[s].colLower, blockSteps[s].colUpper, - blockSteps[s].colCost, origColIndex[blockSteps[s].col], - blockSteps[s].numPlus, blockSteps[s].numMinus}; + FmeStepHeader header{blockSteps[s].colLower, blockSteps[s].colUpper, + origColIndex[blockSteps[s].col], + blockSteps[s].numPlus, blockSteps[s].numMinus}; reductionValues.push(header); } From 6545812239d7ee1c6ce46bd426d1be8d78d19133 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 09:23:14 +0200 Subject: [PATCH 146/196] Working on basis postsolve again --- highs/presolve/HPresolve.cpp | 3 +- highs/presolve/HighsPostsolveStack.cpp | 187 +++++++++++-------------- 2 files changed, 83 insertions(+), 107 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index c58918146d2..f041fabf3a5 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5950,8 +5950,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif bool tryProbing = mipsolver != nullptr; - bool tryFourierMotzkin = - mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; + bool tryFourierMotzkin = true; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; bool dependentEquationsCalled = mipsolver != nullptr; diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index d5e38b6fd66..f7128c7a23b 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1493,16 +1493,26 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( HighsInt numSteps = static_cast(steps.size()); + struct ImpliedBound { + double value; + HighsInt rowIdx = -1; // index into plus/minus arrays, -1 = own bound + bool isPlus = false; + bool isUpper = false; + }; + std::vector bindingBounds(numSteps); + // primal postsolve (Algorithm 3): process in reverse elimination order for (HighsInt s = numSteps - 1; s >= 0; --s) { const auto& step = steps[s]; HighsInt col = step.header.col; - double impliedLower = step.header.colLower; - double impliedUpper = step.header.colUpper; + ImpliedBound lower = {step.header.colLower, -1, false, false}; + ImpliedBound upper = {step.header.colUpper, -1, false, true}; auto tightenBounds = [&](const std::vector& headers, const std::vector& coefs, - const std::vector>& entries) { + const std::vector>& entries, + bool isPlus, ImpliedBound& bLower, + ImpliedBound& bUpper) { for (size_t r = 0; r < headers.size(); ++r) { double aij = coefs[r]; HighsCDouble sum = 0.0; @@ -1516,24 +1526,34 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( direction > 0 ? headers[r].rowLower : headers[r].rowUpper; if (direction * rhs_upper != kHighsInf) { double bound = static_cast((rhs_upper - sum) / aij); - impliedUpper = std::min(impliedUpper, bound); + if (bound < bUpper.value) { + bUpper = {bound, static_cast(r), isPlus, true}; + } } if (direction * rhs_lower != -kHighsInf) { double bound = static_cast((rhs_lower - sum) / aij); - impliedLower = std::max(impliedLower, bound); + if (bound > bLower.value) { + bLower = {bound, static_cast(r), isPlus, false}; + } } } }; - tightenBounds(step.plusHeaders, step.plusCoefs, step.plusEntries); - tightenBounds(step.minusHeaders, step.minusCoefs, step.minusEntries); + tightenBounds(step.plusHeaders, step.plusCoefs, step.plusEntries, true, + lower, upper); + tightenBounds(step.minusHeaders, step.minusCoefs, step.minusEntries, false, + lower, upper); - if (impliedLower <= tol && impliedUpper >= -tol) + if (lower.value <= tol && upper.value >= -tol) { solution.col_value[col] = 0.0; - else if (impliedLower > 0.0) - solution.col_value[col] = impliedLower; - else - solution.col_value[col] = impliedUpper; + bindingBounds[s] = {}; + } else if (lower.value > 0.0) { + solution.col_value[col] = lower.value; + bindingBounds[s] = lower; + } else { + solution.col_value[col] = upper.value; + bindingBounds[s] = upper; + } } if (!solution.dual_valid) return; @@ -1586,9 +1606,9 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( HighsInt col = step.header.col; HighsInt numPlus = step.header.numPlus; HighsInt numMinus = step.header.numMinus; + const ImpliedBound& binding = bindingBounds[s]; - // compute row slacks for parent rows: slack_i = min(u - act, act - l) - // divided by |a_ij| for normalization + // compute row activity for determining row basis status auto computeSlack = [&](const std::vector& headers, const std::vector& coefs, const std::vector>& entries, @@ -1607,108 +1627,65 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( return rawSlack / std::abs(coefs[r]); }; - // determine which parent rows are involved in at least one new row - std::vector plusInvolved(numPlus, false); - std::vector minusInvolved(numMinus, false); - for (const auto& nr : step.newRows) { - if (nr.plusParentIdx >= 0) plusInvolved[nr.plusParentIdx] = true; - if (nr.minusParentIdx >= 0) minusInvolved[nr.minusParentIdx] = true; - } - - // default: x_j is non-basic at the value assigned by primal postsolve - if (step.header.colLower == -kHighsInf && step.header.colUpper == kHighsInf) - basis.col_status[col] = std::abs(solution.col_value[col]) <= tol - ? HighsBasisStatus::kZero - : HighsBasisStatus::kBasic; - else if (solution.col_value[col] <= step.header.colLower + tol) - basis.col_status[col] = HighsBasisStatus::kLower; - else if (solution.col_value[col] >= step.header.colUpper - tol) - basis.col_status[col] = HighsBasisStatus::kUpper; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - - auto parentAvailable = [&](const std::vector& headers, - HighsInt idx) { - return idx >= 0 && - basis.row_status[headers[idx].row] != HighsBasisStatus::kBasic; - }; - - // process new rows in reverse order (highest index first = Algorithm 5) + // propagate basis status from descendant rows to parent rows for (HighsInt k = static_cast(step.newRows.size()) - 1; k >= 0; --k) { const auto& nr = step.newRows[k]; - HighsInt pIdx = nr.plusParentIdx; - HighsInt mIdx = nr.minusParentIdx; - - if (basis.row_status[nr.row] != HighsBasisStatus::kBasic) continue; - - // basic propagation: determine which parent gets the basic status - bool pAvail = parentAvailable(step.plusHeaders, pIdx); - bool mAvail = parentAvailable(step.minusHeaders, mIdx); - - double pSlack = - pAvail ? computeSlack(step.plusHeaders, step.plusCoefs, - step.plusEntries, pIdx) - : pIdx < 0 - ? std::max(step.header.colUpper - solution.col_value[col], 0.0) - : 0.0; - double mSlack = - mAvail ? computeSlack(step.minusHeaders, step.minusCoefs, - step.minusEntries, mIdx) - : mIdx < 0 - ? std::max(solution.col_value[col] - step.header.colLower, 0.0) - : 0.0; - - if (pSlack > tol && mSlack <= tol) { - if (pAvail) - basis.row_status[step.plusHeaders[pIdx].row] = + if (basis.row_status[nr.row] == HighsBasisStatus::kBasic) { + if (nr.plusParentIdx >= 0) + basis.row_status[step.plusHeaders[nr.plusParentIdx].row] = HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } else if (mSlack > tol && pSlack <= tol) { - if (mAvail) - basis.row_status[step.minusHeaders[mIdx].row] = - HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } else if (pSlack > tol) { - if (pAvail) - basis.row_status[step.plusHeaders[pIdx].row] = + if (nr.minusParentIdx >= 0) + basis.row_status[step.minusHeaders[nr.minusParentIdx].row] = HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; - } else if (mSlack > tol) { - if (mAvail) - basis.row_status[step.minusHeaders[mIdx].row] = - HighsBasisStatus::kBasic; - else - basis.col_status[col] = HighsBasisStatus::kBasic; } else { - basis.col_status[col] = HighsBasisStatus::kBasic; + if (nr.plusParentIdx >= 0) + basis.row_status[step.plusHeaders[nr.plusParentIdx].row] = + HighsBasisStatus::kNonbasic; + if (nr.minusParentIdx >= 0) + basis.row_status[step.minusHeaders[nr.minusParentIdx].row] = + HighsBasisStatus::kNonbasic; } } - // vanished constraint check: parent rows not involved in any new row - if (step.newRows.empty()) { - // free variable case: no new rows at all + // handle parent rows not involved in any descendant (vanished constraints) + std::vector plusInvolved(numPlus, false); + std::vector minusInvolved(numMinus, false); + for (const auto& nr : step.newRows) { + if (nr.plusParentIdx >= 0) plusInvolved[nr.plusParentIdx] = true; + if (nr.minusParentIdx >= 0) minusInvolved[nr.minusParentIdx] = true; + } + for (HighsInt p = 0; p < numPlus; ++p) { + if (plusInvolved[p]) continue; + double slack = + computeSlack(step.plusHeaders, step.plusCoefs, step.plusEntries, p); + basis.row_status[step.plusHeaders[p].row] = + slack > tol ? HighsBasisStatus::kBasic + : HighsBasisStatus::kNonbasic; + } + for (HighsInt m = 0; m < numMinus; ++m) { + if (minusInvolved[m]) continue; + double slack = computeSlack(step.minusHeaders, step.minusCoefs, + step.minusEntries, m); + basis.row_status[step.minusHeaders[m].row] = + slack > tol ? HighsBasisStatus::kBasic + : HighsBasisStatus::kNonbasic; + } + + // set x_j basis status using binding row info from primal postsolve + if (binding.rowIdx >= 0) { + // a parent row is binding: that row is nonbasic (tight), x_j is basic + HighsInt bindingRow = binding.isPlus + ? step.plusHeaders[binding.rowIdx].row + : step.minusHeaders[binding.rowIdx].row; + basis.row_status[bindingRow] = HighsBasisStatus::kNonbasic; basis.col_status[col] = HighsBasisStatus::kBasic; + } else if (solution.col_value[col] <= step.header.colLower + tol) { + basis.col_status[col] = HighsBasisStatus::kLower; + } else if (solution.col_value[col] >= step.header.colUpper - tol) { + basis.col_status[col] = HighsBasisStatus::kUpper; } else { - for (HighsInt p = 0; p < numPlus; ++p) { - if (plusInvolved[p]) continue; - double slack = - computeSlack(step.plusHeaders, step.plusCoefs, step.plusEntries, p); - basis.row_status[step.plusHeaders[p].row] = - slack > tol ? HighsBasisStatus::kBasic - : HighsBasisStatus::kNonbasic; - } - for (HighsInt m = 0; m < numMinus; ++m) { - if (minusInvolved[m]) continue; - double slack = computeSlack(step.minusHeaders, step.minusCoefs, - step.minusEntries, m); - basis.row_status[step.minusHeaders[m].row] = - slack > tol ? HighsBasisStatus::kBasic - : HighsBasisStatus::kNonbasic; - } + basis.col_status[col] = HighsBasisStatus::kBasic; } } } From c1e721c2a9f1fa63cfaf89ba65eef4d3fa4f4f73 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 11:27:52 +0200 Subject: [PATCH 147/196] Basis postsolve --- highs/presolve/HighsPostsolveStack.cpp | 154 +++++++++++++------------ 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index f7128c7a23b..67f21387c06 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1493,26 +1493,17 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( HighsInt numSteps = static_cast(steps.size()); - struct ImpliedBound { - double value; - HighsInt rowIdx = -1; // index into plus/minus arrays, -1 = own bound - bool isPlus = false; - bool isUpper = false; - }; - std::vector bindingBounds(numSteps); - // primal postsolve (Algorithm 3): process in reverse elimination order for (HighsInt s = numSteps - 1; s >= 0; --s) { const auto& step = steps[s]; HighsInt col = step.header.col; - ImpliedBound lower = {step.header.colLower, -1, false, false}; - ImpliedBound upper = {step.header.colUpper, -1, false, true}; + double lower = step.header.colLower; + double upper = step.header.colUpper; auto tightenBounds = [&](const std::vector& headers, const std::vector& coefs, const std::vector>& entries, - bool isPlus, ImpliedBound& bLower, - ImpliedBound& bUpper) { + double& lowerBound, double& upperBound) { for (size_t r = 0; r < headers.size(); ++r) { double aij = coefs[r]; HighsCDouble sum = 0.0; @@ -1526,34 +1517,26 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( direction > 0 ? headers[r].rowLower : headers[r].rowUpper; if (direction * rhs_upper != kHighsInf) { double bound = static_cast((rhs_upper - sum) / aij); - if (bound < bUpper.value) { - bUpper = {bound, static_cast(r), isPlus, true}; - } + upperBound = std::min(upperBound, bound); } if (direction * rhs_lower != -kHighsInf) { double bound = static_cast((rhs_lower - sum) / aij); - if (bound > bLower.value) { - bLower = {bound, static_cast(r), isPlus, false}; - } + lowerBound = std::max(lowerBound, bound); } } }; - tightenBounds(step.plusHeaders, step.plusCoefs, step.plusEntries, true, - lower, upper); - tightenBounds(step.minusHeaders, step.minusCoefs, step.minusEntries, false, - lower, upper); + tightenBounds(step.plusHeaders, step.plusCoefs, step.plusEntries, lower, + upper); + tightenBounds(step.minusHeaders, step.minusCoefs, step.minusEntries, lower, + upper); - if (lower.value <= tol && upper.value >= -tol) { + if (lower <= tol && upper >= -tol) solution.col_value[col] = 0.0; - bindingBounds[s] = {}; - } else if (lower.value > 0.0) { - solution.col_value[col] = lower.value; - bindingBounds[s] = lower; - } else { - solution.col_value[col] = upper.value; - bindingBounds[s] = upper; - } + else if (lower > 0.0) + solution.col_value[col] = lower; + else + solution.col_value[col] = upper; } if (!solution.dual_valid) return; @@ -1606,7 +1589,6 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( HighsInt col = step.header.col; HighsInt numPlus = step.header.numPlus; HighsInt numMinus = step.header.numMinus; - const ImpliedBound& binding = bindingBounds[s]; // compute row activity for determining row basis status auto computeSlack = [&](const std::vector& headers, @@ -1627,66 +1609,86 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( return rawSlack / std::abs(coefs[r]); }; - // propagate basis status from descendant rows to parent rows - for (HighsInt k = static_cast(step.newRows.size()) - 1; k >= 0; - --k) { - const auto& nr = step.newRows[k]; - if (basis.row_status[nr.row] == HighsBasisStatus::kBasic) { - if (nr.plusParentIdx >= 0) - basis.row_status[step.plusHeaders[nr.plusParentIdx].row] = - HighsBasisStatus::kBasic; - if (nr.minusParentIdx >= 0) - basis.row_status[step.minusHeaders[nr.minusParentIdx].row] = - HighsBasisStatus::kBasic; - } else { - if (nr.plusParentIdx >= 0) - basis.row_status[step.plusHeaders[nr.plusParentIdx].row] = - HighsBasisStatus::kNonbasic; - if (nr.minusParentIdx >= 0) - basis.row_status[step.minusHeaders[nr.minusParentIdx].row] = - HighsBasisStatus::kNonbasic; + // propagate basis from descendants to parents/col. + // basis dimension increases by (numPlus + numMinus - numNewRows). + // need that many new basic items plus one for each basic descendant + // removed. total new basic needed = (numPlus + numMinus - numNewRows) + + // numBasicDesc. + auto setRowNonbasic = [&](const FmeRowHeader& h) { + if (h.rowLower == h.rowUpper) + basis.row_status[h.row] = HighsBasisStatus::kLower; + else if (h.rowLower == -kHighsInf) + basis.row_status[h.row] = HighsBasisStatus::kUpper; + else if (h.rowUpper == kHighsInf) + basis.row_status[h.row] = HighsBasisStatus::kLower; + else { + basis.row_status[h.row] = solution.row_dual[h.row] < 0 + ? HighsBasisStatus::kUpper + : HighsBasisStatus::kLower; } - } + }; - // handle parent rows not involved in any descendant (vanished constraints) - std::vector plusInvolved(numPlus, false); - std::vector minusInvolved(numMinus, false); - for (const auto& nr : step.newRows) { - if (nr.plusParentIdx >= 0) plusInvolved[nr.plusParentIdx] = true; - if (nr.minusParentIdx >= 0) minusInvolved[nr.minusParentIdx] = true; - } + HighsInt numNewRows = static_cast(step.newRows.size()); + HighsInt numBasicDesc = 0; + for (const auto& nr : step.newRows) + if (basis.row_status[nr.row] == HighsBasisStatus::kBasic) numBasicDesc++; + + HighsInt basicNeeded = (numPlus + numMinus - numNewRows) + numBasicDesc; + assert(basicNeeded <= numPlus + numMinus); + HighsInt basicAssigned = 0; + + // assign basic to parents with nonzero slack, non-basic otherwise for (HighsInt p = 0; p < numPlus; ++p) { - if (plusInvolved[p]) continue; double slack = computeSlack(step.plusHeaders, step.plusCoefs, step.plusEntries, p); - basis.row_status[step.plusHeaders[p].row] = - slack > tol ? HighsBasisStatus::kBasic - : HighsBasisStatus::kNonbasic; + if (slack > tol && basicAssigned < basicNeeded) { + basis.row_status[step.plusHeaders[p].row] = HighsBasisStatus::kBasic; + basicAssigned++; + } else { + setRowNonbasic(step.plusHeaders[p]); + } } for (HighsInt m = 0; m < numMinus; ++m) { - if (minusInvolved[m]) continue; double slack = computeSlack(step.minusHeaders, step.minusCoefs, step.minusEntries, m); - basis.row_status[step.minusHeaders[m].row] = - slack > tol ? HighsBasisStatus::kBasic - : HighsBasisStatus::kNonbasic; + if (slack > tol && basicAssigned < basicNeeded) { + basis.row_status[step.minusHeaders[m].row] = HighsBasisStatus::kBasic; + basicAssigned++; + } else { + setRowNonbasic(step.minusHeaders[m]); + } } - // set x_j basis status using binding row info from primal postsolve - if (binding.rowIdx >= 0) { - // a parent row is binding: that row is nonbasic (tight), x_j is basic - HighsInt bindingRow = binding.isPlus - ? step.plusHeaders[binding.rowIdx].row - : step.minusHeaders[binding.rowIdx].row; - basis.row_status[bindingRow] = HighsBasisStatus::kNonbasic; + // col is basic if between bounds, or if we still need more basic items + if (solution.col_value[col] > step.header.colLower + tol && + solution.col_value[col] < step.header.colUpper - tol) { basis.col_status[col] = HighsBasisStatus::kBasic; + basicAssigned++; + } else if (basicAssigned < basicNeeded) { + basis.col_status[col] = HighsBasisStatus::kBasic; + basicAssigned++; } else if (solution.col_value[col] <= step.header.colLower + tol) { basis.col_status[col] = HighsBasisStatus::kLower; - } else if (solution.col_value[col] >= step.header.colUpper - tol) { - basis.col_status[col] = HighsBasisStatus::kUpper; } else { - basis.col_status[col] = HighsBasisStatus::kBasic; + basis.col_status[col] = HighsBasisStatus::kUpper; + } + + // if still short, flip tight parents to basic (degenerate basic at bound) + for (HighsInt p = 0; p < numPlus && basicAssigned < basicNeeded; ++p) { + if (basis.row_status[step.plusHeaders[p].row] != + HighsBasisStatus::kBasic) { + basis.row_status[step.plusHeaders[p].row] = HighsBasisStatus::kBasic; + basicAssigned++; + } + } + for (HighsInt m = 0; m < numMinus && basicAssigned < basicNeeded; ++m) { + if (basis.row_status[step.minusHeaders[m].row] != + HighsBasisStatus::kBasic) { + basis.row_status[step.minusHeaders[m].row] = HighsBasisStatus::kBasic; + basicAssigned++; + } } + assert(basicAssigned == basicNeeded); } } From bf438ad1fd2c66f1229c838724a2f4a72c1c7338 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 13:19:23 +0200 Subject: [PATCH 148/196] Fix handling of ranged rows --- highs/presolve/HPresolve.cpp | 5 ++--- highs/presolve/HighsPostsolveStack.cpp | 26 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index f041fabf3a5..f0198fba9b7 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5950,7 +5950,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif bool tryProbing = mipsolver != nullptr; - bool tryFourierMotzkin = true; + HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; bool dependentEquationsCalled = mipsolver != nullptr; @@ -5979,8 +5979,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { applyConflictGraphSubstitutions(postsolve_stack, numDelCol)); } - if (tryFourierMotzkin && - analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) + if (analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); if (analysis_.allow_rule_[kPresolveRuleAggregator]) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 67f21387c06..fa1369bbfb7 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1610,10 +1610,10 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( }; // propagate basis from descendants to parents/col. - // basis dimension increases by (numPlus + numMinus - numNewRows). - // need that many new basic items plus one for each basic descendant - // removed. total new basic needed = (numPlus + numMinus - numNewRows) + - // numBasicDesc. + // ranged rows appear in both plus and minus but are one physical row. + // distinct parents = numPlus + numMinus - numRanged. + // basis dimension increases by (distinct parents - numNewRows). + // total new basic needed = (distinct parents - numNewRows) + numBasicDesc. auto setRowNonbasic = [&](const FmeRowHeader& h) { if (h.rowLower == h.rowUpper) basis.row_status[h.row] = HighsBasisStatus::kLower; @@ -1633,8 +1633,20 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( for (const auto& nr : step.newRows) if (basis.row_status[nr.row] == HighsBasisStatus::kBasic) numBasicDesc++; - HighsInt basicNeeded = (numPlus + numMinus - numNewRows) + numBasicDesc; - assert(basicNeeded <= numPlus + numMinus); + // mark ranged rows (appearing in both plus and minus sets) + std::vector isMinusRowRanged(numMinus, false); + HighsInt numRanged = 0; + for (HighsInt m = 0; m < numMinus; ++m) + for (HighsInt p = 0; p < numPlus; ++p) + if (step.minusHeaders[m].row == step.plusHeaders[p].row) { + isMinusRowRanged[m] = true; + numRanged++; + break; + } + + HighsInt basicNeeded = + (numPlus + numMinus - numRanged - numNewRows) + numBasicDesc; + assert(basicNeeded <= numPlus + numMinus - numRanged); HighsInt basicAssigned = 0; // assign basic to parents with nonzero slack, non-basic otherwise @@ -1649,6 +1661,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } } for (HighsInt m = 0; m < numMinus; ++m) { + if (isMinusRowRanged[m]) continue; double slack = computeSlack(step.minusHeaders, step.minusCoefs, step.minusEntries, m); if (slack > tol && basicAssigned < basicNeeded) { @@ -1682,6 +1695,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } } for (HighsInt m = 0; m < numMinus && basicAssigned < basicNeeded; ++m) { + if (isMinusRowRanged[m]) continue; if (basis.row_status[step.minusHeaders[m].row] != HighsBasisStatus::kBasic) { basis.row_status[step.minusHeaders[m].row] = HighsBasisStatus::kBasic; From ac18a6b33e02e5ea00951e3d9ae8b041a73b2c2e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 13:25:43 +0200 Subject: [PATCH 149/196] Add an assertion --- highs/presolve/HighsPostsolveStack.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index fa1369bbfb7..4855d9d0633 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1690,6 +1690,8 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( for (HighsInt p = 0; p < numPlus && basicAssigned < basicNeeded; ++p) { if (basis.row_status[step.plusHeaders[p].row] != HighsBasisStatus::kBasic) { + assert(computeSlack(step.plusHeaders, step.plusCoefs, step.plusEntries, + p) <= tol); basis.row_status[step.plusHeaders[p].row] = HighsBasisStatus::kBasic; basicAssigned++; } @@ -1698,6 +1700,8 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( if (isMinusRowRanged[m]) continue; if (basis.row_status[step.minusHeaders[m].row] != HighsBasisStatus::kBasic) { + assert(computeSlack(step.minusHeaders, step.minusCoefs, + step.minusEntries, m) <= tol); basis.row_status[step.minusHeaders[m].row] = HighsBasisStatus::kBasic; basicAssigned++; } From 4a1620022f1c1a709d639282f555a6f596f0df0d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 14:06:14 +0200 Subject: [PATCH 150/196] Comment out assertion about numbers of rows/columns in TestCAPI --- check/TestCAPI.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/check/TestCAPI.c b/check/TestCAPI.c index 6d81536cc88..a5300bb38a0 100644 --- a/check/TestCAPI.c +++ b/check/TestCAPI.c @@ -685,8 +685,9 @@ void testNames() { HighsInt presolved_num_col = Highs_getPresolvedNumCol(highs); HighsInt presolved_num_row = Highs_getPresolvedNumRow(highs); - assert(presolved_num_col == num_col); - assert(presolved_num_row == num_row-1); + // Fourier-Motzkin presolve reduction may add columns/rows + //assert(presolved_num_col == num_col); + //assert(presolved_num_row == num_row-1); char presolved_name[5]; From 251d4e05f8bd1749ecd074425912eeb26d611d8a Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 14:12:45 +0200 Subject: [PATCH 151/196] Remove unused code --- highs/presolve/HPresolve.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index f0198fba9b7..7fe62d00ef3 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6913,8 +6913,6 @@ HPresolve::Result HPresolve::fourierMotzkin( if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleFourierMotzkin); using FmeRow = HighsPostsolveStack::FmeRowData; - using FmeDescendant = HighsPostsolveStack::FmeDescendant; - using FmeNewRow = HighsPostsolveStack::FmeNewRow; using FmeAncestryEntry = HighsPostsolveStack::FmeAncestryEntry; using FmeBlockStep = HighsPostsolveStack::FmeBlockStep; From 9f2ffc77724b2fe85ba4494de4b983d0913d2475 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 14:20:57 +0200 Subject: [PATCH 152/196] Switch off FM for distillation LP --- check/TestPresolve.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index cbbad8fdd07..e665510956a 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -138,11 +138,14 @@ TEST_CASE("presolve", "[highs_test_presolve]") { // Have to set matrix dimensions to match presolved_model.lp_ lp.setMatrixDimensions(); highs.passModel(lp); + // Disable Fourier-Motzkin so this LP is not reduced + highs.setOptionValue("presolve_rule_off", 1 << kPresolveRuleFourierMotzkin); REQUIRE(highs.presolve() == HighsStatus::kOk); REQUIRE(lp.equalButForNames(presolved_model.lp_)); REQUIRE(highs.getModelPresolveStatus() == HighsPresolveStatus::kNotReduced); REQUIRE(highs.getModelStatus() == HighsModelStatus::kNotset); REQUIRE(!presolved_model.isEmpty()); + highs.setOptionValue("presolve_rule_off", 0); special_lps.primalDualInfeasible1Lp(lp, require_model_status); highs.passModel(lp); From c21cae65e8ee00f52249ab6678d41b963beb3407 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 14:50:03 +0200 Subject: [PATCH 153/196] Fix test --- check/TestCAPI.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/check/TestCAPI.c b/check/TestCAPI.c index a5300bb38a0..5a2d2aab944 100644 --- a/check/TestCAPI.c +++ b/check/TestCAPI.c @@ -678,8 +678,7 @@ void testNames() { printf("Row %" HIGHSINT_FORMAT " has name %s\n", iRow, name); } - // Check extraction of names for the presolved LP, in which the - // first row is removed + // Check extraction of names for the presolved LP Highs_presolve(highs); if (dev_run) Highs_writePresolvedModel(highs, ""); @@ -689,7 +688,7 @@ void testNames() { //assert(presolved_num_col == num_col); //assert(presolved_num_row == num_row-1); - char presolved_name[5]; + char presolved_name[512]; return_status = Highs_getPresolvedColName(highs, -1, presolved_name); assert(return_status == kHighsStatusError); @@ -1404,7 +1403,10 @@ void passPresolveGetLp() { double* presolved_row_upper = (double*)malloc(sizeof(double) * presolved_num_row); HighsInt* presolved_a_start = - (HighsInt*)malloc(sizeof(HighsInt) * (presolved_num_col + 1)); + (HighsInt*)malloc(sizeof(HighsInt) * + (presolved_a_format == kHighsMatrixFormatColwise + ? presolved_num_col + 1 + : presolved_num_row + 1)); HighsInt* presolved_a_index = (HighsInt*)malloc(sizeof(HighsInt) * presolved_num_nz); double* presolved_a_value = @@ -1431,9 +1433,9 @@ void passPresolveGetLp() { assert(return_status == kHighsStatusOk); return_status = Highs_run(local_highs); - double* col_value = (double*)malloc(sizeof(double) * num_col); - double* col_dual = (double*)malloc(sizeof(double) * num_col); - double* row_dual = (double*)malloc(sizeof(double) * num_row); + double* col_value = (double*)malloc(sizeof(double) * presolved_num_col); + double* col_dual = (double*)malloc(sizeof(double) * presolved_num_col); + double* row_dual = (double*)malloc(sizeof(double) * presolved_num_row); return_status = Highs_getSolution(local_highs, col_value, col_dual, NULL, row_dual); From 42d3c52a8fc3e2bee1f7496382181eeb50246dd2 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 24 Jun 2026 21:25:34 +0200 Subject: [PATCH 154/196] Mark col as deleted before removing rows --- highs/presolve/HPresolve.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 7fe62d00ef3..4081a04a9da 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7605,6 +7605,10 @@ HPresolve::Result HPresolve::fourierMotzkin( stepNewRows.push_back({newModelRow, pIdx, mIdx}); } + // mark column as deleted + markColDeleted(col); + ++numColsEliminated; + // remove old rows containing col (skip bound rows) for (HighsInt rp : iPlus) { if (rp < 0) continue; @@ -7620,10 +7624,6 @@ HPresolve::Result HPresolve::fourierMotzkin( ++numRowsEliminated; } - // mark column as deleted - markColDeleted(col); - ++numColsEliminated; - // update affected candidates in the heap saveAffectedCols.swap(affectedCols); for (HighsInt k : saveAffectedCols) { From c09a170273c809b00137b1151039e8979495518c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 25 Jun 2026 14:24:17 +0200 Subject: [PATCH 155/196] Fix test after merge --- check/TestSemiVariables.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/check/TestSemiVariables.cpp b/check/TestSemiVariables.cpp index 00d59b6563f..1a1cc26ee4f 100644 --- a/check/TestSemiVariables.cpp +++ b/check/TestSemiVariables.cpp @@ -337,7 +337,8 @@ TEST_CASE("3015", "[highs_test_semi_variables]") { highs.setOptionValue("output_flag", dev_run); // Disable Fourier-Motzkin presolve so that the semi-variable // infeasibility is still triggered with default mip_feasibility_tolerance - highs.setOptionValue("presolve_rule_off", 131072); + highs.setOptionValue("presolve_rule_off", + 1 << kPresolveRuleFourierMotzkin); highs.readModel(filename); HighsStatus status = highs.run(); REQUIRE(status == HighsStatus::kError); From 6fb1ce53bb9b7d15da4fbb60461f23dcb1be70c2 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 26 Jun 2026 08:54:18 +0200 Subject: [PATCH 156/196] Add lambda for logging --- highs/presolve/HPresolve.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 59877428a7f..78304fe3e96 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7416,6 +7416,16 @@ HPresolve::Result HPresolve::fourierMotzkin( {stepIndex, parentRowIndex, scale, isMinus}); }; + auto printLog = [&](HighsInt colsRemoved, HighsInt rowRemoved, + HighsInt rowsAdded) { + highsLogDev(options->log_options, HighsLogType::kInfo, + "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT + " cols and %" HIGHSINT_FORMAT + " rows, and added %" HIGHSINT_FORMAT " rows\n", + static_cast(colsRemoved), static_cast(rowRemoved), + static_cast(rowsAdded)); + }; + // workspace vectors std::vector candidates; std::vector iPlus; @@ -7482,11 +7492,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // reformulateObjective pushes other reductions onto the data stack if (!blockSteps.empty()) { postsolve_stack.fourierMotzkinBlockFinalise(blockSteps, rowAncestry); - highsLogDev(options->log_options, HighsLogType::kInfo, - "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT - " cols and %" HIGHSINT_FORMAT - " rows, and added %" HIGHSINT_FORMAT " rows\n", - numColsEliminated, numRowsEliminated, numRowsAdded); + printLog(numColsEliminated, numRowsEliminated, numRowsAdded); blockSteps.clear(); rowAncestry.clear(); numColsEliminated = 0; @@ -7679,11 +7685,7 @@ HPresolve::Result HPresolve::fourierMotzkin( postsolve_stack.fourierMotzkinBlockFinalise(blockSteps, rowAncestry); // log message - highsLogDev(options->log_options, HighsLogType::kInfo, - "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT - " cols and %" HIGHSINT_FORMAT - " rows, and added %" HIGHSINT_FORMAT " rows\n", - numColsEliminated, numRowsEliminated, numRowsAdded); + printLog(numColsEliminated, numRowsEliminated, numRowsAdded); } return finalise(); From 0bee908ae3690c3edf4def7df9166bf278f81660 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 26 Jun 2026 09:26:04 +0200 Subject: [PATCH 157/196] Use HighsHashTable --- highs/presolve/HPresolve.cpp | 13 ++++++------- highs/presolve/HighsPostsolveStack.h | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 78304fe3e96..aa5410e13f7 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7400,14 +7400,13 @@ HPresolve::Result HPresolve::fourierMotzkin( }; auto inheritAncestry = - [&](std::unordered_map>& - rowAncestry, + [&](HighsHashTable>& rowAncestry, HighsInt newModelRow, HighsInt parentRow, HighsInt parentRowIndex, HighsInt stepIndex, double scale, bool isMinus) { if (parentRow < 0) return; - auto it = rowAncestry.find(parentRow); - if (it != rowAncestry.end()) { - for (const auto& a : it->second) + auto parentAncestry = rowAncestry.find(parentRow); + if (parentAncestry) { + for (const auto& a : *parentAncestry) rowAncestry[newModelRow].push_back( {a.step, a.parentRowIndex, a.scale * scale, a.isMinus}); } @@ -7477,8 +7476,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // FM block data for postsolve std::vector blockSteps; - // maps surviving row to its ancestry (which parent rows it descends from) - std::unordered_map> rowAncestry; + // surviving row to its ancestry (which parent rows it descends from) + HighsHashTable> rowAncestry; // main loop: eliminate variables from heap while (!heap.empty()) { diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 9f45cc4d874..97eb7d03290 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -18,7 +18,6 @@ #include #include #include -#include #include #include "lp_data/HConst.h" @@ -26,6 +25,7 @@ #include "lp_data/HighsOptions.h" #include "util/HighsCDouble.h" #include "util/HighsDataStack.h" +#include "util/HighsHash.h" #include "util/HighsMatrixSlice.h" // class HighsOptions; @@ -705,7 +705,7 @@ class HighsPostsolveStack { void fourierMotzkinBlockFinalise( const std::vector& blockSteps, - const std::unordered_map>& + const HighsHashTable>& rowAncestry) { HighsInt numSteps = static_cast(blockSteps.size()); @@ -719,9 +719,9 @@ class HighsPostsolveStack { minusDescendantsAll[s].resize(blockSteps[s].numMinus); } for (const auto& entry : rowAncestry) { - HighsInt row = entry.first; + HighsInt row = entry.key(); HighsInt origRow = origRowIndex[row]; - for (const auto& a : entry.second) { + for (const auto& a : entry.value()) { if (a.isMinus) minusDescendantsAll[a.step][a.parentRowIndex].push_back( {origRow, a.scale}); From 77965b74d0ca61abceffdfa97945a1918cde90d4 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 26 Jun 2026 09:52:21 +0200 Subject: [PATCH 158/196] Fix log message --- highs/presolve/HPresolve.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index aa5410e13f7..7bdaef872c3 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7415,14 +7415,13 @@ HPresolve::Result HPresolve::fourierMotzkin( {stepIndex, parentRowIndex, scale, isMinus}); }; - auto printLog = [&](HighsInt colsRemoved, HighsInt rowRemoved, + auto printLog = [&](HighsInt colsRemoved, HighsInt rowsRemoved, HighsInt rowsAdded) { highsLogDev(options->log_options, HighsLogType::kInfo, "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT " cols and %" HIGHSINT_FORMAT " rows, and added %" HIGHSINT_FORMAT " rows\n", - static_cast(colsRemoved), static_cast(rowRemoved), - static_cast(rowsAdded)); + colsRemoved, rowsRemoved, rowsAdded); }; // workspace vectors From fbd05e2dd077d96609108a24143c45646746649e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 26 Jun 2026 10:22:03 +0200 Subject: [PATCH 159/196] Switch back to unordered_map --- highs/presolve/HPresolve.cpp | 11 ++++++----- highs/presolve/HighsPostsolveStack.h | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 7bdaef872c3..6cf788e06a6 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7400,13 +7400,14 @@ HPresolve::Result HPresolve::fourierMotzkin( }; auto inheritAncestry = - [&](HighsHashTable>& rowAncestry, + [&](std::unordered_map>& + rowAncestry, HighsInt newModelRow, HighsInt parentRow, HighsInt parentRowIndex, HighsInt stepIndex, double scale, bool isMinus) { if (parentRow < 0) return; - auto parentAncestry = rowAncestry.find(parentRow); - if (parentAncestry) { - for (const auto& a : *parentAncestry) + auto it = rowAncestry.find(parentRow); + if (it != rowAncestry.end()) { + for (const auto& a : it->second) rowAncestry[newModelRow].push_back( {a.step, a.parentRowIndex, a.scale * scale, a.isMinus}); } @@ -7476,7 +7477,7 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector blockSteps; // surviving row to its ancestry (which parent rows it descends from) - HighsHashTable> rowAncestry; + std::unordered_map> rowAncestry; // main loop: eliminate variables from heap while (!heap.empty()) { diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 97eb7d03290..9f45cc4d874 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "lp_data/HConst.h" @@ -25,7 +26,6 @@ #include "lp_data/HighsOptions.h" #include "util/HighsCDouble.h" #include "util/HighsDataStack.h" -#include "util/HighsHash.h" #include "util/HighsMatrixSlice.h" // class HighsOptions; @@ -705,7 +705,7 @@ class HighsPostsolveStack { void fourierMotzkinBlockFinalise( const std::vector& blockSteps, - const HighsHashTable>& + const std::unordered_map>& rowAncestry) { HighsInt numSteps = static_cast(blockSteps.size()); @@ -719,9 +719,9 @@ class HighsPostsolveStack { minusDescendantsAll[s].resize(blockSteps[s].numMinus); } for (const auto& entry : rowAncestry) { - HighsInt row = entry.key(); + HighsInt row = entry.first; HighsInt origRow = origRowIndex[row]; - for (const auto& a : entry.value()) { + for (const auto& a : entry.second) { if (a.isMinus) minusDescendantsAll[a.step][a.parentRowIndex].push_back( {origRow, a.scale}); From 60e00c652e22692d26c4d53ff39db9c4781c1956 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 26 Jun 2026 11:20:48 +0200 Subject: [PATCH 160/196] Redundancy checks --- highs/presolve/HPresolve.cpp | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 6cf788e06a6..602d1cf1d21 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7206,6 +7206,38 @@ HPresolve::Result HPresolve::fourierMotzkin( return neRed > 0 || (neRed == 0 && mrRed > 0); }; + auto mergeOriginals = + [&kUpperBoundRow, &kLowerBoundRow]( + const std::unordered_map>& rowOriginals, + HighsInt plusRow, HighsInt minusRow, + HighsInt col) -> std::set { + std::set merged; + if (plusRow == kUpperBoundRow) + merged.insert(-(2 * col + 1)); + else if (plusRow == kLowerBoundRow) + merged.insert(-(2 * col + 2)); + else { + auto itP = rowOriginals.find(plusRow); + if (itP != rowOriginals.end()) + merged.insert(itP->second.begin(), itP->second.end()); + else + merged.insert(plusRow); + } + if (minusRow == kUpperBoundRow) + merged.insert(-(2 * col + 1)); + else if (minusRow == kLowerBoundRow) + merged.insert(-(2 * col + 2)); + else { + auto itM = rowOriginals.find(minusRow); + if (itM != rowOriginals.end()) + merged.insert(itM->second.begin(), itM->second.end()); + else + merged.insert(minusRow); + } + return merged; + }; + + // reformulate objective as a constraint: min c^T x + offset becomes // min z with c^T x - z <= -offset. this allows FME to eliminate // continuous columns with nonzero cost. @@ -7479,6 +7511,9 @@ HPresolve::Result HPresolve::fourierMotzkin( // surviving row to its ancestry (which parent rows it descends from) std::unordered_map> rowAncestry; + // distinct original parent rows for each derived row (Cernikov check) + std::unordered_map> rowOriginals; + // main loop: eliminate variables from heap while (!heap.empty()) { HighsInt col = heap[0].col; @@ -7494,6 +7529,7 @@ HPresolve::Result HPresolve::fourierMotzkin( printLog(numColsEliminated, numRowsEliminated, numRowsAdded); blockSteps.clear(); rowAncestry.clear(); + rowOriginals.clear(); numColsEliminated = 0; numRowsEliminated = 0; numRowsAdded = 0; @@ -7581,6 +7617,12 @@ HPresolve::Result HPresolve::fourierMotzkin( HPRESOLVE_CHECKED_CALL(checkNewRow(nr, redundant)); if (redundant) continue; + // Cernikov redundancy check + auto merged = + mergeOriginals(rowOriginals, nr.plusIndex, nr.minusIndex, col); + if (static_cast(merged.size()) > numColsEliminated + 1) + continue; + std::vector entries; entries.reserve(nr.entries.size()); for (const auto& e : nr.entries) @@ -7626,6 +7668,8 @@ HPresolve::Result HPresolve::fourierMotzkin( origin.plusScale, false); inheritAncestry(rowAncestry, newModelRow, origin.minusRow, mIdx, stepIdx, origin.minusScale, true); + rowOriginals[newModelRow] = + mergeOriginals(rowOriginals, origin.plusRow, origin.minusRow, col); stepNewRows.push_back({newModelRow, pIdx, mIdx}); } @@ -7637,12 +7681,14 @@ HPresolve::Result HPresolve::fourierMotzkin( for (HighsInt rp : iPlus) { if (rp < 0) continue; rowAncestry.erase(rp); + rowOriginals.erase(rp); removeRow(rp); ++numRowsEliminated; } for (HighsInt rm : iMinus) { if (rm < 0) continue; rowAncestry.erase(rm); + rowOriginals.erase(rm); if (rowDeleted[rm]) continue; removeRow(rm); ++numRowsEliminated; From b3463e0dadb6378445a7c03c07bc7dd41b5e801f Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Fri, 26 Jun 2026 12:53:18 +0100 Subject: [PATCH 161/196] Added specific FM test to TestPresolveRules.cpp and reworded FM reduciton statement --- check/TestPresolveRules.cpp | 36 ++++++++++++++++++++++++++++ highs/presolve/HPresolve.cpp | 8 +++---- highs/presolve/HPresolve.h | 1 + highs/presolve/HPresolveAnalysis.cpp | 4 ++-- highs/presolve/HPresolveTest.cpp | 21 ++++++++++++++++ 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 6a1717b72fe..6b665f6c2ae 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -79,6 +79,41 @@ TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { h.resetGlobalScheduler(true); } +TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { + + HighsLp lp; + + Highs h; + // h.setOptionValue("output_flag", dev_run); + h.setOptionValue("presolve_rule_test", kPresolveRuleFourierMotzkin); + h.setOptionValue("presolve_rule_logging", true); + h.setOptionValue("log_dev_level", 1); + + // From + lp.num_col_ = 4; + lp.num_row_ = 3; + + lp.col_cost_.assign(lp.num_col_, 0); + lp.col_lower_.assign(lp.num_col_, 0); + lp.col_upper_.assign(lp.num_col_, kHighsInf); + lp.col_upper_[0] = 40.0; + + lp.row_lower_.assign(lp.num_row_, -kHighsInf); + lp.row_upper_ = {-30, 50, 40}; + lp.a_matrix_.format_ = MatrixFormat::kRowwise; + lp.a_matrix_.start_ = {0, 3, 6, 9}; + lp.a_matrix_.index_ = { 0, 1, 3, 1, 2, 3, 1, 2, 3}; + lp.a_matrix_.value_ = {-1, 1, -1, 2, 1, 2, 3, -1, 3}; + + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + + presolveOffOn("test-fourier-motzkin", lp, h); + + h.resetGlobalScheduler(true); + +} + + void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, const HighsInt require_presolved_model_num_col, const HighsInt require_presolved_model_num_row, @@ -145,3 +180,4 @@ void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, } } } + diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 6cf788e06a6..72be2b99e3a 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7419,10 +7419,10 @@ HPresolve::Result HPresolve::fourierMotzkin( auto printLog = [&](HighsInt colsRemoved, HighsInt rowsRemoved, HighsInt rowsAdded) { highsLogDev(options->log_options, HighsLogType::kInfo, - "Fourier-Motzkin eliminated %" HIGHSINT_FORMAT - " cols and %" HIGHSINT_FORMAT - " rows, and added %" HIGHSINT_FORMAT " rows\n", - colsRemoved, rowsRemoved, rowsAdded); + "Fourier-Motzkin added %" HIGHSINT_FORMAT " rows and eliminated %" HIGHSINT_FORMAT + " rows and %" HIGHSINT_FORMAT + " columns\n", + rowsAdded, rowsRemoved, colsRemoved); }; // workspace vectors diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 18580ff9896..f6230687d7b 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -518,6 +518,7 @@ class HPresolve { Result presolveRuleTest(HighsPostsolveStack& postsolve_stack); Result presolveRuleTestColStuffing(HighsPostsolveStack& postsolve_stack); + Result presolveRuleTestFourierMotzkin(HighsPostsolveStack& postsolve_stack); // Not currently called static void debug(const HighsLp& lp, const HighsOptions& options); diff --git a/highs/presolve/HPresolveAnalysis.cpp b/highs/presolve/HPresolveAnalysis.cpp index a08d278cdef..4ecdd3d676e 100644 --- a/highs/presolve/HPresolveAnalysis.cpp +++ b/highs/presolve/HPresolveAnalysis.cpp @@ -48,7 +48,7 @@ void HPresolveAnalysis::setup(const HighsLp* model_, if (!allow || (!options->presolve_rule_off && options_->log_dev_level)) highsLogUser(options->log_options, HighsLogType::kInfo, - " Rule %2d (set bit %2d = %6d): %s\n", + " Rule %2d (set bit %2d = %7d): %s\n", int(rule_type), int(rule_type), int(bit), utilPresolveRuleTypeToString(rule_type).c_str()); } else if (!allow && !silent) { @@ -56,7 +56,7 @@ void HPresolveAnalysis::setup(const HighsLp* model_, // attempt is made, don't allow it to be off and comment // negatively highsLogUser(options->log_options, HighsLogType::kWarning, - "Cannot disallow rule %2d (bit %2d = %5d): %s\n", + "Cannot disallow rule %2d (bit %2d = %7d): %s\n", int(rule_type), int(rule_type), int(bit), utilPresolveRuleTypeToString(rule_type).c_str()); } diff --git a/highs/presolve/HPresolveTest.cpp b/highs/presolve/HPresolveTest.cpp index 77a24d1e98d..304f54a9238 100644 --- a/highs/presolve/HPresolveTest.cpp +++ b/highs/presolve/HPresolveTest.cpp @@ -14,9 +14,12 @@ 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 == kPresolveRuleFourierMotzkin) { + return presolveRuleTestFourierMotzkin(postsolve_stack); } return Result::kOk; } + HPresolve::Result HPresolve::presolveRuleTestColStuffing( HighsPostsolveStack& postsolve_stack) { assert(options->presolve_rule_test == kPresolveRuleColStuffing); @@ -36,4 +39,22 @@ HPresolve::Result HPresolve::presolveRuleTestColStuffing( // Possibly remove the row return rowPresolve(postsolve_stack, 0); } + +HPresolve::Result HPresolve::presolveRuleTestFourierMotzkin( + HighsPostsolveStack& postsolve_stack) { + assert(options->presolve_rule_test == kPresolveRuleFourierMotzkin); + highsLogUser(options->log_options, HighsLogType::kInfo, + "HPresolve::presolveRuleTestFourierMotzkin\n"); + + HPresolve::Result result = fourierMotzkin(postsolve_stack); + if (result != Result::kOk) return result; + + highsLogUser(options->log_options, HighsLogType::kInfo, + "HPresolve::presolveRuleTestFourierMotzkin: Removed %d " + "rows and %d columns\n", + int(numDeletedRows), int(numDeletedCols)); + // Possibly remove the row + // result = rowPresolve(postsolve_stack, 0); + return result; +} } // namespace presolve From a042db8de7b33324c31b11a9feaa2c57edbe2c98 Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Fri, 26 Jun 2026 13:29:16 +0100 Subject: [PATCH 162/196] Merged latest into this branch --- check/TestPresolveRules.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 6b665f6c2ae..47b25769740 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -107,7 +107,14 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { REQUIRE(h.passModel(lp) == HighsStatus::kOk); - presolveOffOn("test-fourier-motzkin", lp, h); + presolveOffOn("FM example from paper", lp, h); + + lp.col_upper_[0] = 5.0; + lp.row_upper_ = {-30, 75, 50}; + + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + + presolveOffOn("FM example from paper - tightened", lp, h); h.resetGlobalScheduler(true); From a212fe3c4d1fe54edbe9c8a9fe01b6ad80653262 Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Fri, 26 Jun 2026 13:44:42 +0100 Subject: [PATCH 163/196] Constructed simple example giving simplex iteration after postsolve --- check/TestPresolveRules.cpp | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 47b25769740..51559a01e7f 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -93,7 +93,18 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { lp.num_col_ = 4; lp.num_row_ = 3; - lp.col_cost_.assign(lp.num_col_, 0); + const bool zero_cost = false; + HighsInt require_presolved_model_num_col = 0; + HighsInt require_presolved_model_num_row = 0; + HighsInt require_presolved_model_num_nz = 0; + if (zero_cost) { + lp.col_cost_.assign(lp.num_col_, 0); + } else { + lp.col_cost_ = {1, 2, 3, 4}; + require_presolved_model_num_col = 1; + require_presolved_model_num_row = 8; + require_presolved_model_num_nz = 8; + } lp.col_lower_.assign(lp.num_col_, 0); lp.col_upper_.assign(lp.num_col_, kHighsInf); lp.col_upper_[0] = 40.0; @@ -105,16 +116,18 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { lp.a_matrix_.index_ = { 0, 1, 3, 1, 2, 3, 1, 2, 3}; lp.a_matrix_.value_ = {-1, 1, -1, 2, 1, 2, 3, -1, 3}; - REQUIRE(h.passModel(lp) == HighsStatus::kOk); - - presolveOffOn("FM example from paper", lp, h); + // REQUIRE(h.passModel(lp) == HighsStatus::kOk); + // presolveOffOn("FM example from paper", lp, h); lp.col_upper_[0] = 5.0; lp.row_upper_ = {-30, 75, 50}; REQUIRE(h.passModel(lp) == HighsStatus::kOk); - presolveOffOn("FM example from paper - tightened", lp, h); + presolveOffOn("FM example from paper - tightened", lp, h, + require_presolved_model_num_col, + require_presolved_model_num_row, + require_presolved_model_num_nz); h.resetGlobalScheduler(true); @@ -180,7 +193,8 @@ void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); REQUIRE(h.getInfo().num_primal_infeasibilities == 0); REQUIRE(h.getInfo().num_dual_infeasibilities == 0); - REQUIRE(h.getInfo().simplex_iteration_count == 0); + if (reduce_to_empty) + 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); From 329f0ee64a30a76bf055ef5e70111cd6ce725e20 Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Fri, 26 Jun 2026 13:54:04 +0100 Subject: [PATCH 164/196] Derived failing FM postsolve test --- check/TestPresolveRules.cpp | 62 +++++++++++++++++------------------- check/TestSemiVariables.cpp | 3 +- highs/presolve/HPresolve.cpp | 6 ++-- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 51559a01e7f..20757fbd6b4 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -80,59 +80,57 @@ TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { } TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { - - HighsLp lp; - Highs h; // h.setOptionValue("output_flag", dev_run); h.setOptionValue("presolve_rule_test", kPresolveRuleFourierMotzkin); h.setOptionValue("presolve_rule_logging", true); h.setOptionValue("log_dev_level", 1); - - // From + + // From + HighsLp lp; + lp.num_col_ = 4; lp.num_row_ = 3; - - const bool zero_cost = false; - HighsInt require_presolved_model_num_col = 0; - HighsInt require_presolved_model_num_row = 0; - HighsInt require_presolved_model_num_nz = 0; - if (zero_cost) { - lp.col_cost_.assign(lp.num_col_, 0); - } else { - lp.col_cost_ = {1, 2, 3, 4}; - require_presolved_model_num_col = 1; - require_presolved_model_num_row = 8; - require_presolved_model_num_nz = 8; - } + + lp.col_cost_.assign(lp.num_col_, 0); lp.col_lower_.assign(lp.num_col_, 0); lp.col_upper_.assign(lp.num_col_, kHighsInf); lp.col_upper_[0] = 40.0; - + lp.row_lower_.assign(lp.num_row_, -kHighsInf); lp.row_upper_ = {-30, 50, 40}; lp.a_matrix_.format_ = MatrixFormat::kRowwise; lp.a_matrix_.start_ = {0, 3, 6, 9}; - lp.a_matrix_.index_ = { 0, 1, 3, 1, 2, 3, 1, 2, 3}; - lp.a_matrix_.value_ = {-1, 1, -1, 2, 1, 2, 3, -1, 3}; - - // REQUIRE(h.passModel(lp) == HighsStatus::kOk); - // presolveOffOn("FM example from paper", lp, h); + lp.a_matrix_.index_ = {0, 1, 3, 1, 2, 3, 1, 2, 3}; + lp.a_matrix_.value_ = {-1, 1, -1, 2, 1, 2, 3, -1, 3}; + + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + presolveOffOn("FM example from paper", lp, h); lp.col_upper_[0] = 5.0; lp.row_upper_ = {-30, 75, 50}; REQUIRE(h.passModel(lp) == HighsStatus::kOk); - presolveOffOn("FM example from paper - tightened", lp, h, - require_presolved_model_num_col, - require_presolved_model_num_row, - require_presolved_model_num_nz); + presolveOffOn("FM example from paper - tightened", lp, h); - h.resetGlobalScheduler(true); + lp.col_cost_ = {1, 2, 3, 4}; -} + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + const bool run_failing_test = false; + if (run_failing_test) { + HighsInt require_presolved_model_num_col = 1; + HighsInt require_presolved_model_num_row = 8; + HighsInt require_presolved_model_num_nz = 8; + presolveOffOn("FM example from paper - tightened and with costs", lp, h, + require_presolved_model_num_col, + require_presolved_model_num_row, + require_presolved_model_num_nz); + } + + h.resetGlobalScheduler(true); +} void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, const HighsInt require_presolved_model_num_col, @@ -193,12 +191,10 @@ void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); REQUIRE(h.getInfo().num_primal_infeasibilities == 0); REQUIRE(h.getInfo().num_dual_infeasibilities == 0); - if (reduce_to_empty) - REQUIRE(h.getInfo().simplex_iteration_count == 0); + if (reduce_to_empty) 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); } } } - diff --git a/check/TestSemiVariables.cpp b/check/TestSemiVariables.cpp index 1a1cc26ee4f..0443d7eee4f 100644 --- a/check/TestSemiVariables.cpp +++ b/check/TestSemiVariables.cpp @@ -337,8 +337,7 @@ TEST_CASE("3015", "[highs_test_semi_variables]") { highs.setOptionValue("output_flag", dev_run); // Disable Fourier-Motzkin presolve so that the semi-variable // infeasibility is still triggered with default mip_feasibility_tolerance - highs.setOptionValue("presolve_rule_off", - 1 << kPresolveRuleFourierMotzkin); + highs.setOptionValue("presolve_rule_off", 1 << kPresolveRuleFourierMotzkin); highs.readModel(filename); HighsStatus status = highs.run(); REQUIRE(status == HighsStatus::kError); diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 2702d62a25b..e8ebd31e806 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7423,9 +7423,9 @@ HPresolve::Result HPresolve::fourierMotzkin( auto printLog = [&](HighsInt colsRemoved, HighsInt rowsRemoved, HighsInt rowsAdded) { highsLogDev(options->log_options, HighsLogType::kInfo, - "Fourier-Motzkin added %" HIGHSINT_FORMAT " rows and eliminated %" HIGHSINT_FORMAT - " rows and %" HIGHSINT_FORMAT - " columns\n", + "Fourier-Motzkin added %" HIGHSINT_FORMAT + " rows and eliminated %" HIGHSINT_FORMAT + " rows and %" HIGHSINT_FORMAT " columns\n", rowsAdded, rowsRemoved, colsRemoved); }; From 76d3278662d81bd57ae66856955ba06e4fb04496 Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Fri, 26 Jun 2026 14:08:26 +0100 Subject: [PATCH 165/196] Cleaned up; formatted --- check/TestPresolveRules.cpp | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 20757fbd6b4..bb169939f90 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -4,7 +4,7 @@ #include "Highs.h" #include "catch.hpp" -const bool dev_run = false; +const bool dev_run = true; void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, const HighsInt require_presolved_model_num_col = 0, @@ -86,7 +86,13 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { h.setOptionValue("presolve_rule_logging", true); h.setOptionValue("log_dev_level", 1); - // From + const bool lp0 = true; + const bool lp1 = true; // Makes eliminations marginal, and leaves x2=0 + const bool lp2 = false; // Failing test + + // From "A novel linear optimization presolve technique based on + // Fourier-Motzkin elimination", Zhang, Ploskas and Sahinidis, + // Mathematical Programming Computation (2026) 18:345–378 HighsLp lp; lp.num_col_ = 4; @@ -104,22 +110,24 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { lp.a_matrix_.index_ = {0, 1, 3, 1, 2, 3, 1, 2, 3}; lp.a_matrix_.value_ = {-1, 1, -1, 2, 1, 2, 3, -1, 3}; - REQUIRE(h.passModel(lp) == HighsStatus::kOk); - presolveOffOn("FM example from paper", lp, h); + if (lp0) { + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + presolveOffOn("FM example from paper", lp, h); + } lp.col_upper_[0] = 5.0; lp.row_upper_ = {-30, 75, 50}; - REQUIRE(h.passModel(lp) == HighsStatus::kOk); - - presolveOffOn("FM example from paper - tightened", lp, h); + if (lp1) { + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + presolveOffOn("FM example from paper - tightened", lp, h); + } lp.col_cost_ = {1, 2, 3, 4}; REQUIRE(h.passModel(lp) == HighsStatus::kOk); - const bool run_failing_test = false; - if (run_failing_test) { + if (lp2) { HighsInt require_presolved_model_num_col = 1; HighsInt require_presolved_model_num_row = 8; HighsInt require_presolved_model_num_nz = 8; From 91bdc7bd2fe1a0744d99815ebf4f480e8139275e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 26 Jun 2026 15:17:58 +0200 Subject: [PATCH 166/196] Fix check --- highs/presolve/HPresolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 602d1cf1d21..070a7789553 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7620,7 +7620,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // Cernikov redundancy check auto merged = mergeOriginals(rowOriginals, nr.plusIndex, nr.minusIndex, col); - if (static_cast(merged.size()) > numColsEliminated + 1) + if (static_cast(merged.size()) > numColsEliminated + 2) continue; std::vector entries; From 0c04422cf96523f2b1c825a8bb60af78a17ae318 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 29 Jun 2026 11:43:05 +0200 Subject: [PATCH 167/196] Fix basis postsolve --- check/TestPresolveRules.cpp | 2 +- highs/presolve/HighsPostsolveStack.cpp | 225 ++++++++++++++++--------- 2 files changed, 147 insertions(+), 80 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index bb169939f90..a1052161808 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -88,7 +88,7 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { const bool lp0 = true; const bool lp1 = true; // Makes eliminations marginal, and leaves x2=0 - const bool lp2 = false; // Failing test + const bool lp2 = true; // Failing test // From "A novel linear optimization presolve technique based on // Fourier-Motzkin elimination", Zhang, Ploskas and Sahinidis, diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 4855d9d0633..88615baf954 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1490,6 +1490,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( const std::vector& steps, const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) { const double tol = options.mip_feasibility_tolerance; + const double dual_tol = options.dual_feasibility_tolerance; HighsInt numSteps = static_cast(steps.size()); @@ -1581,53 +1582,86 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( solution.col_dual[col] = static_cast(colDual); } - // basis postsolve (Algorithm 5): process in reverse elimination order + // basis postsolve: use dual solution to determine basis status if (!basis.valid) return; + const bool debug_print = options.log_dev_level > 0; + + // Pre-compute lower and upper slacks for each row + auto computeSlacks = + [&](HighsInt col, const std::vector& headers, + const std::vector& coefs, + const std::vector>& entries, + std::vector& lowerSlacks, std::vector& upperSlacks) { + HighsInt n = static_cast(headers.size()); + lowerSlacks.resize(n); + upperSlacks.resize(n); + for (HighsInt r = 0; r < n; ++r) { + HighsCDouble activity = + static_cast(coefs[r]) * solution.col_value[col]; + for (const auto& nz : entries[r]) + activity += static_cast(nz.value) * + solution.col_value[nz.index]; + double act = static_cast(activity); + lowerSlacks[r] = headers[r].rowLower != -kHighsInf + ? act - headers[r].rowLower + : kHighsInf; + upperSlacks[r] = headers[r].rowUpper != kHighsInf + ? headers[r].rowUpper - act + : kHighsInf; + } + }; + + // A row must be basic if it has zero dual and activity strictly + // between bounds (complementary slackness) + auto rowMustBeBasic = [&](HighsInt row, double lowerSlack, + double upperSlack) { + return std::abs(solution.row_dual[row]) <= dual_tol && lowerSlack > tol && + upperSlack > tol; + }; + + // Minimum slack scaled by coefficient (for assertions in pass 4) + auto computeSlack = [&](double lowerSlack, double upperSlack, double coef) { + return std::min(lowerSlack, upperSlack) / std::abs(coef); + }; + + // Flip tight nonbasic row to basic (degenerate) + auto forceRowBasic = [&](HighsInt row, double lowerSlack, double upperSlack, + double coef, HighsInt& basicAssigned) { + if (basis.row_status[row] == HighsBasisStatus::kBasic) return; + assert(std::abs(solution.row_dual[row]) <= dual_tol); + assert(computeSlack(lowerSlack, upperSlack, coef) <= tol); + basis.row_status[row] = HighsBasisStatus::kBasic; + basicAssigned++; + }; + + // Assign row as basic (if zero dual and budget allows) or non-basic + auto assignRowStatus = [&](HighsInt row, double lowerSlack, double upperSlack, + HighsInt& basicAssigned, HighsInt basicNeeded) { + if (basis.row_status[row] == HighsBasisStatus::kBasic) return; + if (std::abs(solution.row_dual[row]) <= dual_tol && + basicAssigned < basicNeeded) { + basis.row_status[row] = HighsBasisStatus::kBasic; + basicAssigned++; + } else { + double dual = solution.row_dual[row]; + if (dual > dual_tol) + basis.row_status[row] = HighsBasisStatus::kLower; + else if (dual < -dual_tol) + basis.row_status[row] = HighsBasisStatus::kUpper; + else + basis.row_status[row] = upperSlack < lowerSlack + ? HighsBasisStatus::kUpper + : HighsBasisStatus::kLower; + } + }; + for (HighsInt s = numSteps - 1; s >= 0; --s) { const auto& step = steps[s]; HighsInt col = step.header.col; HighsInt numPlus = step.header.numPlus; HighsInt numMinus = step.header.numMinus; - // compute row activity for determining row basis status - auto computeSlack = [&](const std::vector& headers, - const std::vector& coefs, - const std::vector>& entries, - HighsInt r) -> double { - HighsCDouble activity = - static_cast(coefs[r]) * solution.col_value[col]; - for (const auto& nz : entries[r]) - activity += - static_cast(nz.value) * solution.col_value[nz.index]; - double act = static_cast(activity); - double rawSlack = kHighsInf; - if (headers[r].rowUpper != kHighsInf) - rawSlack = std::min(rawSlack, headers[r].rowUpper - act); - if (headers[r].rowLower != -kHighsInf) - rawSlack = std::min(rawSlack, act - headers[r].rowLower); - return rawSlack / std::abs(coefs[r]); - }; - - // propagate basis from descendants to parents/col. - // ranged rows appear in both plus and minus but are one physical row. - // distinct parents = numPlus + numMinus - numRanged. - // basis dimension increases by (distinct parents - numNewRows). - // total new basic needed = (distinct parents - numNewRows) + numBasicDesc. - auto setRowNonbasic = [&](const FmeRowHeader& h) { - if (h.rowLower == h.rowUpper) - basis.row_status[h.row] = HighsBasisStatus::kLower; - else if (h.rowLower == -kHighsInf) - basis.row_status[h.row] = HighsBasisStatus::kUpper; - else if (h.rowUpper == kHighsInf) - basis.row_status[h.row] = HighsBasisStatus::kLower; - else { - basis.row_status[h.row] = solution.row_dual[h.row] < 0 - ? HighsBasisStatus::kUpper - : HighsBasisStatus::kLower; - } - }; - HighsInt numNewRows = static_cast(step.newRows.size()); HighsInt numBasicDesc = 0; for (const auto& nr : step.newRows) @@ -1646,67 +1680,100 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( HighsInt basicNeeded = (numPlus + numMinus - numRanged - numNewRows) + numBasicDesc; - assert(basicNeeded <= numPlus + numMinus - numRanged); HighsInt basicAssigned = 0; - // assign basic to parents with nonzero slack, non-basic otherwise + std::vector plusLowerSlack, plusUpperSlack; + std::vector minusLowerSlack, minusUpperSlack; + computeSlacks(col, step.plusHeaders, step.plusCoefs, step.plusEntries, + plusLowerSlack, plusUpperSlack); + computeSlacks(col, step.minusHeaders, step.minusCoefs, step.minusEntries, + minusLowerSlack, minusUpperSlack); + + // Determine col status + bool colMustBeBasic = + solution.col_value[col] > step.header.colLower + tol && + solution.col_value[col] < step.header.colUpper - tol; + bool colCanBeBasic = + colMustBeBasic || std::abs(solution.col_dual[col]) <= dual_tol; + + if (debug_print) + printf( + "FM basis step %d: col=%d val=%.6g lb=%.6g ub=%.6g dual=%.6g " + "mustBasic=%d canBasic=%d basicNeeded=%d " + "numPlus=%d numMinus=%d numRanged=%d numNewRows=%d numBasicDesc=%d\n", + int(s), int(col), solution.col_value[col], step.header.colLower, + step.header.colUpper, solution.col_dual[col], int(colMustBeBasic), + int(colCanBeBasic), int(basicNeeded), int(numPlus), int(numMinus), + int(numRanged), int(numNewRows), int(numBasicDesc)); + + // Pass 1: assign all must-be-basic (col and rows) + if (colMustBeBasic) { + basis.col_status[col] = HighsBasisStatus::kBasic; + basicAssigned++; + } for (HighsInt p = 0; p < numPlus; ++p) { - double slack = - computeSlack(step.plusHeaders, step.plusCoefs, step.plusEntries, p); - if (slack > tol && basicAssigned < basicNeeded) { + if (rowMustBeBasic(step.plusHeaders[p].row, plusLowerSlack[p], + plusUpperSlack[p])) { basis.row_status[step.plusHeaders[p].row] = HighsBasisStatus::kBasic; basicAssigned++; - } else { - setRowNonbasic(step.plusHeaders[p]); } } for (HighsInt m = 0; m < numMinus; ++m) { if (isMinusRowRanged[m]) continue; - double slack = computeSlack(step.minusHeaders, step.minusCoefs, - step.minusEntries, m); - if (slack > tol && basicAssigned < basicNeeded) { + if (rowMustBeBasic(step.minusHeaders[m].row, minusLowerSlack[m], + minusUpperSlack[m])) { basis.row_status[step.minusHeaders[m].row] = HighsBasisStatus::kBasic; basicAssigned++; - } else { - setRowNonbasic(step.minusHeaders[m]); } } - // col is basic if between bounds, or if we still need more basic items - if (solution.col_value[col] > step.header.colLower + tol && - solution.col_value[col] < step.header.colUpper - tol) { - basis.col_status[col] = HighsBasisStatus::kBasic; - basicAssigned++; - } else if (basicAssigned < basicNeeded) { - basis.col_status[col] = HighsBasisStatus::kBasic; - basicAssigned++; - } else if (solution.col_value[col] <= step.header.colLower + tol) { - basis.col_status[col] = HighsBasisStatus::kLower; - } else { - basis.col_status[col] = HighsBasisStatus::kUpper; - } + if (debug_print) + printf(" after must-be-basic: basicAssigned=%d/%d (col %s)\n", + int(basicAssigned), int(basicNeeded), + colMustBeBasic ? "BASIC" : "pending"); - // if still short, flip tight parents to basic (degenerate basic at bound) - for (HighsInt p = 0; p < numPlus && basicAssigned < basicNeeded; ++p) { - if (basis.row_status[step.plusHeaders[p].row] != - HighsBasisStatus::kBasic) { - assert(computeSlack(step.plusHeaders, step.plusCoefs, step.plusEntries, - p) <= tol); - basis.row_status[step.plusHeaders[p].row] = HighsBasisStatus::kBasic; + // Pass 2: assign can-be-basic col (if not already assigned) + if (!colMustBeBasic) { + if (colCanBeBasic && basicAssigned < basicNeeded) { + basis.col_status[col] = HighsBasisStatus::kBasic; basicAssigned++; + } else if (solution.col_value[col] <= step.header.colLower + tol) { + basis.col_status[col] = HighsBasisStatus::kLower; + } else { + basis.col_status[col] = HighsBasisStatus::kUpper; } } + + if (debug_print) + printf(" col %d -> %s (basicAssigned=%d)\n", int(col), + basis.col_status[col] == HighsBasisStatus::kBasic ? "BASIC" + : basis.col_status[col] == HighsBasisStatus::kLower ? "LOWER" + : "UPPER", + int(basicAssigned)); + + // Pass 3: assign can-be-basic rows (zero dual, at bound) + for (HighsInt p = 0; p < numPlus; ++p) + assignRowStatus(step.plusHeaders[p].row, plusLowerSlack[p], + plusUpperSlack[p], basicAssigned, basicNeeded); + for (HighsInt m = 0; m < numMinus; ++m) { + if (isMinusRowRanged[m]) continue; + assignRowStatus(step.minusHeaders[m].row, minusLowerSlack[m], + minusUpperSlack[m], basicAssigned, basicNeeded); + } + + // Pass 4: if still short, flip tight non-basic rows to basic (degenerate) + for (HighsInt p = 0; p < numPlus && basicAssigned < basicNeeded; ++p) + forceRowBasic(step.plusHeaders[p].row, plusLowerSlack[p], + plusUpperSlack[p], step.plusCoefs[p], basicAssigned); for (HighsInt m = 0; m < numMinus && basicAssigned < basicNeeded; ++m) { if (isMinusRowRanged[m]) continue; - if (basis.row_status[step.minusHeaders[m].row] != - HighsBasisStatus::kBasic) { - assert(computeSlack(step.minusHeaders, step.minusCoefs, - step.minusEntries, m) <= tol); - basis.row_status[step.minusHeaders[m].row] = HighsBasisStatus::kBasic; - basicAssigned++; - } + forceRowBasic(step.minusHeaders[m].row, minusLowerSlack[m], + minusUpperSlack[m], step.minusCoefs[m], basicAssigned); } - assert(basicAssigned == basicNeeded); + + if (debug_print && basicAssigned != basicNeeded) + printf("FM basis step %d: col=%d basicAssigned=%d != basicNeeded=%d\n", + int(s), int(col), int(basicAssigned), int(basicNeeded)); } } From 30659411261834ffd92afb078138f2d7776369ac Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 29 Jun 2026 15:43:15 +0200 Subject: [PATCH 168/196] Remove debug printfs --- highs/presolve/HighsPostsolveStack.cpp | 30 +------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 88615baf954..e7b2b825c46 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1585,8 +1585,6 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( // basis postsolve: use dual solution to determine basis status if (!basis.valid) return; - const bool debug_print = options.log_dev_level > 0; - // Pre-compute lower and upper slacks for each row auto computeSlacks = [&](HighsInt col, const std::vector& headers, @@ -1696,16 +1694,6 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( bool colCanBeBasic = colMustBeBasic || std::abs(solution.col_dual[col]) <= dual_tol; - if (debug_print) - printf( - "FM basis step %d: col=%d val=%.6g lb=%.6g ub=%.6g dual=%.6g " - "mustBasic=%d canBasic=%d basicNeeded=%d " - "numPlus=%d numMinus=%d numRanged=%d numNewRows=%d numBasicDesc=%d\n", - int(s), int(col), solution.col_value[col], step.header.colLower, - step.header.colUpper, solution.col_dual[col], int(colMustBeBasic), - int(colCanBeBasic), int(basicNeeded), int(numPlus), int(numMinus), - int(numRanged), int(numNewRows), int(numBasicDesc)); - // Pass 1: assign all must-be-basic (col and rows) if (colMustBeBasic) { basis.col_status[col] = HighsBasisStatus::kBasic; @@ -1727,11 +1715,6 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } } - if (debug_print) - printf(" after must-be-basic: basicAssigned=%d/%d (col %s)\n", - int(basicAssigned), int(basicNeeded), - colMustBeBasic ? "BASIC" : "pending"); - // Pass 2: assign can-be-basic col (if not already assigned) if (!colMustBeBasic) { if (colCanBeBasic && basicAssigned < basicNeeded) { @@ -1744,14 +1727,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } } - if (debug_print) - printf(" col %d -> %s (basicAssigned=%d)\n", int(col), - basis.col_status[col] == HighsBasisStatus::kBasic ? "BASIC" - : basis.col_status[col] == HighsBasisStatus::kLower ? "LOWER" - : "UPPER", - int(basicAssigned)); - - // Pass 3: assign can-be-basic rows (zero dual, at bound) + // Pass 3: assign can-be-basic rows (zero dual) for (HighsInt p = 0; p < numPlus; ++p) assignRowStatus(step.plusHeaders[p].row, plusLowerSlack[p], plusUpperSlack[p], basicAssigned, basicNeeded); @@ -1770,10 +1746,6 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( forceRowBasic(step.minusHeaders[m].row, minusLowerSlack[m], minusUpperSlack[m], step.minusCoefs[m], basicAssigned); } - - if (debug_print && basicAssigned != basicNeeded) - printf("FM basis step %d: col=%d basicAssigned=%d != basicNeeded=%d\n", - int(s), int(col), int(basicAssigned), int(basicNeeded)); } } From a626d6f87473d7915f86073a096b955141c33c2a Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 29 Jun 2026 15:45:42 +0200 Subject: [PATCH 169/196] Don't run failing test --- check/TestPresolveRules.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index a1052161808..bb169939f90 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -88,7 +88,7 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { const bool lp0 = true; const bool lp1 = true; // Makes eliminations marginal, and leaves x2=0 - const bool lp2 = true; // Failing test + const bool lp2 = false; // Failing test // From "A novel linear optimization presolve technique based on // Fourier-Motzkin elimination", Zhang, Ploskas and Sahinidis, From 6424320fed402bd93289c433159e8cd67d2b1e63 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 29 Jun 2026 17:08:56 +0200 Subject: [PATCH 170/196] Clean up --- highs/presolve/HPresolve.cpp | 72 +++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 9dd396f71e9..56913f3b3c3 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7210,37 +7210,40 @@ HPresolve::Result HPresolve::fourierMotzkin( return neRed > 0 || (neRed == 0 && mrRed > 0); }; + auto insertOriginals = + [&](std::set& rows, + const std::unordered_map>& originals, + HighsInt row, HighsInt col) { + if (row == kUpperBoundRow) + rows.insert(-(2 * col + 1)); + else if (row == kLowerBoundRow) + rows.insert(-(2 * col + 2)); + else { + auto it = originals.find(row); + if (it != originals.end()) + rows.insert(it->second.begin(), it->second.end()); + else + rows.insert(row); + } + }; + auto mergeOriginals = - [&kUpperBoundRow, &kLowerBoundRow]( - const std::unordered_map>& rowOriginals, - HighsInt plusRow, HighsInt minusRow, - HighsInt col) -> std::set { - std::set merged; - if (plusRow == kUpperBoundRow) - merged.insert(-(2 * col + 1)); - else if (plusRow == kLowerBoundRow) - merged.insert(-(2 * col + 2)); - else { - auto itP = rowOriginals.find(plusRow); - if (itP != rowOriginals.end()) - merged.insert(itP->second.begin(), itP->second.end()); - else - merged.insert(plusRow); - } - if (minusRow == kUpperBoundRow) - merged.insert(-(2 * col + 1)); - else if (minusRow == kLowerBoundRow) - merged.insert(-(2 * col + 2)); - else { - auto itM = rowOriginals.find(minusRow); - if (itM != rowOriginals.end()) - merged.insert(itM->second.begin(), itM->second.end()); - else - merged.insert(minusRow); - } - return merged; - }; + [&](std::set& rows, + const std::unordered_map>& originals, + HighsInt plusRow, HighsInt minusRow, HighsInt col) { + rows.clear(); + insertOriginals(rows, originals, plusRow, col); + insertOriginals(rows, originals, minusRow, col); + }; + auto cernikovRedundant = + [&](std::set& rows, + const std::unordered_map>& originals, + HighsInt plusRow, HighsInt minusRow, HighsInt col, + HighsInt numColsElim) { + mergeOriginals(rows, originals, plusRow, minusRow, col); + return static_cast(rows.size()) > numColsElim + 2; + }; // reformulate objective as a constraint: min c^T x + offset becomes // min z with c^T x - z <= -offset. this allows FME to eliminate @@ -7517,6 +7520,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // distinct original parent rows for each derived row (Cernikov check) std::unordered_map> rowOriginals; + std::set mergedOriginals; // main loop: eliminate variables from heap while (!heap.empty()) { @@ -7622,9 +7626,8 @@ HPresolve::Result HPresolve::fourierMotzkin( if (redundant) continue; // Cernikov redundancy check - auto merged = - mergeOriginals(rowOriginals, nr.plusIndex, nr.minusIndex, col); - if (static_cast(merged.size()) > numColsEliminated + 2) + if (cernikovRedundant(mergedOriginals, rowOriginals, nr.plusIndex, + nr.minusIndex, col, numColsEliminated)) continue; std::vector entries; @@ -7672,8 +7675,9 @@ HPresolve::Result HPresolve::fourierMotzkin( origin.plusScale, false); inheritAncestry(rowAncestry, newModelRow, origin.minusRow, mIdx, stepIdx, origin.minusScale, true); - rowOriginals[newModelRow] = - mergeOriginals(rowOriginals, origin.plusRow, origin.minusRow, col); + mergeOriginals(mergedOriginals, rowOriginals, origin.plusRow, + origin.minusRow, col); + rowOriginals[newModelRow] = mergedOriginals; stepNewRows.push_back({newModelRow, pIdx, mIdx}); } From 32924a4a110973e4c6fc7feb1ba7c7cc78b7553b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 29 Jun 2026 21:38:35 +0200 Subject: [PATCH 171/196] Enable test but skip PDLP --- check/TestPresolveRules.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index bb169939f90..8faad3592a3 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -88,7 +88,7 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { const bool lp0 = true; const bool lp1 = true; // Makes eliminations marginal, and leaves x2=0 - const bool lp2 = false; // Failing test + const bool lp2 = true; // From "A novel linear optimization presolve technique based on // Fourier-Motzkin elimination", Zhang, Ploskas and Sahinidis, @@ -129,8 +129,8 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { if (lp2) { HighsInt require_presolved_model_num_col = 1; - HighsInt require_presolved_model_num_row = 8; - HighsInt require_presolved_model_num_nz = 8; + HighsInt require_presolved_model_num_row = 6; + HighsInt require_presolved_model_num_nz = 6; presolveOffOn("FM example from paper - tightened and with costs", lp, h, require_presolved_model_num_col, require_presolved_model_num_row, @@ -150,7 +150,7 @@ void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, // 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; + const HighsInt to_k = reduce_to_empty ? 2 : 4; for (int k = 0; k < to_k; k++) { std::string solver = kSimplexString; std::string run_crossover = kHighsOnString; @@ -169,9 +169,6 @@ void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, solver = kIpmString; run_crossover = kHighsOffString; basis_postsolve = false; - } else { - solver = kHiPdlpString; - basis_postsolve = false; } } std::string presolve = presolve_on ? kHighsOnString : kHighsOffString; From 5b6822686ce3f5bf3d7559a8333060674fbcb5e4 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 29 Jun 2026 22:34:55 +0200 Subject: [PATCH 172/196] Simplify --- highs/presolve/HPresolve.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 56913f3b3c3..ed13f74e318 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7509,6 +7509,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // counters for numbers of eliminations HighsInt numColsEliminated = 0; + HighsInt numColsEliminatedTotal = 0; HighsInt numRowsEliminated = 0; HighsInt numRowsAdded = 0; @@ -7537,7 +7538,6 @@ HPresolve::Result HPresolve::fourierMotzkin( printLog(numColsEliminated, numRowsEliminated, numRowsAdded); blockSteps.clear(); rowAncestry.clear(); - rowOriginals.clear(); numColsEliminated = 0; numRowsEliminated = 0; numRowsAdded = 0; @@ -7627,7 +7627,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // Cernikov redundancy check if (cernikovRedundant(mergedOriginals, rowOriginals, nr.plusIndex, - nr.minusIndex, col, numColsEliminated)) + nr.minusIndex, col, numColsEliminatedTotal)) continue; std::vector entries; @@ -7684,6 +7684,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // mark column as deleted markColDeleted(col); ++numColsEliminated; + ++numColsEliminatedTotal; // remove old rows containing col (skip bound rows) for (HighsInt rp : iPlus) { From dbe5c45c75843a884fd4d86c7741a52a356b9cbd Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 30 Jun 2026 12:06:43 +0200 Subject: [PATCH 173/196] Clean up postsolve a little --- highs/presolve/HighsPostsolveStack.cpp | 126 ++++++++++++++----------- 1 file changed, 72 insertions(+), 54 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index e7b2b825c46..08ff72e13f9 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1585,7 +1585,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( // basis postsolve: use dual solution to determine basis status if (!basis.valid) return; - // Pre-compute lower and upper slacks for each row + // pre-compute lower and upper slacks for each row auto computeSlacks = [&](HighsInt col, const std::vector& headers, const std::vector& coefs, @@ -1610,7 +1610,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } }; - // A row must be basic if it has zero dual and activity strictly + // row must be basic if it has zero dual and activity strictly // between bounds (complementary slackness) auto rowMustBeBasic = [&](HighsInt row, double lowerSlack, double upperSlack) { @@ -1618,40 +1618,35 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( upperSlack > tol; }; - // Minimum slack scaled by coefficient (for assertions in pass 4) - auto computeSlack = [&](double lowerSlack, double upperSlack, double coef) { - return std::min(lowerSlack, upperSlack) / std::abs(coef); - }; - - // Flip tight nonbasic row to basic (degenerate) - auto forceRowBasic = [&](HighsInt row, double lowerSlack, double upperSlack, - double coef, HighsInt& basicAssigned) { - if (basis.row_status[row] == HighsBasisStatus::kBasic) return; - assert(std::abs(solution.row_dual[row]) <= dual_tol); - assert(computeSlack(lowerSlack, upperSlack, coef) <= tol); + // assign row as basic + auto assignBasicRowStatus = [&](HighsInt row, HighsInt& basicAssigned) { + if (basis.row_status[row] == HighsBasisStatus::kBasic || + std::abs(solution.row_dual[row]) > dual_tol) + return false; basis.row_status[row] = HighsBasisStatus::kBasic; basicAssigned++; + return true; + }; + + // assign row as non-basic + auto assignNonBasicRowStatus = [&](HighsInt row, double lowerSlack, + double upperSlack) { + if (solution.row_dual[row] > dual_tol) + basis.row_status[row] = HighsBasisStatus::kLower; + else if (solution.row_dual[row] < -dual_tol) + basis.row_status[row] = HighsBasisStatus::kUpper; + else + basis.row_status[row] = upperSlack < lowerSlack + ? HighsBasisStatus::kUpper + : HighsBasisStatus::kLower; }; - // Assign row as basic (if zero dual and budget allows) or non-basic + // assign row status auto assignRowStatus = [&](HighsInt row, double lowerSlack, double upperSlack, - HighsInt& basicAssigned, HighsInt basicNeeded) { - if (basis.row_status[row] == HighsBasisStatus::kBasic) return; - if (std::abs(solution.row_dual[row]) <= dual_tol && - basicAssigned < basicNeeded) { - basis.row_status[row] = HighsBasisStatus::kBasic; - basicAssigned++; - } else { - double dual = solution.row_dual[row]; - if (dual > dual_tol) - basis.row_status[row] = HighsBasisStatus::kLower; - else if (dual < -dual_tol) - basis.row_status[row] = HighsBasisStatus::kUpper; - else - basis.row_status[row] = upperSlack < lowerSlack - ? HighsBasisStatus::kUpper - : HighsBasisStatus::kLower; - } + HighsInt& basicAssigned, + bool forceNonBasic = false) { + if (forceNonBasic || !assignBasicRowStatus(row, basicAssigned)) + assignNonBasicRowStatus(row, lowerSlack, upperSlack); }; for (HighsInt s = numSteps - 1; s >= 0; --s) { @@ -1676,10 +1671,28 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( break; } + // non-basic propagation: if a generated row is non-basic (with nonzero + // dual), both its parents are forced non-basic. mark them so the greedy + // passes skip them. + std::vector forcedNonBasicPlus(numPlus, false); + std::vector forcedNonBasicMinus(numMinus, false); + for (const auto& nr : step.newRows) { + HighsInt p = nr.plusParentIdx; + HighsInt m = nr.minusParentIdx; + if (p < 0 || m < 0 || + basis.row_status[nr.row] == HighsBasisStatus::kBasic || + std::abs(solution.row_dual[nr.row]) <= dual_tol) + continue; + forcedNonBasicPlus[p] = true; + forcedNonBasicMinus[m] = true; + } + + // how many basic variables are needed? HighsInt basicNeeded = (numPlus + numMinus - numRanged - numNewRows) + numBasicDesc; HighsInt basicAssigned = 0; + // compute slacks std::vector plusLowerSlack, plusUpperSlack; std::vector minusLowerSlack, minusUpperSlack; computeSlacks(col, step.plusHeaders, step.plusCoefs, step.plusEntries, @@ -1687,35 +1700,32 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( computeSlacks(col, step.minusHeaders, step.minusCoefs, step.minusEntries, minusLowerSlack, minusUpperSlack); - // Determine col status + // determine col status bool colMustBeBasic = solution.col_value[col] > step.header.colLower + tol && solution.col_value[col] < step.header.colUpper - tol; bool colCanBeBasic = colMustBeBasic || std::abs(solution.col_dual[col]) <= dual_tol; - // Pass 1: assign all must-be-basic (col and rows) + // pass 1: assign all must-be-basic (col and rows) if (colMustBeBasic) { basis.col_status[col] = HighsBasisStatus::kBasic; basicAssigned++; } for (HighsInt p = 0; p < numPlus; ++p) { + if (forcedNonBasicPlus[p]) continue; if (rowMustBeBasic(step.plusHeaders[p].row, plusLowerSlack[p], - plusUpperSlack[p])) { - basis.row_status[step.plusHeaders[p].row] = HighsBasisStatus::kBasic; - basicAssigned++; - } + plusUpperSlack[p])) + assignBasicRowStatus(step.plusHeaders[p].row, basicAssigned); } for (HighsInt m = 0; m < numMinus; ++m) { - if (isMinusRowRanged[m]) continue; + if (isMinusRowRanged[m] || forcedNonBasicMinus[m]) continue; if (rowMustBeBasic(step.minusHeaders[m].row, minusLowerSlack[m], - minusUpperSlack[m])) { - basis.row_status[step.minusHeaders[m].row] = HighsBasisStatus::kBasic; - basicAssigned++; - } + minusUpperSlack[m])) + assignBasicRowStatus(step.minusHeaders[m].row, basicAssigned); } - // Pass 2: assign can-be-basic col (if not already assigned) + // pass 2: assign can-be-basic col (if not already assigned) if (!colMustBeBasic) { if (colCanBeBasic && basicAssigned < basicNeeded) { basis.col_status[col] = HighsBasisStatus::kBasic; @@ -1727,24 +1737,32 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } } - // Pass 3: assign can-be-basic rows (zero dual) - for (HighsInt p = 0; p < numPlus; ++p) + // pass 3: assign can-be-basic rows + for (HighsInt p = 0; p < numPlus; ++p) { + if (basis.row_status[step.plusHeaders[p].row] == HighsBasisStatus::kBasic) + continue; assignRowStatus(step.plusHeaders[p].row, plusLowerSlack[p], - plusUpperSlack[p], basicAssigned, basicNeeded); + plusUpperSlack[p], basicAssigned, + forcedNonBasicPlus[p] || basicAssigned >= basicNeeded); + } for (HighsInt m = 0; m < numMinus; ++m) { if (isMinusRowRanged[m]) continue; + if (basis.row_status[step.minusHeaders[m].row] == + HighsBasisStatus::kBasic) + continue; assignRowStatus(step.minusHeaders[m].row, minusLowerSlack[m], - minusUpperSlack[m], basicAssigned, basicNeeded); + minusUpperSlack[m], basicAssigned, + forcedNonBasicMinus[m] || basicAssigned >= basicNeeded); } - // Pass 4: if still short, flip tight non-basic rows to basic (degenerate) - for (HighsInt p = 0; p < numPlus && basicAssigned < basicNeeded; ++p) - forceRowBasic(step.plusHeaders[p].row, plusLowerSlack[p], - plusUpperSlack[p], step.plusCoefs[p], basicAssigned); + // pass 4: if still short, flip tight non-basic rows to basic (degenerate) + for (HighsInt p = 0; p < numPlus && basicAssigned < basicNeeded; ++p) { + if (forcedNonBasicPlus[p]) continue; + assignBasicRowStatus(step.plusHeaders[p].row, basicAssigned); + } for (HighsInt m = 0; m < numMinus && basicAssigned < basicNeeded; ++m) { - if (isMinusRowRanged[m]) continue; - forceRowBasic(step.minusHeaders[m].row, minusLowerSlack[m], - minusUpperSlack[m], step.minusCoefs[m], basicAssigned); + if (isMinusRowRanged[m] || forcedNonBasicMinus[m]) continue; + assignBasicRowStatus(step.minusHeaders[m].row, basicAssigned); } } } From 04bca58ed453958ea3701c52fa36178beb3d852c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 30 Jun 2026 15:55:32 +0200 Subject: [PATCH 174/196] Some more postsolve cleanup --- highs/presolve/HighsPostsolveStack.cpp | 135 +++++++++++++++---------- 1 file changed, 84 insertions(+), 51 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 08ff72e13f9..d7ab47a7471 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1649,16 +1649,61 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( assignNonBasicRowStatus(row, lowerSlack, upperSlack); }; + // collect a single candidate for basic assignment + auto collectCandidate = + [&](HighsInt row, bool forcedNonBasic, double lowerSlack, + double upperSlack, const std::vector& entries, + HighsInt parentIndex, bool isMinus, + std::vector>& candidates) { + if (basis.row_status[row] == HighsBasisStatus::kBasic) return; + if (forcedNonBasic || std::abs(solution.row_dual[row]) > dual_tol) { + assignNonBasicRowStatus(row, lowerSlack, upperSlack); + return; + } + HighsInt nonBasicCount = 0; + for (const auto& nz : entries) + if (basis.col_status[nz.index] != HighsBasisStatus::kBasic) + nonBasicCount++; + candidates.emplace_back(nonBasicCount, parentIndex, isMinus); + }; + for (HighsInt s = numSteps - 1; s >= 0; --s) { const auto& step = steps[s]; HighsInt col = step.header.col; HighsInt numPlus = step.header.numPlus; HighsInt numMinus = step.header.numMinus; - HighsInt numNewRows = static_cast(step.newRows.size()); - HighsInt numBasicDesc = 0; - for (const auto& nr : step.newRows) - if (basis.row_status[nr.row] == HighsBasisStatus::kBasic) numBasicDesc++; + // compute slacks + std::vector plusLowerSlack; + std::vector plusUpperSlack; + std::vector minusLowerSlack; + std::vector minusUpperSlack; + computeSlacks(col, step.plusHeaders, step.plusCoefs, step.plusEntries, + plusLowerSlack, plusUpperSlack); + computeSlacks(col, step.minusHeaders, step.minusCoefs, step.minusEntries, + minusLowerSlack, minusUpperSlack); + + // non-basic propagation: if a generated row is non-basic (with nonzero + // dual), both its parents are forced non-basic. mark them so the greedy + // passes skip them. only force if the parent doesn't must-be-basic. + std::vector forcedNonBasicPlus(numPlus, false); + std::vector forcedNonBasicMinus(numMinus, false); + for (const auto& nr : step.newRows) { + // get indices of parent rows + HighsInt p = nr.plusParentIdx; + HighsInt m = nr.minusParentIdx; + // skip basic rows (zero dual) and degenerate non-basic rows (with zero + // dual) + if (p < 0 || m < 0 || std::abs(solution.row_dual[nr.row]) <= dual_tol) + continue; + // mark rows that do not have to be basic + if (!rowMustBeBasic(step.plusHeaders[p].row, plusLowerSlack[p], + plusUpperSlack[p])) + forcedNonBasicPlus[p] = true; + if (!rowMustBeBasic(step.minusHeaders[m].row, minusLowerSlack[m], + minusUpperSlack[m])) + forcedNonBasicMinus[m] = true; + } // mark ranged rows (appearing in both plus and minus sets) std::vector isMinusRowRanged(numMinus, false); @@ -1671,35 +1716,18 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( break; } - // non-basic propagation: if a generated row is non-basic (with nonzero - // dual), both its parents are forced non-basic. mark them so the greedy - // passes skip them. - std::vector forcedNonBasicPlus(numPlus, false); - std::vector forcedNonBasicMinus(numMinus, false); - for (const auto& nr : step.newRows) { - HighsInt p = nr.plusParentIdx; - HighsInt m = nr.minusParentIdx; - if (p < 0 || m < 0 || - basis.row_status[nr.row] == HighsBasisStatus::kBasic || - std::abs(solution.row_dual[nr.row]) <= dual_tol) - continue; - forcedNonBasicPlus[p] = true; - forcedNonBasicMinus[m] = true; - } + // count number of basic new rows + HighsInt numNewRows = static_cast(step.newRows.size()); + HighsInt numBasicNewRows = 0; + for (const auto& nr : step.newRows) + if (basis.row_status[nr.row] == HighsBasisStatus::kBasic) + numBasicNewRows++; // how many basic variables are needed? HighsInt basicNeeded = - (numPlus + numMinus - numRanged - numNewRows) + numBasicDesc; + (numPlus + numMinus - numRanged - numNewRows) + numBasicNewRows; HighsInt basicAssigned = 0; - // compute slacks - std::vector plusLowerSlack, plusUpperSlack; - std::vector minusLowerSlack, minusUpperSlack; - computeSlacks(col, step.plusHeaders, step.plusCoefs, step.plusEntries, - plusLowerSlack, plusUpperSlack); - computeSlacks(col, step.minusHeaders, step.minusCoefs, step.minusEntries, - minusLowerSlack, minusUpperSlack); - // determine col status bool colMustBeBasic = solution.col_value[col] > step.header.colLower + tol && @@ -1737,32 +1765,37 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } } - // pass 3: assign can-be-basic rows - for (HighsInt p = 0; p < numPlus; ++p) { - if (basis.row_status[step.plusHeaders[p].row] == HighsBasisStatus::kBasic) - continue; - assignRowStatus(step.plusHeaders[p].row, plusLowerSlack[p], - plusUpperSlack[p], basicAssigned, - forcedNonBasicPlus[p] || basicAssigned >= basicNeeded); - } + // pass 3: assign can-be-basic rows, sorted by non-basic support count + // to reduce risk of rank deficiency in degenerate cases + std::vector> candidates; + for (HighsInt p = 0; p < numPlus; ++p) + collectCandidate(step.plusHeaders[p].row, forcedNonBasicPlus[p], + plusLowerSlack[p], plusUpperSlack[p], + step.plusEntries[p], p, false, candidates); for (HighsInt m = 0; m < numMinus; ++m) { if (isMinusRowRanged[m]) continue; - if (basis.row_status[step.minusHeaders[m].row] == - HighsBasisStatus::kBasic) - continue; - assignRowStatus(step.minusHeaders[m].row, minusLowerSlack[m], - minusUpperSlack[m], basicAssigned, - forcedNonBasicMinus[m] || basicAssigned >= basicNeeded); + collectCandidate(step.minusHeaders[m].row, forcedNonBasicMinus[m], + minusLowerSlack[m], minusUpperSlack[m], + step.minusEntries[m], m, true, candidates); } - - // pass 4: if still short, flip tight non-basic rows to basic (degenerate) - for (HighsInt p = 0; p < numPlus && basicAssigned < basicNeeded; ++p) { - if (forcedNonBasicPlus[p]) continue; - assignBasicRowStatus(step.plusHeaders[p].row, basicAssigned); - } - for (HighsInt m = 0; m < numMinus && basicAssigned < basicNeeded; ++m) { - if (isMinusRowRanged[m] || forcedNonBasicMinus[m]) continue; - assignBasicRowStatus(step.minusHeaders[m].row, basicAssigned); + // sort descending by non-basic support count + std::sort(candidates.begin(), candidates.end(), + [](const auto& a, const auto& b) { + return std::get<0>(a) > std::get<0>(b); + }); + for (const auto& cand : candidates) { + HighsInt parentIndex = std::get<1>(cand); + if (std::get<2>(cand)) { + assignRowStatus(step.minusHeaders[parentIndex].row, + minusLowerSlack[parentIndex], + minusUpperSlack[parentIndex], basicAssigned, + basicAssigned >= basicNeeded); + } else { + assignRowStatus(step.plusHeaders[parentIndex].row, + plusLowerSlack[parentIndex], + plusUpperSlack[parentIndex], basicAssigned, + basicAssigned >= basicNeeded); + } } } } From e8655910a8d1b30329979b3e377961e06e41b6d9 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 30 Jun 2026 16:03:39 +0200 Subject: [PATCH 175/196] Fix build error --- highs/presolve/HighsPostsolveStack.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index d7ab47a7471..eab2ac94bb7 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1780,7 +1780,8 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } // sort descending by non-basic support count std::sort(candidates.begin(), candidates.end(), - [](const auto& a, const auto& b) { + [](const std::tuple& a, + const std::tuple& b) { return std::get<0>(a) > std::get<0>(b); }); for (const auto& cand : candidates) { From 73dd2582364170a61269a36831ac44a195fa4270 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 2 Jul 2026 15:37:17 +0200 Subject: [PATCH 176/196] Sort candidates --- highs/presolve/HPresolve.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ed13f74e318..b054ed72bba 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7002,6 +7002,8 @@ HPresolve::Result HPresolve::fourierMotzkin( candidates.clear(); for (HighsInt col = 0; col < model->num_col_; col++) if (isCandidate(col)) candidates.push_back(col); + pdqsort(candidates.begin(), candidates.end(), + [&](HighsInt a, HighsInt b) { return colsize[a] < colsize[b]; }); return !candidates.empty(); }; From 25db9495b4e090b8a373cf512618168982924f35 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 2 Jul 2026 17:07:20 +0200 Subject: [PATCH 177/196] Add heap struct --- highs/presolve/HPresolve.cpp | 211 +++++++++++++++++++---------------- 1 file changed, 112 insertions(+), 99 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b054ed72bba..8fe64ea859f 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6943,10 +6943,99 @@ HPresolve::Result HPresolve::fourierMotzkin( const double maxCoef = 1e3; // structs - struct candidate { - HighsInt col; - int64_t neRed; - int64_t mrRed; + struct Heap { + struct Entry { + HighsInt col; + int64_t neRed; + int64_t mrRed; + }; + + std::vector entries; + std::vector pos; + + bool empty() const { return entries.empty(); } + HighsInt top() const { return entries[0].col; } + bool contains(HighsInt col) const { return pos[col] != -1; } + + void reset(HighsInt numCol, HighsInt reserveSize) { + entries.clear(); + entries.reserve(reserveSize); + pos.assign(numCol, -1); + } + + void push(HighsInt col, int64_t neRed, int64_t mrRed) { + pos[col] = size(); + entries.push_back({col, neRed, mrRed}); + } + + void insert(HighsInt col, int64_t neRed, int64_t mrRed) { + push(col, neRed, mrRed); + siftUp(pos[col]); + } + + void remove(HighsInt col) { + HighsInt p = pos[col]; + if (p == -1) return; + swap(p, size() - 1); + pos[col] = -1; + entries.pop_back(); + siftUp(p); + siftDown(p); + } + + void update(HighsInt col, int64_t neRed, int64_t mrRed) { + HighsInt p = pos[col]; + if (p == -1) return; + entries[p].neRed = neRed; + entries[p].mrRed = mrRed; + siftUp(p); + siftDown(p); + } + + void heapify() { + for (HighsInt i = size() / 2 - 1; i >= 0; --i) siftDown(i); + } + + private: + HighsInt size() const { return static_cast(entries.size()); } + + bool better(HighsInt i, HighsInt j) const { + if (entries[i].neRed != entries[j].neRed) + return entries[i].neRed > entries[j].neRed; + return entries[i].mrRed > entries[j].mrRed; + } + + void swap(HighsInt i, HighsInt j) { + if (i == j) return; + std::swap(entries[i], entries[j]); + pos[entries[i].col] = i; + pos[entries[j].col] = j; + } + + void siftUp(HighsInt i) { + if (i >= size()) return; + while (i > 0) { + HighsInt parent = (i - 1) / 2; + if (!better(i, parent)) break; + swap(i, parent); + i = parent; + } + } + + void siftDown(HighsInt i) { + HighsInt n = size(); + if (i >= n) return; + while (true) { + HighsInt best = i; + HighsInt left = 2 * i + 1; + HighsInt right = 2 * i + 2; + if (left < n && better(left, best)) best = left; + if (right < n && better(right, best)) best = right; + if (best == i) break; + swap(i, best); + i = best; + } + } }; struct newRowEntry { @@ -7317,87 +7406,16 @@ HPresolve::Result HPresolve::fourierMotzkin( shrinkProblem(postsolve_stack); }; - auto heapBetter = [](const candidate& a, const candidate& b) { - if (a.neRed != b.neRed) return a.neRed > b.neRed; - return a.mrRed > b.mrRed; - }; - - auto heapSwap = [&](std::vector& heap, - std::vector& heapPos, HighsInt i, HighsInt j) { - if (i == j) return; - std::swap(heap[i], heap[j]); - heapPos[heap[i].col] = i; - heapPos[heap[j].col] = j; - }; - - auto heapSiftUp = [&](std::vector& heap, - std::vector& heapPos, HighsInt i) { - if (i >= static_cast(heap.size())) return; - while (i > 0) { - HighsInt parent = (i - 1) / 2; - if (!heapBetter(heap[i], heap[parent])) break; - heapSwap(heap, heapPos, i, parent); - i = parent; - } - }; - - auto heapSiftDown = [&](std::vector& heap, - std::vector& heapPos, HighsInt i) { - HighsInt heapSize = static_cast(heap.size()); - if (i >= heapSize) return; - while (true) { - HighsInt best = i; - HighsInt left = 2 * i + 1; - HighsInt right = 2 * i + 2; - if (left < heapSize && heapBetter(heap[left], heap[best])) best = left; - if (right < heapSize && heapBetter(heap[right], heap[best])) best = right; - if (best == i) break; - heapSwap(heap, heapPos, i, best); - i = best; - } - }; - - auto heapRemove = [&](std::vector& heap, - std::vector& heapPos, HighsInt col) { - HighsInt pos = heapPos[col]; - if (pos == -1) return; - HighsInt last = static_cast(heap.size()) - 1; - heapSwap(heap, heapPos, pos, last); - heapPos[col] = -1; - heap.pop_back(); - heapSiftUp(heap, heapPos, pos); - heapSiftDown(heap, heapPos, pos); - }; - - auto heapUpdate = [&](std::vector& heap, - std::vector& heapPos, HighsInt col, - int64_t neRed, int64_t mrRed) { - HighsInt pos = heapPos[col]; - if (pos == -1) return; - heap[pos].neRed = neRed; - heap[pos].mrRed = mrRed; - heapSiftUp(heap, heapPos, pos); - heapSiftDown(heap, heapPos, pos); - }; - - auto heapify = [&](std::vector& heap, - std::vector& heapPos) { - for (HighsInt i = static_cast(heap.size()) / 2 - 1; i >= 0; --i) - heapSiftDown(heap, heapPos, i); - }; - auto collectCandidatesAndBuildHeap = - [&](std::vector& candidates, std::vector& heap, - std::vector& heapPos, std::vector& iPlus, - std::vector& iMinus, std::vector& pPlus, - std::vector& pMinus, std::vector& affectedCols, + [&](std::vector& candidates, Heap& heap, + std::vector& iPlus, std::vector& iMinus, + std::vector& pPlus, std::vector& pMinus, + std::vector& affectedCols, const std::vector& objRowCols) { // compute candidates if (!computeCandidates(candidates)) return false; // set up data structures for heap - heap.clear(); - heap.reserve(candidates.size()); - heapPos.assign(model->num_col_, -1); + heap.reset(model->num_col_, static_cast(candidates.size())); pPlus.assign(model->num_col_, 0); pMinus.assign(model->num_col_, 0); iPlus.reserve(model->num_row_); @@ -7413,12 +7431,11 @@ HPresolve::Result HPresolve::fourierMotzkin( affectedCols.clear(); if (!elimCandidate || !isReduction(neRed, mrRed)) continue; // add to heap - heapPos[col] = static_cast(heap.size()); - heap.push_back({col, neRed, mrRed}); + heap.push(col, neRed, mrRed); } if (heap.empty()) return false; // heapify - heapify(heap, heapPos); + heap.heapify(); return true; }; @@ -7475,8 +7492,7 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector affectedCols; // indexed max-heap - std::vector heap; - std::vector heapPos; + Heap heap; // precompute the objective row: columns with nonzero cost // used to simulate the objective constraint in checkRows before @@ -7489,8 +7505,8 @@ HPresolve::Result HPresolve::fourierMotzkin( } // compute candidates and build initial heap - if (!collectCandidatesAndBuildHeap(candidates, heap, heapPos, iPlus, iMinus, - pPlus, pMinus, affectedCols, objRowCols)) + if (!collectCandidatesAndBuildHeap(candidates, heap, iPlus, iMinus, pPlus, + pMinus, affectedCols, objRowCols)) return finalise(); // vectors for computing new row entries @@ -7527,8 +7543,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // main loop: eliminate variables from heap while (!heap.empty()) { - HighsInt col = heap[0].col; - heapRemove(heap, heapPos, col); + HighsInt col = heap.top(); + heap.remove(col); // if this candidate has nonzero cost and objective has not yet been // reformulated, perform the reformulation now and rebuild the heap @@ -7550,9 +7566,8 @@ HPresolve::Result HPresolve::fourierMotzkin( objRowCols.clear(); newRowMark.resize(model->num_col_, -1); // re-compute candidates and re-build heap - if (!collectCandidatesAndBuildHeap(candidates, heap, heapPos, iPlus, - iMinus, pPlus, pMinus, affectedCols, - objRowCols)) + if (!collectCandidatesAndBuildHeap(candidates, heap, iPlus, iMinus, pPlus, + pMinus, affectedCols, objRowCols)) return finalise(); continue; } @@ -7711,7 +7726,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // check if variable is a candidate bool isCandidateCol = isCandidate(k); // skip variable if it is not on the heap and no candidate - if (heapPos[k] == -1 && !isCandidateCol) continue; + if (!heap.contains(k) && !isCandidateCol) continue; // check column non-zeros int64_t ne, mr; bool elimCandidate = @@ -7720,15 +7735,13 @@ HPresolve::Result HPresolve::fourierMotzkin( affectedCols.clear(); if (!elimCandidate || !isReduction(ne, mr)) { // no candidate or not beneficial -> remove from heap - heapRemove(heap, heapPos, k); - } else if (heapPos[k] == -1) { + heap.remove(k); + } else if (!heap.contains(k)) { // new candidate -> insert into heap - heapPos[k] = static_cast(heap.size()); - heap.push_back({k, ne, mr}); - heapSiftUp(heap, heapPos, heapPos[k]); + heap.insert(k, ne, mr); } else { // update heap - heapUpdate(heap, heapPos, k, ne, mr); + heap.update(k, ne, mr); } } saveAffectedCols.clear(); From a4e16ef2c41875bd195679c85e4e13ded57e9138 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 3 Jul 2026 09:55:51 +0200 Subject: [PATCH 178/196] Move some code around --- highs/presolve/HPresolve.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 8fe64ea859f..856ff94aec7 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6942,6 +6942,11 @@ HPresolve::Result HPresolve::fourierMotzkin( // max. absolute coefficient const double maxCoef = 1e3; + // sentinel row indices for variable bounds and objective row + const HighsInt kUpperBoundRow = -2; + const HighsInt kLowerBoundRow = -3; + const HighsInt kObjectiveRow = -4; + // structs struct Heap { struct Entry { @@ -7066,11 +7071,6 @@ HPresolve::Result HPresolve::fourierMotzkin( return checkLimits(postsolve_stack); }; - // sentinel row indices for variable bounds and objective row - const HighsInt kUpperBoundRow = -2; - const HighsInt kLowerBoundRow = -3; - const HighsInt kObjectiveRow = -4; - auto acceptCoef = [&](double val) { double absval = std::abs(val); return absval == 0.0 || (absval >= 1.0 / maxCoef && absval <= maxCoef); From 67a8109e39e463ef51c8223418943a3e271c68d8 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 3 Jul 2026 10:13:37 +0200 Subject: [PATCH 179/196] Stop early --- highs/presolve/HPresolve.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 856ff94aec7..83d72cb44c1 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6942,6 +6942,11 @@ HPresolve::Result HPresolve::fourierMotzkin( // max. absolute coefficient const double maxCoef = 1e3; + // max. number of consecutive failures (while trying to build the heap) + const HighsInt maxNumFails = 100; + // max. size of the heap + const HighsInt maxHeapSize = 10000; + // sentinel row indices for variable bounds and objective row const HighsInt kUpperBoundRow = -2; const HighsInt kLowerBoundRow = -3; @@ -6959,6 +6964,7 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector pos; bool empty() const { return entries.empty(); } + HighsInt size() const { return static_cast(entries.size()); } HighsInt top() const { return entries[0].col; } bool contains(HighsInt col) const { return pos[col] != -1; } @@ -7002,8 +7008,6 @@ HPresolve::Result HPresolve::fourierMotzkin( } private: - HighsInt size() const { return static_cast(entries.size()); } - bool better(HighsInt i, HighsInt j) const { if (entries[i].neRed != entries[j].neRed) return entries[i].neRed > entries[j].neRed; @@ -7422,6 +7426,7 @@ HPresolve::Result HPresolve::fourierMotzkin( iMinus.reserve(model->num_row_); affectedCols.reserve(model->num_col_); // inspect candidates + HighsInt numFails = 0; for (HighsInt col : candidates) { int64_t neRed; int64_t mrRed; @@ -7429,9 +7434,15 @@ HPresolve::Result HPresolve::fourierMotzkin( checkNonZeros(col, objRowCols, iPlus, iMinus, pPlus, pMinus, affectedCols, neRed, mrRed); affectedCols.clear(); - if (!elimCandidate || !isReduction(neRed, mrRed)) continue; + if (!elimCandidate || !isReduction(neRed, mrRed)) { + // count number of failures + if (++numFails > maxNumFails) break; + continue; + } // add to heap + numFails = 0; heap.push(col, neRed, mrRed); + if (heap.size() >= maxHeapSize) break; } if (heap.empty()) return false; // heapify From d8fc93855f9b20a7e59319f3c815087620971b1b Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 3 Jul 2026 11:02:22 +0200 Subject: [PATCH 180/196] Add tryFourierMotzkin flag --- highs/presolve/HPresolve.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 83d72cb44c1..7d34770fc93 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -5978,6 +5978,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif bool tryProbing = mipsolver != nullptr; + bool tryFourierMotzkin = true; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; @@ -6007,8 +6008,12 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { applyConflictGraphSubstitutions(postsolve_stack, numDelCol)); } - if (analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) + if (tryFourierMotzkin && + analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { + storeCurrentProblemSize(); HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); + tryFourierMotzkin = problemSizeReduction() > 0.0; + } if (analysis_.allow_rule_[kPresolveRuleAggregator]) HPRESOLVE_CHECKED_CALL(aggregator(postsolve_stack)); @@ -7425,7 +7430,7 @@ HPresolve::Result HPresolve::fourierMotzkin( iPlus.reserve(model->num_row_); iMinus.reserve(model->num_row_); affectedCols.reserve(model->num_col_); - // inspect candidates + // inspect candidates (with limits) HighsInt numFails = 0; for (HighsInt col : candidates) { int64_t neRed; From 3f54fd188635cebf7f81777486f2fbc8178f4a67 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 3 Jul 2026 11:29:16 +0200 Subject: [PATCH 181/196] Fix flag --- highs/presolve/HPresolve.cpp | 25 +++++++++++++------------ highs/presolve/HPresolve.h | 3 ++- highs/presolve/HPresolveTest.cpp | 3 ++- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 7d34770fc93..cfe3074b08e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6010,9 +6010,10 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { if (tryFourierMotzkin && analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { - storeCurrentProblemSize(); - HPRESOLVE_CHECKED_CALL(fourierMotzkin(postsolve_stack)); - tryFourierMotzkin = problemSizeReduction() > 0.0; + HighsInt numColsEliminated; + HPRESOLVE_CHECKED_CALL( + fourierMotzkin(postsolve_stack, numColsEliminated)); + tryFourierMotzkin = numColsEliminated > 0; } if (analysis_.allow_rule_[kPresolveRuleAggregator]) @@ -6935,7 +6936,7 @@ HPresolve::Result HPresolve::aggregator(HighsPostsolveStack& postsolve_stack) { } HPresolve::Result HPresolve::fourierMotzkin( - HighsPostsolveStack& postsolve_stack) { + HighsPostsolveStack& postsolve_stack, HighsInt& numColsEliminated) { assert(analysis_.allow_rule_[kPresolveRuleFourierMotzkin]); const bool logging_on = analysis_.logging_on_; if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleFourierMotzkin); @@ -7542,8 +7543,8 @@ HPresolve::Result HPresolve::fourierMotzkin( std::vector saveAffectedCols; // counters for numbers of eliminations - HighsInt numColsEliminated = 0; - HighsInt numColsEliminatedTotal = 0; + numColsEliminated = 0; + HighsInt numColsEliminatedBlock = 0; HighsInt numRowsEliminated = 0; HighsInt numRowsAdded = 0; @@ -7569,10 +7570,10 @@ HPresolve::Result HPresolve::fourierMotzkin( // reformulateObjective pushes other reductions onto the data stack if (!blockSteps.empty()) { postsolve_stack.fourierMotzkinBlockFinalise(blockSteps, rowAncestry); - printLog(numColsEliminated, numRowsEliminated, numRowsAdded); + printLog(numColsEliminatedBlock, numRowsEliminated, numRowsAdded); blockSteps.clear(); rowAncestry.clear(); - numColsEliminated = 0; + numColsEliminatedBlock = 0; numRowsEliminated = 0; numRowsAdded = 0; } @@ -7660,7 +7661,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // Cernikov redundancy check if (cernikovRedundant(mergedOriginals, rowOriginals, nr.plusIndex, - nr.minusIndex, col, numColsEliminatedTotal)) + nr.minusIndex, col, numColsEliminated)) continue; std::vector entries; @@ -7716,8 +7717,8 @@ HPresolve::Result HPresolve::fourierMotzkin( // mark column as deleted markColDeleted(col); + ++numColsEliminatedBlock; ++numColsEliminated; - ++numColsEliminatedTotal; // remove old rows containing col (skip bound rows) for (HighsInt rp : iPlus) { @@ -7765,12 +7766,12 @@ HPresolve::Result HPresolve::fourierMotzkin( if (checkLimits(postsolve_stack) != Result::kOk) break; } - if (numColsEliminated > 0) { + if (numColsEliminatedBlock > 0) { // finalize the FM block postsolve_stack.fourierMotzkinBlockFinalise(blockSteps, rowAncestry); // log message - printLog(numColsEliminated, numRowsEliminated, numRowsAdded); + printLog(numColsEliminatedBlock, numRowsEliminated, numRowsAdded); } return finalise(); diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index f6230687d7b..e46d7297592 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -475,7 +475,8 @@ class HPresolve { Result aggregator(HighsPostsolveStack& postsolve_stack); - Result fourierMotzkin(HighsPostsolveStack& postsolve_stack); + Result fourierMotzkin(HighsPostsolveStack& postsolve_stack, + HighsInt& numColsEliminated); Result removeRowSingletons(HighsPostsolveStack& postsolve_stack); diff --git a/highs/presolve/HPresolveTest.cpp b/highs/presolve/HPresolveTest.cpp index 304f54a9238..6e0df138d4f 100644 --- a/highs/presolve/HPresolveTest.cpp +++ b/highs/presolve/HPresolveTest.cpp @@ -46,7 +46,8 @@ HPresolve::Result HPresolve::presolveRuleTestFourierMotzkin( highsLogUser(options->log_options, HighsLogType::kInfo, "HPresolve::presolveRuleTestFourierMotzkin\n"); - HPresolve::Result result = fourierMotzkin(postsolve_stack); + HighsInt numColsEliminated; + HPresolve::Result result = fourierMotzkin(postsolve_stack, numColsEliminated); if (result != Result::kOk) return result; highsLogUser(options->log_options, HighsLogType::kInfo, From 90049fc5fe636deb38019b56c59090cff6090589 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 3 Jul 2026 11:41:27 +0200 Subject: [PATCH 182/196] Simplify --- highs/presolve/HPresolve.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index cfe3074b08e..48748baa04f 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6008,18 +6008,19 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { applyConflictGraphSubstitutions(postsolve_stack, numDelCol)); } + HighsInt numColsEliminatedFourierMotzkin = 0; if (tryFourierMotzkin && - analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) { - HighsInt numColsEliminated; + analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) HPRESOLVE_CHECKED_CALL( - fourierMotzkin(postsolve_stack, numColsEliminated)); - tryFourierMotzkin = numColsEliminated > 0; - } + fourierMotzkin(postsolve_stack, numColsEliminatedFourierMotzkin)); if (analysis_.allow_rule_[kPresolveRuleAggregator]) HPRESOLVE_CHECKED_CALL(aggregator(postsolve_stack)); - if (problemSizeReduction() > 0.05) continue; + // check if there were reductions + bool haveReductions = problemSizeReduction() > 0.05; + tryFourierMotzkin = haveReductions || numColsEliminatedFourierMotzkin > 0; + if (haveReductions) continue; if (trySparsify && analysis_.allow_rule_[kPresolveRuleSparsify]) { HighsInt numNz = numNonzeros(); From ca7294a73965713d799cf0cb5aa48c61fea9aa98 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 6 Jul 2026 08:21:24 +0200 Subject: [PATCH 183/196] Tidy up HPresolve::dominatedColumns --- highs/presolve/HPresolve.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 48748baa04f..c3deafab52d 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1328,12 +1328,8 @@ HPresolve::Result HPresolve::dominatedColumns( if (!tryToFix) numDomChecksPredBndAnalysis++; // check for domination if (checkDomination(direction, col, direction_k, k)) { - // Re-check the implied bound condition since earlier fixings in - // this dominatedColumns call may have changed the model state - bool currentBoundImplied = - direction > 0 ? isUpperImplied(col) : isLowerImplied(col); if (tryToFix && - (currentBoundImplied || + (boundImplied || mipsolver->mipdata_->cliquetable.haveCommonClique( HighsCliqueTable::CliqueVar(col, direction > 0 ? 1 : 0), HighsCliqueTable::CliqueVar(k, direction_k > 0 ? 1 : 0)))) { @@ -1365,9 +1361,8 @@ HPresolve::Result HPresolve::dominatedColumns( // lambda for finding a domination relationship in the given row auto checkRow = [&](HighsInt row, HighsInt col, HighsInt direction, - double bestVal, bool boundImplied, bool hasCliques) { + double bestVal, bool hasCliques) { storeRow(row); - bool onlyPredBndAnalysis = !boundImplied && !hasCliques; for (const HighsSliceNonzero& nonz : getStoredRow()) { // get column index HighsInt k = nonz.index(); @@ -1381,8 +1376,12 @@ HPresolve::Result HPresolve::dominatedColumns( // check if variables have the same type bool sameVarType = varsHaveSameType(col, k); + // check if bound is implied (computed fresh due to earlier fixings) + bool boundImplied = + direction > 0 ? isUpperImplied(col) : isLowerImplied(col); + // skip checks if nothing to do - if (onlyPredBndAnalysis && !sameVarType) continue; + if (!boundImplied && !hasCliques && !sameVarType) continue; // try to fix variables or strengthen bounds // check already known non-zeros in respective columns in advance to @@ -1417,15 +1416,13 @@ HPresolve::Result HPresolve::dominatedColumns( if (bestRowMinus != -1 && (allowPredBndAnalysis || lowerImplied || hasNegCliques)) HPRESOLVE_CHECKED_CALL(checkRow(bestRowMinus, j, HighsInt{-1}, - ajBestRowMinus, lowerImplied, - hasNegCliques)); + ajBestRowMinus, hasNegCliques)); // use row 'bestRowPlus' if (!colDeleted[j] && bestRowPlus != -1 && (allowPredBndAnalysis || upperImplied || hasPosCliques)) - HPRESOLVE_CHECKED_CALL(checkRow(bestRowPlus, j, HighsInt{1}, - ajBestRowPlus, upperImplied, - hasPosCliques)); + HPRESOLVE_CHECKED_CALL( + checkRow(bestRowPlus, j, HighsInt{1}, ajBestRowPlus, hasPosCliques)); // do not use predictive bound analysis if it requires many domination // checks and only yields few fixings or improved bounds on average From a37c797daa03f1c6850c400d472c7e9e7a1cc480 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 5 Aug 2026 15:06:12 +0200 Subject: [PATCH 184/196] Add option to control FM objective reformulation --- highs/lp_data/HighsOptions.h | 7 +++++++ highs/presolve/HPresolve.cpp | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index 194ddf95355..56e0a549856 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -457,6 +457,7 @@ struct HighsOptionsStruct { HighsInt presolve_substitution_maxfillin; HighsInt presolve_rule_off; HighsInt presolve_rule_test; + HighsInt presolve_fm_level; bool presolve_rule_logging; bool presolve_remove_slacks; bool no_unnecessary_rebuild_refactor; @@ -629,6 +630,7 @@ struct HighsOptionsStruct { presolve_substitution_maxfillin(0), presolve_rule_off(0), presolve_rule_test(0), + presolve_fm_level(0), presolve_rule_logging(false), presolve_remove_slacks(false), no_unnecessary_rebuild_refactor(false), @@ -1660,6 +1662,11 @@ class HighsOptions : public HighsOptionsStruct { &presolve_rule_test, 0, 0, kPresolveRuleMax); records.push_back(record_int); + record_int = new OptionRecordInt("presolve_fm_level", + "Fourier-Motzkin elimination level", + advanced, &presolve_fm_level, 0, 1, 1); + records.push_back(record_int); + record_bool = new OptionRecordBool( "presolve_rule_logging", "Log effectiveness of presolve rules for LP", advanced, &presolve_rule_logging, false); diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index be89fa639d6..336a7adf933 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7092,6 +7092,8 @@ HPresolve::Result HPresolve::fourierMotzkin( if (col == model->fme_obj_col_) return false; if (model->integrality_[col] != HighsVarType::kContinuous) return false; if (!acceptCoef(model->col_cost_[col])) return false; + if (options->presolve_fm_level < 1 && model->col_cost_[col] != 0.0) + return false; for (const auto& nz : getColumnVector(col)) if (isEquation(nz.index()) || !acceptCoef(nz.value())) return false; return true; @@ -7494,10 +7496,11 @@ HPresolve::Result HPresolve::fourierMotzkin( auto printLog = [&](HighsInt colsRemoved, HighsInt rowsRemoved, HighsInt rowsAdded) { highsLogDev(options->log_options, HighsLogType::kInfo, - "Fourier-Motzkin added %" HIGHSINT_FORMAT - " rows and eliminated %" HIGHSINT_FORMAT + "Fourier-Motzkin (%s objective reformulation) added " + "%" HIGHSINT_FORMAT " rows and eliminated %" HIGHSINT_FORMAT " rows and %" HIGHSINT_FORMAT " columns\n", - rowsAdded, rowsRemoved, colsRemoved); + options->presolve_fm_level >= 1 ? "with" : "without", rowsAdded, + rowsRemoved, colsRemoved); }; // workspace vectors From 020b59ba019e40ae1fdd938557f00a4d361dd630 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 5 Aug 2026 15:18:31 +0200 Subject: [PATCH 185/196] Add overload for getOrigColIndex and getOrigRowIndex --- highs/mip/HighsMipSolverData.cpp | 8 ++++---- highs/mip/HighsPseudocost.cpp | 23 +++++++++++------------ highs/presolve/HPresolve.cpp | 14 +++++++------- highs/presolve/HighsPostsolveStack.h | 2 ++ 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 2b82798d7e0..e63c0560025 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -1372,12 +1372,12 @@ void HighsMipSolverData::performRestart() { HighsInt numCol = basis.col_status.size(); for (HighsInt i = 0; i < numCol; ++i) - root_basis.col_status[postSolveStack.getOrigColIndex()[i]] = + root_basis.col_status[postSolveStack.getOrigColIndex(i)] = basis.col_status[i]; HighsInt numRow = basis.row_status.size(); for (HighsInt i = 0; i < numRow; ++i) - root_basis.row_status[postSolveStack.getOrigRowIndex()[i]] = + root_basis.row_status[postSolveStack.getOrigRowIndex(i)] = basis.row_status[i]; mipsolver.rootbasis = &root_basis; @@ -1501,7 +1501,7 @@ void HighsMipSolverData::basisTransfer() { ++i) { if (!postSolveStack.isOrigRow(i)) break; HighsBasisStatus status = - mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex()[i]]; + mipsolver.rootbasis->row_status[postSolveStack.getOrigRowIndex(i)]; firstrootbasis.row_status[i] = status; } @@ -1510,7 +1510,7 @@ void HighsMipSolverData::basisTransfer() { ++i) { if (!postSolveStack.isOrigCol(i)) break; HighsBasisStatus status = - mipsolver.rootbasis->col_status[postSolveStack.getOrigColIndex()[i]]; + 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 98ac3da7f2d..6c670c19e79 100644 --- a/highs/mip/HighsPseudocost.cpp +++ b/highs/mip/HighsPseudocost.cpp @@ -48,8 +48,7 @@ HighsPseudocost::HighsPseudocost(const HighsMipSolver& mipsolver) for (HighsInt i = 0; i != mipsolver.numCol(); ++i) { if (!mipsolver.mipdata_->postSolveStack.isOrigCol(i)) continue; - HighsInt origCol = - mipsolver.mipdata_->postSolveStack.getOrigColIndex()[i]; + HighsInt origCol = mipsolver.mipdata_->postSolveStack.getOrigColIndex(i); pseudocostup[i] = mipsolver.pscostinit->pseudocostup[origCol]; nsamplesup[i] = mipsolver.pscostinit->nsamplesup[origCol]; @@ -118,21 +117,21 @@ HighsPseudocostInitialization::HighsPseudocostInitialization( for (HighsInt i = 0; i != ncols; ++i) { if (!postsolveStack.isOrigCol(i)) continue; - pseudocostup[postsolveStack.getOrigColIndex()[i]] = pscost.pseudocostup[i]; - pseudocostdown[postsolveStack.getOrigColIndex()[i]] = + pseudocostup[postsolveStack.getOrigColIndex(i)] = pscost.pseudocostup[i]; + pseudocostdown[postsolveStack.getOrigColIndex(i)] = pscost.pseudocostdown[i]; - nsamplesup[postsolveStack.getOrigColIndex()[i]] = + nsamplesup[postsolveStack.getOrigColIndex(i)] = std::min(maxCount, pscost.nsamplesup[i]); - nsamplesdown[postsolveStack.getOrigColIndex()[i]] = + nsamplesdown[postsolveStack.getOrigColIndex(i)] = std::min(maxCount, pscost.nsamplesdown[i]); - inferencesup[postsolveStack.getOrigColIndex()[i]] = pscost.inferencesup[i]; - inferencesdown[postsolveStack.getOrigColIndex()[i]] = + inferencesup[postsolveStack.getOrigColIndex(i)] = pscost.inferencesup[i]; + inferencesdown[postsolveStack.getOrigColIndex(i)] = pscost.inferencesdown[i]; - ninferencesup[postsolveStack.getOrigColIndex()[i]] = 1; - ninferencesdown[postsolveStack.getOrigColIndex()[i]] = 1; - conflictscoreup[postsolveStack.getOrigColIndex()[i]] = + ninferencesup[postsolveStack.getOrigColIndex(i)] = 1; + ninferencesdown[postsolveStack.getOrigColIndex(i)] = 1; + conflictscoreup[postsolveStack.getOrigColIndex(i)] = pscost.conflictscoreup[i] / pscost.conflict_weight; - conflictscoredown[postsolveStack.getOrigColIndex()[i]] = + conflictscoredown[postsolveStack.getOrigColIndex(i)] = pscost.conflictscoredown[i] / pscost.conflict_weight; } } diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 336a7adf933..7a353e81beb 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -48,7 +48,7 @@ namespace presolve { void HPresolve::debugPrintRow(HighsPostsolveStack& postsolve_stack, HighsInt row) { printf("(row %" HIGHSINT_FORMAT ") %.15g (impl: %.15g) <= ", - postsolve_stack.getOrigRowIndex()[row], model->row_lower_[row], + postsolve_stack.getOrigRowIndex(row), model->row_lower_[row], impliedRowBounds.getSumLower(row)); for (const HighsSliceNonzero& nonzero : getSortedRowVector(row)) { @@ -59,7 +59,7 @@ void HPresolve::debugPrintRow(HighsPostsolveStack& postsolve_stack, : 'x'; char signchar = nonzero.value() < 0 ? '-' : '+'; printf("%c%g %c%" HIGHSINT_FORMAT " ", signchar, std::abs(nonzero.value()), - colchar, postsolve_stack.getOrigColIndex()[nonzero.index()]); + colchar, postsolve_stack.getOrigColIndex(nonzero.index())); } printf("<= %.15g (impl: %.15g)\n", model->row_upper_[row], @@ -9214,16 +9214,16 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { temp_sol.col_dual.resize(model.num_col_); temp_sol.col_value.resize(model.num_col_); for (HighsInt i = 0; i != model.num_col_; ++i) { - temp_sol.col_dual[i] = sol.col_dual[tmp.getOrigColIndex()[i]]; - temp_sol.col_value[i] = sol.col_value[tmp.getOrigColIndex()[i]]; - temp_basis.col_status[i] = basis.col_status[tmp.getOrigColIndex()[i]]; + temp_sol.col_dual[i] = sol.col_dual[tmp.getOrigColIndex(i)]; + temp_sol.col_value[i] = sol.col_value[tmp.getOrigColIndex(i)]; + temp_basis.col_status[i] = basis.col_status[tmp.getOrigColIndex(i)]; } temp_basis.row_status.resize(model.num_row_); temp_sol.row_dual.resize(model.num_row_); for (HighsInt i = 0; i != model.num_row_; ++i) { - temp_sol.row_dual[i] = sol.row_dual[tmp.getOrigRowIndex()[i]]; - temp_basis.row_status[i] = basis.row_status[tmp.getOrigRowIndex()[i]]; + temp_sol.row_dual[i] = sol.row_dual[tmp.getOrigRowIndex(i)]; + temp_basis.row_status[i] = basis.row_status[tmp.getOrigRowIndex(i)]; } temp_sol.row_value.resize(model.num_row_); calculateRowValuesQuad(model, sol); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 9f45cc4d874..90b7b505270 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -375,8 +375,10 @@ class HighsPostsolveStack { public: const std::vector& getOrigColIndex() const { return origColIndex; } + HighsInt getOrigColIndex(HighsInt col) const { return origColIndex[col]; } const std::vector& getOrigRowIndex() const { return origRowIndex; } + HighsInt getOrigRowIndex(HighsInt row) const { return origRowIndex[row]; } bool isOrigCol(HighsInt col) const { return origColIndex[col] < origNumCol; } From 6633cf74d723e1121d4c13da517a8ec37c54ecc0 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 7 Aug 2026 09:21:42 +0200 Subject: [PATCH 186/196] Don't build vector of non-zero objective coefficients if it is not used --- highs/presolve/HPresolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 7a353e81beb..58f21e36fe4 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7518,7 +7518,7 @@ HPresolve::Result HPresolve::fourierMotzkin( // used to simulate the objective constraint in checkRows before // reformulation actually happens std::vector objRowCols; - if (model->fme_obj_col_ == -1) { + if (model->fme_obj_col_ == -1 && options->presolve_fm_level >= 1) { for (HighsInt j = 0; j < model->num_col_; ++j) { if (!colDeleted[j] && model->col_cost_[j] != 0.0) objRowCols.push_back(j); } From 3c926e966b9714db6585825bd1872fc73f10532e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 7 Aug 2026 09:38:05 +0200 Subject: [PATCH 187/196] Clear original row index --- highs/presolve/HPresolve.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 58f21e36fe4..c5ff6764b6c 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7576,6 +7576,7 @@ HPresolve::Result HPresolve::fourierMotzkin( printLog(numColsEliminatedBlock, numRowsEliminated, numRowsAdded); blockSteps.clear(); rowAncestry.clear(); + rowOriginals.clear(); numColsEliminatedBlock = 0; numRowsEliminated = 0; numRowsAdded = 0; From daadc06acf27274cc0b0aa0f210b512fae8e95b9 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 7 Aug 2026 09:44:19 +0200 Subject: [PATCH 188/196] Fix commented out line --- check/TestPresolveRules.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 8faad3592a3..2e49aefe515 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -81,13 +81,13 @@ TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { Highs h; - // h.setOptionValue("output_flag", dev_run); + h.setOptionValue("output_flag", dev_run); h.setOptionValue("presolve_rule_test", kPresolveRuleFourierMotzkin); h.setOptionValue("presolve_rule_logging", true); h.setOptionValue("log_dev_level", 1); const bool lp0 = true; - const bool lp1 = true; // Makes eliminations marginal, and leaves x2=0 + const bool lp1 = true; // Makes eliminations marginal, and leaves x2=0 const bool lp2 = true; // From "A novel linear optimization presolve technique based on From 06b577d6f5d2895d45903d947ffe011ba9f6b5b7 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 7 Aug 2026 10:04:22 +0200 Subject: [PATCH 189/196] Fix tolerance --- highs/presolve/HighsPostsolveStack.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 4874b941c7a..e2a14d098b1 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1489,7 +1489,7 @@ HighsPostsolveStack::popFourierMotzkinBlock(HighsDataStack& stack) { void HighsPostsolveStack::undoFourierMotzkinBlock( const std::vector& steps, const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) { - const double tol = options.mip_feasibility_tolerance; + const double tol = options.primal_feasibility_tolerance; const double dual_tol = options.dual_feasibility_tolerance; HighsInt numSteps = static_cast(steps.size()); From fa3140e59aa7172751b3f5fcb86ddc2c130bee21 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 10 Aug 2026 12:31:20 +0200 Subject: [PATCH 190/196] Merge --- .github/workflows/action-sanitizers-bazel.yml | 1 + check/Avgas.h | 2 +- check/TestCheckSolution.cpp | 2 +- check/TestPresolve.cpp | 152 +++ check/TestRunData.cpp | 283 ++-- cmake/sources.cmake | 2 + docs/c_api_gen/build.jl | 2 +- docs/src/options/definitions.md | 16 +- docs/src/parallel.md | 69 +- highs/Highs.h | 4 +- highs/HighsExternalApi.h | 2 +- highs/interfaces/highs_c_api.cpp | 4 +- highs/io/FilereaderLp.cpp | 4 +- highs/io/HMPSIO.h | 2 +- highs/io/HMpsFF.cpp | 4 +- highs/io/HMpsFF.h | 6 +- highs/io/HighsIO.cpp | 5 + highs/io/HighsIO.h | 1 + highs/ipm/IpxSolution.h | 2 +- highs/ipm/hipo/auxiliary/Auxiliary.cpp | 5 + highs/ipm/hipo/auxiliary/Auxiliary.h | 11 + highs/ipm/hipo/auxiliary/IntConfig.h | 2 +- highs/ipm/hipo/factorhighs/Analyse.cpp | 141 +- highs/ipm/hipo/factorhighs/Analyse.h | 8 + highs/ipm/hipo/factorhighs/DenseFact.h | 4 +- .../ipm/hipo/factorhighs/DenseFactHybrid.cpp | 8 +- highs/ipm/hipo/factorhighs/FactorHighs.cpp | 11 + highs/ipm/hipo/factorhighs/FactorHighs.h | 3 + .../ipm/hipo/factorhighs/FactorHighsOptions.h | 5 + .../hipo/factorhighs/FactorHighsSettings.h | 5 + .../ipm/hipo/factorhighs/FactorHighs_c_api.h | 2 +- highs/ipm/hipo/factorhighs/Factorise.cpp | 94 +- highs/ipm/hipo/factorhighs/Factorise.h | 3 +- highs/ipm/hipo/factorhighs/FormatHandler.h | 5 +- .../factorhighs/HybridHybridFormatHandler.cpp | 161 ++- .../factorhighs/HybridHybridFormatHandler.h | 9 +- .../hipo/factorhighs/HybridSolveHandler.cpp | 658 +++++---- .../ipm/hipo/factorhighs/HybridSolveHandler.h | 19 +- highs/ipm/hipo/factorhighs/Numeric.cpp | 47 +- highs/ipm/hipo/factorhighs/Numeric.h | 3 +- highs/ipm/hipo/factorhighs/Symbolic.cpp | 19 +- highs/ipm/hipo/factorhighs/Symbolic.h | 32 +- highs/ipm/hipo/ipm/FactorHighsSolver.cpp | 241 ++-- highs/ipm/hipo/ipm/FactorHighsSolver.h | 4 +- highs/ipm/hipo/ipm/KktMatrix.cpp | 25 +- highs/ipm/hipo/ipm/KktMatrix.h | 3 +- highs/ipm/hipo/ipm/Model.cpp | 2 +- highs/ipm/hipo/ipm/Options.h | 21 +- highs/ipm/hipo/ipm/Parameters.h | 43 +- highs/ipm/hipo/ipm/PreProcess.cpp | 20 +- highs/ipm/hipo/ipm/Refine.cpp | 4 +- highs/ipm/hipo/ipm/Solver.cpp | 75 +- highs/ipm/hipo/ipm/Solver.h | 2 + highs/ipm/hipo/ipm/UpLookingSolver.cpp | 2 +- highs/ipm/ipx/ipx_config.h | 2 +- highs/ipm/ipx/lu_factorization.cc | 2 +- highs/ipm/ipx/maxvolume.cc | 4 +- highs/lp_data/HConst.h | 20 +- highs/lp_data/HStruct.h | 2 +- highs/lp_data/Highs.cpp | 28 +- highs/lp_data/HighsCallback.cpp | 2 +- highs/lp_data/HighsCallback.h | 2 +- highs/lp_data/HighsCallbackStruct.h | 2 +- highs/lp_data/HighsInterface.cpp | 47 +- highs/lp_data/HighsLp.h | 1 + highs/lp_data/HighsLpUtils.cpp | 8 +- highs/lp_data/HighsModelUtils.cpp | 6 +- highs/lp_data/HighsOptions.cpp | 16 +- highs/lp_data/HighsOptions.h | 44 +- highs/lp_data/HighsSolve.cpp | 8 +- highs/mip/HighsCliqueTable.h | 4 +- highs/mip/HighsConflictPool.h | 2 +- highs/mip/HighsCutGeneration.cpp | 16 +- highs/mip/HighsCutGeneration.h | 8 +- highs/mip/HighsCutPool.h | 8 +- highs/mip/HighsDomain.cpp | 8 +- highs/mip/HighsDomain.h | 10 +- highs/mip/HighsDomainChange.h | 2 +- highs/mip/HighsDynamicRowMatrix.h | 8 +- highs/mip/HighsGFkSolve.h | 8 +- highs/mip/HighsImplications.cpp | 5 + highs/mip/HighsImplications.h | 2 +- highs/mip/HighsLpRelaxation.cpp | 2 +- highs/mip/HighsMipSolverData.cpp | 11 +- highs/mip/HighsMipSolverData.h | 2 +- highs/mip/HighsModkSeparator.cpp | 2 +- highs/mip/HighsObjectiveFunction.h | 2 +- highs/mip/HighsPathSeparator.cpp | 2 +- highs/mip/HighsPseudocost.h | 2 +- highs/mip/HighsSearch.cpp | 12 +- highs/mip/HighsSeparator.h | 2 +- highs/mip/HighsTransformedLp.h | 2 +- highs/mip/MipTimer.h | 32 +- highs/parallel/HighsSplitDeque.h | 2 +- highs/parallel/HighsTaskExecutor.h | 2 +- highs/pdlp/hipdlp/pdhg.hpp | 2 +- highs/presolve/HPresolve.cpp | 1215 +++++++++++++---- highs/presolve/HPresolve.h | 52 +- highs/presolve/HPresolveAnalysis.cpp | 103 +- highs/presolve/HPresolveAnalysis.h | 28 +- highs/presolve/HighsPostsolveStack.cpp | 39 +- highs/presolve/HighsPostsolveStack.h | 123 +- highs/presolve/HighsSymmetry.cpp | 2 +- highs/presolve/HighsSymmetry.h | 4 +- highs/presolve/PresolveComponent.cpp | 11 +- highs/presolve/PresolveTimer.h | 249 ++++ highs/qpsolver/qpvector.hpp | 2 +- highs/simplex/HEkk.cpp | 4 +- highs/simplex/HEkkDualRHS.cpp | 18 +- highs/simplex/HEkkDualRHS.h | 4 +- highs/simplex/HSimplexNla.cpp | 2 +- highs/simplex/HSimplexNlaProductForm.cpp | 10 +- highs/simplex/HighsSimplexAnalysis.cpp | 6 +- highs/simplex/SimplexConst.h | 2 +- highs/test_kkt/KktCh2.h | 2 +- highs/util/HFactor.cpp | 179 ++- highs/util/HFactor.h | 5 +- highs/util/HFactorConst.h | 2 +- highs/util/HFactorRefactor.cpp | 4 +- highs/util/HSet.h | 2 +- highs/util/HVectorBase.cpp | 2 +- highs/util/HVectorBase.h | 4 +- highs/util/HighsDataStack.h | 2 +- highs/util/HighsDisjointSets.h | 2 +- highs/util/HighsHash.h | 2 +- highs/util/HighsIntegers.h | 2 +- highs/util/HighsMatrixSlice.h | 2 +- highs/util/HighsMemoryAllocation.h | 2 +- highs/util/HighsRbTree.h | 2 +- highs/util/HighsSparseVectorSum.h | 2 +- highs/util/HighsSplay.h | 2 +- highs/util/HighsTimer.h | 17 +- highs/util/HighsType.h | 21 + 133 files changed, 3406 insertions(+), 1336 deletions(-) create mode 100644 highs/presolve/PresolveTimer.h create mode 100644 highs/util/HighsType.h diff --git a/.github/workflows/action-sanitizers-bazel.yml b/.github/workflows/action-sanitizers-bazel.yml index 90ffee65529..8874ae67752 100644 --- a/.github/workflows/action-sanitizers-bazel.yml +++ b/.github/workflows/action-sanitizers-bazel.yml @@ -58,6 +58,7 @@ jobs: run: bazel test -c dbg --config=tsan --runs_per_test 5 //... - name: Upload bazel-testlogs + if: failure() uses: actions/upload-artifact@v7 with: name: bazel-testlogs-tsan diff --git a/check/Avgas.h b/check/Avgas.h index 6e7154cfc2f..d3f79fb15c1 100644 --- a/check/Avgas.h +++ b/check/Avgas.h @@ -15,7 +15,7 @@ #include -#include "util/HighsInt.h" +#include "util/HighsType.h" const HighsInt avgas_num_col = 8; const HighsInt avgas_num_row = 10; diff --git a/check/TestCheckSolution.cpp b/check/TestCheckSolution.cpp index 9cd858646c6..c87b4400505 100644 --- a/check/TestCheckSolution.cpp +++ b/check/TestCheckSolution.cpp @@ -289,7 +289,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") { index.clear(); value.clear(); - std::vector is_set; + std::vector is_set; is_set.assign(lp.num_col_, false); HighsInt num_to_set = 2; assert(num_to_set > 0); diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index 199d73c67b2..624fb9ed440 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -914,6 +914,158 @@ TEST_CASE("presolve-issue-2874", "[highs_test_presolve]") { highs.readModel(model_file); REQUIRE(highs.presolve() == HighsStatus::kOk); REQUIRE(highs.getModelPresolveStatus() == HighsPresolveStatus::kInfeasible); + + highs.resetGlobalScheduler(true); +} + +TEST_CASE("presolve-light", "[highs_test_presolve]") { + std::string model_file = + std::string(HIGHS_DIR) + "/check/instances/afiro.mps"; + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.readModel(model_file); + HighsInt presolved_num_col; + HighsInt presolved_num_row; + HighsInt presolved_num_nz; + for (HighsInt k = 0; k < 2; k++) { + REQUIRE(highs.presolve() == HighsStatus::kOk); + REQUIRE(highs.getModelPresolveStatus() == HighsPresolveStatus::kReduced); + const HighsLp& presolved_lp = highs.getPresolvedLp(); + if (k == 1) { + REQUIRE(presolved_lp.num_col_ > presolved_num_col); + REQUIRE(presolved_lp.num_row_ > presolved_num_row); + REQUIRE(presolved_lp.numNz() > presolved_num_nz); + } + presolved_num_col = presolved_lp.num_col_; + presolved_num_row = presolved_lp.num_row_; + presolved_num_nz = presolved_lp.numNz(); + + if (dev_run) + printf("%s presolved LP has %d columns; %d rows and %d nonzeros\n", + k == 0 ? "Fully" : "Lightly", int(presolved_num_col), + int(presolved_num_row), int(presolved_num_nz)); + highs.setOptionValue("presolve_light", kHighsOnString); + } + + highs.resetGlobalScheduler(true); +} + +TEST_CASE("presolve-initial-sweep-postsolve", "[highs_test_presolve]") { + Highs highs; + highs.setOptionValue("output_flag", dev_run); + if (dev_run) { + highs.setOptionValue("log_dev_level", 1); + highs.setOptionValue("presolve_rule_logging", true); + } + HighsLp lp; + lp.num_col_ = 3; + lp.num_row_ = 1; + lp.col_cost_ = {0, 1, 2}; + lp.col_lower_ = {1, 2, 0}; + lp.col_upper_ = {1, kHighsInf, kHighsInf}; + lp.row_lower_ = {-kHighsInf}; + lp.row_upper_ = {5}; + lp.a_matrix_.start_ = {0, 1, 2, 3}; + lp.a_matrix_.index_ = {0, 0, 0}; + lp.a_matrix_.value_ = {3, 2, -1}; + highs.passModel(lp); + // Presolved to empty, so no simplex iterations if postsolve is correct + highs.run(); + REQUIRE(highs.getInfo().simplex_iteration_count == 0); + if (dev_run) highs.writeSolution("", 1); + + highs.resetGlobalScheduler(true); +} + +TEST_CASE("presolve-initial-sweep", "[highs_test_presolve]") { + Highs highs; + highs.setOptionValue("output_flag", dev_run); + if (dev_run) { + highs.setOptionValue("log_dev_level", 1); + highs.setOptionValue("presolve_rule_logging", true); + } + HighsLp lp; + lp.num_col_ = 2; + lp.num_row_ = 1; + lp.col_cost_ = {-1, 1}; + lp.col_lower_ = {1, 0}; + lp.col_upper_ = {1, kHighsInf}; + lp.row_lower_ = {-kHighsInf}; + lp.row_upper_ = {5}; + lp.a_matrix_.start_ = {0, 1, 1}; + lp.a_matrix_.index_ = {0}; + lp.a_matrix_.value_ = {3}; + HighsStatus pass_model_status = HighsStatus::kOk; + for (HighsInt k = 0; k < 4; k++) { + if (dev_run) printf("\nPass k = %d\n==========\n", int(k)); + REQUIRE(highs.passModel(lp) == pass_model_status); + highs.run(); + if (k == 0) { + // Remove the empty column + lp.num_col_ = 1; + lp.col_cost_ = {-1}; + lp.col_lower_ = {1}; + lp.col_upper_ = {1}; + lp.a_matrix_.start_ = {0, 1}; + } else if (k == 1) { + REQUIRE(highs.getModelStatus() == HighsModelStatus::kOptimal); + lp.col_upper_ = {0}; + pass_model_status = HighsStatus::kWarning; + // Can infeasible column bounds even reach presolve? + } else if (k == 2) { + REQUIRE(highs.getModelStatus() == HighsModelStatus::kInfeasible); + lp.col_lower_ = {1e+20}; + lp.col_upper_ = {kHighsInf}; + lp.row_upper_ = {kHighsInf}; + pass_model_status = HighsStatus::kError; + // Why is this model accepted when error returns? + } else { + REQUIRE(highs.getModelStatus() == HighsModelStatus::kUnbounded); + } + if (dev_run) highs.writeSolution("", 1); + } + highs.resetGlobalScheduler(true); +} + +TEST_CASE("presolve-initial-sweep-all", "[highs_test_presolve]") { + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.setOptionValue("presolve_light", kHighsOnString); + if (dev_run) { + highs.setOptionValue("log_dev_level", 1); + highs.setOptionValue("presolve_rule_logging", true); + } + HighsLp lp; + lp.num_col_ = 7; + lp.num_row_ = 4; + lp.col_cost_ = {1, 1, 1, 1, 1, 1, 1}; + lp.col_lower_ = {0, 1, 0, 1, 1, -kHighsInf, 0}; + lp.col_upper_ = {1, 1, kHighsInf, 3, 1, 1, 1}; + lp.row_lower_ = {2, 8, 10, 13}; + lp.row_upper_ = {4, 9, 16, 26}; + lp.a_matrix_.start_ = {0, 1, 5, 6, 6, 10, 12, 13}; + lp.a_matrix_.index_ = {2, 0, 1, 2, 3, 0, 0, 1, 2, 3, 2, 3, 2}; + lp.a_matrix_.value_ = {6, 1, 4, 7, 11, 2, 3, 5, 8, 12, 9, 13, 10}; + // Cols 1 and 4 fixed at 1; col 3 empty (fixed at LB = 1) then + // + // Rows 0 and 3 singletons; row 1 empty + REQUIRE(highs.passModel(lp) == HighsStatus::kOk); + + highs.run(); + REQUIRE(highs.getModelStatus() == HighsModelStatus::kOptimal); + if (dev_run) highs.writeSolution("", 1); + + // Add a redundant row + std::vector index = {0, 5, 6}; + std::vector value = {1, 1, 1}; + highs.addRow(-kHighsInf, 4, 3, index.data(), value.data()); + + highs.setOptionValue("use_warm_start", false); + highs.run(); + REQUIRE(highs.getModelStatus() == HighsModelStatus::kOptimal); + if (dev_run) highs.writeSolution("", 1); + + highs.resetGlobalScheduler(true); } TEST_CASE("bound_implied", "[highs_test_presolve]") { diff --git a/check/TestRunData.cpp b/check/TestRunData.cpp index 04f1e77cab3..c93de54a07e 100644 --- a/check/TestRunData.cpp +++ b/check/TestRunData.cpp @@ -1,3 +1,4 @@ +#include #include #include "HCheckConfig.h" @@ -5,7 +6,19 @@ #include "catch.hpp" #include "io/HMPSIO.h" -const bool dev_run = false; +const bool dev_run = false; // true;// + +const std::vector solvers{ + // kHighsChooseString + kSimplexString, + // kIpxString, + kHipoString + // kQpAsmString + // kHiPdlpString +}; + +void testRunData(Highs& h, const bool irreducible, const bool reduces_to_empty, + const std::string& run_data_file); TEST_CASE("run-data-md", "[highs_run_data]") { Highs h; @@ -18,120 +31,182 @@ TEST_CASE("run-data-md", "[highs_run_data]") { } TEST_CASE("highs-run-data", "[highs_run_data]") { + // Doesn't work for MIPs yet, but wait until profiling is merged in + // to avoid conflicts + const std::vector models{ + "adlittle", "egout-ac" + // "flugpl" + }; const std::string test_name = Catch::getResultCapture().getCurrentTestName(); - const std::string highs_run_data_file = test_name + ".run_data"; + const std::string run_data_file = test_name + ".run_data"; Highs h; - if (!dev_run) h.setOptionValue("output_flag", false); - const HighsRunData& highs_run_data = h.getRunData(); - - auto testRunData = [&](const std::string& filename) { + h.setOptionValue("output_flag", dev_run); + const bool irreducible = false; + for (auto& model : models) { + std::string filename = + std::string(HIGHS_DIR) + "/check/instances/" + model + ".mps"; + REQUIRE(h.readModel(filename) == HighsStatus::kOk); HighsStatus return_status = h.readModel(filename); REQUIRE(return_status == HighsStatus::kOk); + const bool reduces_to_empty = model == "egout-ac" ? true : false; - // Cannot write run_data since not valid before run() - return_status = h.writeRunData(""); - REQUIRE(return_status == HighsStatus::kWarning); - - HighsRunDataType highs_run_data_type; - return_status = h.getRunDataType("presolved_num_col", highs_run_data_type); - REQUIRE(return_status == HighsStatus::kError); - return_status = - h.getRunDataType("presolved_model_num_col", highs_run_data_type); - REQUIRE(return_status == HighsStatus::kOk); - REQUIRE(highs_run_data_type == HighsRunDataType::kInt); - - return_status = h.getRunDataType("presolving_time", highs_run_data_type); - REQUIRE(return_status == HighsStatus::kError); - return_status = h.getRunDataType("presolve_time", highs_run_data_type); - REQUIRE(return_status == HighsStatus::kOk); - REQUIRE(highs_run_data_type == HighsRunDataType::kDouble); - - // Run data not valid before run() - HighsInt presolved_model_num_col; - return_status = - h.getRunDataValue("presolved_model_num_col", presolved_model_num_col); - REQUIRE(return_status == HighsStatus::kWarning); + for (auto& solver : solvers) + testRunData(h, irreducible, reduces_to_empty, run_data_file); + } + if (!dev_run) std::remove(run_data_file.c_str()); - return_status = h.run(); - REQUIRE(return_status == HighsStatus::kOk); + h.resetGlobalScheduler(true); +} - if (dev_run) { - return_status = h.writeRunData(""); - REQUIRE(return_status == HighsStatus::kOk); +TEST_CASE("highs-run-data-presolve", "[highs_run_data]") { + const std::vector models{"adlittle", "flugpl"}; + const std::string test_name = Catch::getResultCapture().getCurrentTestName(); + const std::string run_data_file = test_name + ".run_data"; + Highs h; + h.setOptionValue("output_flag", dev_run); + const HighsRunData& run_data = h.getRunData(); + const HighsLp& lp = h.getLp(); + for (auto& model : models) { + std::string filename = + std::string(HIGHS_DIR) + "/check/instances/" + model + ".mps"; + REQUIRE(h.readModel(filename) == HighsStatus::kOk); + const bool irreducible = true; + const bool reduces_to_empty = false; + for (auto& solver : solvers) { + h.setOptionValue("solver", solver); + if (dev_run) + printf("\n!>>>>%s-%s<<<<\n", model.c_str(), solver.c_str()); + + REQUIRE(h.presolve() == HighsStatus::kOk); + HighsLp presolved_lp = h.getPresolvedLp(); + + h.passModel(presolved_lp); + h.setOptionValue("solve_relaxation", true); + h.setOptionValue(kPresolveString, kHighsOffString); + testRunData(h, irreducible, reduces_to_empty, run_data_file); } + } - return_status = h.writeRunData(highs_run_data_file); - REQUIRE(return_status == HighsStatus::kOk); - - // Wrong name for objective - return_status = - h.getRunDataValue("presolved_num_col", presolved_model_num_col); - REQUIRE(return_status == HighsStatus::kError); - - // Right name for objective - return_status = - h.getRunDataValue("presolved_model_num_col", presolved_model_num_col); - REQUIRE(return_status == HighsStatus::kOk); - - if (dev_run) - printf("From getRunDataValue: presolved_model_num_col = %d\n", - int(presolved_model_num_col)); - - double presolve_time; - // Wrong name for simplex iteration count - return_status = h.getRunDataValue("presolving_time", presolve_time); - REQUIRE(return_status == HighsStatus::kError); + h.resetGlobalScheduler(true); +} - // Right name for presolve time - return_status = h.getRunDataValue("presolve_time", presolve_time); +void testRunData(Highs& h, const bool irreducible, const bool reduces_to_empty, + const std::string& run_data_file) { + + assert(!(irreducible && reduces_to_empty)); + const HighsRunData& run_data = h.getRunData(); + const HighsLp& lp = h.getLp(); + + std::string presolve; + h.getOptionValue(kPresolveString, presolve); + const bool run_presolve = presolve != kHighsOffString; + + // Cannot write run_data since not valid before run() + HighsStatus return_status = h.writeRunData(""); + REQUIRE(return_status == HighsStatus::kWarning); + + HighsRunDataType run_data_type; + return_status = h.getRunDataType("presolved_num_col", run_data_type); + REQUIRE(return_status == HighsStatus::kError); + return_status = h.getRunDataType("presolved_model_num_col", run_data_type); + REQUIRE(return_status == HighsStatus::kOk); + REQUIRE(run_data_type == HighsRunDataType::kInt); + + return_status = h.getRunDataType("presolving_time", run_data_type); + REQUIRE(return_status == HighsStatus::kError); + return_status = h.getRunDataType("presolve_time", run_data_type); + REQUIRE(return_status == HighsStatus::kOk); + REQUIRE(run_data_type == HighsRunDataType::kDouble); + + // Run data not valid before run() + HighsInt presolved_model_num_col; + return_status = + h.getRunDataValue("presolved_model_num_col", presolved_model_num_col); + REQUIRE(return_status == HighsStatus::kWarning); + + return_status = h.run(); + REQUIRE(return_status == HighsStatus::kOk); + + if (dev_run) { + return_status = h.writeRunData(""); REQUIRE(return_status == HighsStatus::kOk); - - const HighsModelStatus model_status = h.getModelStatus(); - if (dev_run) { - printf("From getModelStatus: model_status = %s\n", - h.modelStatusToString(model_status).c_str()); - printf("From getRunData: presolved_model_num_col = %d\n", - int(highs_run_data.presolved_model_num_col)); - printf("From getRunData: presolved_model_num_row = %d\n", - int(highs_run_data.presolved_model_num_row)); - printf("From getRunData: presolved_model_num_nz = %d\n", - int(highs_run_data.presolved_model_num_nz)); - if (!h.getLp().isMip()) - printf( - "From getRunData: num_simplex_iterations_after_postsolve = %d\n", - int(highs_run_data.num_simplex_iterations_after_postsolve)); - printf("From getRunData: presolve_time = %g\n", - highs_run_data.presolve_time); - printf("From getRunData: solve_time = %g\n", - highs_run_data.solve_time); - printf("From getRunData: postsolve_time = %g\n", - highs_run_data.postsolve_time); - } - REQUIRE(highs_run_data.presolved_model_num_col >= 0); - REQUIRE(highs_run_data.presolved_model_num_row >= 0); - REQUIRE(highs_run_data.presolved_model_num_nz >= 0); + } + + return_status = h.writeRunData(run_data_file); + REQUIRE(return_status == HighsStatus::kOk); + + // Wrong name for objective + return_status = + h.getRunDataValue("presolved_num_col", presolved_model_num_col); + REQUIRE(return_status == HighsStatus::kError); + + // Right name for objective + return_status = + h.getRunDataValue("presolved_model_num_col", presolved_model_num_col); + REQUIRE(return_status == HighsStatus::kOk); + + if (dev_run) + printf("From getRunDataValue: presolved_model_num_col = %d\n", + int(presolved_model_num_col)); + + double presolve_time; + // Wrong name for presolve_time + return_status = h.getRunDataValue("presolving_time", presolve_time); + REQUIRE(return_status == HighsStatus::kError); + + // Right name for presolve time + return_status = h.getRunDataValue("presolve_time", presolve_time); + REQUIRE(return_status == HighsStatus::kOk); + + const HighsModelStatus model_status = h.getModelStatus(); + if (dev_run) { + printf("From getModelStatus: model_status = %s\n", + h.modelStatusToString(model_status).c_str()); + printf("From getRunData: presolved_model_num_col = %d\n", + int(run_data.presolved_model_num_col)); + printf("From getRunData: presolved_model_num_row = %d\n", + int(run_data.presolved_model_num_row)); + printf("From getRunData: presolved_model_num_nz = %d\n", + int(run_data.presolved_model_num_nz)); if (!h.getLp().isMip()) - REQUIRE(highs_run_data.num_simplex_iterations_after_postsolve == 0); - REQUIRE(highs_run_data.presolve_time >= 0); - REQUIRE(highs_run_data.solve_time >= 0); - REQUIRE(highs_run_data.postsolve_time >= 0); - }; - - std::string filename; - filename = std::string(HIGHS_DIR) + "/check/instances/adlittle.mps"; - testRunData(filename); - - filename = std::string(HIGHS_DIR) + "/check/instances/egout-ac.mps"; - testRunData(filename); - - // Doesn't work for MIPs yet, but wait until profiling is merged in - // to avoid conflicts - // - // filename = std::string(HIGHS_DIR) + "/check/instances/flugpl.mps"; - // testRunData(filename); - - if (!dev_run) std::remove(highs_run_data_file.c_str()); - - h.resetGlobalScheduler(true); + printf("From getRunData: num_simplex_iterations_after_postsolve = %d\n", + int(run_data.num_simplex_iterations_after_postsolve)); + printf("From getRunData: presolve_time = %g\n", run_data.presolve_time); + printf("From getRunData: solve_time = %g\n", run_data.solve_time); + printf("From getRunData: postsolve_time = %g\n", run_data.postsolve_time); + } + if (run_presolve) { + REQUIRE(run_data.presolve_time >= 0); + REQUIRE(run_data.presolve_time < kHighsInf); + REQUIRE(run_data.presolved_model_num_col >= 0); + REQUIRE(run_data.presolved_model_num_row >= 0); + REQUIRE(run_data.presolved_model_num_nz >= 0); + if (!irreducible) { + REQUIRE(run_data.presolved_model_num_col < lp.num_col_); + REQUIRE(run_data.presolved_model_num_row < lp.num_row_); + REQUIRE(run_data.presolved_model_num_nz < lp.a_matrix_.numNz()); + } + if (reduces_to_empty) { + REQUIRE(run_data.presolved_model_num_col == 0); + REQUIRE(run_data.presolved_model_num_row == 0); + REQUIRE(run_data.presolved_model_num_nz == 0); + } + REQUIRE(run_data.postsolve_time >= 0); + REQUIRE(run_data.postsolve_time < kHighsInf); + if (!h.getLp().isMip()) { + REQUIRE(run_data.num_simplex_iterations_after_postsolve == 0); + } + } else { + REQUIRE(run_data.presolve_time == kHighsIllegalDoubleMeasure); + REQUIRE(run_data.presolved_model_num_col == kHighsIllegalIntMeasure); + REQUIRE(run_data.presolved_model_num_row == kHighsIllegalIntMeasure); + REQUIRE(run_data.presolved_model_num_nz == kHighsIllegalIntMeasure); + REQUIRE(run_data.postsolve_time == kHighsIllegalDoubleMeasure); + REQUIRE(run_data.num_simplex_iterations_after_postsolve == + kHighsIllegalIntMeasure); + } + REQUIRE(run_data.solve_time >= 0); + REQUIRE(run_data.solve_time < kHighsInf); + h.clearSolver(); } diff --git a/cmake/sources.cmake b/cmake/sources.cmake index 2bec64fb96b..dc6ea0064f7 100644 --- a/cmake/sources.cmake +++ b/cmake/sources.cmake @@ -570,6 +570,7 @@ set(highs_headers presolve/ICrashUtil.h presolve/ICrashX.h presolve/PresolveComponent.h + presolve/PresolveTimer.h qpsolver/a_asm.hpp qpsolver/a_quass.hpp qpsolver/basis.hpp @@ -622,6 +623,7 @@ set(highs_headers util/HighsHash.h util/HighsHashTree.h util/HighsInt.h + util/HighsType.h util/HighsIntegers.h util/HighsLinearSumBounds.h util/HighsMatrixPic.h diff --git a/docs/c_api_gen/build.jl b/docs/c_api_gen/build.jl index 29ca8fb006f..3417d09bfa1 100644 --- a/docs/c_api_gen/build.jl +++ b/docs/c_api_gen/build.jl @@ -16,7 +16,7 @@ libhighs_filename = joinpath(@__DIR__, "libhighs.jl") Generators.build!( Generators.create_context( - [c_api, joinpath(highs_src, "util", "HighsInt.h")], + [c_api, joinpath(highs_src, "util", "HighsType.h")], [Generators.get_default_args(); "-I$highs_src"; "-I$(@__DIR__)"], Dict{String,Any}( "general" => Dict{String,Any}( diff --git a/docs/src/options/definitions.md b/docs/src/options/definitions.md index 08f3b96580e..2ea6d7ab599 100644 --- a/docs/src/options/definitions.md +++ b/docs/src/options/definitions.md @@ -440,9 +440,21 @@ - Default: "choose" ## [hipo\_parallel\_type](@id option-hipo-parallel-type) -- HiPO parallelism: "tree", "node" or "both" +- HiPO parallelism: "tree", "node", "both" or "choose" - Type: string -- Default: "both" +- Default: "choose" + +## [hipo\_parallel\_force](@id option-hipo-parallel-force) +- Bit map to force the use of parallel techniques in HiPO +- Type: integer +- Range: {0, 1023} +- Default: 0 + +## [hipo\_parallel\_forbid](@id option-hipo-parallel-forbid) +- Bit map to forbid the use of parallel techniques in HiPO +- Type: integer +- Range: {0, 1023} +- Default: 0 ## [hipo\_ordering](@id option-hipo-ordering) - HiPO matrix reordering: "choose", "metis", "amd" or "rcm" diff --git a/docs/src/parallel.md b/docs/src/parallel.md index 129c3e55eda..60e1cc3d4a6 100644 --- a/docs/src/parallel.md +++ b/docs/src/parallel.md @@ -46,24 +46,57 @@ parallel dual simplex solver is unlikely to be worth using. ## IPM -The interior point solver HiPO uses multiple threads to process the -elimination tree during the multifrontal factorisation (_tree level_) -and to perform the dense factorisation of the frontal matrices -(_node level_). - -If the [parallel](@ref option-parallel) option is set "on", the level of parallelism is -determined by the [hipo\_parallel\_type](@ref option-hipo-parallel-type) option, -which can be "tree" for tree level only, "node" for node level only, or -"both" for both levels. - -If the [parallel](@ref option-parallel) option is set "choose", the solver selects which -level to use based on a heuristic. When the [parallel](@ref option-parallel) option is set -"choose" or "off", the value of the hipo\_parallel\_type option is ignored. - -In addition, HiPO utilises parallelism to run multiple orderings heuristics on different -Newton system approaches (in order to choose the best one), and to construct the normal -equations matrix. This parallelism is always advantageous, so is performed regardless of -the value of the [parallel](@ref option-parallel) option. +The interior point solver HiPO uses multi-threading in various phases: + +* To run multiple orderings heuristics on different Newton system approaches, + in order to choose the best one. +* To construct the normal equations matrix. +* To process the elimination tree during the multifrontal factorisation + (_tree level_ parallelism). +* To perform the dense factorisation of the frontal matrices (_node level_ parallelism). +* To perform the triangular solves. + +Running multiple orderings and building the normal equations in parallel is always +advantageous, so is performed regardless of the value of the +[parallel](@ref option-parallel) option. + +If the [parallel](@ref option-parallel) option is set "on" or "choose", the parallelism +during the factorisation and the triangular solves is controlled by a heuristic. +Otherwise, these phases do not exploit parallelism. + +If the [parallel](@ref option-parallel) option is set "on", the level of parallelism +in the factorisation can be refined using the +[hipo\_parallel\_type](@ref option-hipo-parallel-type) option, which can be "tree" for +tree level only, "node" for node level only, "both" for both levels, or "choose" to +leave the selection to the solver. + +The options [hipo\_parallel\_force](@ref option-hipo-parallel-force) and +[hipo\_parallel\_forbid](@ref option-hipo-parallel-forbid) can be used to override the +default parallelism settings. They are bit maps in which the various parallel phases are +controlled by the following values: +- 1: analyse phase +- 2: reordering of normal equations +- 4: reordering of augmented system +- 8: building of normal equations structure +- 16: building of normal equations values +- 32: processing of the elimination tree +- 64: dense factorisation of frontal matrices +- 128: forward solve with factorisation +- 256: diagonal solve with factorisation +- 512: backward solve with factorisation + +Setting `hipo_parallel_force` (resp. `hipo_parallel_forbid`) to one of these values, +or a sum of values, forces (resp. forbids) the use of parallelism in the corresponding +phases. These options override any other behaviour enforced by other options. If a given +phase is both forced and forbidden, the default behaviour is used instead. +For instance, setting `hipo_parallel_force` to 81 = 1+16+64 and `hipo_parallel_forbid` +to 68 = 4+64 forces the use of parallelism in the analyse phase and for building the +normal equations values, and forbids it for the reordering of augmented system. + +By default, `hipo_parallel_type` is set to `choose`, `hipo_parallel_force` and +`hipo_parallel_forbid` are set to 0, meaning that the choice of parallelism is left +completely to the internal heuristics. These heuristics are very much likely to be +correct, but the options provide ways of overriding them. The extent to which parallelism is used in HiPO depends on the value of the [threads](@ref option-threads) option (see above). diff --git a/highs/Highs.h b/highs/Highs.h index 83b73039e70..f6c72607b71 100644 --- a/highs/Highs.h +++ b/highs/Highs.h @@ -781,7 +781,7 @@ class Highs { * @brief Get the number of (constraint matrix) nonzeros in the incumbent * model */ - HighsInt getNumNz() const { return model_.lp_.a_matrix_.numNz(); } + HighsInt getNumNz() const { return model_.lp_.numNz(); } /** * @brief Get the number of Hessian matrix nonzeros in the incumbent model @@ -1844,7 +1844,7 @@ class Highs { HighsStatus getIisInterface(); HighsStatus getIisInterfaceReturn( const HighsStatus return_status, const HighsOptions& original_options, - const std::vector& original_callbacks); + const std::vector& original_callbacks); HighsStatus elasticityFilterReturn( const HighsStatus return_status, const std::string& original_model_name, diff --git a/highs/HighsExternalApi.h b/highs/HighsExternalApi.h index 92a832a7f5b..50ace471c39 100644 --- a/highs/HighsExternalApi.h +++ b/highs/HighsExternalApi.h @@ -20,7 +20,7 @@ #include "HighsExtrasApi.h" #include "io/HighsIO.h" #include "util/HighsDynamicLibrary.h" -#include "util/HighsInt.h" +#include "util/HighsType.h" #include "util/stringutil.h" // diff --git a/highs/interfaces/highs_c_api.cpp b/highs/interfaces/highs_c_api.cpp index 9b5acba13dc..87c8e09e10b 100644 --- a/highs/interfaces/highs_c_api.cpp +++ b/highs/interfaces/highs_c_api.cpp @@ -1189,7 +1189,7 @@ HighsInt Highs_getPresolvedNumRow(const void* highs) { } HighsInt Highs_getPresolvedNumNz(const void* highs) { - return ((Highs*)highs)->getPresolvedLp().a_matrix_.numNz(); + return ((Highs*)highs)->getPresolvedLp().numNz(); } // Gets pointers to all the public data members of HighsLp: avoids @@ -1236,7 +1236,7 @@ static HighsInt Highs_getHighsLpData(const HighsLp& lp, const HighsInt a_format, (desired_a_format == MatrixFormat::kRowwise && lp.a_matrix_.isRowwise())) { // Incumbent format is OK - *num_nz = lp.a_matrix_.numNz(); + *num_nz = lp.numNz(); if (a_start) memcpy(a_start, lp.a_matrix_.start_.data(), num_start_entries * sizeof(HighsInt)); diff --git a/highs/io/FilereaderLp.cpp b/highs/io/FilereaderLp.cpp index 09890226162..f38e007d133 100644 --- a/highs/io/FilereaderLp.cpp +++ b/highs/io/FilereaderLp.cpp @@ -251,8 +251,8 @@ FilereaderRetcode FilereaderLp::readModelFromFile(const HighsOptions& options, "coefficient%s in row %d (name \"%s\")\n", int(iCol), lp.col_names_[iCol].c_str(), int(zero_count[iRow]), - zero_count[iRow] > 1 ? "s" : "", int(iRow), - lp.row_names_[iRow].c_str()); + highsIntToPlural(zero_count[iRow]).c_str(), + int(iRow), lp.row_names_[iRow].c_str()); num_report++; } sum_num_duplicate += (num_ocurrence - 1); diff --git a/highs/io/HMPSIO.h b/highs/io/HMPSIO.h index 58acffe6e81..4e09cbd2549 100644 --- a/highs/io/HMPSIO.h +++ b/highs/io/HMPSIO.h @@ -19,7 +19,7 @@ #include #include "io/Filereader.h" -#include "util/HighsInt.h" +#include "util/HighsType.h" using std::string; using std::vector; diff --git a/highs/io/HMpsFF.cpp b/highs/io/HMpsFF.cpp index 643434cbfb5..df29fe7088b 100644 --- a/highs/io/HMpsFF.cpp +++ b/highs/io/HMpsFF.cpp @@ -1269,8 +1269,8 @@ HMpsFF::Parsekey HMpsFF::parseBounds(const HighsLogOptions& log_options, HighsInt num_si = 0; HighsInt num_sc = 0; - std::vector has_lower; - std::vector has_upper; + std::vector has_lower; + std::vector has_upper; has_lower.assign(num_col, false); has_upper.assign(num_col, false); diff --git a/highs/io/HMpsFF.h b/highs/io/HMpsFF.h index ebd5c3707fe..c5d3e66a33e 100644 --- a/highs/io/HMpsFF.h +++ b/highs/io/HMpsFF.h @@ -30,7 +30,7 @@ #include "io/HighsIO.h" #include "model/HighsModel.h" -// #include "util/HighsInt.h" +// #include "util/HighsType.h" #include "util/stringutil.h" using Triplet = std::tuple; @@ -97,7 +97,7 @@ class HMpsFF { // that are defined as integer by markers in the column section, or // as binary by having a BV flag in the BOUNDS section, and without // any LI or UI flags in the BOUNDS section - std::vector col_binary; + std::vector col_binary; // Record where the cost row is encountered HighsInt cost_row_location; @@ -117,7 +117,7 @@ class HMpsFF { // file for the objective or a row. Have to be class data members so // that they can be used by parseName and addRhs in HMpsFF::parseRhs bool has_obj_entry_; - std::vector has_row_entry_; + std::vector has_row_entry_; /// load LP from MPS file as transposed triplet matrix HighsInt parseFile(std::string filename); diff --git a/highs/io/HighsIO.cpp b/highs/io/HighsIO.cpp index fc4d1e17398..8fb1cc33f7f 100644 --- a/highs/io/HighsIO.cpp +++ b/highs/io/HighsIO.cpp @@ -310,6 +310,11 @@ const std::string highsBoolToString(const bool b, const HighsInt field_width) { return b ? " true" : "false"; } +const std::string highsIntToPlural(const HighsInt i, const bool y) { + if (y) return i == 1 ? "y" : "ies"; + return i == 1 ? "" : "s"; +} + const std::string highsTimeToString(const double time) { return #ifndef NDEBUG diff --git a/highs/io/HighsIO.h b/highs/io/HighsIO.h index c35ad89dd06..91a21b8275c 100644 --- a/highs/io/HighsIO.h +++ b/highs/io/HighsIO.h @@ -111,6 +111,7 @@ std::string highsFormatToString(const char* format, ...); const std::string highsBoolToString(const bool b, const HighsInt field_width = 2); +const std::string highsIntToPlural(const HighsInt i, const bool y = false); const std::string highsInsertMdEscapes(const std::string& from_string); const std::string highsInsertMdId(const std::string& from_string); const std::string highsTimeToString(const double time); diff --git a/highs/ipm/IpxSolution.h b/highs/ipm/IpxSolution.h index 3a5eb919a8d..7ad4dd0e64e 100644 --- a/highs/ipm/IpxSolution.h +++ b/highs/ipm/IpxSolution.h @@ -15,7 +15,7 @@ #include -#include "util/HighsInt.h" +#include "util/HighsType.h" typedef HighsInt ipxint; struct IpxSolution { diff --git a/highs/ipm/hipo/auxiliary/Auxiliary.cpp b/highs/ipm/hipo/auxiliary/Auxiliary.cpp index 11c116d1850..580893af81c 100644 --- a/highs/ipm/hipo/auxiliary/Auxiliary.cpp +++ b/highs/ipm/hipo/auxiliary/Auxiliary.cpp @@ -357,4 +357,9 @@ double Clock::stop() const { return d.count(); } +TempTimer::TempTimer(const char* s) : name{s} {} +void TempTimer::start() { clock.start(); } +void TempTimer::stop() { time += clock.stop(); } +TempTimer::~TempTimer() { printf("Measured %10s: %e\n", name.c_str(), time); } + } // namespace hipo diff --git a/highs/ipm/hipo/auxiliary/Auxiliary.h b/highs/ipm/hipo/auxiliary/Auxiliary.h index 3731ce92ffa..b96589a496d 100644 --- a/highs/ipm/hipo/auxiliary/Auxiliary.h +++ b/highs/ipm/hipo/auxiliary/Auxiliary.h @@ -115,6 +115,17 @@ class Clock { double stop() const; }; +struct TempTimer { + std::string name; + Clock clock; + double time = 0.0; + + TempTimer(const char* s); + void start(); + void stop(); + ~TempTimer(); +}; + } // namespace hipo #endif diff --git a/highs/ipm/hipo/auxiliary/IntConfig.h b/highs/ipm/hipo/auxiliary/IntConfig.h index f103a1e5b83..cf647e35126 100644 --- a/highs/ipm/hipo/auxiliary/IntConfig.h +++ b/highs/ipm/hipo/auxiliary/IntConfig.h @@ -2,7 +2,7 @@ #define HIPO_INT_CONFIG_H #include "lp_data/HConst.h" -#include "util/HighsInt.h" +#include "util/HighsType.h" namespace hipo { diff --git a/highs/ipm/hipo/factorhighs/Analyse.cpp b/highs/ipm/hipo/factorhighs/Analyse.cpp index 2a7905a835d..8da2bcc4bc3 100644 --- a/highs/ipm/hipo/factorhighs/Analyse.cpp +++ b/highs/ipm/hipo/factorhighs/Analyse.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -12,6 +13,8 @@ #include "ReturnValues.h" #include "ipm/hipo/auxiliary/Auxiliary.h" #include "ipm/hipo/auxiliary/Logger.h" +#include "util/HighsDisjointSets.h" + namespace hipo { const Int64 int32_limit = std::numeric_limits::max(); @@ -210,7 +213,7 @@ void Analyse::fundamentalSupernodes() { // Find fundamental supernodes. // isSN[i] is true if node i is the start of a fundamental supernode - std::vector is_sn(n_, false); + std::vector is_sn(n_, false); std::vector prev_nonz(n_, -1); @@ -956,6 +959,45 @@ void Analyse::computeCriticalPath() { } } +void Analyse::computeCriticalPathSolve() { + // Compute the critical path within the task elimination tree, and the + // number of operations along the path. This is the number of operations that + // need to be done sequentially while doing tree parallelism. + + std::vector critical_ops(schedule_solve_.count()); + + // linked lists of children + std::vector head, next; + childrenLinkedList(schedule_solve_.task_parent, head, next); + + ops_solve_ = 0.0; + critical_ops_solve_ = 0.0; + + for (Int task = 0; task < schedule_solve_.count(); ++task) { + critical_ops[task] = task_ops_solve_[task]; + ops_solve_ += task_ops_solve_[task]; + } + + for (Int task = 0; task < schedule_solve_.count(); ++task) { + // leaf task + if (head[task] == -1) continue; + + double max_ops{}; + Int child = head[task]; + while (child != -1) { + // critical_ops of this supernode is max over children of + // (ops_of_this_task + critical_ops_of_child) + max_ops = std::max(max_ops, critical_ops[task] + critical_ops[child]); + child = next[child]; + } + critical_ops[task] = max_ops; + } + + for (Int task = 0; task < schedule_solve_.count(); ++task) { + critical_ops_solve_ = std::max(critical_ops_solve_, critical_ops[task]); + } +} + void Analyse::reorderChildren() { std::vector clique_entries(sn_count_); std::vector frontal_entries(sn_count_); @@ -1222,6 +1264,97 @@ void Analyse::computeStackSize() { serial_storage_ = (total_frontal + max_stack_size_) * 8; } +void Analyse::computeTreeScheduleSolve() { + // compute number of operations for each supernode + std::vector sn_ops(sn_count_); + double total_ops = 0; + for (Int sn = 0; sn < sn_count_; ++sn) { + const Int sz = sn_start_[sn + 1] - sn_start_[sn]; + const Int fr = ptr_sn_[sn + 1] - ptr_sn_[sn]; + const double this_sn_dense_ops = + (double)sz * (sz + 1) / 2 + (double)sz * (fr - sz); + sn_ops[sn] += this_sn_dense_ops; + total_ops += this_sn_dense_ops; + } + + std::vector head, next; + childrenLinkedList(sn_parent_, head, next); + + const double task_ops_thresh = + std::max(total_ops * kLargeTaskRelativeThresh, kLargeTaskAbsoluteThres); + + HighsDisjointSets<> sets(sn_count_); + std::vector child_ops(sn_count_, 0.0); + std::map task_numbering; + Int task_count = 0; + + // Assign supernodes to tasks: + // if a supernode is part of a task that is large enough, then the task is + // considered complete. Otherwise, the task is still open and the parent + // supernode will continue it. + for (Int sn = 0; sn < sn_count_; ++sn) { + double this_sn_task_ops = sn_ops[sn]; + Int child = head[sn]; + while (child != -1) { + this_sn_task_ops += child_ops[child]; + child = next[child]; + } + + const bool task_large = this_sn_task_ops > task_ops_thresh; + const bool task_root = sn_parent_[sn] == -1; + + if (task_large || task_root) { + // completed a task + child_ops[sn] = 0.0; + task_numbering[sets.getSet(sn)] = task_count; + task_count++; + } else { + // task still incomplete + child_ops[sn] = this_sn_task_ops; + sets.merge(sn, sn_parent_[sn]); + } + } + + schedule_solve_.sn_per_task.resize(task_count); + task_ops_solve_.assign(task_count, 0.0); + for (Int sn = 0; sn < sn_count_; ++sn) { + const Int task_id = task_numbering[sets.getSet(sn)]; + schedule_solve_.sn_per_task[task_id].push_back(sn); + task_ops_solve_[task_id] += sn_ops[sn]; + } + + // Create tree of dependencies among tasks. + // Since the supernodal tree is postordered, the tree of dependencies among + // tasks should be automatically postordered as well. + schedule_solve_.task_parent.assign(task_count, -1); + for (Int task = 0; task < task_count; ++task) { + for (Int sn : schedule_solve_.sn_per_task[task]) { + Int child = head[sn]; + while (child != -1) { + const Int child_task = task_numbering[sets.getSet(child)]; + if (child_task != task) { + assert(child_task < task); + assert(schedule_solve_.task_parent[child_task] == -1); + schedule_solve_.task_parent[child_task] = task; + } + child = next[child]; + } + } + } + + schedule_solve_.valid = true; + + // verify that task elimination tree has topological ordering + for (Int task = 0; task < task_count; ++task) { + const Int this_parent = schedule_solve_.task_parent[task]; + if (this_parent != -1 && this_parent <= task) { + schedule_solve_.clear(); + logger_->printInfo("Task tree does not have topological ordering\n"); + break; + } + } +} + Int Analyse::run(Symbolic& S) { // Perform analyse phase and store the result into the symbolic object S. // After Run returns, the Analyse object is not valid. @@ -1263,6 +1396,9 @@ Int Analyse::run(Symbolic& S) { computeCriticalPath(); computeStackSize(); + computeTreeScheduleSolve(); + computeCriticalPathSolve(); + // move relevant stuff into S S.n_ = n_; S.sn_ = sn_count_; @@ -1277,6 +1413,8 @@ Int Analyse::run(Symbolic& S) { S.flops_ = dense_ops_; S.max_stack_size_ = max_stack_size_; S.tree_depth_ = maxDepthTree(sn_parent_); + S.ops_solve_ = ops_solve_; + S.critops_solve_ = critical_ops_solve_; // compute largest supernode std::vector sn_size(sn_start_.begin() + 1, sn_start_.end()); @@ -1308,6 +1446,7 @@ Int Analyse::run(Symbolic& S) { S.relind_clique_ = std::move(relind_clique_); S.consecutive_sums_ = std::move(consecutive_sums_); S.clique_block_start_ = std::move(clique_block_start_); + S.schedule_solve_ = std::move(schedule_solve_); S.empty_ = false; diff --git a/highs/ipm/hipo/factorhighs/Analyse.h b/highs/ipm/hipo/factorhighs/Analyse.h index a6432dafef6..8186fe529ae 100644 --- a/highs/ipm/hipo/factorhighs/Analyse.h +++ b/highs/ipm/hipo/factorhighs/Analyse.h @@ -85,6 +85,12 @@ class Analyse { const Logger* logger_; DataCollector& data_; + TreeSchedule schedule_solve_; + + double ops_solve_; + double critical_ops_solve_; + std::vector task_ops_solve_; + // Functions to perform analyse phase void permute(const std::vector& iperm); void eTree(); @@ -103,8 +109,10 @@ class Analyse { void computeStorage(Int fr, Int sz, Int64& fr_entries, Int64& cl_entries) const; void computeCriticalPath(); + void computeCriticalPathSolve(); void computeBlockStart(); void computeStackSize(); + void computeTreeScheduleSolve(); Int checkOverflow() const; public: diff --git a/highs/ipm/hipo/factorhighs/DenseFact.h b/highs/ipm/hipo/factorhighs/DenseFact.h index 408a75d0d72..9134e83018a 100644 --- a/highs/ipm/hipo/factorhighs/DenseFact.h +++ b/highs/ipm/hipo/factorhighs/DenseFact.h @@ -38,8 +38,8 @@ Int denseFactK(char uplo, Int n, double* A, Int lda, Int* pivot_sign, // dense partial factorisation, in "hybrid formats" Int denseFactFH(char format, Int n, Int k, double* A, double* B, const Int* pivot_sign, double thresh, double* totalreg, - Int* swaps, double* pivot_2x2, bool parnode, - DataCollector& data, const FHoptions& options); + Int* swaps, double* pivot_2x2, DataCollector& data, + const FHoptions& options); // function to convert A from lower packed, to lower-blocked-hybrid format Int denseFactFP2FH(double* A, Int nrow, Int ncol, Int nb, DataCollector& data); diff --git a/highs/ipm/hipo/factorhighs/DenseFactHybrid.cpp b/highs/ipm/hipo/factorhighs/DenseFactHybrid.cpp index ec0ba3e309e..dfe55fc2920 100644 --- a/highs/ipm/hipo/factorhighs/DenseFactHybrid.cpp +++ b/highs/ipm/hipo/factorhighs/DenseFactHybrid.cpp @@ -12,8 +12,8 @@ namespace hipo { Int denseFactFH(char format, Int n, Int k, double* A, double* B, const Int* pivot_sign, double thresh, double* totalreg, - Int* swaps, double* pivot_2x2, bool parnode, - DataCollector& data, const FHoptions& options) { + Int* swaps, double* pivot_2x2, DataCollector& data, + const FHoptions& options) { // =========================================================================== // Partial blocked factorisation // Matrix A is in format FH @@ -175,7 +175,7 @@ Int denseFactFH(char format, Int n, Int k, double* A, double* B, const double* Rjj = &R[offset]; // perform gemm (potentially) in parallel - if (parnode) + if (options.parallel_node) dgemmParallel(P, Rjj, Q, col_jj, jb, row_jj, nb, 1.0, data); else callAndTime_dgemm('T', 'N', col_jj, row_jj, jb, -1.0, P, jb, Rjj, jb, @@ -208,7 +208,7 @@ Int denseFactFH(char format, Int n, Int k, double* A, double* B, double beta = format == 'P' ? 0.0 : 1.0; // perform gemm (potentially) in parallel - if (parnode) + if (options.parallel_node) dgemmParallel(P, Rjj, Q, ncol, jb, nrow, nb, beta, data); else callAndTime_dgemm('T', 'N', ncol, nrow, jb, -1.0, P, jb, Rjj, jb, diff --git a/highs/ipm/hipo/factorhighs/FactorHighs.cpp b/highs/ipm/hipo/factorhighs/FactorHighs.cpp index d91a636d86a..6618b859325 100644 --- a/highs/ipm/hipo/factorhighs/FactorHighs.cpp +++ b/highs/ipm/hipo/factorhighs/FactorHighs.cpp @@ -54,6 +54,17 @@ void FHsolver::setBlockSize(Int nb) { void FHsolver::setPivoting(bool pivoting) { options_.pivoting = pivoting; } +void FHsolver::setParallel(bool tree, bool node) { + options_.parallel_tree = tree; + options_.parallel_node = node; +} + +void FHsolver::setParallelSolve(bool forward, bool backward, bool diag) { + options_.parallel_forward = forward; + options_.parallel_backward = backward; + options_.parallel_diag = diag; +} + void FHsolver::setLogger(const Logger* logger, bool use_printf) { if (local_logger_ && logger_) delete logger_; local_logger_ = false; diff --git a/highs/ipm/hipo/factorhighs/FactorHighs.h b/highs/ipm/hipo/factorhighs/FactorHighs.h index bb5643bc009..eb761cae2b0 100644 --- a/highs/ipm/hipo/factorhighs/FactorHighs.h +++ b/highs/ipm/hipo/factorhighs/FactorHighs.h @@ -138,6 +138,9 @@ class FHsolver { // regularisation is applied. void setPivoting(bool pivoting); + void setParallel(bool tree, bool node); + void setParallelSolve(bool forward, bool backward, bool diag); + // Pass the Logger object to be used for logging. Alternatively, printf can be // used for logging, by passing a nullptr and setting use_printf to true. // By default, logging is off. diff --git a/highs/ipm/hipo/factorhighs/FactorHighsOptions.h b/highs/ipm/hipo/factorhighs/FactorHighsOptions.h index a937e43d244..228ba6fc37a 100644 --- a/highs/ipm/hipo/factorhighs/FactorHighsOptions.h +++ b/highs/ipm/hipo/factorhighs/FactorHighsOptions.h @@ -12,6 +12,11 @@ struct FHoptions { Int nb = kBlockSize; bool pivoting = true; bool one_indexing = false; + bool parallel_tree = false; + bool parallel_node = false; + bool parallel_forward = false; + bool parallel_backward = false; + bool parallel_diag = false; }; } // namespace hipo diff --git a/highs/ipm/hipo/factorhighs/FactorHighsSettings.h b/highs/ipm/hipo/factorhighs/FactorHighsSettings.h index ff7811a9388..6ec0b97e1fa 100644 --- a/highs/ipm/hipo/factorhighs/FactorHighsSettings.h +++ b/highs/ipm/hipo/factorhighs/FactorHighsSettings.h @@ -47,6 +47,11 @@ const Int kMinConsecutiveSums = 1; // regularisation const double kDynamicDiagCoeff = 1e-24; +// parallel solve +const double kLargeTaskRelativeThresh = 0.01; +const double kLargeTaskAbsoluteThres = 1e3; +const double kParallelDiagTargetNumTasks = 32; + struct Regul { double primal{}; double dual{}; diff --git a/highs/ipm/hipo/factorhighs/FactorHighs_c_api.h b/highs/ipm/hipo/factorhighs/FactorHighs_c_api.h index 10c060b8de0..56771f92ce7 100644 --- a/highs/ipm/hipo/factorhighs/FactorHighs_c_api.h +++ b/highs/ipm/hipo/factorhighs/FactorHighs_c_api.h @@ -1,7 +1,7 @@ #ifndef FACTOR_HIGHS_C_API_H #define FACTOR_HIGHS_C_API_H -#include "util/HighsInt.h" +#include "util/HighsType.h" /* C API to HiPO linear solver It is meant to be used outside of HiGHS as a standalone linear solver. diff --git a/highs/ipm/hipo/factorhighs/Factorise.cpp b/highs/ipm/hipo/factorhighs/Factorise.cpp index 69acde20fae..2e7e3886d61 100644 --- a/highs/ipm/hipo/factorhighs/Factorise.cpp +++ b/highs/ipm/hipo/factorhighs/Factorise.cpp @@ -108,7 +108,7 @@ void Factorise::processSupernode(Int sn) { highs::parallel::TaskGroup tg; HIPO_CLOCK_CREATE; - const bool parallel = S_.parTree(); + const bool parallel = FH_opt_.parallel_tree; const bool serial = !parallel; if (flag_stop_.load(std::memory_order_relaxed)) return; @@ -205,49 +205,7 @@ void Factorise::processSupernode(Int sn) { assert(child == child_sn); } - // determine size of clique of child - const Int child_begin = S_.snStart(child_sn); - const Int child_end = S_.snStart(child_sn + 1); - - // number of nodes in child sn - const Int child_size = child_end - child_begin; - - // size of clique of child sn - const Int nc = S_.ptr(child_sn + 1) - S_.ptr(child_sn) - child_size; - - // ASSEMBLE INTO FRONTAL - HIPO_CLOCK_START(2); - // go through the columns of the contribution of the child - for (Int col = 0; col < nc; ++col) { - // relative index of column in the frontal matrix - Int j = S_.relindClique(child_sn, col); - - if (j < sn_size) { - // assemble into frontal - - // go through the rows of the contribution of the child - Int row = col; - while (row < nc) { - // relative index of the entry in the matrix frontal - const Int i = S_.relindClique(child_sn, row); - - // how many entries to sum - Int consecutive = S_.consecutiveSums(child_sn, row); - - FH->assembleFrontalMultiple(consecutive, child_clique, nc, child_sn, - row, col, i, j); - - row += consecutive; - } - } else - break; - } - HIPO_CLOCK_STOP(2, data_, kTimeFactoriseAssembleChildrenFrontal); - - // ASSEMBLE INTO CLIQUE - HIPO_CLOCK_START(2); - FH->assembleClique(child_clique, nc, child_sn); - HIPO_CLOCK_STOP(2, data_, kTimeFactoriseAssembleChildrenClique); + FH->assembleChild(child_sn, child_clique); // Schur contribution of the child is no longer needed if (parallel) { @@ -297,10 +255,31 @@ void Factorise::processSupernode(Int sn) { HIPO_CLOCK_STOP(2, data_, kTimeFactoriseTerminate); } -bool Factorise::run(Numeric& num) { - HIPO_CLOCK_CREATE; +void Factorise::processTreeSerial() { + if (!stack_) { + // processing the tree in serial requires a CliqueStack + flag_stop_.store(true, std::memory_order_relaxed); + return; + } + if (stack_->empty()) stack_->init(S_.maxStackSize()); + for (Int sn = 0; sn < S_.sn(); ++sn) { + processSupernode(sn); + } +} +void Factorise::processTreeParallel() { highs::parallel::TaskGroup tg; + // spawn roots + for (Int sn = 0; sn < S_.sn(); ++sn) { + if (S_.snParent(sn) == -1) { + tg.spawn([=]() { processSupernode(sn); }); + } + } + tg.taskWait(); +} + +bool Factorise::run(Numeric& num) { + HIPO_CLOCK_CREATE; total_reg_.assign(n_, 0.0); @@ -313,27 +292,10 @@ bool Factorise::run(Numeric& num) { // the memory of previous factorisations. sn_columns_.resize(S_.sn()); - if (S_.parTree()) { - Int spawned_roots{}; - // spawn tasks for root supernodes - for (Int sn = 0; sn < S_.sn(); ++sn) { - if (S_.snParent(sn) == -1) { - tg.spawn([=]() { processSupernode(sn); }); - ++spawned_roots; - } - } - - // sync tasks for root supernodes - tg.taskWait(); + if (FH_opt_.parallel_tree) { + processTreeParallel(); } else { - // processing the tree in serial requires a CliqueStack - if (!stack_) return true; - if (stack_->empty()) stack_->init(S_.maxStackSize()); - - // go through each supernode serially - for (Int sn = 0; sn < S_.sn(); ++sn) { - processSupernode(sn); - } + processTreeSerial(); } if (flag_stop_.load(std::memory_order_relaxed)) return true; diff --git a/highs/ipm/hipo/factorhighs/Factorise.h b/highs/ipm/hipo/factorhighs/Factorise.h index 46f3b27daf3..e00b2264e23 100644 --- a/highs/ipm/hipo/factorhighs/Factorise.h +++ b/highs/ipm/hipo/factorhighs/Factorise.h @@ -66,8 +66,9 @@ class Factorise { CliqueStack* stack_; - public: void permute(const std::vector& iperm); + void processTreeSerial(); + void processTreeParallel(); void processSupernode(Int sn); public: diff --git a/highs/ipm/hipo/factorhighs/FormatHandler.h b/highs/ipm/hipo/factorhighs/FormatHandler.h index 5b26415951e..23d04fd295b 100644 --- a/highs/ipm/hipo/factorhighs/FormatHandler.h +++ b/highs/ipm/hipo/factorhighs/FormatHandler.h @@ -73,10 +73,7 @@ class FormatHandler { virtual void initFrontal() = 0; virtual void initClique() = 0; virtual void assembleFrontal(Int i, Int j, double val) = 0; - virtual void assembleFrontalMultiple(Int& num, const double* child, Int nc, - Int child_sn, Int row, Int col, Int i, - Int j) = 0; - virtual void assembleClique(const double* child, Int nc, Int child_sn) = 0; + virtual void assembleChild(Int child_sn, const double* child) = 0; virtual Int denseFactorise(double reg_thresh) = 0; // ================================================================= diff --git a/highs/ipm/hipo/factorhighs/HybridHybridFormatHandler.cpp b/highs/ipm/hipo/factorhighs/HybridHybridFormatHandler.cpp index 6144fb58afa..00ece0b4191 100644 --- a/highs/ipm/hipo/factorhighs/HybridHybridFormatHandler.cpp +++ b/highs/ipm/hipo/factorhighs/HybridHybridFormatHandler.cpp @@ -50,35 +50,39 @@ void HybridHybridFormatHandler::initClique() { } void HybridHybridFormatHandler::assembleFrontal(Int i, Int j, double val) { - Int block = j / nb_; - Int ldb = ldf_ - block * nb_; - Int ii = i - block * nb_; - Int jj = j - block * nb_; - frontal_[diag_start_[block] + ii + ldb * jj] = val; + Int block_id = j / nb_; + Int ldb = ldf_ - block_id * nb_; + Int i_in_block = i - block_id * nb_; + Int j_in_block = j - block_id * nb_; + frontal_[diag_start_[block_id] + i_in_block + ldb * j_in_block] = val; } -void HybridHybridFormatHandler::assembleFrontalMultiple(Int& num, - const double* child, - Int nc, Int child_sn, - Int row, Int col, Int i, - Int j) { - const Int jblock = col / nb_; - const Int jb = std::min(nb_, nc - nb_ * jblock); - const Int row_ = row - jblock * nb_; - const Int col_ = col - jblock * nb_; - const Int64 start_block = S_->cliqueBlockStart(child_sn, jblock); - - Int block = j / nb_; - Int ldb = ldf_ - block * nb_; - Int ii = i - block * nb_; - Int jj = j - block * nb_; +void HybridHybridFormatHandler::assembleFrontalMultiple( + Int& num, const double* child_data, Int child_size, Int child_sn, Int row_c, + Int col_c, Int row_f, Int col_f) { + const Int block_child_id = col_c / nb_; + const Int jb_child = std::min(nb_, child_size - nb_ * block_child_id); + const Int row_c_local = row_c - block_child_id * nb_; + const Int col_c_local = col_c - block_child_id * nb_; + const Int64 start_block_child = + S_->cliqueBlockStart(child_sn, block_child_id); + + Int block_frontal_id = col_f / nb_; + Int ldb = ldf_ - block_frontal_id * nb_; + Int row_f_local = row_f - block_frontal_id * nb_; + Int col_f_local = col_f - block_frontal_id * nb_; if (num > kMinConsecutiveSums) - callAndTime_daxpy(num, 1.0, &child[start_block + col_ + jb * row_], jb, - &frontal_[diag_start_[block] + ii + ldb * jj], 1, data_); + callAndTime_daxpy( + num, 1.0, + &child_data[start_block_child + col_c_local + jb_child * row_c_local], + jb_child, + &frontal_[diag_start_[block_frontal_id] + row_f_local + + ldb * col_f_local], + 1, data_); else { - frontal_[diag_start_[block] + ii + ldb * jj] += - child[start_block + col_ + jb * row_]; + frontal_[diag_start_[block_frontal_id] + row_f_local + ldb * col_f_local] += + child_data[start_block_child + col_c_local + jb_child * row_c_local]; num = 1; } } @@ -98,84 +102,86 @@ Int HybridHybridFormatHandler::denseFactorise(double reg_thresh) { status = denseFactFH('H', ldf_, sn_size_, frontal_.data(), clique_ptr_, pivot_sign, reg_thresh, local_reg_.data(), swaps_.data(), - pivot_2x2_.data(), S_->parNode(), data_, FH_opt_); + pivot_2x2_.data(), data_, FH_opt_); return status; } -void HybridHybridFormatHandler::assembleClique(const double* child, Int nc, - Int child_sn) { +void HybridHybridFormatHandler::assembleClique(const double* child_data, + Int child_size, Int child_sn) { // assemble the child clique into the current clique by blocks of columns. // within a block, assemble by rows. - const Int n_blocks = (nc - 1) / nb_ + 1; + const Int blocks = (child_size - 1) / nb_ + 1; Int row_start{}; // go through the blocks of columns of the child sn - for (Int b = 0; b < n_blocks; ++b) { - const Int64 b_start = S_->cliqueBlockStart(child_sn, b); + for (Int block = 0; block < blocks; ++block) { + const Int64 block_start = S_->cliqueBlockStart(child_sn, block); const Int col_start = row_start; - const Int col_end = std::min(col_start + nb_, nc); + const Int col_end = std::min(col_start + nb_, child_size); // go through the rows within this block - for (Int row = row_start; row < nc; ++row) { - const Int i = S_->relindClique(child_sn, row) - sn_size_; + for (Int row = row_start; row < child_size; ++row) { + const Int row_clique = S_->relindClique(child_sn, row) - sn_size_; // already assembled into frontal - if (i < 0) continue; + if (row_clique < 0) continue; // go through the columns of the block Int col = col_start; while (col < col_end) { - Int j = S_->relindClique(child_sn, col); - if (j < sn_size_) { + Int col_clique = S_->relindClique(child_sn, col); + if (col_clique < sn_size_) { ++col; continue; } - j -= sn_size_; + col_clique -= sn_size_; // information and sizes of child sn - const Int jblock_c = b; - const Int jb_c = std::min(nb_, nc - nb_ * jblock_c); - const Int row_ = row - jblock_c * nb_; - const Int col_ = col - jblock_c * nb_; - const Int64 start_block_c = b_start; + const Int jb_child = std::min(nb_, child_size - nb_ * block); + const Int row_local = row - block * nb_; + const Int col_local = col - block * nb_; // sun consecutive entries in a row. // consecutive need to be reduced, to account for edge of the block const Int zeros_stored_row = - std::max((Int)0, jb_c - (row - row_start) - 1); + std::max((Int)0, jb_child - (row - row_start) - 1); Int consecutive = S_->consecutiveSums(child_sn, col); const Int left_in_child = col_end - col - zeros_stored_row; consecutive = std::min(consecutive, left_in_child); // consecutive need to account also for edge of block in parent - const Int block_in_parent = j / nb_; + const Int block_in_parent = col_clique / nb_; const Int col_end_parent = std::min((block_in_parent + 1) * nb_, ldc_); - const Int left_in_parent = col_end_parent - j; + const Int left_in_parent = col_end_parent - col_clique; consecutive = std::min(consecutive, left_in_parent); // needed to deal with zeros stored in upper right part of block if (consecutive == 0) break; // information and sizes of current sn - const Int jblock = block_in_parent; - const Int jb = std::min(nb_, ldc_ - nb_ * jblock); - const Int i_ = i - jblock * nb_; - const Int j_ = j - jblock * nb_; - const Int64 start_block = S_->cliqueBlockStart(sn_, jblock); + const Int jb_clique = std::min(nb_, ldc_ - nb_ * block_in_parent); + const Int row_clique_local = row_clique - block_in_parent * nb_; + const Int col_clique_local = col_clique - block_in_parent * nb_; + const Int64 block_start_clique = + S_->cliqueBlockStart(sn_, block_in_parent); if (consecutive > kMinConsecutiveSums) { - callAndTime_daxpy(consecutive, 1.0, - &child[start_block_c + col_ + jb_c * row_], 1, - &clique_ptr_[start_block + j_ + jb * i_], 1, data_); + callAndTime_daxpy( + consecutive, 1.0, + &child_data[block_start + col_local + jb_child * row_local], 1, + &clique_ptr_[block_start_clique + col_clique_local + + jb_clique * row_clique_local], + 1, data_); col += consecutive; } else { - clique_ptr_[start_block + j_ + jb * i_] += - child[start_block_c + col_ + jb_c * row_]; + clique_ptr_[block_start_clique + col_clique_local + + jb_clique * row_clique_local] += + child_data[block_start + col_local + jb_child * row_local]; col++; } @@ -186,6 +192,51 @@ void HybridHybridFormatHandler::assembleClique(const double* child, Int nc, } } +void HybridHybridFormatHandler::assembleChild(Int child_sn, + const double* child_data) { + HIPO_CLOCK_CREATE; + + const Int child_begin = S_->snStart(child_sn); + const Int child_end = S_->snStart(child_sn + 1); + const Int child_sn_size = child_end - child_begin; + const Int child_clique_size = + S_->ptr(child_sn + 1) - S_->ptr(child_sn) - child_sn_size; + + // ASSEMBLE INTO FRONTAL + HIPO_CLOCK_START(2); + // go through the columns of the contribution of the child + for (Int col = 0; col < child_clique_size; ++col) { + // relative index of column in the frontal matrix + Int col_f = S_->relindClique(child_sn, col); + + if (col_f < sn_size_) { + // assemble into frontal + + // go through the rows of the contribution of the child + Int row = col; + while (row < child_clique_size) { + // relative index of the entry in the matrix frontal + const Int row_f = S_->relindClique(child_sn, row); + + // how many entries to sum + Int consecutive = S_->consecutiveSums(child_sn, row); + + assembleFrontalMultiple(consecutive, child_data, child_clique_size, + child_sn, row, col, row_f, col_f); + + row += consecutive; + } + } else + break; + } + HIPO_CLOCK_STOP(2, data_, kTimeFactoriseAssembleChildrenFrontal); + + // ASSEMBLE INTO CLIQUE + HIPO_CLOCK_START(2); + assembleClique(child_data, child_clique_size, child_sn); + HIPO_CLOCK_STOP(2, data_, kTimeFactoriseAssembleChildrenClique); +} + void HybridHybridFormatHandler::extremeEntries() { #ifdef HIPO_COLLECT_EXPENSIVE_DATA double minD = 1e100; diff --git a/highs/ipm/hipo/factorhighs/HybridHybridFormatHandler.h b/highs/ipm/hipo/factorhighs/HybridHybridFormatHandler.h index ea811942340..30b6dea2f04 100644 --- a/highs/ipm/hipo/factorhighs/HybridHybridFormatHandler.h +++ b/highs/ipm/hipo/factorhighs/HybridHybridFormatHandler.h @@ -14,13 +14,14 @@ class HybridHybridFormatHandler : public FormatHandler { void initFrontal() override; void initClique() override; void assembleFrontal(Int i, Int j, double val) override; - void assembleFrontalMultiple(Int& num, const double* child, Int nc, - Int child_sn, Int row, Int col, Int i, - Int j) override; + void assembleChild(Int child_sn, const double* child) override; Int denseFactorise(double reg_thresh) override; - void assembleClique(const double* child, Int nc, Int child_sn) override; void extremeEntries() override; + void assembleFrontalMultiple(Int& num, const double* child, Int nc, + Int child_sn, Int row, Int col, Int i, Int j); + void assembleClique(const double* child, Int nc, Int child_sn); + public: HybridHybridFormatHandler(const Symbolic& S, Int sn, DataCollector& data, std::vector& frontal, double* clique_ptr, diff --git a/highs/ipm/hipo/factorhighs/HybridSolveHandler.cpp b/highs/ipm/hipo/factorhighs/HybridSolveHandler.cpp index 7e954acacb2..57fe45022db 100644 --- a/highs/ipm/hipo/factorhighs/HybridSolveHandler.cpp +++ b/highs/ipm/hipo/factorhighs/HybridSolveHandler.cpp @@ -13,58 +13,146 @@ HybridSolveHandler::HybridSolveHandler( const Symbolic& S, const std::vector>& sn_columns, const std::vector>& swaps, const std::vector>& any_swap, - const std::vector>& pivot_2x2, - std::vector& gemv_work, DataCollector& data, + const std::vector>& pivot_2x2, DataCollector& data, const FHoptions& options) : SolveHandler(S, sn_columns, data, options), swaps_{swaps}, any_swaps_{any_swap}, - pivot_2x2_{pivot_2x2}, - gemv_workspace_{gemv_work} {} + pivot_2x2_{pivot_2x2} { + childrenLinkedList(S_.schedule().task_parent, first_child_, next_child_); + + bool need_parallel_work = + options_.parallel_backward || options_.parallel_forward; + bool need_serial_work = + !options_.parallel_backward || !options_.parallel_forward; + + // allocate workspace for gemv + if (need_parallel_work) { + parallel_gemv_workspace_.resize(S_.schedule().count()); + for (Int task = 0; task < S_.schedule().count(); ++task) { + Int64 largest_front = 0; + for (Int sn : S_.schedule().sn_per_task[task]) { + largest_front = std::max(largest_front, S_.ptr(sn + 1) - S_.ptr(sn)); + } + parallel_gemv_workspace_[task].resize(largest_front); + } + } + if (need_serial_work) { + serial_gemv_workspace_.resize(S_.largestFront()); + } +} -void HybridSolveHandler::forwardSolve(double* x) const { - // Forward solve. +void HybridSolveHandler::processForwardSn(Int sn, double* x, + std::vector& work, Int task, + Int end_col_in_task) const { // Blas calls: dtrsv, dgemv - // supernode columns in format FH HIPO_CLOCK_CREATE; - const Int nb = options_.nb; - for (Int sn = 0; sn < S_.sn(); ++sn) { - // leading size of supernode - const Int ldSn = S_.ptr(sn + 1) - S_.ptr(sn); + // if running in parallel, the updates to supernodes that do not belong to + // this task have to be deferred to avoid data races. Therefore, they are + // stored in a buffer and applied serially later. + const bool defer = task >= 0; - // number of columns in the supernode - const Int sn_size = S_.snStart(sn + 1) - S_.snStart(sn); + // leading size of supernode + const Int ldSn = S_.ptr(sn + 1) - S_.ptr(sn); - // first colums of the supernode - const Int sn_start = S_.snStart(sn); + // number of columns in the supernode + const Int sn_size = S_.snStart(sn + 1) - S_.snStart(sn); - // index to access S->rows for this supernode - const Int64 start_row = S_.ptr(sn); + // first colums of the supernode + const Int sn_start = S_.snStart(sn); - // number of blocks of columns - const Int n_blocks = (sn_size - 1) / nb + 1; + // index to access S->rows for this supernode + const Int64 start_row = S_.ptr(sn); + + // number of blocks of columns + const Int n_blocks = (sn_size - 1) / nb + 1; + + // index to access snColumns[sn] + Int64 SnCol_ind{}; - // index to access snColumns[sn] - Int64 SnCol_ind{}; + if (sn_size < nb) { + // Fast solve + // If supernode is small, avoid making BLAS calls + const Int jb = sn_size; + const Int x_start = sn_start; - if (sn_size < nb) { - // Fast solve - // If supernode is small, avoid making BLAS calls - const Int jb = sn_size; - const Int x_start = sn_start; + const Int* current_swaps = nullptr; + bool any_swaps_in_block = false; + + if (options_.pivoting) { + HIPO_CLOCK_START(2); + any_swaps_in_block = any_swaps_[sn][0]; + if (any_swaps_in_block) { + current_swaps = swaps_[sn].data(); + // apply swaps to portion of rhs that is affected + permuteWithSwaps(&x[x_start], current_swaps, jb); + } + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); + } + + HIPO_CLOCK_START(2); + for (Int row = 0; row < jb; ++row) { + for (Int col = 0; col < row; ++col) { + x[x_start + row] -= sn_columns_[sn][col + jb * row] * x[x_start + col]; + } + } + + for (Int row = jb; row < ldSn; ++row) { + if (defer) { + assert(end_col_in_task >= 0); + const Int row_to_write = S_.rows(start_row + row); + if (row_to_write < end_col_in_task) { + for (Int col = 0; col < jb; ++col) { + x[S_.rows(start_row + row)] -= + sn_columns_[sn][col + jb * row] * x[x_start + col]; + } + } else { + task_rows_[task].push_back(S_.rows(start_row + row)); + task_vals_[task].push_back(0.0); + for (Int col = 0; col < jb; ++col) + task_vals_[task].back() += + sn_columns_[sn][col + jb * row] * x[x_start + col]; + } + } else { + for (Int col = 0; col < jb; ++col) { + x[S_.rows(start_row + row)] -= + sn_columns_[sn][col + jb * row] * x[x_start + col]; + } + } + } + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); + + if (any_swaps_in_block && options_.pivoting) { + HIPO_CLOCK_START(2); + // apply inverse swaps + permuteWithSwaps(&x[x_start], current_swaps, jb, true); + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); + } + + } else { + // go through blocks of columns for this supernode + for (Int j = 0; j < n_blocks; ++j) { + // number of columns in the block + const Int jb = std::min(nb, sn_size - nb * j); + + // number of entries in diagonal part + const Int diag_entries = jb * jb; + + // index to access vector x + const Int x_start = sn_start + nb * j; const Int* current_swaps = nullptr; bool any_swaps_in_block = false; if (options_.pivoting) { HIPO_CLOCK_START(2); - any_swaps_in_block = any_swaps_[sn][0]; + any_swaps_in_block = any_swaps_[sn][j]; if (any_swaps_in_block) { - current_swaps = swaps_[sn].data(); + current_swaps = &swaps_[sn][nb * j]; // apply swaps to portion of rhs that is affected permuteWithSwaps(&x[x_start], current_swaps, jb); } @@ -72,20 +160,39 @@ void HybridSolveHandler::forwardSolve(double* x) const { } HIPO_CLOCK_START(2); - for (Int row = 0; row < jb; ++row) { - for (Int col = 0; col < row; ++col) { - x[x_start + row] -= - sn_columns_[sn][col + jb * row] * x[x_start + col]; - } - } + callAndTime_dtrsv('U', 'T', 'U', jb, &sn_columns_[sn][SnCol_ind], jb, + &x[x_start], 1, data_); - for (Int row = jb; row < ldSn; ++row) { - for (Int col = 0; col < jb; ++col) { - x[S_.rows(start_row + row)] -= - sn_columns_[sn][col + jb * row] * x[x_start + col]; + SnCol_ind += diag_entries; + + // temporary space for gemv + const Int gemv_size = ldSn - nb * j - jb; + if (gemv_size > 0) { + callAndTime_dgemv('T', jb, gemv_size, 1.0, &sn_columns_[sn][SnCol_ind], + jb, &x[x_start], 1, 0.0, work.data(), 1, data_); + + SnCol_ind += jb * gemv_size; + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); + + HIPO_CLOCK_START(2); + // scatter solution of gemv + for (Int i = 0; i < gemv_size; ++i) { + const Int row = S_.rows(start_row + nb * j + jb + i); + + if (defer) { + assert(end_col_in_task >= 0); + if (row < end_col_in_task) { + x[row] -= work[i]; + } else { + task_rows_[task].push_back(row); + task_vals_[task].push_back(work[i]); + } + } else { + x[row] -= work[i]; + } } + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_sparse); } - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); if (any_swaps_in_block && options_.pivoting) { HIPO_CLOCK_START(2); @@ -93,134 +200,186 @@ void HybridSolveHandler::forwardSolve(double* x) const { permuteWithSwaps(&x[x_start], current_swaps, jb, true); HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); } + } + } +} - } else { - // go through blocks of columns for this supernode - for (Int j = 0; j < n_blocks; ++j) { - // number of columns in the block - const Int jb = std::min(nb, sn_size - nb * j); - - // number of entries in diagonal part - const Int diag_entries = jb * jb; - - // index to access vector x - const Int x_start = sn_start + nb * j; - - const Int* current_swaps = nullptr; - bool any_swaps_in_block = false; - - if (options_.pivoting) { - HIPO_CLOCK_START(2); - any_swaps_in_block = any_swaps_[sn][j]; - if (any_swaps_in_block) { - current_swaps = &swaps_[sn][nb * j]; - // apply swaps to portion of rhs that is affected - permuteWithSwaps(&x[x_start], current_swaps, jb); - } - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); - } +void HybridSolveHandler::processForwardTask(Int task, double* x) const { + // wait for children to complete + highs::parallel::TaskGroup tg; + Int child = first_child_[task]; + while (child != -1) { + tg.spawn([=, &x]() { processForwardTask(child, x); }); + child = next_child_[child]; + } + tg.taskWait(); + + task_rows_[task].clear(); + task_vals_[task].clear(); + + const Int lead_sn = S_.schedule().sn_per_task[task].back(); + Int end_col_in_task = S_.snStart(lead_sn + 1); + + // assembel contributions of children + child = first_child_[task]; + while (child != -1) { + for (Int i = 0; i < static_cast(task_rows_[child].size()); ++i) { + if (task_rows_[child][i] < end_col_in_task) + x[task_rows_[child][i]] -= task_vals_[child][i]; + else { + task_rows_[task].push_back(task_rows_[child][i]); + task_vals_[task].push_back(task_vals_[child][i]); + } + } + child = next_child_[child]; + } - HIPO_CLOCK_START(2); - callAndTime_dtrsv('U', 'T', 'U', jb, &sn_columns_[sn][SnCol_ind], jb, - &x[x_start], 1, data_); - - SnCol_ind += diag_entries; - - // temporary space for gemv - const Int gemv_size = ldSn - nb * j - jb; - if (gemv_size > 0) { - callAndTime_dgemv('T', jb, gemv_size, 1.0, - &sn_columns_[sn][SnCol_ind], jb, &x[x_start], 1, - 0.0, gemv_workspace_.data(), 1, data_); - - SnCol_ind += jb * gemv_size; - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); - - HIPO_CLOCK_START(2); - // scatter solution of gemv - for (Int i = 0; i < gemv_size; ++i) { - const Int row = S_.rows(start_row + nb * j + jb + i); - x[row] -= gemv_workspace_[i]; - } - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_sparse); - } + for (Int sn : S_.schedule().sn_per_task[task]) { + processForwardSn(sn, x, parallel_gemv_workspace_[task], task, + end_col_in_task); + } +} - if (any_swaps_in_block && options_.pivoting) { - HIPO_CLOCK_START(2); - // apply inverse swaps - permuteWithSwaps(&x[x_start], current_swaps, jb, true); - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); - } +void HybridSolveHandler::forwardSolve(double* x) const { + if (options_.parallel_forward && S_.schedule().valid) { + // Hard to parallelise: a sn depends on its children in the tree; multiple + // children may be writing to the same location in x at the same time. + // Special care is needed for the writes, involving private buffers. + task_rows_.resize(S_.schedule().count()); + task_vals_.resize(S_.schedule().count()); + + highs::parallel::TaskGroup tg; + for (Int task = 0; task < S_.schedule().count(); ++task) { + if (S_.schedule().task_parent[task] == -1) { + tg.spawn([=, &x]() { processForwardTask(task, x); }); } } + tg.taskWait(); + + } else { + for (Int sn = 0; sn < S_.sn(); ++sn) { + processForwardSn(sn, x, serial_gemv_workspace_); + } } } -void HybridSolveHandler::backwardSolve(double* x) const { - // Backward solve. +void HybridSolveHandler::processBackwardSn(Int sn, double* x, + std::vector& work) const { // Blas calls: dtrsv, dgemv - // supernode columns in format FH HIPO_CLOCK_CREATE; - const Int nb = options_.nb; - // go through the sn in reverse order - for (Int sn = S_.sn() - 1; sn >= 0; --sn) { - // leading size of supernode - const Int ldSn = S_.ptr(sn + 1) - S_.ptr(sn); + // leading size of supernode + const Int ldSn = S_.ptr(sn + 1) - S_.ptr(sn); - // number of columns in the supernode - const Int sn_size = S_.snStart(sn + 1) - S_.snStart(sn); + // number of columns in the supernode + const Int sn_size = S_.snStart(sn + 1) - S_.snStart(sn); - // first colums of the supernode - const Int sn_start = S_.snStart(sn); + // first colums of the supernode + const Int sn_start = S_.snStart(sn); - // index to access S->rows for this supernode - const Int64 start_row = S_.ptr(sn); + // index to access S->rows for this supernode + const Int64 start_row = S_.ptr(sn); - // number of blocks of columns - const Int n_blocks = (sn_size - 1) / nb + 1; + // number of blocks of columns + const Int n_blocks = (sn_size - 1) / nb + 1; + + // index to access snColumns[sn] + // initialised with the total number of entries of snColumns[sn] + Int64 SnCol_ind = sn_columns_[sn].size() - extra_space_frontal; + + if (sn_size < nb) { + // Fast solve + // If supernode is small, avoid making BLAS calls + const Int jb = sn_size; + const Int x_start = sn_start; + + const Int* current_swaps = nullptr; + bool any_swaps_in_block = false; + + if (options_.pivoting) { + HIPO_CLOCK_START(2); + any_swaps_in_block = any_swaps_[sn][0]; + if (any_swaps_in_block) { + current_swaps = swaps_[sn].data(); + // apply swaps to portion of rhs that is affected + permuteWithSwaps(&x[x_start], current_swaps, jb); + } + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); + } + + HIPO_CLOCK_START(2); + for (Int row = ldSn - 1; row >= jb; --row) { + for (Int col = jb - 1; col >= 0; --col) { + x[x_start + col] -= + sn_columns_[sn][col + row * jb] * x[S_.rows(start_row + row)]; + } + } - // index to access snColumns[sn] - // initialised with the total number of entries of snColumns[sn] - Int64 SnCol_ind = sn_columns_[sn].size() - extra_space_frontal; + for (Int row = jb - 1; row >= 0; --row) { + for (Int col = row - 1; col >= 0; --col) { + x[x_start + col] -= sn_columns_[sn][col + row * jb] * x[x_start + row]; + } + } + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); - if (sn_size < nb) { - // Fast solve - // If supernode is small, avoid making BLAS calls - const Int jb = sn_size; - const Int x_start = sn_start; + if (any_swaps_in_block && options_.pivoting) { + HIPO_CLOCK_START(2); + // apply inverse swaps + permuteWithSwaps(&x[x_start], current_swaps, jb, true); + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); + } + + } else { + // go through blocks of columns for this supernode in reverse order + for (Int j = n_blocks - 1; j >= 0; --j) { + // number of columns in the block + const Int jb = std::min(nb, sn_size - nb * j); + + // number of entries in diagonal part + const Int diag_entries = jb * jb; + + // index to access vector x + const Int x_start = sn_start + nb * j; const Int* current_swaps = nullptr; bool any_swaps_in_block = false; if (options_.pivoting) { HIPO_CLOCK_START(2); - any_swaps_in_block = any_swaps_[sn][0]; + any_swaps_in_block = any_swaps_[sn][j]; if (any_swaps_in_block) { - current_swaps = swaps_[sn].data(); + current_swaps = &swaps_[sn][nb * j]; // apply swaps to portion of rhs that is affected permuteWithSwaps(&x[x_start], current_swaps, jb); } HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); } - HIPO_CLOCK_START(2); - for (Int row = ldSn - 1; row >= jb; --row) { - for (Int col = jb - 1; col >= 0; --col) { - x[x_start + col] -= - sn_columns_[sn][col + row * jb] * x[S_.rows(start_row + row)]; + // temporary space for gemv + const Int gemv_size = ldSn - nb * j - jb; + if (gemv_size > 0) { + HIPO_CLOCK_START(2); + // scatter entries into y + for (Int i = 0; i < gemv_size; ++i) { + const Int row = S_.rows(start_row + nb * j + jb + i); + work[i] = x[row]; } - } + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_sparse); - for (Int row = jb - 1; row >= 0; --row) { - for (Int col = row - 1; col >= 0; --col) { - x[x_start + col] -= - sn_columns_[sn][col + row * jb] * x[x_start + row]; - } + HIPO_CLOCK_START(2); + SnCol_ind -= jb * gemv_size; + callAndTime_dgemv('N', jb, gemv_size, -1.0, &sn_columns_[sn][SnCol_ind], + jb, work.data(), 1, 1.0, &x[x_start], 1, data_); + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); } + + HIPO_CLOCK_START(2); + SnCol_ind -= diag_entries; + callAndTime_dtrsv('U', 'N', 'U', jb, &sn_columns_[sn][SnCol_ind], jb, + &x[x_start], 1, data_); HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); if (any_swaps_in_block && options_.pivoting) { @@ -229,151 +388,152 @@ void HybridSolveHandler::backwardSolve(double* x) const { permuteWithSwaps(&x[x_start], current_swaps, jb, true); HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); } + } + } +} - } else { - // go through blocks of columns for this supernode in reverse order - for (Int j = n_blocks - 1; j >= 0; --j) { - // number of columns in the block - const Int jb = std::min(nb, sn_size - nb * j); - - // number of entries in diagonal part - const Int diag_entries = jb * jb; - - // index to access vector x - const Int x_start = sn_start + nb * j; - - const Int* current_swaps = nullptr; - bool any_swaps_in_block = false; - - if (options_.pivoting) { - HIPO_CLOCK_START(2); - any_swaps_in_block = any_swaps_[sn][j]; - if (any_swaps_in_block) { - current_swaps = &swaps_[sn][nb * j]; - // apply swaps to portion of rhs that is affected - permuteWithSwaps(&x[x_start], current_swaps, jb); - } - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); - } - - // temporary space for gemv - const Int gemv_size = ldSn - nb * j - jb; - if (gemv_size > 0) { - HIPO_CLOCK_START(2); - // scatter entries into y - for (Int i = 0; i < gemv_size; ++i) { - const Int row = S_.rows(start_row + nb * j + jb + i); - gemv_workspace_[i] = x[row]; - } - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_sparse); - - HIPO_CLOCK_START(2); - SnCol_ind -= jb * gemv_size; - callAndTime_dgemv( - 'N', jb, gemv_size, -1.0, &sn_columns_[sn][SnCol_ind], jb, - gemv_workspace_.data(), 1, 1.0, &x[x_start], 1, data_); - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); - } +void HybridSolveHandler::processBackwardTask(Int task, double* x) const { + for (auto rit = S_.schedule().sn_per_task[task].rbegin(); + rit != S_.schedule().sn_per_task[task].rend(); ++rit) { + const Int sn = *rit; + processBackwardSn(sn, x, parallel_gemv_workspace_[task]); + } - HIPO_CLOCK_START(2); - SnCol_ind -= diag_entries; - callAndTime_dtrsv('U', 'N', 'U', jb, &sn_columns_[sn][SnCol_ind], jb, - &x[x_start], 1, data_); - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); + // wait for children to complete + highs::parallel::TaskGroup tg; + Int child = first_child_[task]; + while (child != -1) { + tg.spawn([=, &x]() { processBackwardTask(child, x); }); + child = next_child_[child]; + } + tg.taskWait(); +} - if (any_swaps_in_block && options_.pivoting) { - HIPO_CLOCK_START(2); - // apply inverse swaps - permuteWithSwaps(&x[x_start], current_swaps, jb, true); - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); - } +void HybridSolveHandler::backwardSolve(double* x) const { + if (options_.parallel_backward && S_.schedule().valid) { + // Easy to parallelise: a sn depends on its ancestors in the tree; the + // ancestor is the only sn running in a given branch when it writes the + // update, so no special care needs to be taken for the writes. Respecting + // the dependencies of the tree is enough. + highs::parallel::TaskGroup tg; + for (Int task = 0; task < S_.schedule().count(); ++task) { + if (S_.schedule().task_parent[task] == -1) { + tg.spawn([=, &x]() { processBackwardTask(task, x); }); } } + tg.taskWait(); + + } else { + for (Int sn = S_.sn() - 1; sn >= 0; --sn) { + processBackwardSn(sn, x, serial_gemv_workspace_); + } } } -void HybridSolveHandler::diagSolve(double* x) const { - // Diagonal solve - +void HybridSolveHandler::processDiagSn(Int sn, double* x) const { // supernode columns in format FH HIPO_CLOCK_CREATE; - const Int nb = options_.nb; - for (Int sn = 0; sn < S_.sn(); ++sn) { - // leading size of supernode - const Int ldSn = S_.ptr(sn + 1) - S_.ptr(sn); + // leading size of supernode + const Int ldSn = S_.ptr(sn + 1) - S_.ptr(sn); - // number of columns in the supernode - const Int sn_size = S_.snStart(sn + 1) - S_.snStart(sn); + // number of columns in the supernode + const Int sn_size = S_.snStart(sn + 1) - S_.snStart(sn); - // first colums of the supernode - const Int sn_start = S_.snStart(sn); + // first colums of the supernode + const Int sn_start = S_.snStart(sn); - // number of blocks of columns - const Int n_blocks = (sn_size - 1) / nb + 1; + // number of blocks of columns + const Int n_blocks = (sn_size - 1) / nb + 1; - // index to access diagonal part of block - Int diag_start{}; + // index to access diagonal part of block + Int diag_start{}; - // go through blocks of columns for this supernode - for (Int j = 0; j < n_blocks; ++j) { - // number of columns in the block - const Int jb = std::min(nb, sn_size - nb * j); + // go through blocks of columns for this supernode + for (Int j = 0; j < n_blocks; ++j) { + // number of columns in the block + const Int jb = std::min(nb, sn_size - nb * j); - const Int* current_swaps = &swaps_[sn][nb * j]; - if (options_.pivoting) { - HIPO_CLOCK_START(2); + const Int* current_swaps = nullptr; + bool any_swaps_in_block = false; + + if (options_.pivoting) { + HIPO_CLOCK_START(2); + any_swaps_in_block = any_swaps_[sn][j]; + if (any_swaps_in_block) { + current_swaps = &swaps_[sn][nb * j]; // apply swaps to portion of rhs that is affected permuteWithSwaps(&x[sn_start + nb * j], current_swaps, jb); - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); } + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); + } - HIPO_CLOCK_START(2); - - const double* current_2x2 = &pivot_2x2_[sn][nb * j]; - Int step = 1; - - // go through columns of block - for (Int col = 0; col < jb; col += step) { - if (current_2x2[col] == 0.0) { - // 1x1 pivots - step = 1; - const double inv_d = sn_columns_[sn][diag_start + col + jb * col]; - x[sn_start + nb * j + col] *= inv_d; - } else { - // 2x2 pivots - step = 2; - - // inverse of 2x2 pivot - const double i_d1 = sn_columns_[sn][diag_start + col + jb * col]; - const double i_d2 = - sn_columns_[sn][diag_start + col + 1 + jb * (col + 1)]; - const double i_off = current_2x2[col]; - - double x1 = x[sn_start + nb * j + col]; - double x2 = x[sn_start + nb * j + col + 1]; - - x[sn_start + nb * j + col] = i_d1 * x1 + i_off * x2; - x[sn_start + nb * j + col + 1] = i_d2 * x2 + i_off * x1; - } + HIPO_CLOCK_START(2); + + const double* current_2x2 = &pivot_2x2_[sn][nb * j]; + Int step = 1; + + // go through columns of block + for (Int col = 0; col < jb; col += step) { + if (current_2x2[col] == 0.0) { + // 1x1 pivots + step = 1; + const double inv_d = sn_columns_[sn][diag_start + col + jb * col]; + x[sn_start + nb * j + col] *= inv_d; + } else { + // 2x2 pivots + step = 2; + + // inverse of 2x2 pivot + const double i_d1 = sn_columns_[sn][diag_start + col + jb * col]; + const double i_d2 = + sn_columns_[sn][diag_start + col + 1 + jb * (col + 1)]; + const double i_off = current_2x2[col]; + + double x1 = x[sn_start + nb * j + col]; + double x2 = x[sn_start + nb * j + col + 1]; + + x[sn_start + nb * j + col] = i_d1 * x1 + i_off * x2; + x[sn_start + nb * j + col + 1] = i_d2 * x2 + i_off * x1; } + } - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_dense); - if (options_.pivoting) { - HIPO_CLOCK_START(2); - // apply inverse swaps - permuteWithSwaps(&x[sn_start + nb * j], current_swaps, jb, true); - HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); - } + if (any_swaps_in_block && options_.pivoting) { + HIPO_CLOCK_START(2); + // apply inverse swaps + permuteWithSwaps(&x[sn_start + nb * j], current_swaps, jb, true); + HIPO_CLOCK_STOP(2, data_, kTimeSolveSolve_swap); + } - // move diag_start forward by number of diagonal entries in block - diag_start += jb * jb; + // move diag_start forward by number of diagonal entries in block + diag_start += jb * jb; - // move diag_start forward by number of sub-diagonal entries in block - diag_start += (ldSn - nb * j - jb) * jb; + // move diag_start forward by number of sub-diagonal entries in block + diag_start += (ldSn - nb * j - jb) * jb; + } +} + +void HybridSolveHandler::diagSolve(double* x) const { + if (options_.parallel_diag) { + // Embarassingly parallel: each sn reads/writes independent entries in x. + highs::parallel::for_each( + 0, S_.sn(), + [&](Int start, Int end) { + for (Int sn = start; sn < end; ++sn) { + processDiagSn(sn, x); + } + }, + // choose grainsize so that the number of tasks in the for_each loop + // that execute the function is roughly kParallelDiagTargetNumTasks + std::ceil((double)S_.size() / kParallelDiagTargetNumTasks)); + + } else { + for (Int sn = 0; sn < S_.sn(); ++sn) { + processDiagSn(sn, x); } } } @@ -473,4 +633,4 @@ void HybridSolveHandler::inertia(Int& pos, Int& neg, Int& zero, } } -} // namespace hipo \ No newline at end of file +} // namespace hipo diff --git a/highs/ipm/hipo/factorhighs/HybridSolveHandler.h b/highs/ipm/hipo/factorhighs/HybridSolveHandler.h index 9ec4697127f..225aeefb637 100644 --- a/highs/ipm/hipo/factorhighs/HybridSolveHandler.h +++ b/highs/ipm/hipo/factorhighs/HybridSolveHandler.h @@ -9,7 +9,21 @@ class HybridSolveHandler : public SolveHandler { const std::vector>& swaps_; const std::vector>& any_swaps_; const std::vector>& pivot_2x2_; - std::vector& gemv_workspace_; + + mutable std::vector> parallel_gemv_workspace_; + mutable std::vector serial_gemv_workspace_; + + std::vector first_child_, next_child_; + mutable std::vector> task_rows_; + mutable std::vector> task_vals_; + + void processForwardTask(Int task, double* x) const; + void processBackwardTask(Int task, double* x) const; + + void processForwardSn(Int sn, double* x, std::vector& work, + Int task = -1, Int end_col_in_task = -1) const; + void processBackwardSn(Int sn, double* x, std::vector& work) const; + void processDiagSn(Int sn, double* x) const; public: void forwardSolve(double* x) const override; @@ -22,8 +36,7 @@ class HybridSolveHandler : public SolveHandler { const std::vector>& swaps, const std::vector>& any_swap, const std::vector>& pivot_2x2, - std::vector& gemv_work, DataCollector& data, - const FHoptions& options); + DataCollector& data, const FHoptions& options); }; } // namespace hipo diff --git a/highs/ipm/hipo/factorhighs/Numeric.cpp b/highs/ipm/hipo/factorhighs/Numeric.cpp index 7980a1077b9..c6ff4abf4c9 100644 --- a/highs/ipm/hipo/factorhighs/Numeric.cpp +++ b/highs/ipm/hipo/factorhighs/Numeric.cpp @@ -12,38 +12,33 @@ namespace hipo { -Int Numeric::prepare() { - if (!sn_columns_ || !S_ || !data_ || !options_) return kRetInvalidPointer; - SH_.reset(new HybridSolveHandler(*S_, *sn_columns_, swaps_, any_swaps_, - pivot_2x2_, gemv_workspace_, *data_, - *options_)); - if (!SH_) return kRetGeneric; - - // memory allocation should happen only the first time, then memory is reused. - // No need to zero memory each time, as it is overwritten by solveHandler. - gemv_workspace_.resize(S_->largestFront()); - +void Numeric::computeAnySwaps() { // compute which blocks of columns require swaps - if (options_->pivoting) { - any_swaps_.resize(S_->sn()); - const Int nb = options_->nb; - for (Int sn = 0; sn < S_->sn(); ++sn) { - const Int sn_size = S_->snStart(sn + 1) - S_->snStart(sn); - const Int n_blocks = (sn_size - 1) / nb + 1; - any_swaps_[sn].assign(n_blocks, 0); - - for (Int j = 0; j < n_blocks; ++j) { - const Int jb = std::min(nb, sn_size - nb * j); - for (Int i = 0; i < jb; ++i) { - if (swaps_[sn][nb * j + i] != i) { - any_swaps_[sn][j] = 1; - break; - } + any_swaps_.resize(S_->sn()); + const Int nb = options_->nb; + for (Int sn = 0; sn < S_->sn(); ++sn) { + const Int sn_size = S_->snStart(sn + 1) - S_->snStart(sn); + const Int n_blocks = (sn_size - 1) / nb + 1; + any_swaps_[sn].assign(n_blocks, 0); + + for (Int j = 0; j < n_blocks; ++j) { + const Int jb = std::min(nb, sn_size - nb * j); + for (Int i = 0; i < jb; ++i) { + if (swaps_[sn][nb * j + i] != i) { + any_swaps_[sn][j] = 1; + break; } } } } +} +Int Numeric::prepare() { + if (!sn_columns_ || !S_ || !data_ || !options_) return kRetInvalidPointer; + SH_.reset(new HybridSolveHandler(*S_, *sn_columns_, swaps_, any_swaps_, + pivot_2x2_, *data_, *options_)); + if (!SH_) return kRetGeneric; + if (options_->pivoting) computeAnySwaps(); return kRetOk; } diff --git a/highs/ipm/hipo/factorhighs/Numeric.h b/highs/ipm/hipo/factorhighs/Numeric.h index 0b5d7b72c9d..1219fb632c9 100644 --- a/highs/ipm/hipo/factorhighs/Numeric.h +++ b/highs/ipm/hipo/factorhighs/Numeric.h @@ -32,13 +32,14 @@ class Numeric { DataCollector* data_ = nullptr; const FHoptions* options_; std::unique_ptr SH_; - std::vector gemv_workspace_; friend class Factorise; // dynamic regularisation applied to the matrix std::vector total_reg_{}; + void computeAnySwaps(); + public: Int prepare(); diff --git a/highs/ipm/hipo/factorhighs/Symbolic.cpp b/highs/ipm/hipo/factorhighs/Symbolic.cpp index 08c4448ea3a..d135e92a079 100644 --- a/highs/ipm/hipo/factorhighs/Symbolic.cpp +++ b/highs/ipm/hipo/factorhighs/Symbolic.cpp @@ -6,11 +6,10 @@ namespace hipo { -Symbolic::Symbolic() {} - -void Symbolic::setParallel(bool par_tree, bool par_node) { - parallel_tree_ = par_tree; - parallel_node_ = par_node; +void TreeSchedule::clear() { + sn_per_task.clear(); + task_parent.clear(); + valid = false; } Int64 Symbolic::nz() const { return nz_; } @@ -37,16 +36,18 @@ Int64 Symbolic::cliqueSize(Int sn) const { } Int64 Symbolic::maxStackSize() const { return max_stack_size_; } Int Symbolic::largestFront() const { return largest_front_; } -bool Symbolic::parTree() const { return parallel_tree_; } -bool Symbolic::parNode() const { return parallel_node_; } double Symbolic::storage() const { return serial_storage_; } Int Symbolic::depth() const { return tree_depth_; } +double Symbolic::solveTreeSpeedup() const { + return ops_solve_ / critops_solve_; +} const std::vector& Symbolic::ptr() const { return ptr_; } const std::vector& Symbolic::iperm() const { return iperm_; } const std::vector& Symbolic::snParent() const { return sn_parent_; } const std::vector& Symbolic::snStart() const { return sn_start_; } const std::vector& Symbolic::pivotSign() const { return pivot_sign_; } +const TreeSchedule& Symbolic::schedule() const { return schedule_solve_; } static std::string memoryString(double mem) { std::stringstream ss; @@ -78,6 +79,10 @@ void Symbolic::print(const Logger& logger, bool verbose) const { log_stream << textline("Max tree speedup:") << fix(flops_ / critops_, 0, 2) << '\n'; log_stream << textline("Tree depth:") << integer(tree_depth_, 0) << '\n'; + log_stream << textline("Number of solve tasks:") + << integer(schedule_solve_.count(), 0) << '\n'; + log_stream << textline("Solve tree speedup:") + << fix(ops_solve_ / critops_solve_, 0, 2) << '\n'; log_stream << textline("Artificial nz:") << sci(artificial_nz_, 0, 1) << '\n'; log_stream << textline("Artificial ops:") << sci(artificial_ops_, 0, 1) diff --git a/highs/ipm/hipo/factorhighs/Symbolic.h b/highs/ipm/hipo/factorhighs/Symbolic.h index 77f1ba3ed8b..95b6995cae0 100644 --- a/highs/ipm/hipo/factorhighs/Symbolic.h +++ b/highs/ipm/hipo/factorhighs/Symbolic.h @@ -8,14 +8,26 @@ namespace hipo { +// Object to manage the parallel schedule of supernodes for tree parallelism +struct TreeSchedule { + bool valid = false; + + // sn_per_task[i] contains the supernodes to process as part of task i. They + // are numbered in increasing order and must be processed in the given order, + // to guarantee that dependencies are satisfied. + std::vector> sn_per_task; + + // task elimination tree, giving the tree of dependencies among tasks + std::vector task_parent; + + void clear(); + Int count() const { return task_parent.size(); } +}; + // Symbolic factorisation object class Symbolic { bool empty_ = true; - // Options for parallelism - bool parallel_tree_ = false; - bool parallel_node_ = false; - // Statistics about symbolic factorisation Int n_{}; Int64 nz_{}; @@ -33,6 +45,9 @@ class Symbolic { Int sn_size_10_{}; Int sn_size_100_{}; + double ops_solve_{}; + double critops_solve_{}; + // Inverse permutation std::vector iperm_{}; @@ -97,12 +112,11 @@ class Symbolic { Int64 max_stack_size_{}; Int tree_depth_{}; + TreeSchedule schedule_solve_; + friend class Analyse; public: - Symbolic(); - void setParallel(bool par_tree, bool par_node); - // provide const access to symbolic factorisation bool empty() const { return empty_; } Int64 nz() const; @@ -124,14 +138,14 @@ class Symbolic { Int64 maxStackSize() const; Int largestFront() const; Int depth() const; - bool parTree() const; - bool parNode() const; + double solveTreeSpeedup() const; double storage() const; const std::vector& ptr() const; const std::vector& iperm() const; const std::vector& snParent() const; const std::vector& snStart() const; const std::vector& pivotSign() const; + const TreeSchedule& schedule() const; void print(const Logger& logger, bool verbose = false) const; }; diff --git a/highs/ipm/hipo/ipm/FactorHighsSolver.cpp b/highs/ipm/hipo/ipm/FactorHighsSolver.cpp index a7805bf6ee6..9eff5d80c33 100644 --- a/highs/ipm/hipo/ipm/FactorHighsSolver.cpp +++ b/highs/ipm/hipo/ipm/FactorHighsSolver.cpp @@ -179,8 +179,10 @@ Int FactorHighsSolver::solveNE(const std::vector& rhs, Int FactorHighsSolver::setup() { if (kkt_.S.empty()) { Clock clock; + setParallelBeforeSymbolic(); if (Int status = setNla()) return status; - setParallel(); + setParallelAfterSymbolic(); + printParallel(); info_.times[kAnalyseTime] += clock.stop(); if (!options_.timeless_log) { @@ -192,7 +194,7 @@ Int FactorHighsSolver::setup() { kkt_.S.print(logger_, logger_.debug(1)); - if (kkt_.S.storage() > kLargeStorageGB * 1024 * 1024 * 1024) { + if (kkt_.S.storage() > kParallelLargeStorageGB * 1024 * 1024 * 1024) { logger_.printw("Large amount of memory required\n"); } @@ -214,9 +216,9 @@ Int FactorHighsSolver::chooseNla() { const bool NE_possible = !(model_.nonSeparableQp() || model_.m() == 0); const bool expect_AS_much_cheaper = - model_.nzNElb() > model_.nzAS() * kNzBoundsRatio; + model_.nzNElb() > model_.nzAS() * kSkipSystemNzBoundsRatio; const bool expect_NE_much_cheaper = - model_.nzAS() > model_.nzNEub() * kNzBoundsRatio; + model_.nzAS() > model_.nzNEub() * kSkipSystemNzBoundsRatio; const bool skip_AS = NE_possible && expect_NE_much_cheaper; const bool skip_NE = AS_possible && expect_AS_much_cheaper; @@ -262,7 +264,7 @@ Int FactorHighsSolver::chooseNla() { // If NE has more nonzeros than the factor of AS, then it's likely that AS // will be preferred, so stop computation of NE. - Int64 NE_nz_limit = symb_AS.nz() * kSymbNzMult; + Int64 NE_nz_limit = symb_AS.nz() * kSystemSymbNzMult; if (failure_AS || NE_nz_limit > kHighsIInf) NE_nz_limit = kHighsIInf; kkt_.NE_nz_limit.store(NE_nz_limit, std::memory_order_relaxed); } @@ -271,10 +273,15 @@ Int FactorHighsSolver::chooseNla() { // In parallel, run AS analyse and build NE structure. NE analyse runs only // after AS analyse is finished, so that it can be skipped based on the number // of nz of NE matrix and AS factor. - highs::parallel::TaskGroup tg; - tg.spawn([&]() { run_analyse_AS(); }); - tg.spawn([&]() { run_structure_NE(); }); - tg.taskWait(); + if (options_.getParallel(ParallelTechnique::kAnalyse)) { + highs::parallel::TaskGroup tg; + tg.spawn([&]() { run_analyse_AS(); }); + tg.spawn([&]() { run_structure_NE(); }); + tg.taskWait(); + } else { + run_analyse_AS(); + run_structure_NE(); + } // if NE was skipped but AS failed, use NE if (skip_NE && failure_AS) { @@ -310,8 +317,8 @@ Int FactorHighsSolver::chooseNla() { } else { // Total number of operations, given by dense flops and sparse indexing // operations, weighted with an empirical factor - double ops_NE = symb_NE.flops() + symb_NE.spops() * kSpopsWeight; - double ops_AS = symb_AS.flops() + symb_AS.spops() * kSpopsWeight; + double ops_NE = symb_NE.flops() + symb_NE.spops() * kSystemSpopsWeight; + double ops_AS = symb_AS.flops() + symb_AS.spops() * kSystemSpopsWeight; double sn_size_NE = (double)symb_NE.size() / symb_NE.sn(); double sn_size_AS = (double)symb_AS.size() / symb_AS.sn(); @@ -319,9 +326,9 @@ Int FactorHighsSolver::chooseNla() { double ratio_ops = ops_NE / ops_AS; double ratio_sn = sn_size_AS / sn_size_NE; - bool NE_much_more_expensive = ratio_ops > kRatioOpsThresh; - bool AS_not_too_expensive = ratio_ops > 1.0 / kRatioOpsThresh; - bool sn_AS_larger_than_NE = ratio_sn > kRatioSnThresh; + bool NE_much_more_expensive = ratio_ops > kSystemRatioOpsThresh; + bool AS_not_too_expensive = ratio_ops > 1.0 / kSystemRatioOpsThresh; + bool sn_AS_larger_than_NE = ratio_sn > kSystemRatioSnThresh; if (NE_much_more_expensive || (sn_AS_larger_than_NE && AS_not_too_expensive)) { @@ -369,8 +376,7 @@ Int FactorHighsSolver::chooseOrdering(const std::vector& rows, const Int k = orderings_to_try.size(); - // vector is not thread-safe - std::vector failure(k, 0); + std::vector failure(k, false); if (nla == "NE") { if (ptr.back() >= kkt_.NE_nz_limit.load(std::memory_order_relaxed)) { @@ -435,8 +441,16 @@ Int FactorHighsSolver::chooseOrdering(const std::vector& rows, } }; - highs::parallel::for_each( - 0, k, [&](Int start, Int end) { run_ordering_and_analyse(start); }, 1); + const bool parallel_ordering = + nla == "NE" ? options_.getParallel(ParallelTechnique::kOrderNE) + : options_.getParallel(ParallelTechnique::kOrderAS); + + if (parallel_ordering) { + highs::parallel::for_each( + 0, k, [&](Int start, Int end) { run_ordering_and_analyse(start); }, 1); + } else { + for (Int i = 0; i < k; ++i) run_ordering_and_analyse(i); + } Int num_success = 0; for (bool b : failure) { @@ -460,11 +474,11 @@ Int FactorHighsSolver::chooseOrdering(const std::vector& rows, } } - // find orderings with flops within kFlopsOrderingThresh of the best + // find orderings with flops within kOrderingFlopsThresh of the best std::vector consider; for (Int i = 0; i < k; ++i) { if (!failure[i] && - symbolics[i].flops() <= kFlopsOrderingThresh * best_flops) { + symbolics[i].flops() <= kOrderingFlopsThresh * best_flops) { consider.push_back(i); } } @@ -482,7 +496,7 @@ Int FactorHighsSolver::chooseOrdering(const std::vector& rows, } // fix selection if one or more require too much memory - const double bytes_thresh = kLargeStorageGB * 1024 * 1024 * 1024; + const double bytes_thresh = kParallelLargeStorageGB * 1024 * 1024 * 1024; double best_memory = kHighsInf; Int ind_best_memory = -1; for (Int i = 0; i < k; ++i) { @@ -549,91 +563,132 @@ Int FactorHighsSolver::setNla() { return kOk; } +void FactorHighsSolver::setParallelBeforeSymbolic() { + const bool parallel_analyse_default = true; + options_.chooseParallel(ParallelTechnique::kAnalyse, + parallel_analyse_default); + + const bool parallel_order_NE_default = true; + options_.chooseParallel(ParallelTechnique::kOrderNE, + parallel_order_NE_default); + + const bool parallel_order_AS_default = true; + options_.chooseParallel(ParallelTechnique::kOrderNE, + parallel_order_AS_default); + + const double A_nz_per_col = (double)model_.A().numNz() / model_.A().num_col_; + const double A_nz_per_row = (double)model_.A().numNz() / model_.A().num_row_; + const bool A_is_dense = A_nz_per_col > kParallelNEnzPerColThresh || + A_nz_per_row > kParallelNEnzPerRowThresh; + const bool A_is_large = model_.A().num_row_ > kParallelNEsizeThresh || + model_.A().num_col_ > kParallelNEsizeThresh; + + const bool parallel_NE_struct_default = A_is_dense && A_is_large; + options_.chooseParallel(ParallelTechnique::kNEStruct, + parallel_NE_struct_default); + + const bool parallel_NE_values_default = A_is_large; + options_.chooseParallel(ParallelTechnique::kNEValues, + parallel_NE_values_default); +} + static bool usingAppleBlas() { return strstr(HighsExtras::blas::getInfo()->provider, "Apple") != nullptr; } -void FactorHighsSolver::setParallel() { +void FactorHighsSolver::setParallelAfterSymbolic() { bool parallel_tree = false; bool parallel_node = false; - std::stringstream log_stream; - log_stream << textline("Parallelism:"); - - if (options_.parallel == kHighsOffString) { - log_stream << "None requested\n"; - } else if (options_.parallel == kHighsOnString) { - if (options_.parallel_type == kHipoBothString) { - parallel_tree = true; - parallel_node = true; - log_stream << "Full requested\n"; - } else if (options_.parallel_type == kHipoTreeString) { - parallel_tree = true; - log_stream << "Tree requested\n"; - } else if (options_.parallel_type == kHipoNodeString) { - parallel_node = true; - log_stream << "Node requested\n"; - } else - assert(1 == 0); - - } else if (options_.parallel == kHighsChooseString) { - if (highs::parallel::num_threads() == 1) { - parallel_node = false; - parallel_tree = false; - } else if (usingAppleBlas()) { - // Blas on Apple do not work well with parallel_node, but parallel_tree - // seems to always be beneficial. - parallel_node = false; + if (usingAppleBlas()) { + // Blas on Apple do not work well with parallel_node, but parallel_tree + // seems to always be beneficial. + parallel_node = false; + parallel_tree = true; + } else { + // Otherwise, parallel_node is active because it is triggered only if the + // frontal matrix is large enough anyway. + parallel_node = true; + + // parallel_tree instead is chosen with a heuristic + + double tree_speedup = kkt_.S.flops() / kkt_.S.critops(); + double sn_size = (double)kkt_.S.size() / kkt_.S.sn(); + + bool enough_sn = kkt_.S.sn() > kParallelMinNumberSn; + bool enough_flops = kkt_.S.flops() > kParallelLargeFlopsThresh; + bool speedup_is_large = tree_speedup > kParallelLargeSpeedupThresh; + bool sn_are_large = sn_size > kParallelLargeSnThresh; + bool sn_are_not_small = sn_size > kParallelSmallSnThresh; + + // parallel_tree is active if the supernodes are large, or if there is a + // large expected speedup and the supernodes are not too small, provided + // that the number of flops and supernodes is not too small. + if (enough_sn && enough_flops && + (sn_are_large || (speedup_is_large && sn_are_not_small))) { parallel_tree = true; - } else { - // Otherwise, parallel_node is active because it is triggered only if the - // frontal matrix is large enough anyway. - parallel_node = true; - - // parallel_tree instead is chosen with a heuristic - - double tree_speedup = kkt_.S.flops() / kkt_.S.critops(); - double sn_size = (double)kkt_.S.size() / kkt_.S.sn(); - - bool enough_sn = kkt_.S.sn() > kMinNumberSn; - bool enough_flops = kkt_.S.flops() > kLargeFlopsThresh; - bool speedup_is_large = tree_speedup > kLargeSpeedupThresh; - bool sn_are_large = sn_size > kLargeSnThresh; - bool sn_are_not_small = sn_size > kSmallSnThresh; - - // parallel_tree is active if the supernodes are large, or if there is a - // large expected speedup and the supernodes are not too small, provided - // that the number of flops and supernodes is not too small. - if (enough_sn && enough_flops && - (sn_are_large || (speedup_is_large && sn_are_not_small))) { - parallel_tree = true; - } } + } - // If serial memory is too large, switch off tree parallelism to avoid - // running out of memory - double num_GB = kkt_.S.storage() / 1024 / 1024 / 1024; - if (num_GB > kLargeStorageGB) { - parallel_tree = false; - } + // If serial memory is too large, switch off tree parallelism to avoid + // running out of memory + double num_GB = kkt_.S.storage() / 1024 / 1024 / 1024; + if (num_GB > kParallelLargeStorageGB) { + parallel_tree = false; + } - // switch off tree parallelism if depth of recursion is too large - if (kkt_.S.depth() > kMaxTreeDepth) parallel_tree = false; + // switch off tree parallelism if depth of recursion is too large + if (kkt_.S.depth() > kParallelMaxTreeDepth) parallel_tree = false; - if (parallel_tree && parallel_node) - log_stream << "Full preferred\n"; - else if (parallel_tree && !parallel_node) - log_stream << "Tree preferred\n"; - else if (!parallel_tree && parallel_node) - log_stream << "Node preferred\n"; - else - log_stream << "None preferred\n"; + options_.chooseParallel(ParallelTechnique::kTree, parallel_tree); + options_.chooseParallel(ParallelTechnique::kNode, parallel_node); - } else - assert(1 == 0); + // choose parallelism for solve phase + bool parallel_forward = false; + bool parallel_backward = false; + bool parallel_diag = false; + if (kkt_.S.size() > kParallelSolveMinSize) { + parallel_diag = true; - logger_.print(log_stream.str().c_str()); - kkt_.S.setParallel(parallel_tree, parallel_node); + if (kkt_.S.solveTreeSpeedup() > kParallelForwardMinSpeedup) + parallel_forward = true; + + if (kkt_.S.solveTreeSpeedup() > kParallelBackwardMinSpeedup) + parallel_backward = true; + } + + options_.chooseParallel(ParallelTechnique::kForwardSolve, parallel_forward); + options_.chooseParallel(ParallelTechnique::kBackwardSolve, parallel_backward); + options_.chooseParallel(ParallelTechnique::kDiagonalSolve, parallel_diag); + + for (Int i = 0; i < static_cast(ParallelTechnique::kCount); ++i) + assert(options_.parallel[i] == ParallelType::kOn || + options_.parallel[i] == ParallelType::kOff); + + FH_.setParallel(options_.getParallel(ParallelTechnique::kTree), + options_.getParallel(ParallelTechnique::kNode)); + + FH_.setParallelSolve(options_.getParallel(ParallelTechnique::kForwardSolve), + options_.getParallel(ParallelTechnique::kBackwardSolve), + options_.getParallel(ParallelTechnique::kDiagonalSolve)); +} + +void FactorHighsSolver::printParallel() const { + std::stringstream log_stream; + log_stream + << textline("Parallelism:") + << (options_.getParallel(ParallelTechnique::kAnalyse) ? "A" : "_") + << (options_.getParallel(ParallelTechnique::kOrderNE) ? "O" : "_") + << (options_.getParallel(ParallelTechnique::kOrderAS) ? "O" : "_") << "|" + << (options_.getParallel(ParallelTechnique::kNEStruct) ? "S" : "_") + << (options_.getParallel(ParallelTechnique::kNEValues) ? "V" : "_") << "|" + << (options_.getParallel(ParallelTechnique::kTree) ? "T" : "_") + << (options_.getParallel(ParallelTechnique::kNode) ? "N" : "_") << "|" + << (options_.getParallel(ParallelTechnique::kForwardSolve) ? "F" : "_") + << (options_.getParallel(ParallelTechnique::kDiagonalSolve) ? "D" : "_") + << (options_.getParallel(ParallelTechnique::kBackwardSolve) ? "B" : "_") + << '\n'; + logger_.printInfo(log_stream.str().c_str()); } // ========================================================================= @@ -647,4 +702,4 @@ void FactorHighsSolver::getReg(std::vector& reg) { FH_.getRegularisation(reg.data()); } -} // namespace hipo \ No newline at end of file +} // namespace hipo diff --git a/highs/ipm/hipo/ipm/FactorHighsSolver.h b/highs/ipm/hipo/ipm/FactorHighsSolver.h index a3e9093d212..e7641f85387 100644 --- a/highs/ipm/hipo/ipm/FactorHighsSolver.h +++ b/highs/ipm/hipo/ipm/FactorHighsSolver.h @@ -31,7 +31,9 @@ class FactorHighsSolver : public LinearSolver { Int chooseNla(); Int setNla(); - void setParallel(); + void setParallelBeforeSymbolic(); + void setParallelAfterSymbolic(); + void printParallel() const; Int chooseOrdering(const std::vector& rows, const std::vector& ptr, const std::vector& signs, Symbolic& S, std::string& ordering, const std::string& nla); diff --git a/highs/ipm/hipo/ipm/KktMatrix.cpp b/highs/ipm/hipo/ipm/KktMatrix.cpp index 3cc2c0caf7f..42b726a9420 100644 --- a/highs/ipm/hipo/ipm/KktMatrix.cpp +++ b/highs/ipm/hipo/ipm/KktMatrix.cpp @@ -5,8 +5,8 @@ namespace hipo { KktMatrix::KktMatrix(const Model& m, const Regularisation& r, Info& i, - const Logger& l) - : model{m}, regul{r}, info{i}, logger{l} {} + const Logger& l, const Options& o) + : model{m}, regul{r}, info{i}, logger{l}, options{o} {} Int KktMatrix::buildASstructure() { // Build lower triangular structure of the augmented system. @@ -187,18 +187,7 @@ Int KktMatrix::buildNEstructure() { } }; - // computing the structure in parallel only if matrix A is dense and large - const double nz_per_col = (double)model.A().numNz() / model.A().num_col_; - const double nz_per_row = (double)model.A().numNz() / model.A().num_row_; - const bool is_dense = nz_per_col > kParallelNEnzPerColThresh || - nz_per_row > kParallelNEnzPerRowThresh; - const bool is_large = model.A().num_row_ > kParallelNEsizeThresh || - model.A().num_col_ > kParallelNEsizeThresh; - const bool parallel = - is_dense && is_large && highs::parallel::num_threads() > 1; - - if (parallel) { - logger.printInfo("NE structure in parallel\n"); + if (options.getParallel(ParallelTechnique::kNEStruct)) { std::vector> rowsNE_local(m); highs::parallel::for_each( @@ -227,7 +216,6 @@ Int KktMatrix::buildNEstructure() { rowsNE.insert(rowsNE.end(), v.begin(), v.end()); } else { - logger.printInfo("NE structure in serial\n"); rowsNE.reserve(model.nzNElb()); std::vector is_nz(m, false); std::vector temp_index(m); @@ -298,12 +286,7 @@ Int KktMatrix::buildNEvalues(const std::vector& scaling) { } }; - // computing the values in parallel only if matrix A is large - const bool is_large = model.A().num_row_ > kParallelNEsizeThresh || - model.A().num_col_ > kParallelNEsizeThresh; - const bool parallel = is_large && highs::parallel::num_threads() > 1; - - if (parallel) { + if (options.getParallel(ParallelTechnique::kNEValues)) { highs::parallel::for_each( 0, m, [&](Int start, Int end) { diff --git a/highs/ipm/hipo/ipm/KktMatrix.h b/highs/ipm/hipo/ipm/KktMatrix.h index c99e8ae63c7..22f847fd417 100644 --- a/highs/ipm/hipo/ipm/KktMatrix.h +++ b/highs/ipm/hipo/ipm/KktMatrix.h @@ -29,9 +29,10 @@ struct KktMatrix { const Regularisation& regul; Info& info; const Logger& logger; + const Options& options; KktMatrix(const Model& model, const Regularisation& regul, Info& info, - const Logger& logger); + const Logger& logger, const Options& options); Int buildASstructure(); Int buildASvalues(const std::vector& scaling); diff --git a/highs/ipm/hipo/ipm/Model.cpp b/highs/ipm/hipo/ipm/Model.cpp index 258ed462e62..f5979fbe0e7 100644 --- a/highs/ipm/hipo/ipm/Model.cpp +++ b/highs/ipm/hipo/ipm/Model.cpp @@ -44,7 +44,7 @@ Int Model::init(const HighsLp& lp, const HighsHessian& Q) { void Model::nzBounds() { // compute lower and upper bounds for the number of nonzeros in normal // equations. - std::vector mark(m_, false); + std::vector mark(m_, false); NE_nz_lb_ = A_.num_row_; NE_nz_ub_ = A_.num_row_; for (Int col = 0; col < A_.num_col_; ++col) { diff --git a/highs/ipm/hipo/ipm/Options.h b/highs/ipm/hipo/ipm/Options.h index aefd40c3234..6c21443710b 100644 --- a/highs/ipm/hipo/ipm/Options.h +++ b/highs/ipm/hipo/ipm/Options.h @@ -3,16 +3,17 @@ #include "Parameters.h" #include "io/HighsIO.h" +#include "lp_data/HConst.h" #include "lp_data/HighsOptions.h" namespace hipo { +enum class ParallelType { kOff, kChoose, kOn }; + struct Options { // Solver options std::string nla = kHighsChooseString; std::string crossover = kHighsOffString; - std::string parallel = kHighsChooseString; - std::string parallel_type = kHipoBothString; std::string ordering = kHighsChooseString; std::string factor = kHighsChooseString; @@ -25,13 +26,29 @@ struct Options { double time_limit = -1.0; Int block_size = 0; Int random_seed = 0; + ParallelType parallel[static_cast(ParallelTechnique::kCount)]; // Logging bool display = true; bool timeless_log = false; const HighsLogOptions* log_options = nullptr; + + inline bool getParallel(ParallelTechnique bit) const { + return static_cast(parallel[static_cast(bit)]); + } + inline void setParallel(ParallelTechnique bit, ParallelType type) { + parallel[static_cast(bit)] = type; + } + inline void chooseParallel(ParallelTechnique bit, bool default_behaviour) { + ParallelType type_default = + default_behaviour ? ParallelType::kOn : ParallelType::kOff; + if (parallel[static_cast(bit)] == ParallelType::kChoose) + setParallel(bit, type_default); + } }; +inline bool testParallelBit(Int option, Int bit) { return option & (1 << bit); } + } // namespace hipo #endif \ No newline at end of file diff --git a/highs/ipm/hipo/ipm/Parameters.h b/highs/ipm/hipo/ipm/Parameters.h index f4859d6b9c7..935bdab427e 100644 --- a/highs/ipm/hipo/ipm/Parameters.h +++ b/highs/ipm/hipo/ipm/Parameters.h @@ -21,23 +21,22 @@ const double kSmallProduct = 1e-3; const double kLargeProduct = 1e3; // parameters for choice of AS or NE -const double kSpopsWeight = 30.0; -const double kRatioOpsThresh = 10.0; -const double kRatioSnThresh = 1.5; -const double kSymbNzMult = 5.0; +const double kSystemSpopsWeight = 30.0; +const double kSystemRatioOpsThresh = 10.0; +const double kSystemRatioSnThresh = 1.5; +const double kSystemSymbNzMult = 5.0; // parameters for choice of parallelism -const double kLargeFlopsThresh = 1e7; -const double kLargeSpeedupThresh = 1; -const double kLargeSnThresh = 20.0; -const double kSmallSnThresh = 5.0; -const Int kMinNumberSn = 10; -const double kLargeStorageGB = 20.0; -const double kLargeFillin = 50.0; -const double kMaxTreeDepth = 1000; +const double kParallelLargeFlopsThresh = 1e7; +const double kParallelLargeSpeedupThresh = 1; +const double kParallelLargeSnThresh = 20.0; +const double kParallelSmallSnThresh = 5.0; +const Int kParallelMinNumberSn = 10; +const double kParallelLargeStorageGB = 20.0; +const double kParallelMaxTreeDepth = 1000; // parameters for choice of ordering -const double kFlopsOrderingThresh = 1.2; +const double kOrderingFlopsThresh = 1.2; // parameters for choice of factorisation const double kUplookFlopsThresh = 1e6; @@ -47,27 +46,33 @@ const double kUplookSpopsRatioLower = 20; const double kUplookSpopsRatioUpper = 100; // parameters for skipping AS or NE -const double kNzBoundsRatio = 50.0; +const double kSkipSystemNzBoundsRatio = 50.0; // parameters for iterative refinement -const Int kMaxIterRefine = 3; -const double kTolRefine = 1e-12; +const Int kRefineMaxIter = 3; +const double kRefineTol = 1e-12; // parameters for scaling -const double kSmallScalingCoeff = 1e-4; -const double kLargeScalingCoeff = 1e4; -const double kSmallBoundDiff = 1e-3; +const double kScalingSmallCoeff = 1e-4; +const double kScalingLargeCoeff = 1e4; +const double kScalingSmallBoundDiff = 1e-3; // parameters for free variables const double kFreeVarsInitialBound = 1e4; const double kFreeVarsCloseRatio = 0.5; +// parameters for parallel NE const Int kParallelNEStructTasks = 50; // 32 < . <= 64 const Int kParallelNEValuesTasks = 100; // 64 < . <= 128 const Int kParallelNEnzPerColThresh = 10; const Int kParallelNEnzPerRowThresh = 30; const Int kParallelNEsizeThresh = 1e4; +// parameters for parallel solve +const double kParallelSolveMinSize = 1e4; +const double kParallelForwardMinSpeedup = 2; +const double kParallelBackwardMinSpeedup = 1.2; + // static regularisation struct Regularisation { double primal = 1e-12; diff --git a/highs/ipm/hipo/ipm/PreProcess.cpp b/highs/ipm/hipo/ipm/PreProcess.cpp index df81ba3b3c9..875b43fa4f6 100644 --- a/highs/ipm/hipo/ipm/PreProcess.cpp +++ b/highs/ipm/hipo/ipm/PreProcess.cpp @@ -364,14 +364,14 @@ void PreprocessScaling::apply(Model& model) { const double u = upper[i] / colscale[i]; const double diff = std::abs(u - l); - if (diff / coeff < kSmallBoundDiff) - coeff = std::sqrt(coeff * diff / kSmallBoundDiff); + if (diff / coeff < kScalingSmallBoundDiff) + coeff = std::sqrt(coeff * diff / kScalingSmallBoundDiff); } if (!std::isinf(coeff) && !std::isnan(coeff)) colscale[i] *= coeff; - colscale[i] = std::max(colscale[i], kSmallScalingCoeff); - colscale[i] = std::min(colscale[i], kLargeScalingCoeff); + colscale[i] = std::max(colscale[i], kScalingSmallCoeff); + colscale[i] = std::min(colscale[i], kScalingLargeCoeff); } }; auto rowScaling = [&]() { @@ -390,8 +390,8 @@ void PreprocessScaling::apply(Model& model) { // apply row scaling for (Int i = 0; i < m; ++i) { if (norm_rows[i] > 0.0) rowscale[i] *= 1.0 / std::sqrt(norm_rows[i]); - rowscale[i] = std::max(rowscale[i], kSmallScalingCoeff); - rowscale[i] = std::min(rowscale[i], kLargeScalingCoeff); + rowscale[i] = std::max(rowscale[i], kScalingSmallCoeff); + rowscale[i] = std::min(rowscale[i], kScalingLargeCoeff); } }; @@ -416,13 +416,13 @@ void PreprocessScaling::apply(Model& model) { // ********************************************************************* for (Int i = 0; i < n; ++i) { - colscale[i] = std::max(colscale[i], kSmallScalingCoeff); - colscale[i] = std::min(colscale[i], kLargeScalingCoeff); + colscale[i] = std::max(colscale[i], kScalingSmallCoeff); + colscale[i] = std::min(colscale[i], kScalingLargeCoeff); colscale[i] = roundToPowerOf2(colscale[i]); } for (Int i = 0; i < m; ++i) { - rowscale[i] = std::max(rowscale[i], kSmallScalingCoeff); - rowscale[i] = std::min(rowscale[i], kLargeScalingCoeff); + rowscale[i] = std::max(rowscale[i], kScalingSmallCoeff); + rowscale[i] = std::min(rowscale[i], kScalingLargeCoeff); rowscale[i] = roundToPowerOf2(rowscale[i]); } diff --git a/highs/ipm/hipo/ipm/Refine.cpp b/highs/ipm/hipo/ipm/Refine.cpp index 190a855147a..c9681660219 100644 --- a/highs/ipm/hipo/ipm/Refine.cpp +++ b/highs/ipm/hipo/ipm/Refine.cpp @@ -21,8 +21,8 @@ void Solver::refine(NewtonDir& delta) { double old_omega{}; - for (Int iter = 0; iter < kMaxIterRefine; ++iter) { - if (omega < kTolRefine) break; + for (Int iter = 0; iter < kRefineMaxIter; ++iter) { + if (omega < kRefineTol) break; correction.clear(); solve6x6(correction, it_->ires); diff --git a/highs/ipm/hipo/ipm/Solver.cpp b/highs/ipm/hipo/ipm/Solver.cpp index 5f40146d534..01a15ad5b78 100644 --- a/highs/ipm/hipo/ipm/Solver.cpp +++ b/highs/ipm/hipo/ipm/Solver.cpp @@ -20,6 +20,52 @@ Int Solver::load(const HighsLp& lp, const HighsHessian& Q) { return kOk; } +void Solver::chooseAllowedParallelism(const HighsOptions& Hoptions) { + for (Int i = 0; i < static_cast(ParallelTechnique::kCount); ++i) + options_.parallel[i] = ParallelType::kChoose; + + // set default based on option `parallel` + options_.setParallel(ParallelTechnique::kAnalyse, ParallelType::kOn); + options_.setParallel(ParallelTechnique::kOrderNE, ParallelType::kOn); + options_.setParallel(ParallelTechnique::kOrderAS, ParallelType::kOn); + if (Hoptions.parallel == kHighsOffString) { + options_.setParallel(ParallelTechnique::kTree, ParallelType::kOff); + options_.setParallel(ParallelTechnique::kNode, ParallelType::kOff); + options_.setParallel(ParallelTechnique::kForwardSolve, ParallelType::kOff); + options_.setParallel(ParallelTechnique::kDiagonalSolve, ParallelType::kOff); + options_.setParallel(ParallelTechnique::kBackwardSolve, ParallelType::kOff); + } + + // override with option `hipo_parallel_type` + if (Hoptions.parallel == kHighsOnString) { + if (Hoptions.hipo_parallel_type == kHipoTreeString) { + options_.setParallel(ParallelTechnique::kTree, ParallelType::kOn); + options_.setParallel(ParallelTechnique::kNode, ParallelType::kOff); + } else if (Hoptions.hipo_parallel_type == kHipoNodeString) { + options_.setParallel(ParallelTechnique::kTree, ParallelType::kOff); + options_.setParallel(ParallelTechnique::kNode, ParallelType::kOn); + } else if (Hoptions.hipo_parallel_type == kHipoBothString) { + options_.setParallel(ParallelTechnique::kTree, ParallelType::kOn); + options_.setParallel(ParallelTechnique::kNode, ParallelType::kOn); + } + } + + // override if threads is 1 + if (highs::parallel::num_threads() == 1) { + for (Int i = 0; i < static_cast(ParallelTechnique::kCount); ++i) + options_.parallel[i] = ParallelType::kOff; + } + + // override with option `hipo_parallel_force` or `hipo_parallel_forbid` + for (Int i = 0; i < static_cast(ParallelTechnique::kCount); ++i) { + bool force = testParallelBit(Hoptions.hipo_parallel_force, i); + bool forbid = testParallelBit(Hoptions.hipo_parallel_forbid, i); + if (force && forbid) continue; + if (force) options_.parallel[i] = ParallelType::kOn; + if (forbid) options_.parallel[i] = ParallelType::kOff; + } +} + void Solver::setOptions(const HighsOptions& highs_options) { options_.display = true; if (!highs_options.output_flag | !highs_options.log_to_console) @@ -48,13 +94,12 @@ void Solver::setOptions(const HighsOptions& highs_options) { options_.max_iter = highs_options.ipm_iteration_limit; options_.crossover = highs_options.run_crossover; - options_.parallel = highs_options.parallel; - options_.parallel_type = highs_options.hipo_parallel_type; options_.nla = highs_options.hipo_system; options_.ordering = highs_options.hipo_ordering; options_.factor = highs_options.hipo_factor; options_.block_size = highs_options.hipo_block_size; options_.random_seed = highs_options.random_seed + 42; + chooseAllowedParallelism(highs_options); options_orig_ = options_; Hoptions_ = highs_options; @@ -147,7 +192,7 @@ isFailure Solver::initialise() { start_time_ = control_.elapsed(); - kkt_.reset(new KktMatrix(model_, regul_, info_, logger_)); + kkt_.reset(new KktMatrix(model_, regul_, info_, logger_, options_)); if (!kkt_) { info_.error = kErrorFailedAllocation; return true; @@ -306,8 +351,8 @@ void Solver::refineWithIpx() { } void Solver::crossoverWithIpx() { - // at the moment this is almost identical to refineWithIpx, but in the future - // it will use ipx_lps_.CrossoverFromStartingPoint + // at the moment this is almost identical to refineWithIpx, but in the + // future it will use ipx_lps_.CrossoverFromStartingPoint if (prepareIpx()) return; if (prepareIpxStartingPoint()) return; ipx_lps_.Solve(); @@ -982,8 +1027,8 @@ void Solver::bestWeight(const NewtonDir& delta, const NewtonDir& corrector, double& alpha_d) const { // Find the best primal and dual weights for the corrector in the interval // [alpha_p_old * alpha_d_old, 1]. - // Upon return, wp and wd are the optimal weights, alpha_p and alpha_d are the - // corresponding stepsizes. + // Upon return, wp and wd are the optimal weights, alpha_p and alpha_d are + // the corresponding stepsizes. // keep track of best stepsizes alpha_p = 0.0; @@ -1090,7 +1135,8 @@ shouldTerminate Solver::checkBadIter() { } else { if (checkTerminationKkt()) { logger_.printw( - "HiPO stagnated but HiGHS considers the solution acceptable\n"); + "HiPO stagnated but HiGHS considers the solution " + "acceptable\n"); logger_.print("=== Primal-dual feasible point found\n"); setStatus1(kStatusOptimal); } else { @@ -1266,7 +1312,8 @@ void Solver::printHeader() const { if (!options_.timeless_log) logger_.print(" time"); if (logger_.debug(1)) { logger_.print( - " alpha p/d sigma af/co cor solv fact static reg p/d " + " alpha p/d sigma af/co cor solv fact static reg p/d " + " " " minT maxT (xj * zj / mu)_range_&_num max_res"); } logger_.print("\n"); @@ -1425,22 +1472,22 @@ void Solver::chooseNumberOfCorrectors() { // because there are two sweeps through L (forward and backward). double solv_effort = 2.0 * LS_->nz(); - // The factorise phase uses BLAS-3 and can be parallelised, the solve phase - // uses BLAS-2 and cannot be parallelised. To account for this, the + // The factorise phase uses BLAS-3 and can be parallelised, the solve + // phase uses BLAS-2 and cannot be parallelised. To account for this, the // factorisation effort is multiplied by a coefficient < 1, estimated // empirically. double alpha = 1.0 / 112.0; double ratio = alpha * fact_effort / solv_effort; - // At each ipm iteration, there are up to (1+k) directions computed, where k - // is the number of correctors. Each direction requires up (1+f) solves, + // At each ipm iteration, there are up to (1+k) directions computed, where + // k is the number of correctors. Each direction requires up (1+f) solves, // where f is the number of refinement steps. So, up to (1+k)(1+f) solves // are performed per iteration. However, not all refinement steps are used // all the time, so use f/2. // Therefore, we want (1+k)(1+f/2) < ratio. - double thresh = ratio / (1.0 + kMaxIterRefine / 2.0) - 1; + double thresh = ratio / (1.0 + kRefineMaxIter / 2.0) - 1; info_.correctors = std::floor(thresh); info_.correctors = std::max(info_.correctors, (Int)1); diff --git a/highs/ipm/hipo/ipm/Solver.h b/highs/ipm/hipo/ipm/Solver.h index b6fbd275702..4c697a9f291 100644 --- a/highs/ipm/hipo/ipm/Solver.h +++ b/highs/ipm/hipo/ipm/Solver.h @@ -327,6 +327,8 @@ class Solver { isFailure initialiseLinearSolver(); isSuccess switchToMultifrontal(); + + void chooseAllowedParallelism(const HighsOptions& highs_options); }; } // namespace hipo diff --git a/highs/ipm/hipo/ipm/UpLookingSolver.cpp b/highs/ipm/hipo/ipm/UpLookingSolver.cpp index 6a5ba38299f..b31b4df9402 100644 --- a/highs/ipm/hipo/ipm/UpLookingSolver.cpp +++ b/highs/ipm/hipo/ipm/UpLookingSolver.cpp @@ -92,7 +92,7 @@ void UpLookingSolver::factor(const std::vector& ptr, // A must be upper triangular. // L is computed as lower triangular. The diagonal of L is used to store D^-1. - std::vector mark(n_, false); + std::vector mark(n_, false); std::vector stack(n_); Int top = 0; std::vector revpattern(n_); diff --git a/highs/ipm/ipx/ipx_config.h b/highs/ipm/ipx/ipx_config.h index af19dfd0780..8e08a115be3 100644 --- a/highs/ipm/ipx/ipx_config.h +++ b/highs/ipm/ipx/ipx_config.h @@ -3,7 +3,7 @@ #include -#include "util/HighsInt.h" +#include "util/HighsType.h" typedef HighsInt ipxint; #endif /* IPX_CONFIG_H_ */ diff --git a/highs/ipm/ipx/lu_factorization.cc b/highs/ipm/ipx/lu_factorization.cc index 71408501b98..7958f8156d7 100644 --- a/highs/ipm/ipx/lu_factorization.cc +++ b/highs/ipm/ipx/lu_factorization.cc @@ -15,7 +15,7 @@ static SparseMatrix PermutedMatrix(const Int* Bbegin, const Int* Bend, const std::vector& dependent_cols) { Int dim = rowperm.size(); std::vector permuted_row = InversePerm(rowperm); - std::vector dependent(dim, false); + std::vector dependent(dim, false); for (Int k : dependent_cols) dependent[k] = true; diff --git a/highs/ipm/ipx/maxvolume.cc b/highs/ipm/ipx/maxvolume.cc index 0bfed80e5a0..0735545e119 100644 --- a/highs/ipm/ipx/maxvolume.cc +++ b/highs/ipm/ipx/maxvolume.cc @@ -97,7 +97,7 @@ struct Maxvolume::Slice { lhs(m), row(n+m), work(m) {} Vector colscale; Vector invscale_basic; - std::vector tblrow_used; + std::vector tblrow_used; Vector colweights; IndexedVector lhs, row; Vector work; @@ -209,7 +209,7 @@ Int Maxvolume::Driver(Basis& basis, Slice& slice) { Vector& colscale = slice.colscale; Vector& invscale_basic = slice.invscale_basic; - const std::vector& tblrow_used = slice.tblrow_used; + const std::vector& tblrow_used = slice.tblrow_used; Vector& colweights = slice.colweights; IndexedVector& lhs = slice.lhs; IndexedVector& row = slice.row; diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index 846e2762fbf..4449a734593 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -15,7 +15,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" const std::string kHighsCopyrightStatement = "Copyright (c) 2026 under MIT licence terms"; @@ -460,4 +460,22 @@ enum PdlpRestartStrategy { kPdlpRestartStrategyMax = kPdlpRestartStrategyHalpern }; +namespace hipo { +enum class ParallelTechnique { + kMin = 0, + kAnalyse = kMin, + kOrderNE, + kOrderAS, + kNEStruct, + kNEValues, + kTree, + kNode, + kForwardSolve, + kDiagonalSolve, + kBackwardSolve, + kCount, + kMaxSum = (1 << kCount) - 1 +}; +} + #endif /* LP_DATA_HCONST_H_ */ diff --git a/highs/lp_data/HStruct.h b/highs/lp_data/HStruct.h index 13147962bee..bd6bd74ceca 100644 --- a/highs/lp_data/HStruct.h +++ b/highs/lp_data/HStruct.h @@ -179,7 +179,7 @@ struct HighsProfiling { HighsInt num_profiling_clock_ = -1; std::vector name; // These vectors are over threads - std::vector submip; + std::vector submip; std::vector record; std::vector submip_record; bool initialized = false; diff --git a/highs/lp_data/Highs.cpp b/highs/lp_data/Highs.cpp index 9f379c359f3..fd912246ce2 100644 --- a/highs/lp_data/Highs.cpp +++ b/highs/lp_data/Highs.cpp @@ -1587,7 +1587,7 @@ HighsStatus Highs::calledOptimizeModel() { time += timer_.read(timer_.solve_clock); }; - const bool unconstrained_lp = incumbent_lp.a_matrix_.numNz() == 0; + const bool unconstrained_lp = incumbent_lp.numNz() == 0; assert(incumbent_lp.num_row_ || unconstrained_lp); const bool has_basis = basis_.useful; if (has_basis) { @@ -1599,12 +1599,10 @@ HighsStatus Highs::calledOptimizeModel() { if (basis_.valid) assert(basis_.useful); const bool without_presolve = options_.presolve == kHighsOffString; - if ((unconstrained_lp || has_basis || without_presolve) && - solver_will_use_basis) { - // There is a valid basis for the problem, presolve is off, or LP - // has no constraint matrix, and the solver will use the basis - // (otherwise it's better to use presolve, if it's not switched - // off) + if ((has_basis && solver_will_use_basis) || without_presolve || + unconstrained_lp) { + // There is a valid basis for the problem and the solver will use + // it, or presolve is off, or LP has no constraint matrix // // Determine a coherent message about how the LP is being solved std::stringstream lp_solve_ss; @@ -1630,6 +1628,7 @@ HighsStatus Highs::calledOptimizeModel() { timedSolveLp(incumbent_lp, lp_solve, this_solve_original_lp_time); return_status = interpretCallStatus(options_.log_options, call_status, return_status, "callSolveLp"); + this->run_data_.solve_time = this_solve_original_lp_time; if (return_status == HighsStatus::kError) return returnFromOptimizeModel(return_status, undo_mods); } else { @@ -2686,7 +2685,7 @@ HighsStatus Highs::setSolution(const HighsInt num_entries, if (model_.lp_.num_col_ == 0) return return_status; // Warn about duplicates in index HighsInt num_duplicates = 0; - std::vector is_set; + std::vector is_set; is_set.assign(model_.lp_.num_col_, false); const HighsInt to_ix = packed ? num_entries : model_.lp_.num_col_; for (HighsInt iX = 0; iX < to_ix; iX++) { @@ -3968,8 +3967,8 @@ HighsPresolveStatus Highs::runPresolve(const bool force_lp_presolve, original_lp.num_col_ - reduced_lp.num_col_; presolve_.info_.n_rows_removed = original_lp.num_row_ - reduced_lp.num_row_; - presolve_.info_.n_nnz_removed = (HighsInt)original_lp.a_matrix_.numNz() - - (HighsInt)reduced_lp.a_matrix_.numNz(); + presolve_.info_.n_nnz_removed = + (HighsInt)original_lp.numNz() - (HighsInt)reduced_lp.numNz(); // Clear any scaling information inherited by the reduced LP reduced_lp.clearScale(); assert(lpDimensionsOk("RunPresolve: reduced_lp", reduced_lp, @@ -3979,7 +3978,7 @@ HighsPresolveStatus Highs::runPresolve(const bool force_lp_presolve, case HighsPresolveStatus::kReducedToEmpty: { presolve_.info_.n_cols_removed = original_lp.num_col_; presolve_.info_.n_rows_removed = original_lp.num_row_; - presolve_.info_.n_nnz_removed = (HighsInt)original_lp.a_matrix_.numNz(); + presolve_.info_.n_nnz_removed = (HighsInt)original_lp.numNz(); break; } default: @@ -4000,9 +3999,10 @@ HighsPostsolveStatus Highs::runPostsolve() { return HighsPostsolveStatus::kNoPrimalSolutionError; const bool have_dual_solution = presolve_.data_.recovered_solution_.dual_valid; - presolve_.data_.postSolveStack.undo(options_, - presolve_.data_.recovered_solution_, - presolve_.data_.recovered_basis_); + const HighsInt report_3040_col = -21792; + presolve_.data_.postSolveStack.undo( + options_, presolve_.data_.recovered_solution_, + presolve_.data_.recovered_basis_, report_3040_col); // Compute the row activities assert(model_.lp_.a_matrix_.isColwise()); calculateRowValuesQuad(model_.lp_, presolve_.data_.recovered_solution_); diff --git a/highs/lp_data/HighsCallback.cpp b/highs/lp_data/HighsCallback.cpp index 4d7965a49e0..2b3b202e693 100644 --- a/highs/lp_data/HighsCallback.cpp +++ b/highs/lp_data/HighsCallback.cpp @@ -196,7 +196,7 @@ HighsStatus HighsCallbackInput::setSolution(HighsInt num_entries, HighsStatus return_status = HighsStatus::kOk; HighsInt num_duplicates = 0; - std::vector is_set(lp.num_col_, false); + std::vector is_set(lp.num_col_, false); for (HighsInt iX = 0; iX < num_entries; iX++) { HighsInt iCol = index[iX]; diff --git a/highs/lp_data/HighsCallback.h b/highs/lp_data/HighsCallback.h index 6b350dbea9d..15b3f6f46a7 100644 --- a/highs/lp_data/HighsCallback.h +++ b/highs/lp_data/HighsCallback.h @@ -92,7 +92,7 @@ struct HighsCallback { HighsCCallbackType c_callback = nullptr; void* user_callback_data = nullptr; Highs* highs = nullptr; - std::vector active; + std::vector active; HighsCallbackOutput data_out; HighsCallbackInput data_in; bool callbackActive(const int callback_type); diff --git a/highs/lp_data/HighsCallbackStruct.h b/highs/lp_data/HighsCallbackStruct.h index f30c8be2867..92954214b55 100644 --- a/highs/lp_data/HighsCallbackStruct.h +++ b/highs/lp_data/HighsCallbackStruct.h @@ -11,7 +11,7 @@ #ifndef LP_DATA_HIGHSCALLBACKSTRUCT_H_ #define LP_DATA_HIGHSCALLBACKSTRUCT_H_ -#include "util/HighsInt.h" +#include "util/HighsType.h" #ifdef __cplusplus extern "C" { diff --git a/highs/lp_data/HighsInterface.cpp b/highs/lp_data/HighsInterface.cpp index 294f309e472..1b96811cb0b 100644 --- a/highs/lp_data/HighsInterface.cpp +++ b/highs/lp_data/HighsInterface.cpp @@ -66,14 +66,12 @@ void Highs::reportModelStats() const { const HighsInt a_num_nz = lp.a_matrix_.numNz(); const HighsInt q_num_nz = hessian.dim_ > 0 ? hessian.numNz() : 0; if (*log_options.log_dev_level) { - highsLogDev(log_options, HighsLogType::kInfo, "%4s : %s\n", + highsLogDev(log_options, HighsLogType::kInfo, "%-4s : %s\n", problem_type.c_str(), lp.model_name_.c_str()); highsLogDev(log_options, HighsLogType::kInfo, - "Row%s : %" HIGHSINT_FORMAT "\n", - lp.num_row_ == 1 ? "" : "s", lp.num_row_); + "Rows : %" HIGHSINT_FORMAT "\n", lp.num_row_); highsLogDev(log_options, HighsLogType::kInfo, - "Col%s : %" HIGHSINT_FORMAT "\n", - lp.num_col_ == 1 ? "" : "s", lp.num_col_); + "Cols : %" HIGHSINT_FORMAT "\n", lp.num_col_); if (q_num_nz) { highsLogDev(log_options, HighsLogType::kInfo, "Matrix Nz : %" HIGHSINT_FORMAT "\n", a_num_nz); @@ -81,8 +79,7 @@ void Highs::reportModelStats() const { "Hessian Nz: %" HIGHSINT_FORMAT "\n", q_num_nz); } else { highsLogDev(log_options, HighsLogType::kInfo, - "Nonzero%s : %" HIGHSINT_FORMAT "\n", - a_num_nz == 1 ? "" : "s", a_num_nz); + "Nonzeros : %" HIGHSINT_FORMAT "\n", a_num_nz); } if (num_integer) highsLogDev(log_options, HighsLogType::kInfo, @@ -100,26 +97,28 @@ void Highs::reportModelStats() const { stats_line << problem_type; if (lp.model_name_.length()) stats_line << " " << lp.model_name_; stats_line << " has " << lp.num_row_ << " row" - << (lp.num_row_ == 1 ? "" : "s") << "; " << lp.num_col_ << " col" - << (lp.num_col_ == 1 ? "" : "s"); + << highsIntToPlural(lp.num_row_) << "; " << lp.num_col_ << " col" + << highsIntToPlural(lp.num_col_); if (q_num_nz) { stats_line << "; " << a_num_nz << " matrix nonzero" - << (a_num_nz == 1 ? "" : "s"); + << highsIntToPlural(a_num_nz); stats_line << "; " << q_num_nz << " Hessian nonzero" - << (q_num_nz == 1 ? "" : "s"); + << highsIntToPlural(q_num_nz); } else { stats_line << "; " << a_num_nz << " nonzero" - << (a_num_nz == 1 ? "" : "s"); + << highsIntToPlural(a_num_nz); } if (hessian.isOracle()) stats_line << "; Hessian as oracle"; if (num_integer) stats_line << "; " << num_integer << " integer variable" - << (a_num_nz == 1 ? "" : "s") << " (" << num_binary + << highsIntToPlural(num_integer) << " (" << num_binary << " binary)"; if (num_semi_continuous) - stats_line << "; " << num_semi_continuous << " semi-continuous variables"; + stats_line << "; " << num_semi_continuous << " semi-continuous variable" + << highsIntToPlural(num_semi_continuous); if (num_semi_integer) - stats_line << "; " << num_semi_integer << " semi-integer variables"; + stats_line << "; " << num_semi_integer << " semi-integer variable" + << highsIntToPlural(num_semi_integer); highsLogUser(log_options, HighsLogType::kInfo, "%s\n", stats_line.str().c_str()); } @@ -1836,7 +1835,7 @@ HighsStatus Highs::getRangingInterface() { HighsStatus Highs::getIisInterfaceReturn( const HighsStatus return_status, const HighsOptions& original_options, - const std::vector& original_callback_active) { + const std::vector& original_callback_active) { // Restore options and callbacks this->options_ = original_options; for (int i = kCallbackMin; i <= kCallbackMax; i++) { @@ -1979,7 +1978,7 @@ HighsStatus Highs::getIisInterface() { HighsOptions original_options = this->options_; // Save original active callbacks and disable all except for // kCallbackLogging and kCallbackSimplexInterrupt - std::vector original_callback_active = callback_.active; + std::vector original_callback_active = callback_.active; for (int i = kCallbackMin; i <= kCallbackMax; i++) { if (i != kCallbackLogging && i != kCallbackSimplexInterrupt && callback_.active[i]) @@ -2265,8 +2264,8 @@ HighsStatus Highs::elasticityFilter(const double global_lower_penalty, // bound_of_row_of_ecol_is_lower so that the results can be interpreted std::vector col_of_ecol; std::vector row_of_ecol; - std::vector bound_of_row_of_ecol_is_lower; - std::vector bound_of_col_of_ecol_is_lower; + std::vector bound_of_row_of_ecol_is_lower; + std::vector bound_of_col_of_ecol_is_lower; std::vector erow_lower; std::vector erow_upper; std::vector erow_start; @@ -2774,7 +2773,7 @@ HighsStatus Highs::elasticityFilter(const double global_lower_penalty, in_row_index[iis.row_index_[iX]] = iX; // Determine the columns with nonzeros in the row subset - std::vector nonzero_in_row_index(original_num_col, false); + std::vector nonzero_in_row_index(original_num_col, false); if (lp.a_matrix_.isColwise()) { for (HighsInt iCol = 0; iCol < original_num_col; iCol++) { for (HighsInt iEl = lp.a_matrix_.start_[iCol]; @@ -4486,8 +4485,8 @@ void Highs::reportProfiling() const { } const double num_threads_used = used_thread.size(); std::stringstream ss; - std::vector mip_used_sub_solver(kToSubSolver, false); - std::vector submip_used_sub_solver(kToSubSolver, false); + std::vector mip_used_sub_solver(kToSubSolver, false); + std::vector submip_used_sub_solver(kToSubSolver, false); const HighsInt to_k = max_sumip_time > 0 ? 2 : 1; const std::vector& name = this->profiling_->name; double sum_sum_mip_sub_solve_time = 0; @@ -4510,7 +4509,7 @@ void Highs::reportProfiling() const { if (ideal_time <= 0) continue; const std::vector& record = k == 0 ? this->profiling_->record : this->profiling_->submip_record; - std::vector& used_sub_solver = + std::vector& used_sub_solver = k == 0 ? mip_used_sub_solver : submip_used_sub_solver; const std::vector& num_call = record[thread_num].num_call; const std::vector& run_time = record[thread_num].run_time; @@ -4580,7 +4579,7 @@ void Highs::reportProfiling() const { } highsLogUser(options_.log_options, HighsLogType::kInfo, "%s\n", ss.str().c_str()); - std::vector& used_sub_solver = + std::vector& used_sub_solver = k == 0 ? mip_used_sub_solver : submip_used_sub_solver; const std::vector& record = k == 0 ? this->profiling_->record : this->profiling_->submip_record; diff --git a/highs/lp_data/HighsLp.h b/highs/lp_data/HighsLp.h index 73b48b77d00..a9200075d04 100644 --- a/highs/lp_data/HighsLp.h +++ b/highs/lp_data/HighsLp.h @@ -65,6 +65,7 @@ class HighsLp { bool equalNames(const HighsLp& lp) const; bool equalScaling(const HighsLp& lp) const; bool isMip() const; + HighsInt numNz() const { return this->a_matrix_.numNz(); } bool hasSemiVariables() const; bool hasInfiniteCost(const double infinite_cost) const; bool hasMods() const; diff --git a/highs/lp_data/HighsLpUtils.cpp b/highs/lp_data/HighsLpUtils.cpp index bab8d6879ab..01209283dfc 100644 --- a/highs/lp_data/HighsLpUtils.cpp +++ b/highs/lp_data/HighsLpUtils.cpp @@ -77,7 +77,7 @@ HighsStatus assessLp(HighsLp& lp, const HighsOptions& options) { // If the LP has no columns the matrix must be empty and there is // nothing left to test if (lp.num_col_ == 0) { - assert(!lp.a_matrix_.numNz()); + assert(!lp.numNz()); return HighsStatus::kOk; } // From here, any LP has lp.num_col_ > 0 and lp.a_matrix_.start_[lp.num_col_] @@ -95,7 +95,7 @@ HighsStatus assessLp(HighsLp& lp, const HighsOptions& options) { if (return_status == HighsStatus::kError) return return_status; // If entries have been removed from the matrix, resize the index // and value vectors to prevent bug in presolve - HighsInt lp_num_nz = lp.a_matrix_.numNz(); + HighsInt lp_num_nz = lp.numNz(); if ((HighsInt)lp.a_matrix_.index_.size() > lp_num_nz) lp.a_matrix_.index_.resize(lp_num_nz); if ((HighsInt)lp.a_matrix_.value_.size() > lp_num_nz) @@ -3154,7 +3154,7 @@ void reportPresolveReductions(const HighsLogOptions& log_options, const HighsLp& lp, const HighsLp& presolved_lp) { const HighsInt num_col_from = lp.num_col_; const HighsInt num_row_from = lp.num_row_; - const HighsInt num_nz_from = lp.a_matrix_.numNz(); + const HighsInt num_nz_from = lp.numNz(); HighsInt num_col_to = 0; HighsInt num_row_to = 0; HighsInt num_nz_to = 0; @@ -3176,7 +3176,7 @@ void reportPresolveReductions(const HighsLogOptions& log_options, case HighsPresolveStatus::kTimeout: { num_col_to = presolved_lp.num_col_; num_row_to = presolved_lp.num_row_; - num_nz_to = presolved_lp.a_matrix_.numNz(); + num_nz_to = presolved_lp.numNz(); message = presolve_status == HighsPresolveStatus::kTimeout ? "- Timeout" : ""; break; diff --git a/highs/lp_data/HighsModelUtils.cpp b/highs/lp_data/HighsModelUtils.cpp index 882e0e95004..fc2f0d9d1b2 100644 --- a/highs/lp_data/HighsModelUtils.cpp +++ b/highs/lp_data/HighsModelUtils.cpp @@ -598,10 +598,10 @@ void writeGlpsolSolution(FILE* file, const HighsOptions& options, assert(lp.row_names_.size() == static_cast(lp.num_row_)); // Determine number of nonzeros including the objective function // and, hence, determine whether there is an objective function - HighsInt num_nz = lp.a_matrix_.numNz(); + HighsInt num_nz = lp.numNz(); for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) if (lp.col_cost_[iCol]) num_nz++; - const bool empty_cost_row = num_nz == lp.a_matrix_.numNz(); + const bool empty_cost_row = num_nz == lp.numNz(); const bool has_objective = !empty_cost_row || model.hessian_.dim_; // Writes the solution using the GLPK raw style (defined in // api/wrsol.c) or pretty style (defined in api/prsol.c) @@ -702,7 +702,7 @@ void writeGlpsolSolution(FILE* file, const HighsOptions& options, const HighsInt glpsol_num_row = num_row + delta_num_row; // If the cost row isn't reported, then the number of nonzeros is // just the number in the constraint matrix - if (cost_row_location <= 0) num_nz = lp.a_matrix_.numNz(); + if (cost_row_location <= 0) num_nz = lp.numNz(); // Record the discrete nature of the model HighsInt num_integer = 0; HighsInt num_binary = 0; diff --git a/highs/lp_data/HighsOptions.cpp b/highs/lp_data/HighsOptions.cpp index 9d86e4753c0..66953972b80 100644 --- a/highs/lp_data/HighsOptions.cpp +++ b/highs/lp_data/HighsOptions.cpp @@ -170,13 +170,14 @@ bool optionMipIpmSolverOk(const HighsLogOptions& report_log_options, bool optionHipoParallelTypeOk(const HighsLogOptions& report_log_options, const string& value) { if (value == kHipoNodeString || value == kHipoTreeString || - value == kHipoBothString) + value == kHipoBothString || value == kHighsChooseString) return true; - highsLogUser( - report_log_options, HighsLogType::kError, - "Value \"%s\" for %s option is not one of \"%s\", \"%s\" or \"%s\"\n", - value.c_str(), kHipoParallelString.c_str(), kHipoTreeString.c_str(), - kHipoNodeString.c_str(), kHipoBothString.c_str()); + highsLogUser(report_log_options, HighsLogType::kError, + "Value \"%s\" for %s option is not one of \"%s\", \"%s\", " + "\"%s\" or \"%s\"\n", + value.c_str(), kHipoParallelString.c_str(), + kHipoTreeString.c_str(), kHipoNodeString.c_str(), + kHipoBothString.c_str(), kHighsChooseString.c_str()); return false; } @@ -510,6 +511,9 @@ OptionStatus checkOptionValue(const HighsLogOptions& report_log_options, } else if (option.name == kHipoFactorString) { if (!optionHipoFactorOk(report_log_options, value)) return OptionStatus::kIllegalValue; + } else if (option.name == kPresolveLightString) { + if (!optionOffChooseOnOk(report_log_options, option.name, value)) + return OptionStatus::kIllegalValue; } return OptionStatus::kOk; } diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index 56e0a549856..10c21337587 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -290,6 +290,7 @@ const string kModelFileString = "model_file"; const string kReadBasisFileString = "read_basis_file"; const string kWriteBasisFileString = "write_basis_file"; const string kPresolveString = "presolve"; +const string kPresolveLightString = "presolve_light"; const string kSolverString = "solver"; const string kParallelString = "parallel"; const string kThreadsString = "threads"; @@ -405,6 +406,8 @@ struct HighsOptionsStruct { std::string hipo_ordering; std::string hipo_factor; HighsInt hipo_block_size; + HighsInt hipo_parallel_force; + HighsInt hipo_parallel_forbid; // Options for PDLP solver HighsInt pdlp_features_off; @@ -439,6 +442,7 @@ struct HighsOptionsStruct { bool lp_presolve_requires_basis_postsolve; bool mps_parser_type_free; bool use_warm_start; + std::string presolve_light; bool write_matrix_image; bool write_hessian_image; HighsInt keep_n_rows; @@ -588,6 +592,8 @@ struct HighsOptionsStruct { hipo_ordering(""), hipo_factor(""), hipo_block_size(0), + hipo_parallel_force(0), + hipo_parallel_forbid(0), pdlp_features_off(0), pdlp_iteration_limit(0), pdlp_scaling_mode(0), @@ -612,6 +618,7 @@ struct HighsOptionsStruct { lp_presolve_requires_basis_postsolve(false), mps_parser_type_free(false), use_warm_start(true), + presolve_light(""), write_matrix_image(false), write_hessian_image(false), keep_n_rows(0), @@ -1329,18 +1336,16 @@ class HighsOptions : public HighsOptionsStruct { advanced, &hipo_system, kHighsChooseString); records.push_back(record_string); - record_string = - new OptionRecordString(kHipoParallelString, - "HiPO parallelism: \"tree\", " - "\"node\" or \"both\"", - advanced, &hipo_parallel_type, kHipoBothString); + record_string = new OptionRecordString( + kHipoParallelString, + "HiPO parallelism: \"tree\", \"node\", \"both\" or \"choose\"", + advanced, &hipo_parallel_type, kHighsChooseString); records.push_back(record_string); - record_string = - new OptionRecordString(kHipoOrderingString, - "HiPO matrix reordering: \"choose\", \"metis\", " - "\"amd\" or \"rcm\"", - advanced, &hipo_ordering, kHighsChooseString); + record_string = new OptionRecordString( + kHipoOrderingString, + "HiPO matrix reordering: \"choose\", \"metis\", \"amd\" or \"rcm\"", + advanced, &hipo_ordering, kHighsChooseString); records.push_back(record_string); record_string = new OptionRecordString( @@ -1355,6 +1360,19 @@ class HighsOptions : public HighsOptionsStruct { advanced, &hipo_block_size, 0, 128, kHighsIInf); records.push_back(record_int); + record_int = new OptionRecordInt( + "hipo_parallel_force", "Bit mask to force parallel techniques in HiPO", + advanced, &hipo_parallel_force, 0, 0, + static_cast(hipo::ParallelTechnique::kMaxSum)); + records.push_back(record_int); + + record_int = + new OptionRecordInt("hipo_parallel_forbid", + "Bit mask to forbid parallel techniques in HiPO", + advanced, &hipo_parallel_forbid, 0, 0, + static_cast(hipo::ParallelTechnique::kMaxSum)); + records.push_back(record_int); + record_int = new OptionRecordInt( "pdlp_iteration_limit", "Iteration limit for PDLP solver", advanced, &pdlp_iteration_limit, 0, kHighsIInf, kHighsIInf); @@ -1503,6 +1521,12 @@ class HighsOptions : public HighsOptionsStruct { advanced, &use_warm_start, true); records.push_back(record_bool); + record_string = new OptionRecordString( + kPresolveLightString, + "Use only low-cost presolve rules: \"off\", \"choose\" or \"on\"", + advanced, &presolve_light, kHighsChooseString); + records.push_back(record_string); + record_bool = new OptionRecordBool( "write_matrix_image", "Write an image of the constraint matrix to a file", advanced, diff --git a/highs/lp_data/HighsSolve.cpp b/highs/lp_data/HighsSolve.cpp index 39753ba42ef..6d5f546bae7 100644 --- a/highs/lp_data/HighsSolve.cpp +++ b/highs/lp_data/HighsSolve.cpp @@ -80,7 +80,7 @@ HighsStatus solveLp(HighsLpSolverObject& solver_object, } return return_status; }; - if (!solver_object.lp_.num_row_ || solver_object.lp_.a_matrix_.numNz() == 0) { + if (!solver_object.lp_.num_row_ || solver_object.lp_.numNz() == 0) { // LP is unconstrained due to having no rows or a zero constraint // matrix, so solve directly call_status = solveUnconstrainedLp(solver_object); @@ -194,11 +194,11 @@ HighsStatus solveUnconstrainedLp(const HighsOptions& options, const HighsLp& lp, resetModelStatusAndHighsInfo(model_status, highs_info); // Check that the LP really is unconstrained! - assert(lp.num_row_ == 0 || lp.a_matrix_.numNz() == 0); + assert(lp.num_row_ == 0 || lp.numNz() == 0); if (lp.num_row_ > 0) { // LP has rows, but should only be here if the constraint matrix // is zero - if (lp.a_matrix_.numNz() > 0) return HighsStatus::kError; + if (lp.numNz() > 0) return HighsStatus::kError; } highsLogUser(options.log_options, HighsLogType::kInfo, @@ -451,7 +451,7 @@ void assessExcessiveObjectiveBoundScaling(const HighsLogOptions log_options, double min_matrix_value = kHighsInf; double max_matrix_value = -kHighsInf; - const HighsInt num_matrix_nz = lp.a_matrix_.numNz(); + const HighsInt num_matrix_nz = lp.numNz(); for (HighsInt iEl = 0; iEl < num_matrix_nz; iEl++) assessFiniteNonzero(lp.a_matrix_.value_[iEl], min_matrix_value, max_matrix_value); diff --git a/highs/mip/HighsCliqueTable.h b/highs/mip/HighsCliqueTable.h index d05ad3c4839..c9f27e81c9e 100644 --- a/highs/mip/HighsCliqueTable.h +++ b/highs/mip/HighsCliqueTable.h @@ -83,8 +83,8 @@ class HighsCliqueTable { std::vector substitutions; std::vector deletedrows; std::vector> cliqueextensions; - std::vector iscandidate; - std::vector colDeleted; + std::vector iscandidate; + std::vector colDeleted; std::vector cliquehits; std::vector cliquehitinds; diff --git a/highs/mip/HighsConflictPool.h b/highs/mip/HighsConflictPool.h index c13ac077314..8767420d94d 100644 --- a/highs/mip/HighsConflictPool.h +++ b/highs/mip/HighsConflictPool.h @@ -13,7 +13,7 @@ #include #include "mip/HighsDomain.h" -#include "util/HighsInt.h" +#include "util/HighsType.h" class HighsConflictPool { private: diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 40139253a89..5ae328155e5 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -221,12 +221,12 @@ bool HighsCutGeneration::separateLiftedMixedBinaryCover() { HighsInt coversize = cover.size(); std::vector S; S.resize(coversize); - std::vector coverflag; + std::vector coverflag; coverflag.resize(rowlen); if (coversize == 0) return false; - for (HighsInt i = 0; i != coversize; ++i) coverflag[cover[i]] = 1; + for (HighsInt i = 0; i != coversize; ++i) coverflag[cover[i]] = true; pdqsort_branchless(cover.begin(), cover.end(), [&](HighsInt a, HighsInt b) { return vals[a] > vals[b]; }); @@ -286,9 +286,9 @@ bool HighsCutGeneration::separateLiftedMixedIntegerCover() { HighsInt l = -1; - std::vector coverflag; + std::vector coverflag; coverflag.resize(rowlen); - for (HighsInt i : cover) coverflag[i] = 1; + for (HighsInt i : cover) coverflag[i] = true; auto comp = [&](HighsInt a, HighsInt b) { return vals[a] > vals[b]; }; pdqsort_branchless(cover.begin(), cover.end(), comp); @@ -1209,7 +1209,7 @@ bool HighsCutGeneration::generateConflict(const HighsDomain& localdomain, lpRelaxation.getMipSolver().mipdata_->debugSolution.checkCut( inds, vals, rowlen, proofrhs); - complementation.assign(rowlen, 0); + complementation.assign(rowlen, false); upper.resize(rowlen); solval.resize(rowlen); @@ -1227,12 +1227,12 @@ bool HighsCutGeneration::generateConflict(const HighsDomain& localdomain, if (vals[i] < 0 && globaldom.col_upper_[col] != kHighsInf) { rhs -= globaldom.col_upper_[col] * vals[i]; vals[i] = -vals[i]; - complementation[i] = 1; + complementation[i] = true; solval[i] = globaldom.col_upper_[col] - solval[i]; } else { rhs -= globaldom.col_lower_[col] * vals[i]; - complementation[i] = 0; + complementation[i] = false; solval[i] = solval[i] - globaldom.col_lower_[col]; } @@ -1353,7 +1353,7 @@ void HighsCutGeneration::flipComplementation(HighsInt index) { assert(upper[index] != kHighsInf); // flip complementation - complementation[index] = 1 - complementation[index]; + complementation[index] = !complementation[index]; solval[index] = upper[index] - solval[index]; rhs -= upper[index] * vals[index]; vals[index] = -vals[index]; diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index 39e728db6cc..b249958a952 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -18,8 +18,8 @@ #include #include "util/HighsCDouble.h" -#include "util/HighsInt.h" #include "util/HighsRandom.h" +#include "util/HighsType.h" class HighsLpRelaxation; class HighsTransformedLp; @@ -38,8 +38,8 @@ class HighsCutGeneration { HighsCDouble lambda; std::vector upper; std::vector solval; - std::vector complementation; - std::vector isintegral; + std::vector complementation; + std::vector isintegral; const double feastol; const double epsilon; @@ -56,7 +56,7 @@ class HighsCutGeneration { std::vector tmpVals; std::vector tmpInds; - std::vector tmpComplementation; + std::vector tmpComplementation; std::vector tmpSolval; bool determineCover(bool lpSol = true); diff --git a/highs/mip/HighsCutPool.h b/highs/mip/HighsCutPool.h index 72068cc1f21..e9f988cf305 100644 --- a/highs/mip/HighsCutPool.h +++ b/highs/mip/HighsCutPool.h @@ -58,11 +58,11 @@ class HighsCutPool { std::vector ages_; std::deque> numLps_; std::deque> - ageResetWhileLocked_; // Was the cut propagated? - std::vector hasSynced_; // Has the cut been globally synced? + ageResetWhileLocked_; // Was the cut propagated? + std::vector hasSynced_; // Has the cut been globally synced? std::vector rownormalization_; std::vector maxabscoef_; - std::vector rowintegral; + std::vector rowintegral; std::unordered_multimap hashToCutMap; std::vector propagationDomains; std::set> propRows; @@ -159,7 +159,7 @@ class HighsCutPool { void separateLpCutsAfterRestart(HighsCutSet& cutset); - bool cutIsIntegral(HighsInt cut) const { return (rowintegral[cut] != 0); } + bool cutIsIntegral(HighsInt cut) const { return rowintegral[cut]; } HighsInt getNumCuts() const { return matrix_.getNumRows() - matrix_.getNumDelRows(); diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index a7c0b5f9b2b..7bbcc3ef174 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -1946,7 +1946,7 @@ void HighsDomain::markPropagate(HighsInt row) { if (proplower || propupper) { propagateinds_.push_back(row); - propagateflags_[row] = 1; + propagateflags_[row] = true; } } } @@ -1993,7 +1993,7 @@ double HighsDomain::doChangeBound(const HighsDomainChange& boundchg) { updateActivityLbChange(boundchg.column, oldbound, boundchg.boundval); if (!isChangedCol(boundchg.column)) { - changedcolsflags_[boundchg.column] = 1; + changedcolsflags_[boundchg.column] = true; changedcols_.push_back(boundchg.column); } } @@ -2005,7 +2005,7 @@ double HighsDomain::doChangeBound(const HighsDomainChange& boundchg) { updateActivityUbChange(boundchg.column, oldbound, boundchg.boundval); if (!isChangedCol(boundchg.column)) { - changedcolsflags_[boundchg.column] = 1; + changedcolsflags_[boundchg.column] = true; changedcols_.push_back(boundchg.column); } } @@ -2410,7 +2410,7 @@ bool HighsDomain::propagate() { HighsInt numproprows = static_cast(propagateinds.size()); for (HighsInt i = 0; i != numproprows; ++i) { HighsInt row = propagateinds[i]; - propagateflags_[row] = 0; + propagateflags_[row] = false; } if (!infeasible_) { diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 02981bc2218..92747afd954 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -297,7 +297,7 @@ class HighsDomain { void recomputeCapacityThreshold(); }; - std::vector changedcolsflags_; + std::vector changedcolsflags_; std::vector changedcols_; std::vector> propRowNumChangedBounds_; @@ -311,7 +311,7 @@ class HighsDomain { std::vector activitymininf_; std::vector activitymaxinf_; std::vector capacityThreshold_; - std::vector propagateflags_; + std::vector propagateflags_; std::vector propagateinds_; ObjectivePropagation objProp_; @@ -446,7 +446,7 @@ class HighsDomain { void addConflictPool(HighsConflictPool& conflictPool); void clearChangedCols() { - for (HighsInt i : changedcols_) changedcolsflags_[i] = 0; + for (HighsInt i : changedcols_) changedcolsflags_[i] = false; changedcols_.clear(); } @@ -462,12 +462,12 @@ class HighsDomain { void clearChangedCols(size_t start) { for (size_t i = start; i != changedcols_.size(); ++i) - changedcolsflags_[changedcols_[i]] = 0; + changedcolsflags_[changedcols_[i]] = false; changedcols_.resize(start); } - bool isChangedCol(HighsInt col) const { return changedcolsflags_[col] != 0; } + bool isChangedCol(HighsInt col) const { return changedcolsflags_[col]; } void markPropagate(HighsInt row); diff --git a/highs/mip/HighsDomainChange.h b/highs/mip/HighsDomainChange.h index 6e84708b30c..39282302be0 100644 --- a/highs/mip/HighsDomainChange.h +++ b/highs/mip/HighsDomainChange.h @@ -9,7 +9,7 @@ #ifndef HIGHS_DOMAIN_CHANGE_H_ #define HIGHS_DOMAIN_CHANGE_H_ -#include "util/HighsInt.h" +#include "util/HighsType.h" enum class HighsBoundType { kLower, kUpper }; diff --git a/highs/mip/HighsDynamicRowMatrix.h b/highs/mip/HighsDynamicRowMatrix.h index aa3bb7b2550..510b041edc7 100644 --- a/highs/mip/HighsDynamicRowMatrix.h +++ b/highs/mip/HighsDynamicRowMatrix.h @@ -12,7 +12,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" class HighsDynamicRowMatrix { private: @@ -35,7 +35,7 @@ class HighsDynamicRowMatrix { std::vector AheadPos_; std::vector AheadNeg_; - std::vector colsLinked; + std::vector colsLinked; /// vector of column sizes @@ -49,9 +49,7 @@ class HighsDynamicRowMatrix { public: HighsDynamicRowMatrix(HighsInt ncols); - bool columnsLinked(HighsInt rowindex) const { - return (colsLinked[rowindex] != 0); - } + bool columnsLinked(HighsInt rowindex) const { return colsLinked[rowindex]; } void unlinkColumns(HighsInt rowindex); diff --git a/highs/mip/HighsGFkSolve.h b/highs/mip/HighsGFkSolve.h index c28f8af516c..72a827bd272 100644 --- a/highs/mip/HighsGFkSolve.h +++ b/highs/mip/HighsGFkSolve.h @@ -88,7 +88,7 @@ class HighsGFkSolve { std::vector factorColPerm; std::vector factorRowPerm; std::vector colBasisStatus; - std::vector rowUsed; + std::vector rowUsed; // working memory std::vector iterstack; @@ -198,7 +198,7 @@ class HighsGFkSolve { factorColPerm.reserve(maxPivot); factorRowPerm.reserve(maxPivot); colBasisStatus.assign(numCol, 0); - rowUsed.assign(numRow, 0); + rowUsed.assign(numRow, false); HighsInt numPivot = 0; while (!pqueue.empty()) { @@ -285,7 +285,7 @@ class HighsGFkSolve { factorColPerm.push_back(pivotCol); factorRowPerm.push_back(pivotRow); colBasisStatus[pivotCol] = 1; - rowUsed[pivotRow] = 1; + rowUsed[pivotRow] = true; if (numPivot == maxPivot) break; for (HighsInt i = 0; i != pivotRowLen; ++i) { @@ -321,7 +321,7 @@ class HighsGFkSolve { hasSolution[rhsIndex] = true; for (HighsInt i = 0; i != numRow; ++i) { // if the row was used it is linearly independent - if (rowUsed[i] == 1) continue; + if (rowUsed[i]) continue; // if the row is linearly dependent, the right hand side must be zero, // otherwise no solution exists diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 906c2349c4e..b04cf9a9e1c 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -521,6 +521,11 @@ void HighsImplications::rebuild(HighsInt ncols, nextCleanupCall = mipsolver.numNonzero(); for (HighsInt i = 0; i != oldncols; ++i) { + if (int(i) >= int(orig2reducedcol.size())) { + printf("HighsImplications::rebuild i = %d orig2reducedcol.size = %d\n", + int(i), int(orig2reducedcol.size())); + assert(111 == 345); + } HighsInt newi = orig2reducedcol[i]; if (newi == -1 || diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 72c17868a38..d574685de84 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -56,7 +56,7 @@ class HighsImplications { public: const HighsMipSolver& mipsolver; std::vector substitutions; - std::vector colsubstituted; + std::vector colsubstituted; HighsImplications(const HighsMipSolver& mipsolver) : mipsolver(mipsolver) { nextCleanupCall = mipsolver.numNonzero(); numImplications = 0; diff --git a/highs/mip/HighsLpRelaxation.cpp b/highs/mip/HighsLpRelaxation.cpp index 843b27849b8..c727e79a741 100644 --- a/highs/mip/HighsLpRelaxation.cpp +++ b/highs/mip/HighsLpRelaxation.cpp @@ -118,7 +118,7 @@ bool HighsLpRelaxation::LpRow::isIntegral( case kCutPool: return mipsolver.mipdata_->cutpools[cutpoolindex].cutIsIntegral(index); case kModel: - return (mipsolver.mipdata_->rowintegral[index] != 0); + return mipsolver.mipdata_->rowintegral[index]; }; assert(false); diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index e63c0560025..54ac726d0a6 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -818,15 +818,12 @@ void HighsMipSolverData::runMipPresolve( const HighsInt presolve_reduction_limit) { mipsolver.timer_.start(mipsolver.timer_.presolve_clock); presolve::HPresolve presolve; - if (!presolve.okSetInput(mipsolver, presolve_reduction_limit)) { - mipsolver.modelstatus_ = HighsModelStatus::kMemoryLimit; - presolve_status = HighsPresolveStatus::kOutOfMemory; - } else { - mipsolver.modelstatus_ = presolve.run(postSolveStack); - presolve_status = presolve.getPresolveStatus(); - } + presolve.setInput(mipsolver, presolve_reduction_limit); + mipsolver.modelstatus_ = presolve.run(postSolveStack); + presolve_status = presolve.getPresolveStatus(); mipsolver.timer_.stop(mipsolver.timer_.presolve_clock); + if (presolve_status == HighsPresolveStatus::kOutOfMemory) return; // Report the final presolve reductions unless this is a restart if (mipsolver.options_mip_->presolve != kHighsOffString && numRestarts == 0) reportPresolveReductions(mipsolver.options_mip_->log_options, diff --git a/highs/mip/HighsMipSolverData.h b/highs/mip/HighsMipSolverData.h index 12041d116f7..070d90ef0d1 100644 --- a/highs/mip/HighsMipSolverData.h +++ b/highs/mip/HighsMipSolverData.h @@ -102,7 +102,7 @@ struct HighsMipSolverData { std::vector ARindex_; std::vector ARvalue_; std::vector maxAbsRowCoef; - std::vector rowintegral; + std::vector rowintegral; std::vector uplocks; std::vector downlocks; std::vector integer_cols; diff --git a/highs/mip/HighsModkSeparator.cpp b/highs/mip/HighsModkSeparator.cpp index 19727670bda..b1de06cab74 100644 --- a/highs/mip/HighsModkSeparator.cpp +++ b/highs/mip/HighsModkSeparator.cpp @@ -47,7 +47,7 @@ void HighsModkSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, const HighsMipSolver& mipsolver = lpRelaxation.getMipSolver(); const HighsLp& lp = lpRelaxation.getLp(); - std::vector skipRow(lp.num_row_); + std::vector skipRow(lp.num_row_); // mark all rows that have continuous variables with a nonzero solution value // in the transformed LP to be skipped diff --git a/highs/mip/HighsObjectiveFunction.h b/highs/mip/HighsObjectiveFunction.h index 0598146e327..317142b8328 100644 --- a/highs/mip/HighsObjectiveFunction.h +++ b/highs/mip/HighsObjectiveFunction.h @@ -12,7 +12,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" class HighsCliqueTable; class HighsDomain; diff --git a/highs/mip/HighsPathSeparator.cpp b/highs/mip/HighsPathSeparator.cpp index d4fa7ad8e14..d67bab084f9 100644 --- a/highs/mip/HighsPathSeparator.cpp +++ b/highs/mip/HighsPathSeparator.cpp @@ -400,7 +400,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, std::vector inds; std::vector solval; std::vector upper; - std::vector isIntegral; + std::vector isIntegral; inds.reserve(lp.num_col_ + lp.num_row_); solval.reserve(lp.num_col_ + lp.num_row_); upper.reserve(lp.num_col_ + lp.num_row_); diff --git a/highs/mip/HighsPseudocost.h b/highs/mip/HighsPseudocost.h index 52591c4dea3..4abf2884daf 100644 --- a/highs/mip/HighsPseudocost.h +++ b/highs/mip/HighsPseudocost.h @@ -15,7 +15,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" class HighsMipSolver; namespace presolve { diff --git a/highs/mip/HighsSearch.cpp b/highs/mip/HighsSearch.cpp index 2ab00508239..bfb41fb2fd9 100644 --- a/highs/mip/HighsSearch.cpp +++ b/highs/mip/HighsSearch.cpp @@ -251,8 +251,8 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, std::vector upscore; std::vector downscore; - std::vector upscorereliable; - std::vector downscorereliable; + std::vector upscorereliable; + std::vector downscorereliable; std::vector upbound; std::vector downbound; @@ -264,8 +264,8 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, upbound.resize(numfrac, getCurrentLowerBound()); downbound.resize(numfrac, getCurrentLowerBound()); - upscorereliable.resize(numfrac, 0); - downscorereliable.resize(numfrac, 0); + upscorereliable.resize(numfrac, false); + downscorereliable.resize(numfrac, false); // initialize up and down scores of variables that have a // reliable pseudocost so that they do not get evaluated @@ -680,8 +680,8 @@ HighsInt HighsSearch::selectBranchingCandidate(int64_t maxSbIters, // avoid choosing it as branching candidate if possible downscore[candidate] = 0.0; upscore[candidate] = 0.0; - downscorereliable[candidate] = 1; - upscorereliable[candidate] = 1; + downscorereliable[candidate] = true; + upscorereliable[candidate] = true; markBranchingVarUpReliableAtNode(col); markBranchingVarDownReliableAtNode(col); } diff --git a/highs/mip/HighsSeparator.h b/highs/mip/HighsSeparator.h index 2eb5d60cdcd..89367f51d15 100644 --- a/highs/mip/HighsSeparator.h +++ b/highs/mip/HighsSeparator.h @@ -15,7 +15,7 @@ #include -#include "util/HighsInt.h" +#include "util/HighsType.h" const std::string kImplboundSepaString = "Separation: Implied bounds"; const std::string kCliqueSepaString = "Separation: Clique"; diff --git a/highs/mip/HighsTransformedLp.h b/highs/mip/HighsTransformedLp.h index bb7e929224c..3b26e1eaf0c 100644 --- a/highs/mip/HighsTransformedLp.h +++ b/highs/mip/HighsTransformedLp.h @@ -19,8 +19,8 @@ #include "lp_data/HConst.h" #include "mip/HighsImplications.h" #include "util/HighsCDouble.h" -#include "util/HighsInt.h" #include "util/HighsSparseVectorSum.h" +#include "util/HighsType.h" class HighsLpRelaxation; diff --git a/highs/mip/MipTimer.h b/highs/mip/MipTimer.h index 68bb782a93f..83d65bd33d6 100644 --- a/highs/mip/MipTimer.h +++ b/highs/mip/MipTimer.h @@ -119,7 +119,7 @@ enum iClockMip : int { const HighsInt kNumThreadMipClock = kLastMipClock; -const double tolerance_percent_report = 0.1; +static const double kMipClockTolerancePercentReport = 0.1; inline void initialiseMipProfilingNames(std::vector& name) { assert(name.size() == static_cast(kToMipClock)); @@ -467,7 +467,7 @@ class MipTimer { kMipClockSearch, kMipClockPostsolve}; reportMipClockList("MipLevl1", mip_clock_list, mip_timer_clock, - kMipClockTotal, tolerance_percent_report); + kMipClockTotal, kMipClockTolerancePercentReport); }; void reportMipSolveLpClock(const HighsTimerClock& mip_timer_clock) { @@ -481,20 +481,20 @@ class MipTimer { kMipClockHipoSolveLp, kMipClockIpxSolveLp}; reportMipClockList("MipSlvLp", mip_clock_list, mip_timer_clock, - kMipClockTotal); //, tolerance_percent_report); + kMipClockTotal); //, kMipClockTolerancePercentReport); }; void reportMipSubMipSolveClock(const HighsTimerClock& mip_timer_clock) { const std::vector mip_clock_list{kMipClockSubMipSolve}; reportMipClockList("MipSlvLp", mip_clock_list, mip_timer_clock, - kMipClockTotal); //, tolerance_percent_report); + kMipClockTotal); //, kMipClockTolerancePercentReport); }; void reportMipPresolveClock(const HighsTimerClock& mip_timer_clock) { const std::vector mip_clock_list{kMipClockProbingPresolve, kMipClockEnumerationPresolve}; reportMipClockList("MipPrslv", mip_clock_list, mip_timer_clock, - kMipClockRunPresolve, tolerance_percent_report); + kMipClockRunPresolve, kMipClockTolerancePercentReport); }; void reportAltEvaluateRootNodeClock(const HighsTimerClock& mip_timer_clock) { @@ -503,7 +503,7 @@ class MipTimer { kMipClockEvaluateRootNode2}; reportMipClockList( "AltEvaluateRootNode", mip_clock_list, mip_timer_clock, - kMipClockEvaluateRootNode); //, tolerance_percent_report); + kMipClockEvaluateRootNode); //, kMipClockTolerancePercentReport); }; void reportMipEvaluateRootNodeClock(const HighsTimerClock& mip_timer_clock) { @@ -529,7 +529,7 @@ class MipTimer { }; reportMipClockList( "MipEvaluateRootNode", mip_clock_list, mip_timer_clock, - kMipClockEvaluateRootNode); //, tolerance_percent_report); + kMipClockEvaluateRootNode); //, kMipClockTolerancePercentReport); }; void reportMipRootSeparationClock(const HighsTimerClock& mip_timer_clock) { @@ -538,8 +538,9 @@ class MipTimer { kMipClockRootSeparationFinishAnalyticCentreComputation, kMipClockRootSeparationCentralRounding, kMipClockRootSeparationEvaluateRootLp}; - reportMipClockList("MipRootSeparation", mip_clock_list, mip_timer_clock, - kMipClockRootSeparation); //, tolerance_percent_report); + reportMipClockList( + "MipRootSeparation", mip_clock_list, mip_timer_clock, + kMipClockRootSeparation); //, kMipClockTolerancePercentReport); }; void reportMipSearchClock(const HighsTimerClock& mip_timer_clock) { @@ -551,7 +552,7 @@ class MipTimer { // kMipClock@ }; reportMipClockList("MipSerch", mip_clock_list, mip_timer_clock, - kMipClockSearch, tolerance_percent_report); + kMipClockSearch, kMipClockTolerancePercentReport); }; void reportMipDiveClock(const HighsTimerClock& mip_timer_clock) { @@ -559,7 +560,7 @@ class MipTimer { kMipClockDiveEvaluateNode, kMipClockDivePrimalHeuristics, kMipClockTheDive, kMipClockBacktrackPlunge, kMipClockPerformAging2}; reportMipClockList("MipDive_", mip_clock_list, mip_timer_clock, - kMipClockDive, tolerance_percent_report); + kMipClockDive, kMipClockTolerancePercentReport); }; void reportMipDivePrimalHeuristicsClock( @@ -568,7 +569,7 @@ class MipTimer { kMipClockDiveRandomizedRounding, kMipClockDiveRens, kMipClockDiveRins}; reportMipClockList("MipDivePrimalHeuristics", mip_clock_list, mip_timer_clock, kMipClockDivePrimalHeuristics, - tolerance_percent_report); + kMipClockTolerancePercentReport); }; void reportMipNodeSearchClock(const HighsTimerClock& mip_timer_clock) { @@ -577,8 +578,9 @@ class MipTimer { // kMipClockSearchBacktrack, kMipClockOpenNodesToQueue1, kMipClockEvaluateNode1, kMipClockNodeSearchSeparation}; //, kMipClockStoreBasis}; - reportMipClockList("MipNodeSearch", mip_clock_list, mip_timer_clock, - kMipClockNodeSearch); //, tolerance_percent_report); + reportMipClockList( + "MipNodeSearch", mip_clock_list, mip_timer_clock, + kMipClockNodeSearch); //, kMipClockTolerancePercentReport); }; void reportMipSeparationClock(const HighsTimerClock& mip_timer_clock) { @@ -586,7 +588,7 @@ class MipTimer { kMipClockImplboundSepa, kMipClockCliqueSepa, kMipClockTableauSepa, kMipClockPathAggrSepa, kMipClockModKSepa, kMipClockMachineSchedSepa}; reportMipClockList("MipSeparation", mip_clock_list, mip_timer_clock, - kMipClockTotal); //, tolerance_percent_report); + kMipClockTotal); //, kMipClockTolerancePercentReport); }; void csvMipClock(const std::string model_name, diff --git a/highs/parallel/HighsSplitDeque.h b/highs/parallel/HighsSplitDeque.h index 5811b8bfeda..7dd4e71c616 100644 --- a/highs/parallel/HighsSplitDeque.h +++ b/highs/parallel/HighsSplitDeque.h @@ -21,8 +21,8 @@ #include "parallel/HighsCacheAlign.h" #include "parallel/HighsSpinMutex.h" #include "parallel/HighsTask.h" -#include "util/HighsInt.h" #include "util/HighsRandom.h" +#include "util/HighsType.h" #ifdef __has_feature #if __has_feature(thread_sanitizer) diff --git a/highs/parallel/HighsTaskExecutor.h b/highs/parallel/HighsTaskExecutor.h index f960085770a..96a394fcb78 100644 --- a/highs/parallel/HighsTaskExecutor.h +++ b/highs/parallel/HighsTaskExecutor.h @@ -18,8 +18,8 @@ #include "parallel/HighsCacheAlign.h" #include "parallel/HighsSchedulerConstants.h" #include "parallel/HighsSplitDeque.h" -#include "util/HighsInt.h" #include "util/HighsRandom.h" +#include "util/HighsType.h" class HighsTaskExecutor { public: diff --git a/highs/pdlp/hipdlp/pdhg.hpp b/highs/pdlp/hipdlp/pdhg.hpp index aa3fb4a38f6..cc04f0e6d74 100644 --- a/highs/pdlp/hipdlp/pdhg.hpp +++ b/highs/pdlp/hipdlp/pdhg.hpp @@ -211,7 +211,7 @@ class PDLPSolver { HighsInt sense_origin_ = 1; double unscaled_rhs_norm_ = 0.0; double unscaled_c_norm_ = 0.0; - std::vector is_equality_row_; + std::vector is_equality_row_; std::vector constraint_new_idx_; std::vector constraint_types_; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index c5ff6764b6c..955ec3ec0d9 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -18,6 +18,7 @@ #include "lp_data/HConst.h" #include "lp_data/HStruct.h" #include "lp_data/HighsLpUtils.h" +#include "lp_data/HighsModelUtils.h" #include "lp_data/HighsSolution.h" #include "mip/HighsCliqueTable.h" #include "mip/HighsImplications.h" @@ -25,6 +26,7 @@ #include "mip/HighsObjectiveFunction.h" #include "mip/MipTimer.h" #include "presolve/HighsPostsolveStack.h" +#include "presolve/PresolveTimer.h" #include "test_kkt/DevKkt.h" #include "util/HFactor.h" #include "util/HighsCDouble.h" @@ -36,10 +38,12 @@ #define ENABLE_SPARSIFY_FOR_LP 0 -#define HPRESOLVE_CHECKED_CALL(presolveCall) \ - do { \ - HPresolve::Result __result = presolveCall; \ - if (__result != presolve::HPresolve::Result::kOk) return __result; \ +#define HPRESOLVE_CHECKED_CALL(presolveCall) \ + do { \ + HPresolve::Result __result = presolveCall; \ + if (__result != presolve::HPresolve::Result::kOk) { \ + return __result; \ + } \ } while (0) namespace presolve { @@ -67,13 +71,64 @@ void HPresolve::debugPrintRow(HighsPostsolveStack& postsolve_stack, } #endif -bool HPresolve::okSetInput(HighsLp& model_, const HighsOptions& options_, - const HighsInt presolve_reduction_limit, - HighsTimer* timer) { - model = &model_; - options = &options_; +void HPresolve::setInput(HighsLp& model_, const HighsOptions& options_, + const HighsInt presolve_reduction_limit, + HighsTimer* timer) { + this->model = &model_; + this->options = &options_; this->timer = timer; + // Set up the logic to allow presolve rules + this->chooseRules(); + // Set up profiling for presolve rules and logging for their effectiveness + analysis_.setup(this->model, this->options, this->numDeletedRows, + this->numDeletedCols, this->timer); + analysis_.presolveTimerStart(kPresolveClockPresolve); + + if (mipsolver == nullptr) { + this->primal_feastol = options->primal_feasibility_tolerance; + model->integrality_.assign(model->num_col_, HighsVarType::kContinuous); + } else + this->primal_feastol = options->mip_feasibility_tolerance; + + // Take value passed in as reduction limit, allowing different + // values to be used for initial presolve, and after restart + this->reductionLimit = + presolve_reduction_limit < 0 ? kHighsSize_tInf : presolve_reduction_limit; + if (options->presolve != kHighsOffString && + reductionLimit < kHighsSize_tInf) { + highsLogDev(options->log_options, HighsLogType::kInfo, + "HPresolve::setInput reductionLimit = %d\n", + static_cast(this->reductionLimit)); + } + this->in_initial_sweep_ = false; +} + +// for MIP presolve +void HPresolve::setInput(HighsMipSolver& mipsolver, + const HighsInt presolve_reduction_limit) { + this->mipsolver = &mipsolver; + + probingContingent = 1000; + probingNumDelCol = 0; + numProbed = 0; + numProbes.assign(mipsolver.numCol(), 0); + + if (mipsolver.model_ != &mipsolver.mipdata_->presolvedModel) { + mipsolver.mipdata_->presolvedModel = *mipsolver.model_; + mipsolver.model_ = &mipsolver.mipdata_->presolvedModel; + } else { + mipsolver.mipdata_->presolvedModel.col_lower_ = + mipsolver.mipdata_->getDomain().col_lower_; + mipsolver.mipdata_->presolvedModel.col_upper_ = + mipsolver.mipdata_->getDomain().col_upper_; + } + setInput(mipsolver.mipdata_->presolvedModel, *mipsolver.options_mip_, + presolve_reduction_limit, &mipsolver.timer_); +} + +bool HPresolve::okSetupPresolveDataStructures() { + analysis_.presolveTimerStart(kPresolveClockSetupResize); if (!okResize(colLowerSource, model->num_col_, HighsInt{-1})) return false; if (!okResize(colUpperSource, model->num_col_, HighsInt{-1})) return false; if (!okResize(implColLower, model->num_col_, -kHighsInf)) return false; @@ -94,15 +149,12 @@ bool HPresolve::okSetInput(HighsLp& model_, const HighsOptions& options_, if (model->row_upper_[i] == kHighsInf) rowDualLower[i] = 0; } - if (mipsolver == nullptr) { - primal_feastol = options->primal_feasibility_tolerance; - model->integrality_.assign(model->num_col_, HighsVarType::kContinuous); - } else - primal_feastol = options->mip_feasibility_tolerance; + analysis_.presolveTimerStop(kPresolveClockSetupResize); - if (model_.a_matrix_.isRowwise()) { + analysis_.presolveTimerStart(kPresolveClockSetupToCsc); + if (model->a_matrix_.isRowwise()) { // Does this even happen? - assert(model_.a_matrix_.isColwise()); + assert(model->a_matrix_.isColwise()); if (!okFromCSR(model->a_matrix_.value_, model->a_matrix_.index_, model->a_matrix_.start_)) return false; @@ -111,7 +163,18 @@ bool HPresolve::okSetInput(HighsLp& model_, const HighsOptions& options_, model->a_matrix_.start_)) return false; } + analysis_.presolveTimerStop(kPresolveClockSetupToCsc); + // numDeletedCols and numDeletedRows are not cumulative through the + // whole of presolve, but "since the last time the model had no + // deleted columns or rows" - ie from initialisation here, or from a + // call to shrinkProblem + numDeletedCols = 0; + numDeletedRows = 0; + // Need to reset current number of deleted rows and columns in logging + analysis_.resetNumDeleted(); + + analysis_.presolveTimerStart(kPresolveClockSetupResize); // initialize everything as changed, but do not add all indices // since the first thing presolve will do is a scan for easy reductions // of each row and column and set the flag of processed columns to false @@ -124,9 +187,11 @@ bool HPresolve::okSetInput(HighsLp& model_, const HighsOptions& options_, if (!okReserve(changedColIndices, model->num_col_)) return false; if (!okReserve(liftingOpportunities, model->num_row_)) return false; if (!okResize(singleEquationChecked, model->num_row_)) return false; - numDeletedCols = 0; - numDeletedRows = 0; - // initialize substitution opportunities + analysis_.presolveTimerStop(kPresolveClockSetupResize); + return true; +} + +void HPresolve::setupSubstitutionOpportunities() { for (HighsInt row = 0; row != model->num_row_; ++row) { if (!isDualImpliedFree(row)) continue; for (const HighsSliceNonzero& nonzero : getRowVector(row)) { @@ -134,41 +199,6 @@ bool HPresolve::okSetInput(HighsLp& model_, const HighsOptions& options_, substitutionOpportunities.emplace_back(row, nonzero.index()); } } - // Take value passed in as reduction limit, allowing different - // values to be used for initial presolve, and after restart - reductionLimit = - presolve_reduction_limit < 0 ? kHighsSize_tInf : presolve_reduction_limit; - if (options->presolve != kHighsOffString && - reductionLimit < kHighsSize_tInf) { - highsLogDev(options->log_options, HighsLogType::kInfo, - "HPresolve::okSetInput reductionLimit = %d\n", - static_cast(reductionLimit)); - } - return true; -} - -// for MIP presolve -bool HPresolve::okSetInput(HighsMipSolver& mipsolver, - const HighsInt presolve_reduction_limit) { - this->mipsolver = &mipsolver; - - probingContingent = 1000; - probingNumDelCol = 0; - numProbed = 0; - numProbes.assign(mipsolver.numCol(), 0); - - if (mipsolver.model_ != &mipsolver.mipdata_->presolvedModel) { - mipsolver.mipdata_->presolvedModel = *mipsolver.model_; - mipsolver.model_ = &mipsolver.mipdata_->presolvedModel; - } else { - mipsolver.mipdata_->presolvedModel.col_lower_ = - mipsolver.mipdata_->getDomain().col_lower_; - mipsolver.mipdata_->presolvedModel.col_upper_ = - mipsolver.mipdata_->getDomain().col_upper_; - } - - return okSetInput(mipsolver.mipdata_->presolvedModel, *mipsolver.options_mip_, - presolve_reduction_limit, &mipsolver.timer_); } bool HPresolve::rowCoefficientsIntegral(HighsInt row, double scale) const { @@ -245,10 +275,10 @@ bool HPresolve::isRanged(HighsInt row) const { } bool HPresolve::isRedundant(HighsInt row) const { - return (impliedRowBounds.getSumLower(row) >= - model->row_lower_[row] - primal_feastol && - impliedRowBounds.getSumUpper(row) <= - model->row_upper_[row] + primal_feastol); + return impliedRowBounds.getSumLower(row) >= + model->row_lower_[row] - primal_feastol && + impliedRowBounds.getSumUpper(row) <= + model->row_upper_[row] + primal_feastol; } bool HPresolve::yieldsImpliedLowerBound(HighsInt row, double val) const { @@ -449,6 +479,82 @@ HPresolve::StatusResult HPresolve::convertImpliedInteger(HighsInt col, changeColBounds(col, model->col_lower_[col], model->col_upper_[col])); } +void HPresolve::chooseRules() { + const bool silent = silentLog(); + this->allow_rule_.assign(kPresolveRuleCount, true); + std::vector presolve_light_rule_off(kPresolveRuleCount, false); + const bool presolve_light = options->presolve_light == kHighsOnString; + if (presolve_light) { + // Define the rules not used in presolve_light mode + presolve_light_rule_off[kPresolveRuleDependentEquations] = true; + presolve_light_rule_off[kPresolveRuleDependentFreeCols] = true; + presolve_light_rule_off[kPresolveRuleAggregator] = true; + presolve_light_rule_off[kPresolveRuleParallelRowsAndCols] = true; + presolve_light_rule_off[kPresolveRuleSparsify] = true; + presolve_light_rule_off[kPresolveRuleProbing] = true; + presolve_light_rule_off[kPresolveRuleEnumeration] = true; + presolve_light_rule_off[kPresolveRuleDualFixing] = true; + presolve_light_rule_off[kPresolveRuleColStuffing] = true; + } + + if (!silent && options->log_dev_level) { + // State which rules can be off, and what bit to set + highsLogUser(options->log_options, HighsLogType::kInfo, + "Permitted suppression of presolve rules via " + "presolve_rule_off option:\n"); + HighsInt bit = + std::pow(int(2), static_cast(kPresolveRuleFirstAllowOff)); + for (HighsInt rule_type = kPresolveRuleFirstAllowOff; + rule_type < kPresolveRuleCount; rule_type++) { + // This is a rule that can be switched off + highsLogUser(options->log_options, HighsLogType::kInfo, + " Rule %2d (set bit %2d = %6d): %s\n", int(rule_type), + int(rule_type), int(bit), + utilPresolveRuleTypeToString(rule_type).c_str()); + bit *= 2; + } + } + if (options->presolve_rule_off || presolve_light) { + // Some presolve rules are off or presolve_light mode is being used + // + // Transform options->presolve_rule_off into logical settings in + // allow_rule_[*], commenting on the rules switched off + if (!presolve_light && !silent) + highsLogUser(options->log_options, HighsLogType::kInfo, + "Presolve rules not allowed:\n"); + HighsInt bit = 1; + for (HighsInt rule_type = kPresolveRuleMin; rule_type < kPresolveRuleCount; + rule_type++) { + // Identify whether this rule is allowed + const bool rule_off = (options->presolve_rule_off & bit) || + presolve_light_rule_off[rule_type]; + if (rule_type >= kPresolveRuleFirstAllowOff) { + // This is a rule that can be switched off + allow_rule_[rule_type] = !rule_off; + // Possibly comment positively if it is off + if (rule_off && !presolve_light && !silent) + highsLogUser(options->log_options, HighsLogType::kInfo, + " Rule %2d (set bit %2d = %6d): %s\n", int(rule_type), + int(rule_type), int(bit), + utilPresolveRuleTypeToString(rule_type).c_str()); + } else if (rule_off) { + // This is a rule that cannot be switched off so, if an + // attempt is made, don't allow it to be off and possibly + // comment negatively + if (!silent) + highsLogUser(options->log_options, HighsLogType::kWarning, + "Cannot disallow rule %2d (bit %2d = %5d): %s\n", + int(rule_type), int(rule_type), int(bit), + utilPresolveRuleTypeToString(rule_type).c_str()); + // Check that we're not here because presolve_light mode is + // being used + assert(!presolve_light_rule_off[rule_type]); + } + bit *= 2; + } + } +} + void HPresolve::link(HighsInt pos) { Anext[pos] = colhead[Acol[pos]]; Aprev[pos] = -1; @@ -548,8 +654,16 @@ void HPresolve::markChangedCol(HighsInt col) { double HPresolve::getMaxAbsColVal(HighsInt col) const { double maxVal = 0.0; - for (const auto& nz : getColumnVector(col)) - maxVal = std::max(std::abs(nz.value()), maxVal); + if (this->in_initial_sweep_) { + for (HighsInt iEl = model->a_matrix_.start_[col]; + iEl < model->a_matrix_.start_[col + 1]; iEl++) { + double value = model->a_matrix_.value_[iEl]; + maxVal = std::max(std::abs(value), maxVal); + } + } else { + for (const auto& nz : getColumnVector(col)) + maxVal = std::max(std::abs(nz.value()), maxVal); + } return maxVal; } @@ -824,12 +938,31 @@ HighsInt HPresolve::findNonzero(HighsInt row, HighsInt col) { } void HPresolve::shrinkProblem(HighsPostsolveStack& postsolve_stack) { + // printf("HPresolve::shrinkProblem: numDeletedCols = %d; numDeletedRows = + // %d\n", + // int(numDeletedCols), int(numDeletedRows)); + // The final call to shrinkProblem, or if it's called on return from + // if (numDeletedCols == 0 && numDeletedRows == 0) return; HighsInt oldNumCol = model->num_col_; + HighsInt oldNumRow = model->num_row_; + // If HPresolve::shrinkProblem has been called before setting up the + // full presolve data structures - implying that presolve has + // terminated in HPresolve::initialSweep, when the model is + // up-to-date, so no shrinkage is required + if (!hasPresolveDataStructures()) return; + assert(colDeleted.size() == static_cast(oldNumCol)); + assert(rowDeleted.size() == static_cast(oldNumRow)); model->num_col_ = 0; + model->num_row_ = 0; std::vector newColIndex(oldNumCol); + std::vector newRowIndex(oldNumRow); const bool have_col_names = model->col_names_.size() > 0; + const bool have_row_names = model->row_names_.size() > 0; assert(!have_col_names || model->col_names_.size() == static_cast(oldNumCol)); + assert(!have_row_names || + model->row_names_.size() == static_cast(oldNumRow)); + // Shrink the col data for (HighsInt i = 0; i != oldNumCol; ++i) { if (colDeleted[i]) newColIndex[i] = -1; @@ -872,12 +1005,7 @@ void HPresolve::shrinkProblem(HighsPostsolveStack& postsolve_stack) { if (have_col_names) model->col_names_.resize(model->num_col_); changedColFlag.resize(model->num_col_); numDeletedCols = 0; - HighsInt oldNumRow = model->num_row_; - const bool have_row_names = model->row_names_.size() > 0; - assert(!have_row_names || - model->row_names_.size() == static_cast(oldNumRow)); - model->num_row_ = 0; - std::vector newRowIndex(oldNumRow); + // Shrink the row data for (HighsInt i = 0; i != oldNumRow; ++i) { if (rowDeleted[i]) newRowIndex[i] = -1; @@ -1745,12 +1873,13 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { // Check for timeout tt = this->timer->read(); if (tt > options->time_limit) { - highsLogUser(options->log_options, HighsLogType::kInfo, - "Time limit reached in probing: " - "consider not using probing by setting option " - "presolve_rule_off to 2^%-d = %d\n", - int(kPresolveRuleProbing), - int(std::pow(int(2), int(kPresolveRuleProbing)))); + highsLogUser( + options->log_options, HighsLogType::kInfo, + "Time limit reached in probing: " + "consider not using probing by setting option " + "presolve_rule_off to 2^%-d = %d\n", + int(kPresolveRuleProbing), + int(std::pow(int(2), static_cast(kPresolveRuleProbing)))); return Result::kStopped; } @@ -2238,28 +2367,32 @@ HighsTripletTreeSliceInOrder HPresolve::getSortedRowVector(HighsInt row) const { } void HPresolve::markRowDeleted(HighsInt row) { - assert(!rowDeleted[row]); + if (!this->in_initial_sweep_) { + assert(!rowDeleted[row]); - // remove equations from set of equations - if (isEquation(row) && eqiters[row] != equations.end()) { - equations.erase(eqiters[row]); - eqiters[row] = equations.end(); - } + // remove equations from set of equations + if (isEquation(row) && eqiters[row] != equations.end()) { + equations.erase(eqiters[row]); + eqiters[row] = equations.end(); + } - // prevents row from being added to change vector - changedRowFlag[row] = true; - rowDeleted[row] = true; + // prevents row from being added to change vector + changedRowFlag[row] = true; + rowDeleted[row] = true; + } ++numDeletedRows; } void HPresolve::markColDeleted(HighsInt col) { - assert(!colDeleted[col]); + if (!this->in_initial_sweep_) { + assert(!colDeleted[col]); - if (col == model->fme_obj_col_) model->fme_obj_col_ = -1; + if (col == model->fme_obj_col_) model->fme_obj_col_ = -1; - // prevents col from being added to change vector - changedColFlag[col] = true; - colDeleted[col] = true; + // prevents col from being added to change vector + changedColFlag[col] = true; + colDeleted[col] = true; + } ++numDeletedCols; } @@ -2333,6 +2466,28 @@ HPresolve::Result HPresolve::checkColBounds(HighsInt col, bool* isFixed) { return Result::kOk; } +HPresolve::Result HPresolve::checkModelColBounds(HighsInt col, bool& isFixed) { + double boundDiff = model->col_upper_[col] - model->col_lower_[col]; + double max_abs_col_value = 0; + for (HighsInt iEl = model->a_matrix_.start_[col]; + iEl < model->a_matrix_.start_[col + 1]; iEl++) + max_abs_col_value = + std::max(std::fabs(model->a_matrix_.value_[iEl]), max_abs_col_value); + isFixed = false; + if (boundDiff <= primal_feastol && + (boundDiff <= options->small_matrix_value || + max_abs_col_value * boundDiff <= primal_feastol)) { + // check for primal infeasibility + if (boundDiff < -primal_feastol) return Result::kPrimalInfeasible; + // check for unboundedness + if (std::abs(model->col_lower_[col]) == kHighsInf) + return Result::kDualInfeasible; + // column is fixed + isFixed = true; + } + return Result::kOk; +} + void HPresolve::changeRowDualUpper(HighsInt row, double newUpper) { double oldUpper = rowDualUpper[row]; rowDualUpper[row] = newUpper; @@ -3130,7 +3285,7 @@ void HPresolve::toCSR(std::vector& ARval, HPresolve::Result HPresolve::doubletonEq(HighsPostsolveStack& postsolve_stack, HighsInt row, HighsPostsolveStack::RowType rowType) { - assert(analysis_.allow_rule_[kPresolveRuleDoubletonEquation]); + assert(this->allow_rule_[kPresolveRuleDoubletonEquation]); const bool logging_on = analysis_.logging_on_; if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleDoubletonEquation); @@ -3297,30 +3452,38 @@ HPresolve::Result HPresolve::doubletonEq(HighsPostsolveStack& postsolve_stack, } HPresolve::Result HPresolve::singletonRow(HighsPostsolveStack& postsolve_stack, - HighsInt row) { + HighsInt row, const HighsInt col_, + const double val_) { + // Default values are col_ = -1; val_ = 0 + // + // When this->in_initial_sweep_ is true, the column and value of the singleton + // are passed as col_ and val_. Since the presolve data structures + // are not set up, there is vastly less housekeeping to do const bool logging_on = analysis_.logging_on_; - if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleSingletonRow); - assert(!rowDeleted[row]); - assert(rowsize[row] == 1); - - // the tree of nonzeros of this row should just contain the single nonzero - HighsInt nzPos = rowroot[row]; - assert(nzPos != -1); - // nonzero should have the row in the row array - assert(Arow[nzPos] == row); - // tree with one element should not have children - assert(ARleft[nzPos] == -1); - assert(ARright[nzPos] == -1); - - HighsInt col = Acol[nzPos]; - double val = Avalue[nzPos]; + HighsInt nzPos = -1; + if (!this->in_initial_sweep_) { + if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleSingletonRow); + assert(!rowDeleted[row]); + assert(rowsize[row] == 1); + + // the tree of nonzeros of this row should just contain the single nonzero + nzPos = rowroot[row]; + assert(nzPos != -1); + // nonzero should have the row in the row array + assert(Arow[nzPos] == row); + // tree with one element should not have children + assert(ARleft[nzPos] == -1); + assert(ARright[nzPos] == -1); + } + HighsInt col = this->in_initial_sweep_ ? col_ : Acol[nzPos]; + double val = this->in_initial_sweep_ ? val_ : Avalue[nzPos]; // printf("singleton row\n"); // debugPrintRow(row); - // delete row singleton nonzero directly, we have all information that we need - // in local variables + // delete row singleton nonzero directly, we have all information that we + // need in local variables markRowDeleted(row); - unlink(nzPos); + if (!this->in_initial_sweep_) unlink(nzPos); // check for simple if (val > 0) { @@ -3329,8 +3492,11 @@ HPresolve::Result HPresolve::singletonRow(HighsPostsolveStack& postsolve_stack, model->col_lower_[col] * val >= model->row_lower_[row] - primal_feastol) { postsolve_stack.redundantRow(row); - analysis_.logging_on_ = logging_on; - if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleSingletonRow); + if (!this->in_initial_sweep_) { + analysis_.logging_on_ = logging_on; + if (logging_on) + analysis_.stopPresolveRuleLog(kPresolveRuleSingletonRow); + } return checkLimits(postsolve_stack); } } else { @@ -3339,8 +3505,11 @@ HPresolve::Result HPresolve::singletonRow(HighsPostsolveStack& postsolve_stack, model->col_upper_[col] * val >= model->row_lower_[row] - primal_feastol) { postsolve_stack.redundantRow(row); - analysis_.logging_on_ = logging_on; - if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleSingletonRow); + if (!this->in_initial_sweep_) { + analysis_.logging_on_ = logging_on; + if (logging_on) + analysis_.stopPresolveRuleLog(kPresolveRuleSingletonRow); + } return checkLimits(postsolve_stack); } } @@ -3396,6 +3565,7 @@ HPresolve::Result HPresolve::singletonRow(HighsPostsolveStack& postsolve_stack, // set the bound to one of the values. To heuristically get rid of numerical // errors we choose the bound that was not tightened, or the midpoint if // both where tightened. + // if (ub < lb || (ub > lb && (ub - lb) * std::max(std::fabs(val), getMaxAbsColVal(col)) <= primal_feastol)) { @@ -3418,6 +3588,14 @@ HPresolve::Result HPresolve::singletonRow(HighsPostsolveStack& postsolve_stack, postsolve_stack.singletonRow(row, col, val, lowerTightened, upperTightened); + // Got as far as possible for HPresolve::singletonRow with initial + // sweep + if (this->in_initial_sweep_) { + model->col_lower_[col] = lb; + model->col_upper_[col] = ub; + return checkLimits(postsolve_stack); + } + // just update bounds (and row activities) if (lowerTightened) HPRESOLVE_CHECKED_CALL(changeColLower(col, lb)); // update bounds, or remove as fixed column directly @@ -3440,7 +3618,7 @@ HPresolve::Result HPresolve::singletonRow(HighsPostsolveStack& postsolve_stack, } HPresolve::Result HPresolve::singletonCol(HighsPostsolveStack& postsolve_stack, - HighsInt col) { + HighsInt col, const bool timing) { assert(colsize[col] == 1); assert(!colDeleted[col]); HighsInt nzPos = colhead[col]; @@ -3448,17 +3626,26 @@ HPresolve::Result HPresolve::singletonCol(HighsPostsolveStack& postsolve_stack, double colCoef = Avalue[nzPos]; if (rowsize[row] == 1) { + if (timing) + analysis_.presolveTimerStart(kPresolveClockSingletonColSingletonRow); HPRESOLVE_CHECKED_CALL(singletonRow(postsolve_stack, row);); if (!colDeleted[col]) { assert(colsize[col] == 0); - return emptyCol(postsolve_stack, col); + HPresolve::Result result = emptyCol(postsolve_stack, col); + if (timing) + analysis_.presolveTimerStop(kPresolveClockSingletonColSingletonRow); + return result; } + if (timing) + analysis_.presolveTimerStop(kPresolveClockSingletonColSingletonRow); return Result::kOk; } // detect strong / weak domination + if (timing) analysis_.presolveTimerStart(kPresolveClockSingletonColDominated); HPRESOLVE_CHECKED_CALL(detectDominatedCol(postsolve_stack, col, false)); + if (timing) analysis_.presolveTimerStop(kPresolveClockSingletonColDominated); if (colDeleted[col]) return Result::kOk; // check if variable is implied integer @@ -3467,28 +3654,64 @@ HPresolve::Result HPresolve::singletonCol(HighsPostsolveStack& postsolve_stack, static_cast(convertImpliedInteger(col, row))); // dual fixing - HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); - if (colDeleted[col]) return Result::kOk; + if (this->allow_rule_[kPresolveRuleDualFixing]) { + const bool logging_on = analysis_.logging_on_; + if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleDualFixing); + if (timing) + analysis_.presolveTimerStart(kPresolveClockSingletonColDualFixing); + HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); + if (timing) + analysis_.presolveTimerStop(kPresolveClockSingletonColDualFixing); + analysis_.logging_on_ = logging_on; + if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleDualFixing); + if (colDeleted[col]) return Result::kOk; + } // singleton column stuffing - HPRESOLVE_CHECKED_CALL(singletonColStuffing(postsolve_stack, col)); - if (colDeleted[col]) return Result::kOk; + if (this->allow_rule_[kPresolveRuleColStuffing]) { + const bool logging_on = analysis_.logging_on_; + if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleColStuffing); + if (timing) + analysis_.presolveTimerStart(kPresolveClockSingletonColStuffing); + HPRESOLVE_CHECKED_CALL(singletonColStuffing(postsolve_stack, col)); + if (timing) analysis_.presolveTimerStop(kPresolveClockSingletonColStuffing); + analysis_.logging_on_ = logging_on; + if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleColStuffing); + if (colDeleted[col]) return Result::kOk; + }; // update column implied bounds + if (timing) + analysis_.presolveTimerStart(kPresolveClockSingletonColImpliedBounds); HPRESOLVE_CHECKED_CALL(updateColImpliedBounds(row, col, colCoef)); + if (timing) + analysis_.presolveTimerStop(kPresolveClockSingletonColImpliedBounds); // update row dual implied bounds - if (model->integrality_[col] != HighsVarType::kInteger) + if (model->integrality_[col] != HighsVarType::kInteger) { + if (timing) + analysis_.presolveTimerStart( + kPresolveClockSingletonColRowDualImpliedBounds); updateRowDualImpliedBounds(row, col, colCoef); - + if (timing) + analysis_.presolveTimerStop( + kPresolveClockSingletonColRowDualImpliedBounds); + } // now check if column is implied free within an equation and substitute the // column if that is the case + if (timing) + analysis_.presolveTimerStart(kPresolveClockSingletonColDualImpliedFree); if (isDualImpliedFree(row) && isImpliedFree(col) && - analysis_.allow_rule_[kPresolveRuleFreeColSubstitution]) { + this->allow_rule_[kPresolveRuleFreeColSubstitution]) { if (model->integrality_[col] == HighsVarType::kInteger) { StatusResult impliedIntegral = isImpliedIntegral(col); HPRESOLVE_CHECKED_CALL(static_cast(impliedIntegral)); - if (!impliedIntegral) return Result::kOk; + if (!impliedIntegral) { + if (timing) + analysis_.presolveTimerStop( + kPresolveClockSingletonColDualImpliedFree); + return Result::kOk; + } } const bool logging_on = analysis_.logging_on_; @@ -3504,8 +3727,12 @@ HPresolve::Result HPresolve::singletonCol(HighsPostsolveStack& postsolve_stack, analysis_.logging_on_ = logging_on; if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleFreeColSubstitution); + if (timing) + analysis_.presolveTimerStop(kPresolveClockSingletonColDualImpliedFree); return checkLimits(postsolve_stack); } + if (timing) + analysis_.presolveTimerStop(kPresolveClockSingletonColDualImpliedFree); // todo: check for zero cost singleton and remove return Result::kOk; @@ -3531,6 +3758,18 @@ void HPresolve::substituteFreeCol(HighsPostsolveStack& postsolve_stack, substitute(row, col, rhs); } +HPresolve::Result HPresolve::emptyRow(HighsPostsolveStack& postsolve_stack, + HighsInt row) { + // Special case of rowPresolve for rows known to be empty + // + // Check that the row is feasible + if (model->row_upper_[row] < -primal_feastol || + model->row_lower_[row] > primal_feastol) + return Result::kPrimalInfeasible; + postsolve_stack.redundantRow(row); + return checkLimits(postsolve_stack); +} + HPresolve::Result HPresolve::rowPresolve(HighsPostsolveStack& postsolve_stack, HighsInt row) { assert(!rowDeleted[row]); @@ -3687,7 +3926,7 @@ HPresolve::Result HPresolve::rowPresolve(HighsPostsolveStack& postsolve_stack, // Handle doubleton equations if (rowsize[row] == 2 && rowLower == rowUpper && - analysis_.allow_rule_[kPresolveRuleDoubletonEquation]) { + this->allow_rule_[kPresolveRuleDoubletonEquation]) { HighsPostsolveStack::RowType rowType; if (origRowLower == origRowUpper) { rowType = HighsPostsolveStack::RowType::kEq; @@ -4439,7 +4678,7 @@ HPresolve::Result HPresolve::rowPresolve(HighsPostsolveStack& postsolve_stack, return checkLimits(postsolve_stack); }; - if (analysis_.allow_rule_[kPresolveRuleForcingRow]) { + if (this->allow_rule_[kPresolveRuleForcingRow]) { // Allow rule to consider forcing rows // store row and compute dynamism @@ -4499,8 +4738,44 @@ HPresolve::Result HPresolve::emptyCol(HighsPostsolveStack& postsolve_stack, return checkLimits(postsolve_stack); } +HPresolve::Result HPresolve::modelEmptyCol(HighsPostsolveStack& postsolve_stack, + HighsInt col) { + const HighsInt col_nnz = + model->a_matrix_.start_[col + 1] - model->a_matrix_.start_[col]; + assert(col_nnz == 0); + double cost = model->col_cost_[col]; + const double lower = model->col_lower_[col]; + const double upper = model->col_upper_[col]; + + if ((cost > 0 && lower == -kHighsInf) || (cost < 0 && upper == kHighsInf)) { + if (std::abs(cost) <= options->dual_feasibility_tolerance) + cost = 0; + else + return Result::kDualInfeasible; + } + double fixval = kHighsInf; + if (cost > 0) { + fixval = lower; + if (fixval == -kHighsInf) return Result::kDualInfeasible; + } else if (cost < 0 || std::abs(upper) < std::abs(lower)) { + fixval = upper; + if (fixval == kHighsInf) return Result::kDualInfeasible; + } else if (lower != -kHighsInf) { + fixval = lower; + if (fixval == -kHighsInf) return Result::kDualInfeasible; + } else { + fixval = 0.0; + } + assert(fixval != kHighsInf); + postsolve_stack.removedModelFixedCol(col, fixval, cost, col_nnz, nullptr, + nullptr); + markColDeleted(col); + + return checkLimits(postsolve_stack); +} + HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, - HighsInt col) { + HighsInt col, const bool timing) { assert(!colDeleted[col]); const bool logging_on = analysis_.logging_on_; @@ -4509,24 +4784,36 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, HPRESOLVE_CHECKED_CALL(checkColBounds(col, &isFixed)); if (isFixed) { // remove fixed column + if (timing) analysis_.presolveTimerStart(kPresolveClockInitialColIsFixed); postsolve_stack.removedFixedCol(col, model->col_lower_[col], model->col_cost_[col], getColumnVector(col)); removeFixedCol(col); + if (timing) analysis_.presolveTimerStop(kPresolveClockInitialColIsFixed); return checkLimits(postsolve_stack); } - + HPresolve::Result result; switch (colsize[col]) { case 0: - return emptyCol(postsolve_stack, col); + if (timing) analysis_.presolveTimerStart(kPresolveClockInitialColIsEmpty); + result = emptyCol(postsolve_stack, col); + if (timing) analysis_.presolveTimerStop(kPresolveClockInitialColIsEmpty); + return result; case 1: - return singletonCol(postsolve_stack, col); + if (timing) + analysis_.presolveTimerStart(kPresolveClockInitialColIsSingleton); + result = singletonCol(postsolve_stack, col, timing); + if (timing) + analysis_.presolveTimerStop(kPresolveClockInitialColIsSingleton); + return result; default: break; } // detect strong / weak domination + if (timing) analysis_.presolveTimerStart(kPresolveClockInitialColDominated); HPRESOLVE_CHECKED_CALL(detectDominatedCol(postsolve_stack, col)); + if (timing) analysis_.presolveTimerStop(kPresolveClockInitialColDominated); if (colDeleted[col]) return Result::kOk; // column is not (weakly) dominated @@ -4567,7 +4854,11 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, impliedDualRowBounds.getNumInfSumLowerOrig(col)); // check if variable is implied integer + if (timing) + analysis_.presolveTimerStart(kPresolveClockInitialColImpliedInteger); HPRESOLVE_CHECKED_CALL(static_cast(convertImpliedInteger(col))); + if (timing) + analysis_.presolveTimerStop(kPresolveClockInitialColImpliedInteger); // shift "binary" variables to have a lower bound of zero if (model->integrality_[col] != HighsVarType::kContinuous && @@ -4591,12 +4882,32 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, } // dual fixing - HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); - if (colDeleted[col]) return Result::kOk; + if (this->allow_rule_[kPresolveRuleDualFixing]) { + const bool logging_on = analysis_.logging_on_; + if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleDualFixing); + if (timing) + analysis_.presolveTimerStart(kPresolveClockSingletonColDualFixing); + HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); + if (timing) + analysis_.presolveTimerStop(kPresolveClockSingletonColDualFixing); + analysis_.logging_on_ = logging_on; + if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleDualFixing); + if (colDeleted[col]) return Result::kOk; + } // singleton column stuffing - HPRESOLVE_CHECKED_CALL(singletonColStuffing(postsolve_stack, col)); - if (colDeleted[col]) return Result::kOk; + if (this->allow_rule_[kPresolveRuleColStuffing]) { + const bool logging_on = analysis_.logging_on_; + if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleColStuffing); + if (timing) + analysis_.presolveTimerStart(kPresolveClockInitialColSingletonStuffing); + HPRESOLVE_CHECKED_CALL(singletonColStuffing(postsolve_stack, col)); + if (timing) + analysis_.presolveTimerStop(kPresolveClockInitialColSingletonStuffing); + analysis_.logging_on_ = logging_on; + if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleColStuffing); + if (colDeleted[col]) return Result::kOk; + } // update dual implied bounds of all rows in given column if (model->integrality_[col] != HighsVarType::kInteger) @@ -4665,7 +4976,7 @@ HPresolve::Result HPresolve::detectDominatedCol( if (handleSingletonRows) HPRESOLVE_CHECKED_CALL(removeRowSingletons(postsolve_stack)); return checkLimits(postsolve_stack); - } else if (analysis_.allow_rule_[kPresolveRuleForcingCol]) { + } else if (this->allow_rule_[kPresolveRuleForcingCol]) { // check for forcing column (see Andersen and Andersen, Presolving in // linear programming. Math. Program. 71, 221-245, 1995). // the column's lower bound is infinite (direction = 1) or its upper @@ -5825,28 +6136,296 @@ double HPresolve::computeWorstCaseUpperBound(HighsInt col, HighsInt boundCol, return upperBound; } +HPresolve::Result HPresolve::initialSweep( + HighsPostsolveStack& postsolve_stack) { + assert(this->in_initial_sweep_); + const bool logging_on = analysis_.logging_on_; + if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleInitialSweep); + HighsInt num_fixed_col = 0; + HighsInt num_empty_col = 0; + HighsInt num_empty_row = 0; + HighsInt num_singleton_row = 0; + HighsInt num_redundant_row = 0; + HighsInt num_col = 0; + HighsInt nnz = 0; + bool isFixed; + const HighsInt original_num_col = model->num_col_; + const HighsInt original_num_row = model->num_row_; + const bool have_col_names = model->col_names_.size() > 0; + const bool have_row_names = model->row_names_.size() > 0; + std::vector newColIndex(model->num_col_); + std::vector row_count(model->num_row_, 0); + // Col of row is used to identify the column containing each + // singleton row, and val_of_row the matrix entry of the singleton + std::vector col_of_row(model->num_row_, -1); + std::vector val_of_row(model->num_row_, 0); + // Compute the implied bounds on rows + std::vector implied_row_lower(model->num_row_, 0); + std::vector implied_row_upper(model->num_row_, 0); + for (HighsInt iCol = 0; iCol < model->num_col_; iCol++) { + HighsInt col_nnz = + model->a_matrix_.start_[iCol + 1] - model->a_matrix_.start_[iCol]; + HPRESOLVE_CHECKED_CALL(checkModelColBounds(iCol, isFixed)); + if (col_nnz == 0) { + newColIndex[iCol] = -1; + num_empty_col++; + // Remove empty column + HPRESOLVE_CHECKED_CALL(modelEmptyCol(postsolve_stack, iCol)); + } else if (isFixed) { + newColIndex[iCol] = -1; + num_fixed_col++; + // Remove fixed column + HighsInt iEl = model->a_matrix_.start_[iCol]; + postsolve_stack.removedModelFixedCol( + iCol, model->col_lower_[iCol], model->col_cost_[iCol], col_nnz, + &model->a_matrix_.index_[iEl], &model->a_matrix_.value_[iEl]); + removeFixedCol(iCol); + } else { + newColIndex[iCol] = num_col; + model->col_cost_[num_col] = model->col_cost_[iCol]; + model->col_lower_[num_col] = model->col_lower_[iCol]; + model->col_upper_[num_col] = model->col_upper_[iCol]; + model->integrality_[num_col] = model->integrality_[iCol]; + if (have_col_names) + model->col_names_[num_col] = std::move(model->col_names_[iCol]); + HighsInt from_os = model->a_matrix_.start_[iCol]; + HighsInt new_col_start = nnz; + for (HighsInt iEl = 0; iEl < col_nnz; iEl++) { + HighsInt iRow = model->a_matrix_.index_[from_os + iEl]; + double value = model->a_matrix_.value_[from_os + iEl]; + row_count[iRow]++; + col_of_row[iRow] = num_col; + val_of_row[iRow] = value; + model->a_matrix_.index_[nnz] = iRow; + model->a_matrix_.value_[nnz] = value; + nnz++; + implied_row_lower[iRow] += + (value > 0 ? value * model->col_lower_[num_col] + : value * model->col_upper_[num_col]); + implied_row_upper[iRow] += + (value > 0 ? value * model->col_upper_[num_col] + : value * model->col_lower_[num_col]); + } + model->a_matrix_.start_[num_col] = new_col_start; + num_col++; + } + } + model->a_matrix_.start_[num_col] = nnz; + HighsInt num_removed_cols = num_empty_col + num_fixed_col; + assert(num_col + num_removed_cols == model->num_col_); + model->col_cost_.resize(num_col); + model->col_lower_.resize(num_col); + model->col_upper_.resize(num_col); + model->integrality_.resize(num_col); + if (have_col_names) model->col_names_.resize(num_col); + model->num_col_ = num_col; + model->a_matrix_.num_col_ = num_col; + model->a_matrix_.start_.resize(num_col + 1); + model->a_matrix_.index_.resize(nnz); + model->a_matrix_.value_.resize(nnz); + postsolve_stack.compressColIndexMap(newColIndex); + HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); + + auto localIsRedundant = [&](const HighsInt row) { + return implied_row_lower[row] >= model->row_lower_[row] - primal_feastol && + implied_row_upper[row] <= model->row_upper_[row] + primal_feastol; + }; + for (HighsInt iRow = 0; iRow < model->num_row_; iRow++) { + if (row_count[iRow] == 0) + num_empty_row++; + else if (row_count[iRow] == 1) + num_singleton_row++; + else if (localIsRedundant(iRow)) + num_redundant_row++; + } + const bool allow_row_sweep = true; + HighsInt num_removed_rows = + num_empty_row + num_singleton_row + num_redundant_row; + if (allow_row_sweep && + (num_empty_row || num_singleton_row || num_redundant_row)) { + HighsInt num_row = 0; + std::vector has_singleton_row(model->num_col_, false); + std::vector newRowIndex(model->num_row_); + for (HighsInt iRow = 0; iRow < model->num_row_; iRow++) { + if (row_count[iRow] <= 1) { + newRowIndex[iRow] = -1; + if (row_count[iRow] == 0) { + // Empty row + HPRESOLVE_CHECKED_CALL(emptyRow(postsolve_stack, iRow)); + markRowDeleted(iRow); + } else { + // Singleton row + has_singleton_row[col_of_row[iRow]] = true; + assert(val_of_row[iRow]); + HPRESOLVE_CHECKED_CALL(singletonRow( + postsolve_stack, iRow, col_of_row[iRow], val_of_row[iRow])); + } + } else { + if (localIsRedundant(iRow)) { + postsolve_stack.redundantRow(iRow); + newRowIndex[iRow] = -1; + markRowDeleted(iRow); + continue; + } + newRowIndex[iRow] = num_row; + model->row_lower_[num_row] = model->row_lower_[iRow]; + model->row_upper_[num_row] = model->row_upper_[iRow]; + if (have_row_names) + model->row_names_[num_row] = std::move(model->row_names_[iRow]); + num_row++; + } + } + assert(num_row + num_removed_rows == model->num_row_); + + if (num_redundant_row == 0) { + // Only removing entries corresponding to singleton rows so + // there are few to remove and it can be done efficiently + nnz = 0; + HighsInt from_col = 0; + // Lambda for shifting column data and updating row indices + auto shiftCols = [&](const HighsInt to_col) { + for (HighsInt iCol = from_col; iCol < to_col; iCol++) { + HighsInt from_os = model->a_matrix_.start_[iCol]; + HighsInt col_nnz = model->a_matrix_.start_[iCol + 1] - from_os; + HighsInt new_col_start = nnz; + for (HighsInt iEl = 0; iEl < col_nnz; iEl++) { + HighsInt iRow = model->a_matrix_.index_[from_os + iEl]; + HighsInt newRow = newRowIndex[iRow]; + assert(newRow >= 0); + model->a_matrix_.index_[nnz] = newRow; + model->a_matrix_.value_[nnz] = + model->a_matrix_.value_[from_os + iEl]; + nnz++; + } + model->a_matrix_.start_[iCol] = new_col_start; + } + }; + for (HighsInt iCol0 = 0; iCol0 < model->num_col_; iCol0++) { + if (!has_singleton_row[iCol0]) continue; + // Column iCol0 contains a row singleton, so update the matrix + // entries for the columns since the last with a row singleton + shiftCols(iCol0); + HighsInt from_os = model->a_matrix_.start_[iCol0]; + HighsInt col_nnz = model->a_matrix_.start_[iCol0 + 1] - from_os; + HighsInt new_col_start = nnz; + bool found_row_singleton = false; + for (HighsInt iEl = 0; iEl < col_nnz; iEl++) { + HighsInt iRow = model->a_matrix_.index_[from_os + iEl]; + HighsInt newRow = newRowIndex[iRow]; + if (newRow >= 0) { + model->a_matrix_.index_[nnz] = newRow; + model->a_matrix_.value_[nnz] = + model->a_matrix_.value_[from_os + iEl]; + nnz++; + } else { + assert(row_count[iRow] == 1); + assert(col_of_row[iRow] == iCol0); + assert(val_of_row[iRow] == model->a_matrix_.value_[from_os + iEl]); + found_row_singleton = true; + } + } + assert(found_row_singleton); + model->a_matrix_.start_[iCol0] = new_col_start; + from_col = iCol0 + 1; + } + // Update the matrix entries for the columns since the last with a + // row singleton + shiftCols(model->num_col_); + model->a_matrix_.start_[num_col] = nnz; + } else { + // Also removing redundant rows, so make the matrix rowwise and + // remove rows simply above + nnz = 0; + HighsInt from_row = 0; + num_row = 0; + // Lambda for shifting row data and updating row indices + auto shiftRows = [&](const HighsInt to_row) { + for (HighsInt iRow = from_row; iRow < to_row; iRow++) { + HighsInt new_row_start = nnz; + for (HighsInt iEl = model->a_matrix_.start_[iRow]; + iEl < model->a_matrix_.start_[iRow + 1]; iEl++) { + model->a_matrix_.index_[nnz] = model->a_matrix_.index_[iEl]; + model->a_matrix_.value_[nnz] = model->a_matrix_.value_[iEl]; + nnz++; + } + model->a_matrix_.start_[num_row] = new_row_start; + num_row++; + } + }; + // Only removing entries corresponding to singleton rows so + // there are few to remove and it can be done efficiently + // + model->a_matrix_.ensureRowwise(); + for (HighsInt iRow0 = 0; iRow0 < model->num_row_; iRow0++) { + if (newRowIndex[iRow0] >= 0) continue; + // Row iRow0 is removed, so update the matrix entries for the + // rows since the last removed + shiftRows(iRow0); + from_row = iRow0 + 1; + } + // Update the matrix entries for the rows since the last removed + shiftRows(model->num_row_); + assert(num_row + num_removed_rows == model->num_row_); + model->a_matrix_.start_[num_row] = nnz; + model->a_matrix_.num_row_ = num_row; + model->a_matrix_.ensureColwise(); + } + model->row_lower_.resize(num_row); + model->row_upper_.resize(num_row); + if (have_row_names) model->row_names_.resize(num_row); + model->num_row_ = num_row; + model->a_matrix_.num_row_ = num_row; + model->a_matrix_.index_.resize(nnz); + model->a_matrix_.value_.resize(nnz); + postsolve_stack.compressRowIndexMap(newRowIndex); + } + // Add doubleton equations, column singletons, variable locks + + if (num_fixed_col || num_empty_col) + highsLogUser( + options->log_options, HighsLogType::kInfo, + "Initial sweep removes %d + %d = %d / %d empty + fixed columns\n", + int(num_empty_col), int(num_fixed_col), int(num_removed_cols), + int(original_num_col)); + if (num_empty_row || num_singleton_row || num_redundant_row) + highsLogUser(options->log_options, HighsLogType::kInfo, + "Initial sweep identifies %d + %d + %d = %d / %d empty + " + "singleton + redundant rows\n", + int(num_empty_row), int(num_singleton_row), + int(num_redundant_row), int(num_removed_rows), + int(original_num_row)); + analysis_.logging_on_ = logging_on; + if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleInitialSweep); + return checkLimits(postsolve_stack); +} + HPresolve::Result HPresolve::initialRowAndColPresolve( HighsPostsolveStack& postsolve_stack) { // do a full scan over the rows as the singleton arrays and the changed row // arrays are not initialized, also unset changedRowFlag so that the row will // be added to the changed row vector when it is changed after it was // processed + analysis_.presolveTimerStart(kPresolveClockInitialRow); for (HighsInt row = 0; row != model->num_row_; ++row) { if (rowDeleted[row]) continue; HPRESOLVE_CHECKED_CALL(rowPresolve(postsolve_stack, row)); changedRowFlag[row] = false; } + analysis_.presolveTimerStop(kPresolveClockInitialRow); // same for the columns + analysis_.presolveTimerStart(kPresolveClockInitialCol); + const bool timing = analysis_.analyse_presolve_time_; for (HighsInt col = 0; col != model->num_col_; ++col) { if (colDeleted[col]) continue; // round and update bounds if (model->integrality_[col] != HighsVarType::kContinuous) HPRESOLVE_CHECKED_CALL( changeColBounds(col, model->col_lower_[col], model->col_upper_[col])); - HPRESOLVE_CHECKED_CALL(colPresolve(postsolve_stack, col)); + HPRESOLVE_CHECKED_CALL(colPresolve(postsolve_stack, col, timing)); changedColFlag[col] = false; } + analysis_.presolveTimerStop(kPresolveClockInitialCol); return checkLimits(postsolve_stack); } @@ -5856,15 +6435,25 @@ HPresolve::Result HPresolve::fastPresolveLoop( do { storeCurrentProblemSize(); + analysis_.presolveTimerStart(kPresolveClockFastLoopRowSingletons); HPRESOLVE_CHECKED_CALL(removeRowSingletons(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockFastLoopRowSingletons); + analysis_.presolveTimerStart(kPresolveClockFastLoopColSingletons); HPRESOLVE_CHECKED_CALL(presolveChangedRows(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockFastLoopColSingletons); + analysis_.presolveTimerStart(kPresolveClockFastLoopDoubletonEquations); HPRESOLVE_CHECKED_CALL(removeDoubletonEquations(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockFastLoopDoubletonEquations); + analysis_.presolveTimerStart(kPresolveClockFastLoopChangedRows); HPRESOLVE_CHECKED_CALL(presolveColSingletons(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockFastLoopChangedRows); + analysis_.presolveTimerStart(kPresolveClockFastLoopChangedCols); HPRESOLVE_CHECKED_CALL(presolveChangedCols(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockFastLoopChangedCols); } while (problemSizeReduction() > 0.01); @@ -5872,9 +6461,9 @@ HPresolve::Result HPresolve::fastPresolveLoop( } HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { - // for the inner most loop we take the order roughly from the old presolve - // but we nest the rounds with a new outer loop which layers the newer - // presolvers + // For the innermost loop the rounds are nested with an outer loop + // that layers the newer presolvers + // // fast presolve loop // - empty, forcing and dominated rows and row singletons immediately // after each forcing row @@ -5917,16 +6506,58 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { model->sense_ = ObjSense::kMinimize; } + // Need to check for time-out in checkLimits, so make sure that + // the timer is well defined, and that its total time clock is + // running + assert(this->timer); + assert(this->timer->running()); + const bool silent = silentLog(); - if (options->presolve != kHighsOffString) { - if (!silent) - highsLogUser(options->log_options, HighsLogType::kInfo, - "Presolving model\n"); + double report_frequency = 10; + double current_time = this->timer->read(); + HighsInt last_report_size = model->num_col_ + model->num_row_; + double last_report_time = current_time; + if (options->presolve != kHighsOffString && !silent) { + highsLogUser(options->log_options, HighsLogType::kInfo, + "Presolving model\n"); + std::string time_str = highsTimeSecondToString(current_time); + if (options->timeless_log) time_str = ""; + highsLogUser(options->log_options, HighsLogType::kInfo, + "%" HIGHSINT_FORMAT " rows, %" HIGHSINT_FORMAT + " cols, %" HIGHSINT_FORMAT " nonzeros %s\n", + model->num_row_, model->num_col_, model->numNz(), + time_str.c_str()); + } + + if (options->presolve != kHighsOffString && mipsolver == nullptr) { + // Zero numDeletedCols and numDeletedRows since they are used to + // identify reductions due to this presovle rule + numDeletedCols = 0; + numDeletedRows = 0; + // Perform initial sweep to remove fixed columns before forming the + // dynamic constraint matrix data structure + analysis_.presolveTimerStart(kPresolveClockInitialSweep); + // Indicate that initial sweep is running, so that reductions + // operate on the model rather than the dynamic data structure set + // up in okSetupPresolveDataStructures + this->in_initial_sweep_ = true; + HPRESOLVE_CHECKED_CALL(initialSweep(postsolve_stack)); + // Indicate that initial sweep is not running + this->in_initial_sweep_ = false; + analysis_.presolveTimerStop(kPresolveClockInitialSweep); + } + + if (!okSetupPresolveDataStructures()) { + highsLogUser(options->log_options, HighsLogType::kError, + "Insufficient memory for presolve data structures\n"); + // Memory allocation error + return Result::kOutOfMemory; } - // Set up the logic to allow presolve rules, and logging for their - // effectiveness - analysis_.setup(this->model, this->options, this->numDeletedRows, - this->numDeletedCols, silent); + + // initialize substitution opportunities + analysis_.presolveTimerStart(kPresolveClockSetupSubstitutionOpportunities); + setupSubstitutionOpportunities(); + analysis_.presolveTimerStop(kPresolveClockSetupSubstitutionOpportunities); if (options->presolve != kHighsOffString) { if (mipsolver) mipsolver->mipdata_->cliquetable.setPresolveFlag(true); @@ -5937,9 +6568,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { HighsInt numRow = model->num_row_ - numDeletedRows; HighsInt numNonz = static_cast(Avalue.size() - freeslots.size()); - // Only read the run time if it's to be printed - const double run_time = options->output_flag ? this->timer->read() : 0; - std::string time_str = highsTimeSecondToString(run_time); + std::string time_str = highsTimeSecondToString(current_time); if (options->timeless_log) time_str = ""; highsLogUser(options->log_options, HighsLogType::kInfo, "%" HIGHSINT_FORMAT " rows, %" HIGHSINT_FORMAT @@ -5954,11 +6583,15 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { assert(this->timer); assert(this->timer->running()); + // Possibly just perform a bespoke presolve rule test if (options->presolve_rule_test) { HPRESOLVE_CHECKED_CALL(presolveRuleTest(postsolve_stack)); return presolveReturn(); } + + analysis_.presolveTimerStart(kPresolveClockInitial); HPRESOLVE_CHECKED_CALL(initialRowAndColPresolve(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockInitial); HighsInt numParallelRowColCalls = 0; // ReductionType::kEqualityRowAddition(s) has no basis postsolve, @@ -5982,19 +6615,27 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; bool dependentEquationsCalled = mipsolver != nullptr; - HighsInt lastPrintSize = kHighsIInf; // Start of main presolve loop // while (true) { - HighsInt currSize = + assert(model->num_col_ >= numDeletedCols); + assert(model->num_row_ >= numDeletedRows); + HighsInt current_size = model->num_col_ - numDeletedCols + model->num_row_ - numDeletedRows; - if (currSize < 0.85 * lastPrintSize) { - lastPrintSize = currSize; - report(); + if (options->output_flag) { + current_time = this->timer->read(); + if (current_size < 0.85 * last_report_size || + current_time > last_report_time + report_frequency) { + last_report_size = current_size; + last_report_time = current_time; + report(); + } } + analysis_.presolveTimerStart(kPresolveClockFastLoop); HPRESOLVE_CHECKED_CALL(fastPresolveLoop(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockFastLoop); storeCurrentProblemSize(); @@ -6008,22 +6649,28 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { } HighsInt numColsEliminatedFourierMotzkin = 0; - if (tryFourierMotzkin && - analysis_.allow_rule_[kPresolveRuleFourierMotzkin]) + if (tryFourierMotzkin && this->allow_rule_[kPresolveRuleFourierMotzkin]) HPRESOLVE_CHECKED_CALL( fourierMotzkin(postsolve_stack, numColsEliminatedFourierMotzkin)); - if (analysis_.allow_rule_[kPresolveRuleAggregator]) + if (reducedToEmpty()) break; + + if (this->allow_rule_[kPresolveRuleAggregator]) { + analysis_.presolveTimerStart(kPresolveClockAggregator); HPRESOLVE_CHECKED_CALL(aggregator(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockAggregator); + } // check if there were reductions bool haveReductions = problemSizeReduction() > 0.05; tryFourierMotzkin = haveReductions || numColsEliminatedFourierMotzkin > 0; if (haveReductions) continue; - if (trySparsify && analysis_.allow_rule_[kPresolveRuleSparsify]) { + if (trySparsify && this->allow_rule_[kPresolveRuleSparsify]) { HighsInt numNz = numNonzeros(); + analysis_.presolveTimerStart(kPresolveClockSparsify); HPRESOLVE_CHECKED_CALL(sparsify(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockSparsify); double nzReduction = 100.0 * (1.0 - (numNonzeros() / static_cast(numNz))); @@ -6031,21 +6678,22 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { highsLogDev(options->log_options, HighsLogType::kInfo, "Sparsify removed %.1f%% of nonzeros\n", nzReduction); - // #1710 exposes that this should not be - // - // fastPresolveLoop(postsolve_stack); - // - // but + analysis_.presolveTimerStart(kPresolveClockFastLoop); HPRESOLVE_CHECKED_CALL(fastPresolveLoop(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockFastLoop); } trySparsify = false; } - if (analysis_.allow_rule_[kPresolveRuleParallelRowsAndCols] && + if (this->allow_rule_[kPresolveRuleParallelRowsAndCols] && numParallelRowColCalls < 5) { if (shrinkProblemEnabled && (numDeletedCols >= model->num_col_ / 2 || numDeletedRows >= model->num_row_ / 2)) { + // analysis_.presolveTimerStart(kPresolveClock@); + // analysis_.presolveTimerStop(kPresolveClock@); + analysis_.presolveTimerStart(kPresolveClockShrinkProblem); shrinkProblem(postsolve_stack); + analysis_.presolveTimerStop(kPresolveClockShrinkProblem); toCSC(model->a_matrix_.value_, model->a_matrix_.index_, model->a_matrix_.start_); @@ -6053,12 +6701,18 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { model->a_matrix_.start_); } storeCurrentProblemSize(); + analysis_.presolveTimerStart(kPresolveClockParallelRowsAndCols); HPRESOLVE_CHECKED_CALL(detectParallelRowsAndCols(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockParallelRowsAndCols); ++numParallelRowColCalls; if (problemSizeReduction() > 0.05) continue; } + if (postsolve_stack.numReductions() == 35039) { + } + analysis_.presolveTimerStart(kPresolveClockFastLoop); HPRESOLVE_CHECKED_CALL(fastPresolveLoop(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockFastLoop); if (mipsolver != nullptr) { HighsInt num_strengthened = -1; @@ -6071,7 +6725,9 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { num_strengthened); } + analysis_.presolveTimerStart(kPresolveClockFastLoop); HPRESOLVE_CHECKED_CALL(fastPresolveLoop(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockFastLoop); if (mipsolver != nullptr && numCliquesBeforeProbing == -1) { numCliquesBeforeProbing = mipsolver->mipdata_->cliquetable.numCliques(); @@ -6083,14 +6739,13 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { } // enumerate solutions - if (mipsolver != nullptr && - analysis_.allow_rule_[kPresolveRuleEnumeration]) { + if (mipsolver != nullptr && this->allow_rule_[kPresolveRuleEnumeration]) { storeCurrentProblemSize(); HPRESOLVE_CHECKED_CALL(enumerateSolutions(postsolve_stack)); if (problemSizeReduction() > 0.05) continue; } - if (tryProbing && analysis_.allow_rule_[kPresolveRuleProbing]) { + if (tryProbing && this->allow_rule_[kPresolveRuleProbing]) { HPRESOLVE_CHECKED_CALL(detectImpliedIntegers()); storeCurrentProblemSize(); HPRESOLVE_CHECKED_CALL(runProbing(postsolve_stack)); @@ -6104,7 +6759,9 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { if (!dependentEquationsCalled) { if (shrinkProblemEnabled && (numDeletedCols >= model->num_col_ / 2 || numDeletedRows >= model->num_row_ / 2)) { + analysis_.presolveTimerStart(kPresolveClockShrinkProblem); shrinkProblem(postsolve_stack); + analysis_.presolveTimerStop(kPresolveClockShrinkProblem); toCSC(model->a_matrix_.value_, model->a_matrix_.index_, model->a_matrix_.start_); @@ -6112,12 +6769,17 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { model->a_matrix_.start_); } storeCurrentProblemSize(); - if (analysis_.allow_rule_[kPresolveRuleDependentEquations]) { + if (this->allow_rule_[kPresolveRuleDependentEquations]) { + analysis_.presolveTimerStart(kPresolveClockDependentEquations); HPRESOLVE_CHECKED_CALL(removeDependentEquations(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockDependentEquations); dependentEquationsCalled = true; } - if (analysis_.allow_rule_[kPresolveRuleDependentFreeCols]) + if (this->allow_rule_[kPresolveRuleDependentFreeCols]) { + analysis_.presolveTimerStart(kPresolveClockDependentFreeCol); HPRESOLVE_CHECKED_CALL(removeDependentFreeCols(postsolve_stack)); + analysis_.presolveTimerStop(kPresolveClockDependentFreeCol); + } if (problemSizeReduction() > 0.05) continue; } @@ -6136,11 +6798,13 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { break; } - // Now consider removing slacks - if (options->presolve_remove_slacks) - HPRESOLVE_CHECKED_CALL(removeSlacks(postsolve_stack)); + if (!reducedToEmpty()) { + // Now consider removing slacks + if (options->presolve_remove_slacks) + HPRESOLVE_CHECKED_CALL(removeSlacks(postsolve_stack)); - report(); + report(); + } } else { highsLogUser(options->log_options, HighsLogType::kInfo, "\nPresolve is switched off\n"); @@ -6442,7 +7106,7 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { postsolve_stack.debug_prev_row_upper = 0; // Presolve should only be called with a model that has a non-empty // constraint matrix unless it has no rows - assert(model->a_matrix_.numNz() || model->num_row_ == 0); + assert(model->numNz() || model->num_row_ == 0); auto reportReductions = [&]() { if (options->presolve != kHighsOffString && reductionLimit < kHighsSize_tInf) { @@ -6452,21 +7116,39 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { postsolve_stack.numReductions(), reductionLimit); } }; - switch (presolve(postsolve_stack)) { + auto reportProfiling = [&]() { + // Presolve profiling not currently enabled for MIP + this->analysis_.presolveTimerStop(kPresolveClockPresolve); + this->analysis_.reportPresolveTimer(); + }; + + Result result; + try { + result = presolve(postsolve_stack); + } catch (const std::exception& exception) { + highsLogDev(options->log_options, HighsLogType::kError, + "Exception %s in Presolve::presolve\n", exception.what()); + result = Result::kOutOfMemory; + } + switch (result) { case Result::kStopped: case Result::kOk: break; case Result::kPrimalInfeasible: presolve_status_ = HighsPresolveStatus::kInfeasible; reportReductions(); + reportProfiling(); return HighsModelStatus::kInfeasible; case Result::kDualInfeasible: presolve_status_ = HighsPresolveStatus::kUnboundedOrInfeasible; reportReductions(); + reportProfiling(); return HighsModelStatus::kUnboundedOrInfeasible; + case Result::kOutOfMemory: + presolve_status_ = HighsPresolveStatus::kOutOfMemory; + return HighsModelStatus::kMemoryLimit; } reportReductions(); - shrinkProblem(postsolve_stack); if (mipsolver != nullptr) { @@ -6542,8 +7224,13 @@ HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { } } - toCSC(model->a_matrix_.value_, model->a_matrix_.index_, - model->a_matrix_.start_); + // Possibly populate the model matrix from the presolve matrix data + // structure + if (hasPresolveDataStructures()) + toCSC(model->a_matrix_.value_, model->a_matrix_.index_, + model->a_matrix_.start_); + + reportProfiling(); if (model->num_col_ == 0) { // Reduced to empty @@ -6597,31 +7284,40 @@ void HPresolve::computeIntermediateMatrix(std::vector& flagRow, toCSC(model->a_matrix_.value_, model->a_matrix_.index_, model->a_matrix_.start_); - for (HighsInt i = 0; i != model->num_row_; ++i) - flagRow[i] = 1 - rowDeleted[i]; - for (HighsInt i = 0; i != model->num_col_; ++i) - flagCol[i] = 1 - colDeleted[i]; + for (HighsInt i = 0; i != model->num_row_; ++i) flagRow[i] = !rowDeleted[i]; + for (HighsInt i = 0; i != model->num_col_; ++i) flagCol[i] = !colDeleted[i]; } HPresolve::Result HPresolve::removeDependentEquations( HighsPostsolveStack& postsolve_stack) { - assert(analysis_.allow_rule_[kPresolveRuleDependentEquations]); + assert(this->allow_rule_[kPresolveRuleDependentEquations]); const bool logging_on = analysis_.logging_on_; if (equations.empty()) return Result::kOk; + auto returnOk = [&]() { + analysis_.logging_on_ = logging_on; + if (logging_on) + analysis_.stopPresolveRuleLog(kPresolveRuleDependentEquations); + return Result::kOk; + }; + if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleDependentEquations); HighsSparseMatrix matrix; - matrix.num_col_ = equations.size(); + HighsInt num_equations = equations.size(); + matrix.num_col_ = num_equations; matrix.num_row_ = model->num_col_ + 1; - matrix.start_.resize(matrix.num_col_ + 1); + matrix.start_.resize(num_equations + 1); matrix.start_[0] = 0; - const HighsInt maxCapacity = numNonzeros() + matrix.num_col_; + const HighsInt maxCapacity = numNonzeros() + num_equations; matrix.value_.reserve(maxCapacity); matrix.index_.reserve(maxCapacity); - std::vector eqSet(matrix.num_col_); + std::vector eqSet(num_equations); + std::vector row_count; + row_count.assign(model->num_col_, 0); + HighsInt i = 0; for (const std::pair& p : equations) { HighsInt eq = p.second; @@ -6629,8 +7325,10 @@ HPresolve::Result HPresolve::removeDependentEquations( // add entries of equation for (const HighsSliceNonzero& nonz : getRowVector(eq)) { + HighsInt iCol = nonz.index(); + row_count[iCol]++; matrix.value_.push_back(nonz.value()); - matrix.index_.push_back(nonz.index()); + matrix.index_.push_back(iCol); } // add entry for artificial rhs column @@ -6641,7 +7339,22 @@ HPresolve::Result HPresolve::removeDependentEquations( matrix.start_[i] = matrix.value_.size(); } - std::vector colSet(matrix.num_col_); + // Find the number of (true) variables in the system of equations as + // the number of columns with entries in at least one equation + HighsInt num_variables = 0; + for (HighsInt iCol = 0; iCol < model->num_col_; iCol++) + if (row_count[iCol]) num_variables++; + HighsInt num_nz = matrix.numNz(); + const bool silent = silentLog(); + if (!silent) + highsLogUser(options->log_options, HighsLogType::kInfo, + "Considering dependency of %d equation%s in %d variable%s " + "with %d nonzero%s\n", + int(num_equations), highsIntToPlural(num_equations).c_str(), + int(num_variables), highsIntToPlural(num_variables).c_str(), + int(num_nz), highsIntToPlural(num_nz).c_str()); + // Identify any dependent equations + std::vector colSet(num_equations); std::iota(colSet.begin(), colSet.end(), 0); HFactor factor; factor.setup(matrix, colSet); @@ -6654,30 +7367,40 @@ HPresolve::Result HPresolve::removeDependentEquations( // // ToDo: This is strictly non-deterministic, but so conservative // that it'll only reap the cases when factor.build never finishes - const double time_limit = - std::max(1.0, std::min(0.01 * options->time_limit, 1000.0)); + const double kMaxDependentEquationsTime = 100; + const double time_limit = std::max( + 1.0, std::min(0.01 * options->time_limit, kMaxDependentEquationsTime)); factor.setTimeLimit(time_limit); - const bool silent = silentLog(); // Determine rank deficiency of the equations if (!silent) highsLogUser(options->log_options, HighsLogType::kInfo, - "Dependent equations search running on %d equations with time " + "Dependent equations search running with time " "limit of %.2fs\n", - static_cast(matrix.num_col_), time_limit); + time_limit); double time_taken = -this->timer->read(); HighsInt build_return = factor.build(); time_taken += this->timer->read(); + // Analyse what's been removed + HighsInt num_removed_row = 0; + HighsInt num_removed_nz = 0; + HighsInt num_fictitious_rows_skipped = 0; if (build_return == kBuildKernelReturnTimeout) { // HFactor::build has timed out, so just return - if (!silent) + if (!silent) { + if (options->log_dev_level > 0) + highsLogUser( + options->log_options, HighsLogType::kInfo, + "GrepDependentEq,%s,%d,%d,%d,%d,%d,%d,%g,Terminated\n", + model->model_name_.c_str(), static_cast(num_equations), + static_cast(num_variables), static_cast(model->num_col_), + static_cast(num_nz), static_cast(num_removed_row), + static_cast(num_removed_nz), time_taken); highsLogUser(options->log_options, HighsLogType::kInfo, "Dependent equations search terminated after %.3gs due to " "expected time exceeding limit\n", time_taken); - analysis_.logging_on_ = logging_on; - if (logging_on) - analysis_.stopPresolveRuleLog(kPresolveRuleDependentFreeCols); - return Result::kOk; + } + return returnOk(); } else { double pct_off_timeout = 1e2 * std::fabs(time_taken - time_limit) / time_limit; @@ -6691,10 +7414,6 @@ HPresolve::Result HPresolve::removeDependentEquations( // build_return as rank_deficiency must be valid assert(build_return >= 0); const HighsInt rank_deficiency = build_return; - // Analyse what's been removed - HighsInt num_removed_row = 0; - HighsInt num_removed_nz = 0; - HighsInt num_fictitious_rows_skipped = 0; for (HighsInt k = 0; k < rank_deficiency; k++) { if (factor.var_with_no_pivot[k] >= 0) { HighsInt redundant_row = eqSet[factor.var_with_no_pivot[k]]; @@ -6706,22 +7425,29 @@ HPresolve::Result HPresolve::removeDependentEquations( num_fictitious_rows_skipped++; } } - if (!silent) - highsLogUser(options->log_options, HighsLogType::kInfo, - "Dependent equations search removed %d rows and %d nonzeros " - "in %.2fs (limit = %.2fs)\n", - static_cast(num_removed_row), - static_cast(num_removed_nz), time_taken, time_limit); - if (num_fictitious_rows_skipped) - highsLogDev(options->log_options, HighsLogType::kInfo, - ", avoiding %d fictitious rows", - static_cast(num_fictitious_rows_skipped)); - highsLogDev(options->log_options, HighsLogType::kInfo, "\n"); - - analysis_.logging_on_ = logging_on; - if (logging_on) - analysis_.stopPresolveRuleLog(kPresolveRuleDependentEquations); - return Result::kOk; + if (!silent) { + highsLogUser( + options->log_options, HighsLogType::kInfo, + "Search of %d equation%s with %d / %d variable%s and %d nonzero%s " + "removed %d dependent equation%s and %d nonzero%s " + "in %.2fs with bounds in (%.2fs, %.2fs) and limit = %.2fs", + // clang-format off + static_cast(num_equations), highsIntToPlural(num_equations).c_str(), + static_cast(num_variables), + static_cast(model->num_col_), highsIntToPlural(num_variables).c_str(), + static_cast(num_nz), highsIntToPlural(num_nz).c_str(), + static_cast(num_removed_row), highsIntToPlural(num_removed_row).c_str(), + static_cast(num_removed_nz), highsIntToPlural(num_removed_nz).c_str(), + // clang-format on + time_taken, factor.min_time_bound_, factor.max_time_bound_, time_limit); + if (num_fictitious_rows_skipped) + highsLogDev(options->log_options, HighsLogType::kInfo, + ", avoiding %d fictitious row%s", + static_cast(num_fictitious_rows_skipped), + highsIntToPlural(num_fictitious_rows_skipped).c_str()); + highsLogUser(options->log_options, HighsLogType::kInfo, "\n"); + } + return returnOk(); } HPresolve::Result HPresolve::removeDependentFreeCols( @@ -6729,7 +7455,7 @@ HPresolve::Result HPresolve::removeDependentFreeCols( return Result::kOk; // Commented out unreachable code - // assert(analysis_.allow_rule_[kPresolveRuleDependentFreeCols]); + // assert(this->allow_rule_[kPresolveRuleDependentFreeCols]); // const bool logging_on = analysis_.logging_on_; // if (logging_on) // analysis_.startPresolveRuleLog(kPresolveRuleDependentFreeCols); @@ -6816,7 +7542,7 @@ HPresolve::Result HPresolve::removeDependentFreeCols( } HPresolve::Result HPresolve::aggregator(HighsPostsolveStack& postsolve_stack) { - assert(analysis_.allow_rule_[kPresolveRuleAggregator]); + assert(this->allow_rule_[kPresolveRuleAggregator]); const bool logging_on = analysis_.logging_on_; if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleAggregator); substitutionOpportunities.erase( @@ -7894,25 +8620,36 @@ void HPresolve::removeFixedCol(HighsInt col, double fixval) { // column upon removing its non-zeros markColDeleted(col); - for (HighsInt coliter = colhead[col]; coliter != -1;) { - HighsInt colrow = Arow[coliter]; - double colval = Avalue[coliter]; - assert(Acol[coliter] == col); + if (this->in_initial_sweep_) { + for (HighsInt iEl = model->a_matrix_.start_[col]; + iEl < model->a_matrix_.start_[col + 1]; iEl++) { + HighsInt colrow = model->a_matrix_.index_[iEl]; + double colval = model->a_matrix_.value_[iEl]; + if (model->row_lower_[colrow] != -kHighsInf) + model->row_lower_[colrow] -= colval * fixval; + if (model->row_upper_[colrow] != kHighsInf) + model->row_upper_[colrow] -= colval * fixval; + } + } else { + for (HighsInt coliter = colhead[col]; coliter != -1;) { + HighsInt colrow = Arow[coliter]; + double colval = Avalue[coliter]; + assert(Acol[coliter] == col); - HighsInt colpos = coliter; - coliter = Anext[coliter]; + HighsInt colpos = coliter; + coliter = Anext[coliter]; - if (model->row_lower_[colrow] != -kHighsInf) - model->row_lower_[colrow] -= colval * fixval; + if (model->row_lower_[colrow] != -kHighsInf) + model->row_lower_[colrow] -= colval * fixval; - if (model->row_upper_[colrow] != kHighsInf) - model->row_upper_[colrow] -= colval * fixval; + if (model->row_upper_[colrow] != kHighsInf) + model->row_upper_[colrow] -= colval * fixval; - unlink(colpos); + unlink(colpos); - reinsertEquation(colrow); + reinsertEquation(colrow); + } } - model->offset_ += model->col_cost_[col] * fixval; assert(std::isfinite(model->offset_)); model->col_cost_[col] = 0; @@ -7970,6 +8707,11 @@ HPresolve::Result HPresolve::presolveChangedCols( changedCols.swap(changedColIndices); for (HighsInt col : changedCols) { if (colDeleted[col]) continue; + size_t num_reductions = postsolve_stack.numReductions(); + if (num_reductions == 35044) { + printf("HPresolve::presolveChangedCols reductions = %d\n", + int(num_reductions)); + } HPRESOLVE_CHECKED_CALL(colPresolve(postsolve_stack, col)); changedColFlag[col] = colDeleted[col]; } @@ -8243,7 +8985,7 @@ HPresolve::Result HPresolve::detectImpliedIntegers() { HPresolve::Result HPresolve::detectParallelRowsAndCols( HighsPostsolveStack& postsolve_stack) { - assert(analysis_.allow_rule_[kPresolveRuleParallelRowsAndCols]); + assert(this->allow_rule_[kPresolveRuleParallelRowsAndCols]); const bool logging_on = analysis_.logging_on_; if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleParallelRowsAndCols); @@ -9128,8 +9870,7 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { postsolve_stack.initializeIndexMaps(lp.num_row_, lp.num_col_); { HPresolve presolve; - presolve.okSetInput(model, options, options.presolve_reduction_limit); - // presolve.setReductionLimit(1622017); + presolve.setInput(model, options, options.presolve_reduction_limit); if (presolve.run(postsolve_stack) != HighsModelStatus::kNotset) return; Highs highs; highs.passModel(model); @@ -9192,14 +9933,14 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { { HPresolve presolve; - presolve.okSetInput(model, options, options.presolve_reduction_limit); + presolve.setInput(model, options, options.presolve_reduction_limit); presolve.computeIntermediateMatrix(flagRow, flagCol, reductionLim); } #if 1 model = lp; model.integrality_.assign(lp.num_col_, HighsVarType::kContinuous); HPresolve presolve; - presolve.okSetInput(model, options, options.presolve_reduction_limit); + presolve.setInput(model, options, options.presolve_reduction_limit); HighsPostsolveStack tmp; tmp.initializeIndexMaps(model.num_row_, model.num_col_); presolve.setReductionLimit(reductionLim); @@ -9276,7 +10017,7 @@ void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { } HPresolve::Result HPresolve::sparsify(HighsPostsolveStack& postsolve_stack) { - assert(analysis_.allow_rule_[kPresolveRuleSparsify]); + assert(this->allow_rule_[kPresolveRuleSparsify]); std::vector sparsifyRows; const bool logging_on = analysis_.logging_on_; if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleSparsify); diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index e46d7297592..7a6cdbd638c 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -41,6 +41,7 @@ class HPresolve { HighsTimer* timer; HighsMipSolver* mipsolver = nullptr; double primal_feastol; + std::vector allow_rule_; // triplet storage std::vector Avalue; @@ -90,9 +91,9 @@ class HPresolve { HighsLinearSumBounds impliedDualRowBounds; std::vector changedRowIndices; - std::vector changedRowFlag; + std::vector changedRowFlag; std::vector changedColIndices; - std::vector changedColFlag; + std::vector changedColFlag; std::vector> substitutionOpportunities; @@ -108,18 +109,19 @@ class HPresolve { bool shrinkProblemEnabled; size_t reductionLimit; + bool in_initial_sweep_; // vectors storing singleton rows and columns std::vector singletonRows; std::vector singletonColumns; // flags to mark rows/columns as deleted - std::vector rowDeleted; - std::vector colDeleted; + std::vector rowDeleted; + std::vector colDeleted; // flags to skip repeated single-equation handling (dual fixing) on unchanged // rows - std::vector singleEquationChecked; + std::vector singleEquationChecked; std::vector numProbes; @@ -142,6 +144,7 @@ class HPresolve { kPrimalInfeasible, kDualInfeasible, kStopped, + kOutOfMemory }; struct StatusResult { @@ -171,6 +174,15 @@ class HPresolve { // private functions for different shared functionality and matrix // modification + bool reducedToEmpty() const { + return numDeletedCols == model->num_col_ && + numDeletedRows == model->num_row_; + } + + bool hasPresolveDataStructures() const { return colDeleted.size() > 0; } + + void chooseRules(); + void link(HighsInt pos); void unlink(HighsInt pos); @@ -317,6 +329,8 @@ class HPresolve { Result checkColBounds(HighsInt col, bool* isFixed = nullptr); + Result checkModelColBounds(HighsInt col, bool& isFixed); + void changeRowDualUpper(HighsInt row, double newUpper); void changeRowDualLower(HighsInt row, double newLower); @@ -365,18 +379,21 @@ class HPresolve { public: // for LP presolve - bool okSetInput(HighsLp& model_, const HighsOptions& options_, - const HighsInt presolve_reduction_limit, - HighsTimer* timer = nullptr); + void setInput(HighsLp& model_, const HighsOptions& options_, + const HighsInt presolve_reduction_limit, + HighsTimer* timer = nullptr); // for MIP presolve - bool okSetInput(HighsMipSolver& mipsolver, - const HighsInt presolve_reduction_limit); + void setInput(HighsMipSolver& mipsolver, + const HighsInt presolve_reduction_limit); void setReductionLimit(size_t reductionLimit) { this->reductionLimit = reductionLimit; } + bool okSetupPresolveDataStructures(); + void setupSubstitutionOpportunities(); + HighsInt numNonzeros() const { return int(Avalue.size() - freeslots.size()); } void shrinkProblem(HighsPostsolveStack& postsolve_stack); @@ -409,18 +426,25 @@ class HPresolve { Result doubletonEq(HighsPostsolveStack& postsolve_stack, HighsInt row, HighsPostsolveStack::RowType rowType); - Result singletonRow(HighsPostsolveStack& postsolve_stack, HighsInt row); + Result singletonRow(HighsPostsolveStack& postsolve_stack, HighsInt row, + const HighsInt col_ = -1, const double val_ = 0); Result emptyCol(HighsPostsolveStack& postsolve_stack, HighsInt col); - Result singletonCol(HighsPostsolveStack& postsolve_stack, HighsInt col); + Result modelEmptyCol(HighsPostsolveStack& postsolve_stack, HighsInt col); + + Result singletonCol(HighsPostsolveStack& postsolve_stack, HighsInt col, + const bool timing = false); void substituteFreeCol(HighsPostsolveStack& postsolve_stack, HighsInt row, HighsInt col, bool relaxRowDualBounds = false); + Result emptyRow(HighsPostsolveStack& postsolve_stack, HighsInt row); + Result rowPresolve(HighsPostsolveStack& postsolve_stack, HighsInt row); - Result colPresolve(HighsPostsolveStack& postsolve_stack, HighsInt col); + Result colPresolve(HighsPostsolveStack& postsolve_stack, HighsInt col, + const bool timing = false); Result detectDominatedCol(HighsPostsolveStack& postsolve_stack, HighsInt col, bool handleSingletonRows = true); @@ -452,6 +476,8 @@ class HPresolve { double boundColValue = kHighsInf, HighsInt boundColCoeffPattern = 0); + Result initialSweep(HighsPostsolveStack& postsolve_stack); + Result initialRowAndColPresolve(HighsPostsolveStack& postsolve_stack); HighsModelStatus run(HighsPostsolveStack& postsolve_stack); diff --git a/highs/presolve/HPresolveAnalysis.cpp b/highs/presolve/HPresolveAnalysis.cpp index 4ecdd3d676e..e5b9d76efa1 100644 --- a/highs/presolve/HPresolveAnalysis.cpp +++ b/highs/presolve/HPresolveAnalysis.cpp @@ -7,64 +7,33 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "lp_data/HighsModelUtils.h" #include "presolve/HPresolve.h" +#include "presolve/PresolveTimer.h" void HPresolveAnalysis::setup(const HighsLp* model_, const HighsOptions* options_, const HighsInt& numDeletedRows_, const HighsInt& numDeletedCols_, - const bool silent) { + HighsTimer* timer) { model = model_; options = options_; numDeletedRows = &numDeletedRows_; numDeletedCols = &numDeletedCols_; - this->allow_rule_.assign(kPresolveRuleCount, true); - - if (options->presolve_rule_off || options_->log_dev_level) { - // Some presolve rules are off - // - // Transform options->presolve_rule_off into logical settings in - // allow_rule_[*], commenting on the rules switched off - if (!silent) { - if (options->presolve_rule_off) { - highsLogUser(options->log_options, HighsLogType::kInfo, - "Presolve rules not allowed:\n"); - } else { - highsLogUser(options->log_options, HighsLogType::kInfo, - "Permitted suppression of presolve rules via " - "presolve_rule_off option:\n"); - } - } - HighsInt bit = 1; - for (HighsInt rule_type = kPresolveRuleMin; rule_type < kPresolveRuleCount; - rule_type++) { - // Identify whether this rule is allowed - const bool allow = !(options->presolve_rule_off & bit); - if (rule_type >= kPresolveRuleFirstAllowOff) { - // This is a rule that can be switched off, so comment - // positively if it is off - allow_rule_[rule_type] = allow; - if (!silent) - if (!allow || - (!options->presolve_rule_off && options_->log_dev_level)) - highsLogUser(options->log_options, HighsLogType::kInfo, - " Rule %2d (set bit %2d = %7d): %s\n", - int(rule_type), int(rule_type), int(bit), - utilPresolveRuleTypeToString(rule_type).c_str()); - } else if (!allow && !silent) { - // This is a rule that cannot be switched off so, if an - // attempt is made, don't allow it to be off and comment - // negatively - highsLogUser(options->log_options, HighsLogType::kWarning, - "Cannot disallow rule %2d (bit %2d = %7d): %s\n", - int(rule_type), int(rule_type), int(bit), - utilPresolveRuleTypeToString(rule_type).c_str()); - } - bit *= 2; - } + timer_ = timer; + const bool lp_presolve = !model_->isMip() || options->solve_relaxation; + analyse_presolve_time_ = + kHighsAnalysisLevelPresolveTime & options->highs_analysis_level && + lp_presolve; + if (analyse_presolve_time_) { + HighsTimerClock clock; + clock.timer_pointer_ = timer_; + PresolveTimer presolve_timer; + presolve_timer.initialisePresolveClocks(clock); + presolve_clocks_ = clock; } - // Allow logging if option is set and model is not a MIP - allow_logging_ = options_->presolve_rule_logging && !model_->isMip(); + + // Allow logging if option is set and LP presolve is being used + allow_logging_ = options_->presolve_rule_logging && lp_presolve; logging_on_ = allow_logging_; log_rule_type_ = kPresolveRuleIllegal; resetNumDeleted(); @@ -91,7 +60,6 @@ void HPresolveAnalysis::startPresolveRuleLog(const HighsInt rule_type) { const bool debug_print = false; assert(logging_on_); assert(rule_type >= kPresolveRuleMin && rule_type <= kPresolveRuleMax); - assert(allow_rule_[rule_type]); // Prevent any future calls to "start" until logging is on again logging_on_ = false; const HighsInt check_rule = kPresolveRuleIllegal; @@ -123,13 +91,6 @@ void HPresolveAnalysis::startPresolveRuleLog(const HighsInt rule_type) { assert(num_deleted_cols0_ == *numDeletedCols); num_deleted_rows0_ = *numDeletedRows; num_deleted_cols0_ = *numDeletedCols; - const int check_num_deleted_rows0_ = -255; - const int check_num_deleted_cols0_ = -688; - if (num_deleted_rows0_ == check_num_deleted_rows0_ && - num_deleted_cols0_ == check_num_deleted_cols0_) { - printf("num_deleted (%d, %d)\n", int(num_deleted_rows0_), - int(num_deleted_cols0_)); - } } void HPresolveAnalysis::stopPresolveRuleLog(const HighsInt rule_type) { @@ -193,8 +154,12 @@ bool HPresolveAnalysis::analysePresolveRuleLog(const bool report) { "%-25s Rows Cols Calls\n", "Presolve rule removed"); highsLogUser(log_options, HighsLogType::kInfo, "%s\n", rule.c_str()); - for (HighsInt rule_type = kPresolveRuleMin; rule_type < kPresolveRuleCount; - rule_type++) + for (HighsInt k = kPresolveRuleMin; k < kPresolveRuleCount; k++) { + HighsInt rule_type = k; + // Hack so that initial logging is of initial sweep + if (kPresolveRuleInitialSweep > 0) { + rule_type = k == 0 ? kPresolveRuleInitialSweep : k - 1; + } if (presolve_log_.rule[rule_type].call || presolve_log_.rule[rule_type].row_removed || presolve_log_.rule[rule_type].col_removed) @@ -203,6 +168,7 @@ bool HPresolveAnalysis::analysePresolveRuleLog(const bool report) { (int)presolve_log_.rule[rule_type].row_removed, (int)presolve_log_.rule[rule_type].col_removed, (int)presolve_log_.rule[rule_type].call); + } highsLogUser(log_options, HighsLogType::kInfo, "%s\n", rule.c_str()); highsLogUser(log_options, HighsLogType::kInfo, "%-25s %9d %9d\n", "Total reductions", (int)sum_removed_row, @@ -237,3 +203,26 @@ bool HPresolveAnalysis::analysePresolveRuleLog(const bool report) { } return true; } + +void HPresolveAnalysis::presolveTimerStart( + const HighsInt presolve_clock) const { + if (!analyse_presolve_time_) return; + HighsInt highs_timer_clock = presolve_clocks_.clock_[presolve_clock]; + presolve_clocks_.timer_pointer_->start(highs_timer_clock); +} + +void HPresolveAnalysis::presolveTimerStop(const HighsInt presolve_clock) const { + if (!analyse_presolve_time_) return; + HighsInt highs_timer_clock = presolve_clocks_.clock_[presolve_clock]; + presolve_clocks_.timer_pointer_->stop(highs_timer_clock); +} + +void HPresolveAnalysis::reportPresolveTimer() { + if (!analyse_presolve_time_) return; + PresolveTimer presolve_timer; + presolve_timer.reportPresolveCoreClock(model->model_name_, presolve_clocks_); + presolve_timer.reportPresolveInitialColPresolveClock(model->model_name_, + presolve_clocks_); + presolve_timer.reportPresolveSingletonColPresolveClock(model->model_name_, + presolve_clocks_); +} diff --git a/highs/presolve/HPresolveAnalysis.h b/highs/presolve/HPresolveAnalysis.h index 9fa48f32d26..707ccd06035 100644 --- a/highs/presolve/HPresolveAnalysis.h +++ b/highs/presolve/HPresolveAnalysis.h @@ -8,13 +8,18 @@ /**@file presolve/HPresolveAnalysis.h * @brief */ -#ifndef PRESOLVE_HIGHS_PRESOLVE_ANALYSIS_H_ -#define PRESOLVE_HIGHS_PRESOLVE_ANALYSIS_H_ +#ifndef PRESOLVE_HPRESOLVEANALYSIS_H_ +#define PRESOLVE_HPRESOLVEANALYSIS_H_ + +#include "util/HighsTimer.h" class HPresolveAnalysis { + public: + HPresolveAnalysis() : timer_(nullptr), analyse_presolve_time_(false) {} + + HighsTimer* timer_; const HighsLp* model; const HighsOptions* options; - const bool* allow_rule; const HighsInt* numDeletedRows; const HighsInt* numDeletedCols; @@ -23,7 +28,7 @@ class HPresolveAnalysis { HighsInt original_num_row_; public: - std::vector allow_rule_; + std::vector allow_rule_; bool allow_logging_; bool logging_on_; @@ -33,20 +38,23 @@ class HPresolveAnalysis { HighsInt num_deleted_cols0_; HighsPresolveLog presolve_log_; - // for LP presolve - // - // Transform options->presolve_rule_off into logical settings in - // allow_rule_[*], commenting on the rules switched off + HighsTimerClock presolve_clocks_; + bool analyse_presolve_time_; + void setup(const HighsLp* model_, const HighsOptions* options_, const HighsInt& numDeletedRows_, const HighsInt& numDeletedCols_, - const bool silent); + HighsTimer* timer); + void setupPresolveTime(const HighsOptions& options); void resetNumDeleted(); std::string presolveReductionTypeToString(const HighsInt reduction_type); void startPresolveRuleLog(const HighsInt rule_type); void stopPresolveRuleLog(const HighsInt rule_type); bool analysePresolveRuleLog(const bool report = false); + void presolveTimerStart(const HighsInt presolve_clock = 0) const; + void presolveTimerStop(const HighsInt presolve_clock = 0) const; + void reportPresolveTimer(); friend class HPresolve; }; -#endif +#endif /* PRESOLVE_HPRESOLVEANALYSIS_H_ */ diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index e2a14d098b1..d5392d11409 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -38,30 +38,19 @@ void HighsPostsolveStack::initializeIndexMaps(HighsInt numRow, void HighsPostsolveStack::compressIndexMaps( const std::vector& newRowIndex, const std::vector& newColIndex) { - // loop over rows, decrease row counter for deleted rows (marked with -1), - // store original index at new index position otherwise - HighsInt numRow = origRowIndex.size(); - for (size_t i = 0; i != newRowIndex.size(); ++i) { - if (newRowIndex[i] == -1) - --numRow; - else { - origRowIndex[newRowIndex[i]] = origRowIndex[i]; - origRowType[newRowIndex[i]] = origRowType[i]; - } - } - // resize original index array to new size - origRowIndex.resize(numRow); - origRowType.resize(numRow); + compressColIndexMap(newColIndex); + compressRowIndexMap(newRowIndex); +} - // now compress the column array - HighsInt numCol = origColIndex.size(); - for (size_t i = 0; i != newColIndex.size(); ++i) { - if (newColIndex[i] == -1) - --numCol; - else - origColIndex[newColIndex[i]] = origColIndex[i]; - } - origColIndex.resize(numCol); +void HighsPostsolveStack::compressRowIndexMap( + const std::vector& newRowIndex) { + compressIndexMap(newRowIndex, this->origRowIndex); + compressIndexMap(newRowIndex, this->origRowType); +} + +void HighsPostsolveStack::compressColIndexMap( + const std::vector& newColIndex) { + compressIndexMap(newColIndex, this->origColIndex); } void HighsPostsolveStack::LinearTransform::undo(const HighsOptions& options, @@ -390,8 +379,8 @@ void HighsPostsolveStack::ForcingColumnRemovedRow::undo( void HighsPostsolveStack::SingletonRow::undo( const HighsPostsolveStack& postsolveStack, const HighsOptions& options, HighsSolution& solution, HighsBasis& basis) const { - // nothing to do if the rows dual value is zero in the dual solution or - // there is no dual solution + // nothing to do if the row's dual value is zero in the dual + // solution or there is no dual solution if (!solution.dual_valid) return; const HighsBasisStatus colStatus = diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 90b7b505270..9feea8465d6 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -23,6 +23,7 @@ #include "lp_data/HConst.h" #include "lp_data/HStruct.h" +#include "lp_data/HighsModelUtils.h" #include "lp_data/HighsOptions.h" #include "util/HighsCDouble.h" #include "util/HighsDataStack.h" @@ -356,7 +357,7 @@ class HighsPostsolveStack { std::vector origColIndex; std::vector origRowIndex; std::vector origRowType; - std::vector linearlyTransformable; + std::vector linearlyTransformable; std::vector rowValues; std::vector colValues; @@ -451,6 +452,22 @@ class HighsPostsolveStack { void compressIndexMaps(const std::vector& newRowIndex, const std::vector& newColIndex); + void compressRowIndexMap(const std::vector& newRowIndex); + void compressColIndexMap(const std::vector& newColIndex); + + template + void compressIndexMap(const std::vector& newIndex, + std::vector& origIndex) { + HighsInt numEn = origIndex.size(); + for (size_t i = 0; i != newIndex.size(); ++i) { + if (newIndex[i] == -1) + --numEn; + else + origIndex[newIndex[i]] = origIndex[i]; + } + origIndex.resize(numEn); + } + /// transform a column x by a linear mapping with a new column x'. /// I.e. substitute x = scale * x' + constant void linearTransform(HighsInt col, double scale, double constant) { @@ -600,6 +617,19 @@ class HighsPostsolveStack { reductionAdded(ReductionType::kFixedCol); } + void removedModelFixedCol(HighsInt col, double fixValue, double colCost, + HighsInt col_nnz, HighsInt* index, double* value) { + assert(std::isfinite(fixValue)); + colValues.clear(); + for (HighsInt iEl = 0; iEl < col_nnz; iEl++) + colValues.emplace_back(origRowIndex[index[iEl]], value[iEl]); + + reductionValues.push(FixedCol{fixValue, colCost, origColIndex[col], + HighsBasisStatus::kNonbasic}); + reductionValues.push(colValues); + reductionAdded(ReductionType::kFixedCol); + } + void redundantRow(HighsInt row) { reductionValues.push(RedundantRow{origRowIndex[row]}); reductionAdded(ReductionType::kRedundantRow); @@ -863,13 +893,13 @@ class HighsPostsolveStack { bool isColLinearlyTransformable(HighsInt col) const { assert(col >= 0); assert(static_cast(col) < origColIndex.size()); - return (linearlyTransformable[origColIndex[col]] != 0); + return linearlyTransformable[origColIndex[col]]; } template void undoIterateBackwards(std::vector& values, const std::vector& index, - HighsInt origSize) { + HighsInt origSize, T zero) { values.resize(origSize); #ifdef DEBUG_EXTRA // Fill vector with NaN for debugging purposes @@ -881,10 +911,13 @@ class HighsPostsolveStack { } std::copy(valuesNew.cbegin(), valuesNew.cend(), values.begin()); #else + for (size_t i = index.size(); i < static_cast(origSize); i++) + values[i] = zero; for (size_t i = index.size(); i > 0; --i) { - assert(static_cast(index[i - 1]) >= i - 1); - values[index[i - 1]] = values[i - 1]; - if (index[i - 1] != static_cast(i - 1)) values[i - 1] = T{}; + size_t to_i = static_cast(index[i - 1]); + assert(to_i >= i - 1); + values[to_i] = values[i - 1]; + if (to_i > i - 1) values[i - 1] = zero; } #endif } @@ -923,32 +956,93 @@ class HighsPostsolveStack { // expand solution to original index space assert(nextColIndex > 0); - undoIterateBackwards(solution.col_value, origColIndex, nextColIndex); + undoIterateBackwards(solution.col_value, origColIndex, nextColIndex, 0.0); assert(nextRowIndex >= 0); - undoIterateBackwards(solution.row_value, origRowIndex, nextRowIndex); + undoIterateBackwards(solution.row_value, origRowIndex, nextRowIndex, 0.0); if (perform_dual_postsolve) { // if dual solution is given, expand dual solution and basis to original // index space - undoIterateBackwards(solution.col_dual, origColIndex, nextColIndex); + undoIterateBackwards(solution.col_dual, origColIndex, nextColIndex, 0.0); - undoIterateBackwards(solution.row_dual, origRowIndex, nextRowIndex); + undoIterateBackwards(solution.row_dual, origRowIndex, nextRowIndex, 0.0); } if (perform_basis_postsolve) { // if basis is given, expand basis status values to original index space - undoIterateBackwards(basis.col_status, origColIndex, nextColIndex); + undoIterateBackwards(basis.col_status, origColIndex, nextColIndex, + HighsBasisStatus::kNonbasic); - undoIterateBackwards(basis.row_status, origRowIndex, nextRowIndex); + undoIterateBackwards(basis.row_status, origRowIndex, nextRowIndex, + HighsBasisStatus::kNonbasic); } + /* + // Initialise to illegal values so that initial values are logged + double report_col_value = kHighsInf; + double report_col_dual = kHighsInf; + HighsBasisStatus report_col_status = HighsBasisStatus::kNonbasic; + size_t check_reduction = -kHighsIinf; + + auto solutionLogging = [&](const std::string& message) { + printf("\n%s\n", message.c_str()); + for (HighsInt iCol = 0; iCol < origNumCol; iCol++) + printf("Col %9d value = %11.4g; dual = %11.4g; status = %s\n", + int(iCol), solution.col_value[iCol], solution.col_dual[iCol], + utilBasisStatusToString(basis.col_status[iCol]).c_str()); + for (HighsInt iRow = 0; iRow < origNumRow; iRow++) + printf("Row %9d value = %11.4g; dual = %11.4g; status = %s\n", + int(iRow), solution.row_value[iRow], solution.row_dual[iRow], + utilBasisStatusToString(basis.row_status[iRow]).c_str()); + }; + + auto reportColLogging = [&](const HighsInt reduction) { + assert(report_col >= 0); + double col_value = solution.col_value[report_col]; + double col_dual = solution.dual_valid ? solution.col_dual[report_col] : 0; + HighsBasisStatus col_status = basis.valid ? basis.col_status[report_col] + : HighsBasisStatus::kNonbasic; + bool report = col_value != report_col_value; + if (solution.dual_valid) report = report || col_dual != report_col_dual; + if (basis.valid) report = report || col_status != report_col_status; + if (reduction >= 0) { + if (report) + printf("After reduction %9d (type %2d):", int(reduction), + int(reductions[reduction].first)); + } else if (reduction == -1) { + report = true; + printf("Before undo: "); + } else { + report = true; + printf("After last reduction: "); + } + if (!report) return; + printf(" Col %7d value = %11.4g", int(report_col), col_value); + if (solution.dual_valid) printf(", dual = %11.4g", col_dual); + if (basis.valid) + printf(" status = %s", utilBasisStatusToString(col_status).c_str()); + printf("\n"); + report_col_value = col_value; + report_col_dual = col_dual; + report_col_status = col_status; + }; + if (report_col >= 0) reportColLogging(-1); + if (reductions.size() == check_reduction) + solutionLogging("After solving presolved LP"); + */ // now undo the changes for (size_t i = reductions.size(); i > numReductions; --i) { if (report_col >= 0) printf("Before reduction %2d (type %2d): col_value[%2d] = %g\n", int(i - 1), int(reductions[i - 1].first), int(report_col), solution.col_value[report_col]); + /* + if (i - 1 == check_reduction) { + printf("Checking reduction %d\n", int(check_reduction)); + solutionLogging("In reductions loop"); + } + */ switch (reductions[i - 1].first) { case ReductionType::kLinearTransform: { LinearTransform reduction; @@ -1072,10 +1166,9 @@ class HighsPostsolveStack { int(reductions[i - 1].first)); if (kAllowDeveloperAssert) assert(1 == 0); } + // if (report_col >= 0) reportColLogging(i - 1); } - if (report_col >= 0) - printf("After last reduction: col_value[%2d] = %g\n", int(report_col), - solution.col_value[report_col]); + // if (report_col >= 0) reportColLogging(-2); solution.col_value.resize(origNumCol); if (perform_dual_postsolve) solution.col_dual.resize(origNumCol); diff --git a/highs/presolve/HighsSymmetry.cpp b/highs/presolve/HighsSymmetry.cpp index 2820fd6002f..5c52b6f0bb2 100644 --- a/highs/presolve/HighsSymmetry.cpp +++ b/highs/presolve/HighsSymmetry.cpp @@ -729,7 +729,7 @@ HighsInt HighsOrbitopeMatrix::orbitalFixingForFullOrbitope( HighsInt HighsOrbitopeMatrix::orbitalFixing(HighsDomain& domain) const { std::vector rows; - std::vector rowUsed(numRows); + std::vector rowUsed(numRows); rows.reserve(numRows); diff --git a/highs/presolve/HighsSymmetry.h b/highs/presolve/HighsSymmetry.h index 3aac69b57b1..53c96a644dc 100644 --- a/highs/presolve/HighsSymmetry.h +++ b/highs/presolve/HighsSymmetry.h @@ -20,7 +20,7 @@ #include "lp_data/HighsLp.h" #include "util/HighsDisjointSets.h" #include "util/HighsHash.h" -#include "util/HighsInt.h" +#include "util/HighsType.h" /// class that is responsible for assigning distinct colors for each distinct /// double value @@ -182,7 +182,7 @@ class HighsSymmetryDetection { std::vector orbitSize; std::vector cellCreationStack; - std::vector cellInRefinementQueue; + std::vector cellInRefinementQueue; std::vector refinementQueue; std::vector distinguishCands; std::vector automorphisms; diff --git a/highs/presolve/PresolveComponent.cpp b/highs/presolve/PresolveComponent.cpp index dcaa16d7364..91bc1381ff5 100644 --- a/highs/presolve/PresolveComponent.cpp +++ b/highs/presolve/PresolveComponent.cpp @@ -30,15 +30,12 @@ void PresolveComponent::negateReducedLpColDuals() { HighsPresolveStatus PresolveComponent::run() { presolve::HPresolve presolve; - if (!presolve.okSetInput(data_.reduced_lp_, *options_, - options_->presolve_reduction_limit, timer)) { - presolve_status_ = HighsPresolveStatus::kOutOfMemory; - return presolve_status_; - } - + presolve.setInput(data_.reduced_lp_, *options_, + options_->presolve_reduction_limit, timer); presolve.run(data_.postSolveStack); - data_.presolve_log_ = presolve.getPresolveLog(); presolve_status_ = presolve.getPresolveStatus(); + if (presolve_status_ != HighsPresolveStatus::kOutOfMemory) + data_.presolve_log_ = presolve.getPresolveLog(); return presolve_status_; } diff --git a/highs/presolve/PresolveTimer.h b/highs/presolve/PresolveTimer.h new file mode 100644 index 00000000000..923e434e368 --- /dev/null +++ b/highs/presolve/PresolveTimer.h @@ -0,0 +1,249 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* */ +/* This file is part of the HiGHS linear optimization suite */ +/* */ +/* Available as open-source under the MIT License */ +/* */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/**@file presolve/PresolveTimer.h + * @brief Indices of presolve iClocks + */ +#ifndef PRESOLVE_PRESOLVETIMER_H_ +#define PRESOLVE_PRESOLVETIMER_H_ + +// Clocks for profiling presolve +enum iClockPresolve { + kPresolveClockTotal = 0, + kPresolveClockPresolve, + kPresolveClockInitialSweep, + kPresolveClockSetupResize, + kPresolveClockSetupToCsc, + kPresolveClockSetupSubstitutionOpportunities, + kPresolveClockInitial, + kPresolveClockInitialRow, + kPresolveClockInitialCol, + kPresolveClockInitialColIsFixed, + kPresolveClockInitialColIsEmpty, + kPresolveClockInitialColIsSingleton, + kPresolveClockInitialColDominated, + kPresolveClockInitialColImpliedInteger, + kPresolveClockInitialColDualFixing, + kPresolveClockInitialColSingletonStuffing, + kPresolveClockSingletonColSingletonRow, + kPresolveClockSingletonColDominated, + kPresolveClockSingletonColDualFixing, + kPresolveClockSingletonColStuffing, + kPresolveClockSingletonColImpliedBounds, + kPresolveClockSingletonColRowDualImpliedBounds, + kPresolveClockSingletonColDualImpliedFree, + kPresolveClockFastLoop, + kPresolveClockFastLoopRowSingletons, + kPresolveClockFastLoopColSingletons, + kPresolveClockFastLoopDoubletonEquations, + kPresolveClockFastLoopChangedRows, + kPresolveClockFastLoopChangedCols, + kPresolveClockAggregator, + kPresolveClockSparsify, + kPresolveClockParallelRowsAndCols, + kPresolveClockDependentEquations, + kPresolveClockDependentFreeCol, + kPresolveClockShrinkProblem, + // kPresolveClock@, + kNumPresolveClock //!< Number of PRESOLVE clocks +}; + +static const double kPresolveClockTolerancePercentReport = 0.1; + +class PresolveTimer { + public: + void initialisePresolveClocks(HighsTimerClock& presolve_timer_clock) { + HighsTimer* timer_pointer = presolve_timer_clock.timer_pointer_; + std::vector& clock = presolve_timer_clock.clock_; + + clock.resize(kNumPresolveClock); + clock[kPresolveClockTotal] = 0; + clock[kPresolveClockPresolve] = timer_pointer->clock_def("Presolve"); + clock[kPresolveClockInitialSweep] = + timer_pointer->clock_def("Initial sweep"); + clock[kPresolveClockSetupResize] = + timer_pointer->clock_def("Setup: resize"); + clock[kPresolveClockSetupToCsc] = timer_pointer->clock_def("Setup: to CSC"); + clock[kPresolveClockSetupSubstitutionOpportunities] = + timer_pointer->clock_def("Setup substitution opportunities"); + clock[kPresolveClockInitial] = timer_pointer->clock_def("Initial"); + clock[kPresolveClockInitialRow] = timer_pointer->clock_def("Initial row"); + clock[kPresolveClockInitialCol] = timer_pointer->clock_def("Initial col"); + clock[kPresolveClockInitialColIsFixed] = + timer_pointer->clock_def("I-col: is fixed"); + clock[kPresolveClockInitialColIsEmpty] = + timer_pointer->clock_def("I-col: is empty"); + clock[kPresolveClockInitialColIsSingleton] = + timer_pointer->clock_def("I-col: is singleton"); + clock[kPresolveClockInitialColDominated] = + timer_pointer->clock_def("I-col: dominated"); + clock[kPresolveClockInitialColImpliedInteger] = + timer_pointer->clock_def("I-col: implied integer"); + clock[kPresolveClockInitialColDualFixing] = + timer_pointer->clock_def("I-col: dual fixing"); + clock[kPresolveClockInitialColSingletonStuffing] = + timer_pointer->clock_def("I-col: singleton stuffing"); + clock[kPresolveClockSingletonColSingletonRow] = + timer_pointer->clock_def("S-col: singleton row"); + clock[kPresolveClockSingletonColDominated] = + timer_pointer->clock_def("S-col: dominated"); + clock[kPresolveClockSingletonColDualFixing] = + timer_pointer->clock_def("S-col: dual fixing"); + clock[kPresolveClockSingletonColStuffing] = + timer_pointer->clock_def("S-col: singleton stuffing"); + clock[kPresolveClockSingletonColImpliedBounds] = + timer_pointer->clock_def("S-col: impl bounds"); + clock[kPresolveClockSingletonColRowDualImpliedBounds] = + timer_pointer->clock_def("S-col: row dual impl bounds"); + clock[kPresolveClockSingletonColDualImpliedFree] = + timer_pointer->clock_def("S-col: dual impl free"); + clock[kPresolveClockFastLoop] = timer_pointer->clock_def("Fast loop"); + clock[kPresolveClockFastLoopRowSingletons] = + timer_pointer->clock_def("Fast loop: row singletons"); + clock[kPresolveClockFastLoopColSingletons] = + timer_pointer->clock_def("Fast loop: col singletons"); + clock[kPresolveClockFastLoopDoubletonEquations] = + timer_pointer->clock_def("Fast loop: doubleton equations"); + clock[kPresolveClockFastLoopChangedRows] = + timer_pointer->clock_def("Fast loop: changed rows"); + clock[kPresolveClockFastLoopChangedCols] = + timer_pointer->clock_def("Fast loop: changed cols"); + clock[kPresolveClockAggregator] = timer_pointer->clock_def("Aggregator"); + clock[kPresolveClockSparsify] = timer_pointer->clock_def("Sparsify"); + clock[kPresolveClockParallelRowsAndCols] = + timer_pointer->clock_def("Parallel rows and cols"); + clock[kPresolveClockDependentEquations] = + timer_pointer->clock_def("Dependent equations"); + clock[kPresolveClockDependentFreeCol] = + timer_pointer->clock_def("Dependent free columns"); + clock[kPresolveClockShrinkProblem] = + timer_pointer->clock_def("Shrink problem"); + // clock[kPresolveClock@] = timer_pointer->clock_def("@"); + }; + + bool reportPresolveClockList( + const char* grepStamp, const std::vector presolve_clock_list, + const HighsTimerClock& presolve_timer_clock, + const HighsInt kPresolveClockIdeal = kPresolveClockPresolve, + const double tolerance_percent_report_ = -1) { + HighsTimer* timer_pointer = presolve_timer_clock.timer_pointer_; + if (!timer_pointer->printf_flag) return false; + const std::vector& clock = presolve_timer_clock.clock_; + HighsInt presolve_clock_list_size = presolve_clock_list.size(); + std::vector clockList; + clockList.resize(presolve_clock_list_size); + for (HighsInt en = 0; en < presolve_clock_list_size; en++) { + clockList[en] = clock[presolve_clock_list[en]]; + } + const double ideal_sum_time = + timer_pointer->clock_time[clock[kPresolveClockIdeal]]; + const double tolerance_percent_report = + tolerance_percent_report_ >= 0 ? tolerance_percent_report_ : 1e-8; + return timer_pointer->reportOnTolerance( + grepStamp, clockList, ideal_sum_time, tolerance_percent_report); + }; + + void csvPresolveClockList(const std::string& grep_query, + const std::string& model_name, + const std::vector presolve_clock_list, + const HighsTimerClock& presolve_timer_clock, + const HighsInt kPresolveClockIdeal, + const bool header, const bool end_line) { + HighsTimer* timer_pointer = presolve_timer_clock.timer_pointer_; + if (!timer_pointer->printf_flag) return; + const std::vector& clock = presolve_timer_clock.clock_; + const double ideal_sum_time = + timer_pointer->clock_time[clock[kPresolveClockIdeal]]; + if (ideal_sum_time < 1e-2) return; + const HighsInt num_clock = presolve_clock_list.size(); + if (header) { + printf("grep_%s,model,ideal", grep_query.c_str()); + for (HighsInt iX = 0; iX < num_clock; iX++) { + HighsInt iclock = clock[presolve_clock_list[iX]]; + printf(",%s", timer_pointer->clock_names[iclock].c_str()); + } + printf(",Unaccounted"); + if (end_line) printf("\n"); + return; + } + double sum_time = 0; + printf("grep_%s,%s,%11.4g", grep_query.c_str(), model_name.c_str(), + ideal_sum_time); + for (HighsInt iX = 0; iX < num_clock; iX++) { + HighsInt iclock = clock[presolve_clock_list[iX]]; + double time = timer_pointer->read(iclock); + sum_time += time; + printf(",%11.4g", time); + } + printf(",%11.4g", ideal_sum_time - sum_time); + if (end_line) printf("\n"); + } + + void reportPresolveCoreClock(const std::string& model_name, + const HighsTimerClock& presolve_timer_clock) { + const std::vector presolve_clock_list{ + kPresolveClockInitialSweep, kPresolveClockSetupResize, + kPresolveClockSetupToCsc, kPresolveClockSetupSubstitutionOpportunities, + // kPresolveClockInitial, + kPresolveClockInitialRow, kPresolveClockInitialCol, + // kPresolveClockFastLoop, + kPresolveClockFastLoopRowSingletons, + kPresolveClockFastLoopColSingletons, + kPresolveClockFastLoopDoubletonEquations, + kPresolveClockFastLoopChangedRows, kPresolveClockFastLoopChangedCols, + kPresolveClockAggregator, kPresolveClockSparsify, + kPresolveClockParallelRowsAndCols, kPresolveClockDependentEquations, + kPresolveClockDependentFreeCol, kPresolveClockShrinkProblem + // kPresolveClock@ + }; + reportPresolveClockList("PresolveCore_", presolve_clock_list, + presolve_timer_clock, kPresolveClockPresolve, 0.1); + const bool csv_output = false; + if (csv_output) { + csvPresolveClockList("GrepPresolveCore_", model_name, presolve_clock_list, + presolve_timer_clock, kPresolveClockPresolve, true, + true); + csvPresolveClockList("GrepPresolveCore_", model_name, presolve_clock_list, + presolve_timer_clock, kPresolveClockPresolve, false, + true); + } + }; + + void reportPresolveInitialColPresolveClock( + const std::string& model_name, + const HighsTimerClock& presolve_timer_clock) { + const std::vector presolve_clock_list{ + kPresolveClockInitialColIsFixed, + kPresolveClockInitialColIsEmpty, + kPresolveClockInitialColIsSingleton, + kPresolveClockInitialColDominated, + kPresolveClockInitialColImpliedInteger, + kPresolveClockInitialColDualFixing, + kPresolveClockInitialColSingletonStuffing}; + reportPresolveClockList("PresolveInitialCol_", presolve_clock_list, + presolve_timer_clock, kPresolveClockInitialCol, + 0.1); + }; + + void reportPresolveSingletonColPresolveClock( + const std::string& model_name, + const HighsTimerClock& presolve_timer_clock) { + const std::vector presolve_clock_list{ + kPresolveClockSingletonColSingletonRow, + kPresolveClockSingletonColDominated, + kPresolveClockSingletonColDualFixing, + kPresolveClockSingletonColStuffing, + kPresolveClockSingletonColImpliedBounds, + kPresolveClockSingletonColRowDualImpliedBounds, + kPresolveClockSingletonColDualImpliedFree}; + reportPresolveClockList("PresolveSingletonCol_", presolve_clock_list, + presolve_timer_clock, + kPresolveClockInitialColIsSingleton, 0.1); + }; +}; + +#endif /* PRESOLVE_PRESOLVETIMER_H_ */ diff --git a/highs/qpsolver/qpvector.hpp b/highs/qpsolver/qpvector.hpp index 1d5a85b6ed9..34d6d7ff6fc 100644 --- a/highs/qpsolver/qpvector.hpp +++ b/highs/qpsolver/qpvector.hpp @@ -8,7 +8,7 @@ #ifndef __SRC_LIB_VECTOR_HPP__ #define __SRC_LIB_VECTOR_HPP__ -#include +#include #include #include diff --git a/highs/simplex/HEkk.cpp b/highs/simplex/HEkk.cpp index 985872fe96c..4e7fb62e0c6 100644 --- a/highs/simplex/HEkk.cpp +++ b/highs/simplex/HEkk.cpp @@ -453,7 +453,7 @@ HighsStatus HEkk::dualize() { assert(lp_.a_matrix_.isColwise()); original_num_col_ = lp_.num_col_; original_num_row_ = lp_.num_row_; - original_num_nz_ = lp_.a_matrix_.numNz(); + original_num_nz_ = lp_.numNz(); original_offset_ = lp_.offset_; original_col_cost_ = lp_.col_cost_; original_col_lower_ = lp_.col_lower_; @@ -959,7 +959,7 @@ HighsStatus HEkk::undualize() { // Some sanity checks assert(lp_.num_col_ == original_num_col_); assert(lp_.num_row_ == original_num_row_); - assert(lp_.a_matrix_.numNz() == original_num_nz_); + assert(lp_.numNz() == original_num_nz_); HighsInt num_basic_variables = primal_basic_index.size(); bool num_basic_variables_ok = num_basic_variables == original_num_row_; if (!num_basic_variables_ok) diff --git a/highs/simplex/HEkkDualRHS.cpp b/highs/simplex/HEkkDualRHS.cpp index 622c6458a7c..982b592af19 100644 --- a/highs/simplex/HEkkDualRHS.cpp +++ b/highs/simplex/HEkkDualRHS.cpp @@ -386,10 +386,10 @@ void HEkkDualRHS::updateInfeasList(HVector* column) { // The regular sparse way for (HighsInt i = 0; i < columnCount; i++) { HighsInt iRow = variable_index[i]; - if (workMark[iRow] == 0) { + if (!workMark[iRow]) { if (work_infeasibility[iRow]) { workIndex[workCount++] = iRow; - workMark[iRow] = 1; + workMark[iRow] = true; } } } @@ -397,10 +397,10 @@ void HEkkDualRHS::updateInfeasList(HVector* column) { // The hyper sparse way for (HighsInt i = 0; i < columnCount; i++) { HighsInt iRow = variable_index[i]; - if (workMark[iRow] == 0) { + if (!workMark[iRow]) { if (work_infeasibility[iRow] > edge_weight[iRow] * workCutoff) { workIndex[workCount++] = iRow; - workMark[iRow] = 1; + workMark[iRow] = true; } } } @@ -438,12 +438,12 @@ void HEkkDualRHS::createInfeasList(double columnDensity) { double* dwork = ekk_instance_.scattered_dual_edge_weight_.data(); // 1. Build the full list - fill_n(workMark.data(), numRow, 0); + fill_n(workMark.data(), numRow, false); workCount = 0; workCutoff = 0; for (HighsInt iRow = 0; iRow < numRow; iRow++) { if (work_infeasibility[iRow]) { - workMark[iRow] = 1; + workMark[iRow] = true; workIndex[workCount++] = iRow; } } @@ -465,12 +465,12 @@ void HEkkDualRHS::createInfeasList(double columnDensity) { workCutoff = min(maxMerit * 0.99999, cutMerit * 1.00001); // Create again - fill_n(workMark.data(), numRow, 0); + fill_n(workMark.data(), numRow, false); workCount = 0; for (HighsInt iRow = 0; iRow < numRow; iRow++) { if (work_infeasibility[iRow] >= edge_weight[iRow] * workCutoff) { workIndex[workCount++] = iRow; - workMark[iRow] = 1; + workMark[iRow] = true; } } @@ -484,7 +484,7 @@ void HEkkDualRHS::createInfeasList(double columnDensity) { if (work_infeasibility[iRow] > edge_weight[iRow] * cutMerit) { workIndex[workCount++] = iRow; } else { - workMark[iRow] = 0; + workMark[iRow] = false; } } } diff --git a/highs/simplex/HEkkDualRHS.h b/highs/simplex/HEkkDualRHS.h index 8aadcfa9c76..bd7f8932b2f 100644 --- a/highs/simplex/HEkkDualRHS.h +++ b/highs/simplex/HEkkDualRHS.h @@ -116,8 +116,8 @@ class HEkkDualRHS { //!< infeasibilities HighsInt workCount; //!< Number of rows in list with greatest primal //!< infeasibilities - std::vector workMark; //!< Flag set if row is in list of those with - //!< greatest primal infeasibilities + std::vector workMark; //!< Flag set if row is in list of those + //!< with greatest primal infeasibilities std::vector workIndex; //!< List of rows with greatest primal infeasibilities std::vector work_infeasibility; diff --git a/highs/simplex/HSimplexNla.cpp b/highs/simplex/HSimplexNla.cpp index 4c9d6d6961b..bafdf8056bb 100644 --- a/highs/simplex/HSimplexNla.cpp +++ b/highs/simplex/HSimplexNla.cpp @@ -478,7 +478,7 @@ HighsDebugStatus HSimplexNla::debugCheckData(const std::string message) const { assert(!error_found); return HighsDebugStatus::kLogicalError; } - HighsInt nnz = check_lp.a_matrix_.numNz(); + HighsInt nnz = check_lp.numNz(); HighsInt error_el = -1; for (HighsInt iEl = 0; iEl < nnz; iEl++) { if (check_lp.a_matrix_.index_[iEl] != factor_Aindex[iEl]) { diff --git a/highs/simplex/HSimplexNlaProductForm.cpp b/highs/simplex/HSimplexNlaProductForm.cpp index ea2a4d04d2a..f282c92f55a 100644 --- a/highs/simplex/HSimplexNlaProductForm.cpp +++ b/highs/simplex/HSimplexNlaProductForm.cpp @@ -87,8 +87,8 @@ void ProductFormUpdate::ftran(HVector& rhs) const { // list. If RHS fill-in occurs in a row, then we have to add it to // the list. We're not tracking cancellation, so we don't need to // know where a row appears in the list - vector& in_index = rhs.cwork; - for (HighsInt iX = 0; iX < rhs.count; iX++) in_index[rhs.index[iX]] = 1; + vector& in_index = rhs.cwork; + for (HighsInt iX = 0; iX < rhs.count; iX++) in_index[rhs.index[iX]] = true; for (HighsInt iX = 0; iX < update_count_; iX++) { const HighsInt pivot_index = pivot_index_[iX]; @@ -101,13 +101,13 @@ void ProductFormUpdate::ftran(HVector& rhs) const { HighsInt iRow = index_[iEl]; rhs.array[iRow] -= pivot_value * value_[iEl]; if (in_index[iRow]) continue; - in_index[iRow] = 1; + in_index[iRow] = true; rhs.index[rhs.count++] = iRow; } } else { rhs.array[pivot_index] = 0; } } - // Zero the in_index entries used to point into the index list - for (HighsInt iX = 0; iX < rhs.count; iX++) in_index[rhs.index[iX]] = 0; + // Reset the in_index entries used to point into the index list + for (HighsInt iX = 0; iX < rhs.count; iX++) in_index[rhs.index[iX]] = false; } diff --git a/highs/simplex/HighsSimplexAnalysis.cpp b/highs/simplex/HighsSimplexAnalysis.cpp index 1d24b7b2fba..1e9f1aba809 100644 --- a/highs/simplex/HighsSimplexAnalysis.cpp +++ b/highs/simplex/HighsSimplexAnalysis.cpp @@ -1227,10 +1227,10 @@ void HighsSimplexAnalysis::updateInvertFormData(const HFactor& factor) { HighsInt kernel_invert_num_el = factor.invert_num_el - - (factor.basis_matrix_num_el - factor.kernel_num_el); + (factor.basis_matrix_num_el - factor.kernel_num_el) - factor.kernel_dim; assert(factor.kernel_num_el); - double kernel_fill_factor = - (1.0 * kernel_invert_num_el) / factor.kernel_num_el; + double kernel_fill_factor = (1.0 * kernel_invert_num_el) / + (factor.kernel_num_el + factor.kernel_dim); sum_kernel_fill_factor += kernel_fill_factor; running_average_kernel_fill_factor = 0.95 * running_average_kernel_fill_factor + 0.05 * kernel_fill_factor; diff --git a/highs/simplex/SimplexConst.h b/highs/simplex/SimplexConst.h index cc69fcb2ace..dd54beb661b 100644 --- a/highs/simplex/SimplexConst.h +++ b/highs/simplex/SimplexConst.h @@ -11,7 +11,7 @@ #ifndef SIMPLEX_SIMPLEXCONST_H_ #define SIMPLEX_SIMPLEXCONST_H_ -#include "util/HighsInt.h" +#include "util/HighsType.h" enum class SimplexAlgorithm { kNone = 0, kPrimal, kDual }; diff --git a/highs/test_kkt/KktCh2.h b/highs/test_kkt/KktCh2.h index c4be01bc4a2..34435dd1dd2 100644 --- a/highs/test_kkt/KktCh2.h +++ b/highs/test_kkt/KktCh2.h @@ -21,7 +21,7 @@ #include "lp_data/HConst.h" #include "test_kkt/DevKkt.h" -#include "util/HighsInt.h" +#include "util/HighsType.h" namespace presolve { diff --git a/highs/util/HFactor.cpp b/highs/util/HFactor.cpp index 31a16af2540..ebe7b101bcd 100644 --- a/highs/util/HFactor.cpp +++ b/highs/util/HFactor.cpp @@ -70,7 +70,7 @@ static void solveHyper(const HighsInt h_size, const HighsInt* h_lookup, // Take count // Build list - char* list_mark = rhs->cwork.data(); + HighsBool* list_mark = rhs->cwork.data(); HighsInt* list_index = rhs->iwork.data(); HighsInt* list_stack = &rhs->iwork[h_size]; HighsInt list_count = 0; @@ -89,13 +89,13 @@ static void solveHyper(const HighsInt h_size, const HighsInt* h_lookup, HighsInt Hk = h_start[Hi]; // H matrix non zero position HighsInt n_stack = -1; // Usage of the stack (-1 not used) - list_mark[Hi] = 1; // Mark this as touched + list_mark[Hi] = true; // Mark this as touched for (;;) { if (Hk < h_end[Hi]) { HighsInt Hi_sub = h_lookup[h_index[Hk++]]; - if (list_mark[Hi_sub] == 0) { // Go to a child - list_mark[Hi_sub] = 1; // Mark as touched + if (!list_mark[Hi_sub]) { // Go to a child + list_mark[Hi_sub] = true; // Mark as touched list_stack[++n_stack] = Hi; // Store current into stack list_stack[++n_stack] = Hk; Hi = Hi_sub; // Replace current with child @@ -122,7 +122,7 @@ static void solveHyper(const HighsInt h_size, const HighsInt* h_lookup, rhs_count = 0; for (HighsInt iList = list_count - 1; iList >= 0; iList--) { HighsInt i = list_index[iList]; - list_mark[i] = 0; + list_mark[i] = false; HighsInt pivotRow = h_pivot_index[i]; double pivot_multiplier = rhs_array[pivotRow]; if (fabs(pivot_multiplier) > kHighsTiny) { @@ -139,7 +139,7 @@ static void solveHyper(const HighsInt h_size, const HighsInt* h_lookup, rhs_count = 0; for (HighsInt iList = list_count - 1; iList >= 0; iList--) { HighsInt i = list_index[iList]; - list_mark[i] = 0; + list_mark[i] = false; HighsInt pivotRow = h_pivot_index[i]; double pivot_multiplier = rhs_array[pivotRow]; if (fabs(pivot_multiplier) > kHighsTiny) { @@ -281,7 +281,7 @@ void HFactor::setupGeneral( mr_count_before.resize(num_row); mr_index.resize(basis_matrix_limit_size * kMRExtraEntriesMultiplier); - mwz_column_mark.assign(num_row, 0); + mwz_column_mark.assign(num_row, false); mwz_column_index.resize(num_row); mwz_column_array.assign(num_row, 0); @@ -677,7 +677,11 @@ void HFactor::buildSimple() { b_start[iCol + 1] = BcountX; b_var[iCol] = iMat; } - // Record the number of elements in the basis matrix + // Record the number of elements in the basis matrix, remebering + // that BcountX is the number of entries in the nwork structural + // columns, so have to add num_row - nwork to get the entries in + // logical columns. In particular, if the basis matrix is an + // identity, BcountX = nwork = 0 basis_matrix_num_el = num_row - nwork + BcountX; // count1 = 0; @@ -819,16 +823,20 @@ void HFactor::buildSimple() { row_link_first.assign(num_basic + 1, -1); mr_count.assign(num_row, 0); HighsInt mr_countX = 0; - // Determine the number of entries in the kernel - kernel_num_el = 0; + // Determine the initial number of active nonzeros - to be updated + // as values are eliminated and fill-in or cancellation occur + num_active_nz_ = 0; + HighsInt check_nwork = 0; for (HighsInt iRow = 0; iRow < num_row; iRow++) { HighsInt count = mr_count_before[iRow]; if (count > 0) { + // In the active part of the kernel mr_start[iRow] = mr_countX; mr_space[iRow] = count * 2; mr_countX += count * 2; rlinkAdd(iRow, count); - kernel_num_el += count + 1; + num_active_nz_ += count; + check_nwork++; } } mr_index.resize(mr_countX); @@ -853,9 +861,11 @@ void HFactor::buildSimple() { const HighsInt iRow = b_index[k]; const double value = b_value[k]; if (mr_count_before[iRow] > 0) { + // In the active part of the kernel colInsert(iCol, iRow, value); rowInsert(iCol, iRow); } else { + // Above the active part of the kernel colStoreN(iCol, iRow, value); } } @@ -863,8 +873,11 @@ void HFactor::buildSimple() { clinkAdd(iCol, mc_count_a[iCol]); } build_synthetic_tick += (num_row + nwork + MCcountX) * 40 + mr_countX * 20; - // Record the kernel dimension + // Record the dimension and number of entries in the kernel. kernel_dim = nwork; + kernel_num_el = num_active_nz_; + min_time_bound_ = kHighsInf; + max_time_bound_ = 0; assert((HighsInt)this->refactor_info_.pivot_row.size() == num_basic - nwork); } @@ -875,40 +888,124 @@ HighsInt HFactor::buildKernel() { double fake_fill = 0; double fake_eliminate = 0; + HighsInt search_k = 0; const bool progress_report = false; // num_basic != num_row; const HighsInt progress_frequency = 10000; + + // Normally this->time_limit_ is kHighsInf, but if there is a finite + // time limit, it implies that buildKernel can bail out. This is + // (currently) used only with the dependent equations rule in + // presolve + // + // ToDo This bail-out is non-deterministic, but the pseudo-clock + // model for INVERT can be used to make it deterministic + const bool check_for_timeout = this->time_limit_ < kHighsInf; + // To know when to bail out of buildKernel a model of how long + // buildKernel will take is needed + // + // If a pivot is deferred, nwork is increased to force another loop, + // so have to count the number of deferred pivots to know how many + // iterations might be needed + HighsInt num_defer_pivot = 0; // Initial timer frequency: may be reduced if iterations get slow + const HighsInt max_timer_frequency = 1000; HighsInt timer_frequency = 100; - double previous_iteration_time = 0; + // Need to maintain an averate iteration time + double previous_iteration_time = build_timer_->read(); double average_iteration_time = 0; - const bool check_for_timeout = this->time_limit_ < kHighsInf; - HighsInt search_k = 0; - + // Need to maintain an average rate of change of the number of + // active nonzeros - in order to predict when it will go to zero + HighsInt previous_num_active_nz = num_active_nz_; + double average_num_active_nz_change_rate = 0; + // Very occasionally, vectors of indices and values have to be moved + // in order to be resized, in which caes the cost of an iteration is + // abnormally high and needs to be excluded from the average + // calculation, so record when it happens + bool resize_data_shift = false; + // Parameters for the running average claculation + double mu0, mu1; + + // Lambda functions that resize HighsInt/double vectors, with a + // check for them being moved as a result + auto resizeHighsInt = [&](std::vector& i_vector, + const HighsInt to_size) { + HighsInt* from_p = i_vector.data(); + i_vector.resize(to_size); + if (i_vector.data() != from_p) resize_data_shift = true; + }; + + auto resizeDouble = [&](std::vector& d_vector, + const HighsInt to_size) { + double* from_p = d_vector.data(); + d_vector.resize(to_size); + if (d_vector.data() != from_p) resize_data_shift = true; + }; + + // Work out the parameters for the running average claculation + auto runningAverageMu = [&]() { + mu0 = (1.0 * timer_frequency) / (10.0 * max_timer_frequency); + assert(mu0 <= 0.2); + mu1 = 1.0 - mu0; + }; + + runningAverageMu(); const HighsInt check_nwork = -11; while (nwork-- > 0) { // printf("\nnwork = %d\n", (int)nwork); if (nwork == check_nwork) { reportAsm(); } - // Determine whether to return due to exceeding the time limit - if (check_for_timeout && search_k % timer_frequency == 0) { + // Determine whether to return due to (expecting to) exceed the + // time limit + if (check_for_timeout && search_k > 0 && search_k % timer_frequency == 0) { + // Get the current iteration time, and update the running + // average (if there has been no data shift on resize) double current_time = build_timer_->read(); double time_difference = current_time - previous_iteration_time; previous_iteration_time = current_time; double iteration_time = time_difference / (1.0 * timer_frequency); - average_iteration_time = - 0.9 * average_iteration_time + 0.1 * iteration_time; - - if (time_difference > this->time_limit_ / 1e3) - timer_frequency = std::max(HighsInt(1), timer_frequency / 10); - HighsInt iterations_left = kernel_dim - search_k + 1; + if (!resize_data_shift) { + average_iteration_time = + mu1 * average_iteration_time + mu0 * iteration_time; + // Determine whether to reduce the timer frequency + if (timer_frequency > 1 && time_difference > this->time_limit_ / 1e1) { + timer_frequency = std::max(HighsInt(1), timer_frequency / 10); + runningAverageMu(); + } + } + // Determine the current rate of change of the number of active + // nonzeros + double num_active_nz_change_rate = + static_cast(num_active_nz_ - previous_num_active_nz) / + static_cast(timer_frequency); + previous_num_active_nz = num_active_nz_; + average_num_active_nz_change_rate = + mu1 * average_num_active_nz_change_rate + + mu0 * num_active_nz_change_rate; + + // Get an estimate of the number of iterations left, based on + // the bound and the average rate of change of the number of + // active nonzeros (if negative) + HighsInt iterations_left = kernel_dim - search_k + num_defer_pivot + 1; + if (average_num_active_nz_change_rate < -1) { + HighsInt active_nz_iterations_left = + -num_active_nz_ / average_num_active_nz_change_rate; + iterations_left = std::min(active_nz_iterations_left, iterations_left); + } double remaining_time_bound = average_iteration_time * iterations_left; double total_time_bound = current_time + remaining_time_bound; + // Update the record of bounds on total time - for logging in + // presolve + min_time_bound_ = std::min(total_time_bound, min_time_bound_); + max_time_bound_ = std::max(total_time_bound, max_time_bound_); + // Bail out if current or expected time exceeds the limit if (current_time > this->time_limit_ || total_time_bound > this->time_limit_) return kBuildKernelReturnTimeout; + // Clear the record of vectors of indices and values having to + // be moved in order to be resized + resize_data_shift = false; } - /** * 1. Search for the pivot */ @@ -1049,7 +1146,6 @@ HighsInt HFactor::buildKernel() { (int)rank_deficiency); return rank_deficiency; } - /** * 2. Elimination other elements by the pivot */ @@ -1061,6 +1157,9 @@ HighsInt HFactor::buildKernel() { // // Remove the pivot row index from the pivotal column of the // col-wise matrix. Also decreases the column count + // + // One active nonzero is lost + num_active_nz_--; double pivot_multiplier = colDelete(jColPivot, iRowPivot); // Remove the pivot column index from the pivotal row of the // row-wise matrix. Also decreases the row count @@ -1078,6 +1177,7 @@ HighsInt HFactor::buildKernel() { "Defer singular pivot = %11.4g\n", pivot_multiplier); // Matrix is singular, but defer return since other valid pivots // may exist. + num_defer_pivot++; assert(mr_count[iRowPivot] == original_pivotal_row_count - 1); if (mr_count[iRowPivot] == 0) { // The pivot corresponds to a singleton row. Entry is zeroed, @@ -1117,12 +1217,14 @@ HighsInt HFactor::buildKernel() { const double value = mc_value[k] / pivot_multiplier; mwz_column_index[mwz_column_count++] = iRow; mwz_column_array[iRow] = value; - mwz_column_mark[iRow] = 1; + mwz_column_mark[iRow] = true; l_index.push_back(iRow); l_value.push_back(value); mr_count_before[iRow] = mr_count[iRow]; rowDelete(jColPivot, (int)iRow); } + // One active entry is lost for each entry in the pivotal column + num_active_nz_ -= (end_A - start_A); l_start.push_back(l_index.size()); fake_fill += 2 * mc_count_a[jColPivot]; @@ -1149,6 +1251,8 @@ HighsInt HFactor::buildKernel() { const HighsInt my_end = my_start + my_count - 1; double my_pivot = colDelete(iCol, iRowPivot); colStoreN(iCol, iRowPivot, my_pivot); + // One active entry is lost for each entry in the pivotal row + num_active_nz_--; // 2.4.2. Elimination on the overlapping part HighsInt nFillin = mwz_column_count; @@ -1157,7 +1261,7 @@ HighsInt HFactor::buildKernel() { HighsInt iRow = mc_index[my_k]; double value = mc_value[my_k]; if (mwz_column_mark[iRow]) { - mwz_column_mark[iRow] = 0; + mwz_column_mark[iRow] = false; nFillin--; value -= my_pivot * mwz_column_array[iRow]; if (fabs(value) < kHighsTiny) { @@ -1167,6 +1271,8 @@ HighsInt HFactor::buildKernel() { mc_value[my_k] = value; } } + // One active entry is lost for each instance of cancellation + num_active_nz_ -= nCancel; fake_eliminate += mwz_column_count; fake_eliminate += nFillin * 2; @@ -1196,8 +1302,8 @@ HighsInt HFactor::buildKernel() { mc_space[iCol] += max(mc_space[iCol], nFillin); HighsInt p5 = mc_start[iCol] = mc_index.size(); HighsInt p7 = p5 + mc_space[iCol] - mc_count_n[iCol]; - mc_index.resize(p5 + mc_space[iCol]); - mc_value.resize(p5 + mc_space[iCol]); + resizeHighsInt(mc_index, p5 + mc_space[iCol]); + resizeDouble(mc_value, p5 + mc_space[iCol]); copy(&mc_index[p1], &mc_index[p2], &mc_index[p5]); copy(&mc_value[p1], &mc_value[p2], &mc_value[p5]); copy(&mc_index[p3], &mc_index[p4], &mc_index[p7]); @@ -1207,8 +1313,11 @@ HighsInt HFactor::buildKernel() { // 2.4.4.2 Fill into column copy for (HighsInt i = 0; i < mwz_column_count; i++) { HighsInt iRow = mwz_column_index[i]; - if (mwz_column_mark[iRow]) + if (mwz_column_mark[iRow]) { colInsert(iCol, iRow, -my_pivot * mwz_column_array[iRow]); + // One active entry is gained for each instance of fill-in + num_active_nz_++; + } } // 2.4.4.3 Fill into the row copy @@ -1221,7 +1330,7 @@ HighsInt HFactor::buildKernel() { HighsInt p2 = p1 + mr_count[iRow]; HighsInt p3 = mr_start[iRow] = mr_index.size(); mr_space[iRow] *= 2; - mr_index.resize(p3 + mr_space[iRow]); + resizeHighsInt(mr_index, p3 + mr_space[iRow]); copy(&mr_index[p1], &mr_index[p2], &mr_index[p3]); } rowInsert(iCol, iRow); @@ -1231,7 +1340,7 @@ HighsInt HFactor::buildKernel() { // 2.4.5. Reset pivot column mark for (HighsInt i = 0; i < mwz_column_count; i++) - mwz_column_mark[mwz_column_index[i]] = 1; + mwz_column_mark[mwz_column_index[i]] = true; // 2.4.6. Fix max value and link list colFixMax(iCol); @@ -1243,7 +1352,7 @@ HighsInt HFactor::buildKernel() { // 2.5. Clear pivot column buffer for (HighsInt i = 0; i < mwz_column_count; i++) - mwz_column_mark[mwz_column_index[i]] = 0; + mwz_column_mark[mwz_column_index[i]] = false; // 2.6. Correct row links for the remain active part for (HighsInt i = start_A; i < end_A; i++) { @@ -1253,6 +1362,8 @@ HighsInt HFactor::buildKernel() { rlinkAdd(iRow, mr_count[iRow]); } } + // End of loop while(nwork-- > 0) + // Final execution has nwork = 0 } build_synthetic_tick += fake_search * 20 + fake_fill * 160 + fake_eliminate * 80; diff --git a/highs/util/HFactor.h b/highs/util/HFactor.h index 8e452846f89..a3e1d7a1b29 100644 --- a/highs/util/HFactor.h +++ b/highs/util/HFactor.h @@ -336,6 +336,9 @@ class HFactor { HighsInt invert_num_el; HighsInt kernel_dim; HighsInt kernel_num_el; + HighsInt num_active_nz_; + double min_time_bound_; + double max_time_bound_; /** * Data of the factor @@ -408,7 +411,7 @@ class HFactor { // Kernel column buffer vector mwz_column_index; - vector mwz_column_mark; + vector mwz_column_mark; vector mwz_column_array; // Count link list diff --git a/highs/util/HFactorConst.h b/highs/util/HFactorConst.h index 7b6b2b2df02..90be1f50f28 100644 --- a/highs/util/HFactorConst.h +++ b/highs/util/HFactorConst.h @@ -11,7 +11,7 @@ #ifndef HFACTORCONST_H_ #define HFACTORCONST_H_ -#include "util/HighsInt.h" +#include "util/HighsType.h" enum UPDATE_METHOD { kUpdateMethodFt = 1, diff --git a/highs/util/HFactorRefactor.cpp b/highs/util/HFactorRefactor.cpp index e404747b315..6817fd5f9cf 100644 --- a/highs/util/HFactorRefactor.cpp +++ b/highs/util/HFactorRefactor.cpp @@ -40,7 +40,7 @@ HighsInt HFactor::rebuild(HighsTimerClock* factor_timer_clock_pointer) { basis_matrix_num_el = 0; HighsInt stage = num_row; HighsInt rank_deficiency = 0; - vector has_pivot; + std::vector has_pivot; has_pivot.assign(num_row, false); const bool report_unit = false; const bool report_singletons = false; @@ -209,7 +209,7 @@ HighsInt HFactor::rebuild(HighsTimerClock* factor_timer_clock_pointer) { // Need to know whether to consider matrix entries for FtranL // operation. Initially these correspond to all the rows without // pivots - vector not_in_bump = has_pivot; + std::vector not_in_bump = has_pivot; // Monitor density of FtranL result to possibly switch from exploiting // hyper-sparsity double expected_density = 0.0; diff --git a/highs/util/HSet.h b/highs/util/HSet.h index 4eab605d154..7ec5d615fc9 100644 --- a/highs/util/HSet.h +++ b/highs/util/HSet.h @@ -17,7 +17,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" // #include diff --git a/highs/util/HVectorBase.cpp b/highs/util/HVectorBase.cpp index 332cb8f4769..0fe446f004b 100644 --- a/highs/util/HVectorBase.cpp +++ b/highs/util/HVectorBase.cpp @@ -27,7 +27,7 @@ void HVectorBase::setup(HighsInt size_) { count = 0; index.resize(size); array.assign(size, Real{0}); - cwork.assign(size + 6400, 0); // MAX invert + cwork.assign(size + 6400, false); // MAX invert iwork.assign(size * 4, 0); packCount = 0; diff --git a/highs/util/HVectorBase.h b/highs/util/HVectorBase.h index a9a9ec0cfa7..5cc501924bb 100644 --- a/highs/util/HVectorBase.h +++ b/highs/util/HVectorBase.h @@ -13,7 +13,7 @@ #include -#include "util/HighsInt.h" +#include "util/HighsType.h" // using std::map; using std::vector; @@ -45,7 +45,7 @@ class HVectorBase { double synthetic_tick; //!< Synthetic clock for operations with this vector // For update - vector cwork; //!< char working buffer for UPDATE + vector cwork; //!< Working buffer for UPDATE vector iwork; //!< integer working buffer for UPDATE HVectorBase* next; //!< Allows vectors to be linked for PAMI diff --git a/highs/util/HighsDataStack.h b/highs/util/HighsDataStack.h index e459267a5a2..7fc1d0fee0a 100644 --- a/highs/util/HighsDataStack.h +++ b/highs/util/HighsDataStack.h @@ -16,7 +16,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" #if __GNUG__ && __GNUC__ < 5 && !defined(__clang__) #define IS_TRIVIALLY_COPYABLE(T) __has_trivial_copy(T) diff --git a/highs/util/HighsDisjointSets.h b/highs/util/HighsDisjointSets.h index d7f216d4ae3..ef8a6090645 100644 --- a/highs/util/HighsDisjointSets.h +++ b/highs/util/HighsDisjointSets.h @@ -22,7 +22,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" template class HighsDisjointSets { diff --git a/highs/util/HighsHash.h b/highs/util/HighsHash.h index f74034d823e..c387c31f7c2 100644 --- a/highs/util/HighsHash.h +++ b/highs/util/HighsHash.h @@ -21,7 +21,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" #ifdef HIGHS_HAVE_BITSCAN_REVERSE #include diff --git a/highs/util/HighsIntegers.h b/highs/util/HighsIntegers.h index 1b5f613a029..05bcd2271e1 100644 --- a/highs/util/HighsIntegers.h +++ b/highs/util/HighsIntegers.h @@ -16,7 +16,7 @@ #include #include "util/HighsCDouble.h" -#include "util/HighsInt.h" +#include "util/HighsType.h" class HighsIntegers { public: diff --git a/highs/util/HighsMatrixSlice.h b/highs/util/HighsMatrixSlice.h index 67d5e912219..c58c4442e65 100644 --- a/highs/util/HighsMatrixSlice.h +++ b/highs/util/HighsMatrixSlice.h @@ -17,7 +17,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" template class HighsMatrixSlice; diff --git a/highs/util/HighsMemoryAllocation.h b/highs/util/HighsMemoryAllocation.h index 3f372b727f0..f695a37fd88 100644 --- a/highs/util/HighsMemoryAllocation.h +++ b/highs/util/HighsMemoryAllocation.h @@ -14,7 +14,7 @@ #include -#include "util/HighsInt.h" +#include "util/HighsType.h" template bool okResize(std::vector& use_vector, HighsInt dimension, T value = T{}) { diff --git a/highs/util/HighsRbTree.h b/highs/util/HighsRbTree.h index bc7427ec936..720b4f1aabf 100644 --- a/highs/util/HighsRbTree.h +++ b/highs/util/HighsRbTree.h @@ -12,7 +12,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" namespace highs { diff --git a/highs/util/HighsSparseVectorSum.h b/highs/util/HighsSparseVectorSum.h index 61f527932a6..7db0263c94d 100644 --- a/highs/util/HighsSparseVectorSum.h +++ b/highs/util/HighsSparseVectorSum.h @@ -14,7 +14,7 @@ #include #include "util/HighsCDouble.h" -#include "util/HighsInt.h" +#include "util/HighsType.h" class HighsSparseVectorSum { public: diff --git a/highs/util/HighsSplay.h b/highs/util/HighsSplay.h index 18e44e2c920..61ddf73a84b 100644 --- a/highs/util/HighsSplay.h +++ b/highs/util/HighsSplay.h @@ -10,7 +10,7 @@ #include -#include "util/HighsInt.h" +#include "util/HighsType.h" /// top down splay operation to maintain a binary search tree. The search tree /// is assumed to be stored in an array/vector and therefore uses integers diff --git a/highs/util/HighsTimer.h b/highs/util/HighsTimer.h index 85bb50b7181..1fe81bb56d8 100644 --- a/highs/util/HighsTimer.h +++ b/highs/util/HighsTimer.h @@ -19,7 +19,7 @@ #include #include -#include "util/HighsInt.h" +#include "util/HighsType.h" const HighsInt check_clock = -46; const HighsInt simplex_no_basis_clock = 8; @@ -232,6 +232,21 @@ class HighsTimer { clock_time[i_clock] += time; } + /* + void logRunTime(const char* message) const { + if (!printf_flag) return; + double time = this->read(); + std::string time_string = + // std::to_string(time); +#ifndef NDEBUG + std::to_string(time); +#else + std::to_string(static_cast(time)); +#endif + printf("%-30s: %s\n", message, time_string.c_str()); + } + */ + /** * @brief Report timing information for the clock indices in the list */ diff --git a/highs/util/HighsType.h b/highs/util/HighsType.h new file mode 100644 index 00000000000..574487271d4 --- /dev/null +++ b/highs/util/HighsType.h @@ -0,0 +1,21 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* */ +/* This file is part of the HiGHS linear optimization suite */ +/* */ +/* Available as open-source under the MIT License */ +/* */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/**@file HighsType.h + * @brief The definition for basic types to use + */ + +#ifndef UTIL_HIGHS_TYPE_H_ +#define UTIL_HIGHS_TYPE_H_ + +#include "util/HighsInt.h" + +// vector is not thread-safe, so HiGHS uses vector + +typedef uint8_t HighsBool; + +#endif From 4b8bf3d883b69b0b793323b95a9f8a567f88b2fd Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 10 Aug 2026 12:43:07 +0200 Subject: [PATCH 191/196] Merge chaser --- highs/presolve/HPresolveAnalysis.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/highs/presolve/HPresolveAnalysis.cpp b/highs/presolve/HPresolveAnalysis.cpp index e5b9d76efa1..6b53ca56b95 100644 --- a/highs/presolve/HPresolveAnalysis.cpp +++ b/highs/presolve/HPresolveAnalysis.cpp @@ -19,6 +19,8 @@ void HPresolveAnalysis::setup(const HighsLp* model_, numDeletedRows = &numDeletedRows_; numDeletedCols = &numDeletedCols_; + this->allow_rule_.assign(kPresolveRuleCount, true); + timer_ = timer; const bool lp_presolve = !model_->isMip() || options->solve_relaxation; analyse_presolve_time_ = From b98ebc4b13cf16df49d0772124445b59905cba43 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 10 Aug 2026 12:50:52 +0200 Subject: [PATCH 192/196] Disable FM in presolve light mode --- highs/presolve/HPresolve.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 955ec3ec0d9..9966558d324 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -495,6 +495,7 @@ void HPresolve::chooseRules() { presolve_light_rule_off[kPresolveRuleEnumeration] = true; presolve_light_rule_off[kPresolveRuleDualFixing] = true; presolve_light_rule_off[kPresolveRuleColStuffing] = true; + presolve_light_rule_off[kPresolveRuleFourierMotzkin] = true; } if (!silent && options->log_dev_level) { From bf3a382878b7ebcbd9c9d7ffdc898606cfac7563 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 10 Aug 2026 13:50:15 +0200 Subject: [PATCH 193/196] Minor change; use size_t --- highs/presolve/HighsPostsolveStack.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 9feea8465d6..da6e25dbbb5 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -458,7 +458,7 @@ class HighsPostsolveStack { template void compressIndexMap(const std::vector& newIndex, std::vector& origIndex) { - HighsInt numEn = origIndex.size(); + size_t numEn = origIndex.size(); for (size_t i = 0; i != newIndex.size(); ++i) { if (newIndex[i] == -1) --numEn; From 2bd1e5c073b8ab883fdf4414c1a0b74bf7698537 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 10 Aug 2026 14:06:57 +0200 Subject: [PATCH 194/196] Fix usage of allow_rule_ --- highs/presolve/HPresolve.cpp | 2 +- highs/presolve/HPresolveAnalysis.cpp | 2 -- highs/presolve/HPresolveAnalysis.h | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 9966558d324..b3031e11864 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7664,7 +7664,7 @@ HPresolve::Result HPresolve::aggregator(HighsPostsolveStack& postsolve_stack) { HPresolve::Result HPresolve::fourierMotzkin( HighsPostsolveStack& postsolve_stack, HighsInt& numColsEliminated) { - assert(analysis_.allow_rule_[kPresolveRuleFourierMotzkin]); + assert(this->allow_rule_[kPresolveRuleFourierMotzkin]); const bool logging_on = analysis_.logging_on_; if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleFourierMotzkin); diff --git a/highs/presolve/HPresolveAnalysis.cpp b/highs/presolve/HPresolveAnalysis.cpp index 6b53ca56b95..e5b9d76efa1 100644 --- a/highs/presolve/HPresolveAnalysis.cpp +++ b/highs/presolve/HPresolveAnalysis.cpp @@ -19,8 +19,6 @@ void HPresolveAnalysis::setup(const HighsLp* model_, numDeletedRows = &numDeletedRows_; numDeletedCols = &numDeletedCols_; - this->allow_rule_.assign(kPresolveRuleCount, true); - timer_ = timer; const bool lp_presolve = !model_->isMip() || options->solve_relaxation; analyse_presolve_time_ = diff --git a/highs/presolve/HPresolveAnalysis.h b/highs/presolve/HPresolveAnalysis.h index 707ccd06035..3a8b189b8b9 100644 --- a/highs/presolve/HPresolveAnalysis.h +++ b/highs/presolve/HPresolveAnalysis.h @@ -28,8 +28,6 @@ class HPresolveAnalysis { HighsInt original_num_row_; public: - std::vector allow_rule_; - bool allow_logging_; bool logging_on_; From 85e8a5fc4cf84374f8c1d981131560dc05bf9c39 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 11 Aug 2026 13:49:59 +0200 Subject: [PATCH 195/196] Fix merge issue --- highs/lp_data/Highs.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/lp_data/Highs.cpp b/highs/lp_data/Highs.cpp index fd912246ce2..b6010f9edd8 100644 --- a/highs/lp_data/Highs.cpp +++ b/highs/lp_data/Highs.cpp @@ -4002,7 +4002,7 @@ HighsPostsolveStatus Highs::runPostsolve() { const HighsInt report_3040_col = -21792; presolve_.data_.postSolveStack.undo( options_, presolve_.data_.recovered_solution_, - presolve_.data_.recovered_basis_, report_3040_col); + presolve_.data_.recovered_basis_, 0, report_3040_col); // Compute the row activities assert(model_.lp_.a_matrix_.isColwise()); calculateRowValuesQuad(model_.lp_, presolve_.data_.recovered_solution_); From cb5d1e19a9989cfcb961d962fab4368447cecdd6 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 11 Aug 2026 14:36:32 +0200 Subject: [PATCH 196/196] Fix another merge issues --- highs/presolve/HPresolve.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index b3031e11864..10623aea40d 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2388,13 +2388,12 @@ void HPresolve::markColDeleted(HighsInt col) { if (!this->in_initial_sweep_) { assert(!colDeleted[col]); - if (col == model->fme_obj_col_) model->fme_obj_col_ = -1; - // prevents col from being added to change vector changedColFlag[col] = true; colDeleted[col] = true; } ++numDeletedCols; + if (col == model->fme_obj_col_) model->fme_obj_col_ = -1; } HPresolve::Result HPresolve::changeColUpper(HighsInt col, double newUpper) { @@ -6224,6 +6223,8 @@ HPresolve::Result HPresolve::initialSweep( model->a_matrix_.start_.resize(num_col + 1); model->a_matrix_.index_.resize(nnz); model->a_matrix_.value_.resize(nnz); + if (model->fme_obj_col_ >= 0) + model->fme_obj_col_ = newColIndex[model->fme_obj_col_]; postsolve_stack.compressColIndexMap(newColIndex); HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack));