From c505ba9a310794d0f0d226fab5c4dbf32f67b811 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 2 Sep 2026 16:04:32 +0200 Subject: [PATCH 01/29] Add origin row to implication table --- highs/mip/HighsImplications.cpp | 28 ++++++++++++++++------------ highs/mip/HighsImplications.h | 15 +++++++++------ highs/presolve/HPresolve.cpp | 6 ++++-- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 3b3612d2c26..37a03b72f61 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -413,21 +413,21 @@ void HighsImplications::strengthenVarBound(VarBound& vbnd, }; void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, - double vubconstant) { + double vubconstant, HighsInt origin_row) { addVUB(col, vubcol, vubcoef, vubconstant, mipsolver.mipdata_->getDomain().col_upper_[col], - mipsolver.isColIntegral(col)); + mipsolver.isColIntegral(col), origin_row); } void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, double vubconstant, double colupperbound, - bool colisintegral) { + bool colisintegral, HighsInt origin_row) { // assume that VUBs do not have infinite coefficients and infinite constant // terms since such VUBs effectively evaluate to NaN. assert(std::abs(vubcoef) != kHighsInf || std::abs(vubconstant) != kHighsInf); if (tooManyVarBounds()) return; - VarBound vub{vubcoef, vubconstant}; + VarBound vub{vubcoef, vubconstant, origin_row}; if (colisintegral) { // try to strengthen VUB @@ -448,27 +448,28 @@ void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, if (minBound < currentMinBound - mipsolver.mipdata_->feastol) { currentvub.coef = vub.coef; currentvub.constant = vub.constant; + currentvub.origin_row = vub.origin_row; } } else numVarBounds++; } void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, - double vlbconstant) { + double vlbconstant, HighsInt origin_row) { addVLB(col, vlbcol, vlbcoef, vlbconstant, mipsolver.mipdata_->getDomain().col_lower_[col], - mipsolver.isColIntegral(col)); + mipsolver.isColIntegral(col), origin_row); } void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, double vlbconstant, double colllowerbound, - bool colisintegral) { + bool colisintegral, HighsInt origin_row) { // assume that VLBs do not have infinite coefficients and infinite constant // terms since such VLBs effectively evaluate to NaN. assert(std::abs(vlbcoef) != kHighsInf || std::abs(vlbconstant) != kHighsInf); if (tooManyVarBounds()) return; - VarBound vlb{vlbcoef, vlbconstant}; + VarBound vlb{vlbcoef, vlbconstant, origin_row}; if (colisintegral) { // try to strengthen VLB @@ -490,6 +491,7 @@ void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, if (maxBound > currentMaxBound + mipsolver.mipdata_->feastol) { currentvlb.coef = vlb.coef; currentvlb.constant = vlb.constant; + currentvlb.origin_row = vlb.origin_row; } } else numVarBounds++; @@ -554,7 +556,8 @@ void HighsImplications::rebuild(HighsInt ncols, newVubCol)) return; - addVUB(newi, newVubCol, vub.coef, vub.constant); + addVUB(newi, newVubCol, vub.coef, vub.constant, + vub.origin_row >= 0 ? orig2reducedrow[vub.origin_row] : -1); }); oldvlbs[i].for_each([&](HighsInt vlbCol, VarBound vlb) { @@ -566,7 +569,8 @@ void HighsImplications::rebuild(HighsInt ncols, newVlbCol)) return; - addVLB(newi, newVlbCol, vlb.coef, vlb.constant); + addVLB(newi, newVlbCol, vlb.coef, vlb.constant, + vlb.origin_row >= 0 ? orig2reducedrow[vlb.origin_row] : -1); }); if (mipsolver.mipdata_->getDomain().isBinary(newi)) { @@ -592,12 +596,12 @@ void HighsImplications::buildFrom(const HighsImplications& init) { for (HighsInt i = 0; i != numcol; ++i) { init.vubs[i].for_each([&](HighsInt vubCol, VarBound vub) { if (!mipsolver.mipdata_->getDomain().isBinary(vubCol)) return; - addVUB(i, vubCol, vub.coef, vub.constant); + addVUB(i, vubCol, vub.coef, vub.constant, vub.origin_row); }); init.vlbs[i].for_each([&](HighsInt vlbCol, VarBound vlb) { if (!mipsolver.mipdata_->getDomain().isBinary(vlbCol)) return; - addVLB(i, vlbCol, vlb.coef, vlb.constant); + addVLB(i, vlbCol, vlb.coef, vlb.constant, vlb.origin_row); }); if (mipsolver.mipdata_->getDomain().isBinary(i)) { diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index adac9676ccd..fe59a042dd5 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -51,6 +51,7 @@ class HighsImplications { struct VarBound { double coef; double constant; + HighsInt origin_row = -1; double minValue() const { return static_cast(static_cast(constant) + @@ -127,17 +128,19 @@ class HighsImplications { void strengthenVarBound(VarBound& vbnd, HighsInt multiplier) const; - void addVUB(HighsInt col, HighsInt vubcol, double vubcoef, - double vubconstant); + void addVUB(HighsInt col, HighsInt vubcol, double vubcoef, double vubconstant, + HighsInt origin_row = -1); void addVUB(HighsInt col, HighsInt vubcol, double vubcoef, double vubconstant, - double colupperbound, bool colisinteger); + double colupperbound, bool colisinteger, + HighsInt origin_row = -1); - void addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, - double vlbconstant); + void addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, double vlbconstant, + HighsInt origin_row = -1); void addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, double vlbconstant, - double collowerbound, bool colisinteger); + double collowerbound, bool colisinteger, + HighsInt origin_row = -1); void columnTransformed(HighsInt col, double scale, double constant) { // Update implications affected by transformation diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index be9efd1346e..d1a1c100387 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9118,14 +9118,16 @@ void HPresolve::extractVarBounds(HighsInt row) { mipsolver->mipdata_->implications.addVLB( nonzero.index(), binCol, vbCoef, vlbConstant, model->col_lower_[nonzero.index()], - model->integrality_[nonzero.index()] != HighsVarType::kContinuous); + model->integrality_[nonzero.index()] != HighsVarType::kContinuous, + row); // add VUB if (vubConstant != kHighsInf) mipsolver->mipdata_->implications.addVUB( nonzero.index(), binCol, vbCoef, vubConstant, model->col_upper_[nonzero.index()], - model->integrality_[nonzero.index()] != HighsVarType::kContinuous); + model->integrality_[nonzero.index()] != HighsVarType::kContinuous, + row); // stop if no additional variable bounds can be found if (!useLhs && !useRhs) break; From ac4eeab7710b480b57bc3235d522cf8767fba84e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 2 Sep 2026 16:16:17 +0200 Subject: [PATCH 02/29] Missing origin row initialization --- highs/mip/HighsImplications.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 37a03b72f61..0f943c62ea9 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -163,7 +163,7 @@ std::pair HighsImplications::getBestVub( HighsInt col, const HighsSolution& lpSolution, double& bestUb, const HighsDomain& globaldom) const { std::pair bestVub = - std::make_pair(-1, VarBound{0.0, kHighsInf}); + std::make_pair(-1, VarBound{0.0, kHighsInf, -1}); double minbestUb = bestUb; double bestUbDist = kHighsInf; int64_t bestvubnodes = 0; @@ -236,7 +236,7 @@ std::pair HighsImplications::getBestVlb( HighsInt col, const HighsSolution& lpSolution, double& bestLb, const HighsDomain& globaldom) const { std::pair bestVlb = - std::make_pair(-1, VarBound{0.0, -kHighsInf}); + std::make_pair(-1, VarBound{0.0, -kHighsInf, -1}); double maxbestlb = bestLb; double bestLbDist = kHighsInf; int64_t bestvlbnodes = 0; From bab7f477cd84b96cffaeae08364a0bb6994fc609 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 2 Sep 2026 16:27:08 +0200 Subject: [PATCH 03/29] Remove default value --- highs/mip/HighsImplications.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index fe59a042dd5..d4438de9804 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -51,7 +51,7 @@ class HighsImplications { struct VarBound { double coef; double constant; - HighsInt origin_row = -1; + HighsInt origin_row; double minValue() const { return static_cast(static_cast(constant) + From e47cb68cd511ca874c08e41d3094665f84254593 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 2 Sep 2026 16:35:19 +0200 Subject: [PATCH 04/29] Add accessor methods --- highs/mip/HighsImplications.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index d4438de9804..090dc126095 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -180,6 +180,13 @@ class HighsImplications { } } + const HighsHashTree& getVlbs(HighsInt col) const { + return vlbs[col]; + } + const HighsHashTree& getVubs(HighsInt col) const { + return vubs[col]; + } + std::pair getBestVub(HighsInt col, const HighsSolution& lpSolution, double& bestUb, From 67657ac292c8529cb4d50e57388084797e1a5e01 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 2 Sep 2026 21:27:25 +0200 Subject: [PATCH 05/29] Add reverse row map --- highs/mip/HighsImplications.cpp | 7 +++++++ highs/mip/HighsImplications.h | 11 +++++++++++ highs/presolve/HPresolve.cpp | 2 ++ 3 files changed, 20 insertions(+) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 0f943c62ea9..ea897f9a08a 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -452,6 +452,8 @@ void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, } } else numVarBounds++; + + if (origin_row >= 0) rowToVarBounds[origin_row].insert(col, vubcol); } void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, @@ -495,6 +497,8 @@ void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, } } else numVarBounds++; + + if (origin_row >= 0) rowToVarBounds[origin_row].insert(col, vlbcol); } void HighsImplications::rebuild(HighsInt ncols, @@ -529,6 +533,9 @@ void HighsImplications::rebuild(HighsInt ncols, vlbs.clear(); vlbs.shrink_to_fit(); vlbs.resize(ncols); + rowToVarBounds.clear(); + rowToVarBounds.shrink_to_fit(); + rowToVarBounds.resize(mipsolver.numRow()); numImplications = 0; numVarBounds = 0; HighsInt oldncols = oldvubs.size(); diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 090dc126095..6aad4691b10 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -67,6 +67,8 @@ class HighsImplications { std::vector> vubs; std::vector> vlbs; + std::vector> rowToVarBounds; + public: const HighsMipSolver& mipsolver; std::vector substitutions; @@ -95,6 +97,8 @@ class HighsImplications { vubs.shrink_to_fit(); vlbs.clear(); vlbs.shrink_to_fit(); + rowToVarBounds.clear(); + rowToVarBounds.shrink_to_fit(); resize(mipsolver.numCol()); numVarBounds = 0; nextCleanupCall = mipsolver.numNonzero(); @@ -107,6 +111,7 @@ class HighsImplications { colsubstituted.resize(ncols); vubs.resize(ncols); vlbs.resize(ncols); + rowToVarBounds.resize(mipsolver.numRow()); maxVarBounds = calcMaxVarBounds(ncols); } @@ -187,6 +192,12 @@ class HighsImplications { return vubs[col]; } + void rowModified(HighsInt row) { rowToVarBounds[row].clear(); } + + const HighsHashTree& getRowVarBounds(HighsInt row) const { + return rowToVarBounds[row]; + } + std::pair getBestVub(HighsInt col, const HighsSolution& lpSolution, double& bestUb, diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index d1a1c100387..ff7930aaf17 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2208,6 +2208,8 @@ void HPresolve::addToMatrix(const HighsInt row, const HighsInt col, markChangedRow(row); markChangedCol(col); + if (mipsolver != nullptr) mipsolver->mipdata_->implications.rowModified(row); + if (pos == -1) { if (freeslots.empty()) { pos = static_cast(Avalue.size()); From c6427a991eaaf38e93c9a89d371b5d99290ac0a7 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Wed, 2 Sep 2026 21:48:30 +0200 Subject: [PATCH 06/29] Update logic for clearing --- highs/mip/HighsImplications.cpp | 14 ++++++++++++++ highs/mip/HighsImplications.h | 3 ++- highs/presolve/HPresolve.cpp | 2 ++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index ea897f9a08a..b70fb1ea004 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -412,6 +412,20 @@ void HighsImplications::strengthenVarBound(VarBound& vbnd, vbnd.coef = -multiplier * (downaj + std::max(fj - f0, 0.0) / (1.0 - f0)); }; +void HighsImplications::numRowsChanged() { + rowToVarBounds.resize(mipsolver.numRow()); +} + +void HighsImplications::rowModified(HighsInt row) { + rowToVarBounds[row].for_each([&](HighsInt col, HighsInt binaryCol) { + VarBound* vb = vlbs[col].find(binaryCol); + if (vb && vb->origin_row == row) vb->origin_row = -1; + vb = vubs[col].find(binaryCol); + if (vb && vb->origin_row == row) vb->origin_row = -1; + }); + rowToVarBounds[row].clear(); +} + void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, double vubconstant, HighsInt origin_row) { addVUB(col, vubcol, vubcoef, vubconstant, diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 6aad4691b10..25ed017cfa4 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -192,7 +192,8 @@ class HighsImplications { return vubs[col]; } - void rowModified(HighsInt row) { rowToVarBounds[row].clear(); } + void numRowsChanged(); + void rowModified(HighsInt row); const HighsHashTree& getRowVarBounds(HighsInt row) const { return rowToVarBounds[row]; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ff7930aaf17..d13e1f1dbbe 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2335,6 +2335,8 @@ bool HPresolve::addToMatrix( implRowDualUpper.data(), rowDualLowerSource.data(), rowDualUpperSource.data()); + if (mipsolver != nullptr) mipsolver->mipdata_->implications.numRowsChanged(); + for (HighsInt i = 0; i < num_rows; i++) { // new row index HighsInt row = oldNumRows + i; From 8986e41c6f7c137123efcc3b10dd02f51597fffb Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 3 Sep 2026 10:57:15 +0200 Subject: [PATCH 07/29] Use resize --- highs/mip/HighsImplications.cpp | 4 ---- highs/mip/HighsImplications.h | 9 ++++----- highs/presolve/HPresolve.cpp | 7 ++++++- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index b70fb1ea004..ae2e543c501 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -412,10 +412,6 @@ void HighsImplications::strengthenVarBound(VarBound& vbnd, vbnd.coef = -multiplier * (downaj + std::max(fj - f0, 0.0) / (1.0 - f0)); }; -void HighsImplications::numRowsChanged() { - rowToVarBounds.resize(mipsolver.numRow()); -} - void HighsImplications::rowModified(HighsInt row) { rowToVarBounds[row].for_each([&](HighsInt col, HighsInt binaryCol) { VarBound* vb = vlbs[col].find(binaryCol); diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 25ed017cfa4..5292b565f8a 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -77,7 +77,7 @@ class HighsImplications { nextCleanupCall = mipsolver.numNonzero(); numImplications = 0; numVarBounds = 0; - resize(mipsolver.numCol()); + resize(mipsolver.numCol(), mipsolver.numRow()); } std::function @@ -99,19 +99,19 @@ class HighsImplications { vlbs.shrink_to_fit(); rowToVarBounds.clear(); rowToVarBounds.shrink_to_fit(); - resize(mipsolver.numCol()); + resize(mipsolver.numCol(), mipsolver.numRow()); numVarBounds = 0; nextCleanupCall = mipsolver.numNonzero(); } - void resize(HighsInt ncols) { + void resize(HighsInt ncols, HighsInt nrows) { implications.resize(2 * static_cast(ncols)); hasProbed.resize(2 * static_cast(ncols)); reverseImplications.resize(ncols); colsubstituted.resize(ncols); vubs.resize(ncols); vlbs.resize(ncols); - rowToVarBounds.resize(mipsolver.numRow()); + rowToVarBounds.resize(nrows); maxVarBounds = calcMaxVarBounds(ncols); } @@ -192,7 +192,6 @@ class HighsImplications { return vubs[col]; } - void numRowsChanged(); void rowModified(HighsInt row); const HighsHashTree& getRowVarBounds(HighsInt row) const { diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index d13e1f1dbbe..1110ebdea58 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -126,6 +126,9 @@ void HPresolve::setInput(HighsMipSolver& mipsolver, mipsolver.mipdata_->getDomain().col_upper_; } + mipsolver.mipdata_->implications.resize(mipsolver.numCol(), + mipsolver.numRow()); + setInput(mipsolver.mipdata_->presolvedModel, *mipsolver.options_mip_, presolve_reduction_limit, &mipsolver.timer_); } @@ -2335,7 +2338,9 @@ bool HPresolve::addToMatrix( implRowDualUpper.data(), rowDualLowerSource.data(), rowDualUpperSource.data()); - if (mipsolver != nullptr) mipsolver->mipdata_->implications.numRowsChanged(); + if (mipsolver != nullptr) + mipsolver->mipdata_->implications.resize(mipsolver->numCol(), + mipsolver->numRow()); for (HighsInt i = 0; i < num_rows; i++) { // new row index From fc67bc62242aa34c8783ccba44f863bc0a44b3e0 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 3 Sep 2026 11:31:02 +0200 Subject: [PATCH 08/29] Use resize and hash tree changes --- highs/util/HighsHashTree.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/highs/util/HighsHashTree.h b/highs/util/HighsHashTree.h index 729a02c5fdf..ea7fad08302 100644 --- a/highs/util/HighsHashTree.h +++ b/highs/util/HighsHashTree.h @@ -1215,7 +1215,7 @@ class HighsHashTree { static NodePtr copy_recurse(NodePtr node) { switch (node.getType()) { case kEmpty: - throw std::logic_error("Unexpected node type in empty in hash tree"); + return NodePtr(nullptr); case kListLeaf: { ListLeaf* leaf = node.getListLeaf(); @@ -1253,8 +1253,10 @@ class HighsHashTree { BranchNode* newBranch = (BranchNode*)::operator new(getBranchNodeSize(size)); newBranch->occupation = branch->occupation; - for (int i = 0; i < size; ++i) + for (int i = 0; i < size; ++i) { + assert(branch->child(i).getType() != kEmpty); newBranch->child(i) = copy_recurse(branch->child(i)); + } return newBranch; } From 4d98bcbe7c24ef161ac580240da86fef896b44cc Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 3 Sep 2026 16:38:08 +0200 Subject: [PATCH 09/29] Now lifting coefficients --- highs/presolve/HPresolve.cpp | 190 ++++++++++++++++++++++++++++++++--- highs/presolve/HPresolve.h | 4 + 2 files changed, 180 insertions(+), 14 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 1110ebdea58..4fb165f9dbe 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7868,12 +7868,9 @@ void HPresolve::fixColToZero(HighsPostsolveStack& postsolve_stack, if (logging_on) analysis_.stopPresolveRuleLog(kPresolveRuleFixedCol); } -void HPresolve::removeRow(HighsInt row) { +void HPresolve::unlinkRow(HighsInt row) { assert(row >= 0); assert(static_cast(row) < rowroot.size()); - // first mark the row as logically deleted, so that it is not register as - // singleton row upon removing its nonzeros - markRowDeleted(row); storeRow(row); for (HighsInt rowiter : rowpositions) { assert(Arow[rowiter] == row); @@ -7881,6 +7878,15 @@ void HPresolve::removeRow(HighsInt row) { } } +void HPresolve::removeRow(HighsInt row) { + assert(row >= 0); + assert(static_cast(row) < rowroot.size()); + // first mark the row as logically deleted, so that it is not register as + // singleton row upon removing its nonzeros + markRowDeleted(row); + unlinkRow(row); +} + void HPresolve::removeFixedCol(HighsInt col) { const bool logging_on = analysis_.logging_on_; if (logging_on) analysis_.startPresolveRuleLog(kPresolveRuleFixedCol); @@ -9143,6 +9149,161 @@ void HPresolve::extractVarBounds(HighsInt row) { } } +HPresolve::Result HPresolve::aggregateVarBounds( + HighsPostsolveStack& postsolve_stack) { + assert(mipsolver != nullptr); + HighsImplications& implications = mipsolver->mipdata_->implications; + HighsCliqueTable& cliquetable = mipsolver->mipdata_->cliquetable; + + struct colImpliedBounds { + HighsImplications::VarBound originalBound; + HighsImplications::VarBound standardBound; + }; + + HighsHashTree vlbsFromRow; + HighsHashTree vubsFromRow; + + HighsInt overallNumRowsRemoved = 0; + HighsInt overallNumRowsModified = 0; + HighsInt overallNumVarsLifted = 0; + + for (HighsInt col = 0; col < model->num_col_; ++col) { + if (colDeleted[col]) continue; + // get lower bound and upper bound + double lb = model->col_lower_[col]; + double ub = model->col_upper_[col]; + + // skip binary columns — we aggregate VLBs/VUBs on non-binary columns + if (model->integrality_[col] == HighsVarType::kInteger && lb == 0.0 && + ub == 1.0) + continue; + + // collect VLBs and VUBs + implications.getVlbs(col).for_each( + [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { + if (vlb.origin_row >= 0) { + HighsCDouble newCoef = vlb.constant - static_cast(lb); + if (vlb.coef > 0) newCoef += vlb.coef; + vlbsFromRow.insert( + binaryCol, + colImpliedBounds{vlb, HighsImplications::VarBound{ + static_cast(newCoef), lb, + vlb.origin_row}}); + } + }); + implications.getVubs(col).for_each( + [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { + if (vub.origin_row >= 0) { + HighsCDouble newCoef = static_cast(ub) - vub.constant; + if (vub.coef < 0) newCoef -= vub.coef; + vubsFromRow.insert( + binaryCol, + colImpliedBounds{vub, HighsImplications::VarBound{ + static_cast(newCoef), ub, + vub.origin_row}}); + } + }); + + // set up cliques + std::vector vlbsClique; + std::vector vubsClique; + vlbsFromRow.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { + HighsInt val = bounds.originalBound.coef > 0 ? 1 : 0; + vlbsClique.emplace_back(binaryCol, val); + }); + vubsFromRow.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { + HighsInt val = bounds.originalBound.coef < 0 ? 1 : 0; + vubsClique.emplace_back(binaryCol, val); + }); + + // clique partitioning + std::vector vlbsCliquePartitionStart; + std::vector vubsCliquePartitionStart; + cliquetable.cliquePartition(vlbsClique, vlbsCliquePartitionStart); + cliquetable.cliquePartition(vubsClique, vubsCliquePartitionStart); + HighsInt numVlbsCliques = + static_cast(vlbsCliquePartitionStart.size()) - 1; + HighsInt numVubsCliques = + static_cast(vubsCliquePartitionStart.size()) - 1; + + auto mergeCliques = + [&](std::vector& clqVars, + std::vector& partitionStart, + HighsHashTree& boundsMap, + double baseBound, HighsInt direction) { + HighsInt numRowsRemoved = 0; + HighsInt numRowsModified = 0; + HighsInt numVarsLifted = 0; + + HighsInt numCliques = + static_cast(partitionStart.size()) - 1; + + for (HighsInt i = 0; i < numCliques; ++i) { + HighsInt cliqueStart = partitionStart[i]; + HighsInt cliqueEnd = partitionStart[i + 1]; + if (cliqueEnd - cliqueStart < 2) continue; + + HighsInt row = -1; + HighsCDouble rowBound = baseBound; + + for (HighsInt j = cliqueStart; j < cliqueEnd; ++j) { + HighsInt binCol = clqVars[j].col; + HighsInt val = clqVars[j].val; + const auto* bounds = boundsMap.find(binCol); + double a = bounds->standardBound.coef; + HighsInt currentrow = bounds->originalBound.origin_row; + + if (row == -1) { + row = currentrow; + unlinkRow(row); + addToMatrix(row, col, 1.0); + numRowsModified++; + } else { + removeRow(currentrow); + numRowsRemoved++; + } + + numVarsLifted++; + + if (val == 1) { + addToMatrix(row, binCol, -direction * a); + } else { + addToMatrix(row, binCol, direction * a); + rowBound -= direction * a; + } + } + if (direction > 0) { + model->row_lower_[row] = static_cast(rowBound); + model->row_upper_[row] = kHighsInf; + } else { + model->row_lower_[row] = -kHighsInf; + model->row_upper_[row] = static_cast(rowBound); + } + } + overallNumRowsRemoved += numRowsRemoved; + overallNumRowsModified += numRowsModified; + overallNumVarsLifted += numVarsLifted; + }; + + mergeCliques(vlbsClique, vlbsCliquePartitionStart, vlbsFromRow, lb, + HighsInt{1}); + mergeCliques(vubsClique, vubsCliquePartitionStart, vubsFromRow, ub, + HighsInt{-1}); + + // clear vectors + vlbsFromRow.clear(); + vubsFromRow.clear(); + } + + if (overallNumRowsModified > 0) + printf( + "VI aggregation: %d rows modified, %d rows removed, %d vars lifted\n", + (int)overallNumRowsModified, (int)overallNumRowsRemoved, + (int)overallNumVarsLifted); + + return Result::kOk; +} + // Not currently called void HPresolve::debug(const HighsLp& lp, const HighsOptions& options) { HighsSolution reducedsol; @@ -9355,9 +9516,9 @@ HPresolve::Result HPresolve::sparsify(HighsPostsolveStack& postsolve_stack) { possibleScales.clear(); HighsInt misses = 0; - // allow no fillin if a completely continuous row is used to cancel a row - // that has integers as there are instances where this leads to a huge - // deterioration of cut performance + // allow no fillin if a completely continuous row is used to cancel a + // row that has integers as there are instances where this leads to a + // huge deterioration of cut performance HighsInt maxMisses = 1; if (rowsizeInteger[eqrow] == 0 && rowsizeInteger[candRow] != 0) --maxMisses; @@ -9477,11 +9638,12 @@ HPresolve::Result HPresolve::sparsify(HighsPostsolveStack& postsolve_stack) { std::abs(it->first - scale) <= scaleTolerance) { // there already is a scale that is very close and could produce // a matrix value for this nonzero that is below the allowed - // threshold. Therefore we check if the matrix value is small enough - // for this nonzero to be deleted, in which case the number of - // deleted nonzeros for the other scale is increased. If it is not - // small enough we do not use this scale or the other one because - // such small matrix values may lead to numerical troubles. + // threshold. Therefore we check if the matrix value is small + // enough for this nonzero to be deleted, in which case the number + // of deleted nonzeros for the other scale is increased. If it is + // not small enough we do not use this scale or the other one + // because such small matrix values may lead to numerical + // troubles. // scale is already marked to be numerically bad if (it->second == -1) continue; @@ -9514,8 +9676,8 @@ HPresolve::Result HPresolve::sparsify(HighsPostsolveStack& postsolve_stack) { assert(scale != 0.0 || numCancel == 0); - // cancels at least one nonzero if the scale cancels more than there is - // fillin + // cancels at least one nonzero if the scale cancels more than there + // is fillin if (numCancel > 1) sparsifyRows.emplace_back(candRow, scale); } } diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 871a69a223b..200eb2a7482 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -497,6 +497,8 @@ class HPresolve { void removeFixedCol(HighsInt col, double fixval); + void unlinkRow(HighsInt row); + void removeRow(HighsInt row); Result removeDependentEquations(HighsPostsolveStack& postsolve_stack); @@ -529,6 +531,8 @@ class HPresolve { void extractVarBounds(HighsInt row); + Result aggregateVarBounds(HighsPostsolveStack& postsolve_stack); + Result sparsify(HighsPostsolveStack& postsolve_stack); void setRelaxedImpliedBounds(); From a4f872eaaf1773278f8fb6500ed07213bdf6ea60 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 3 Sep 2026 16:52:28 +0200 Subject: [PATCH 10/29] Add to column presolve --- highs/presolve/HPresolve.cpp | 251 ++++++++++++++++------------------- highs/presolve/HPresolve.h | 2 +- 2 files changed, 116 insertions(+), 137 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 4fb165f9dbe..7eae35727bc 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -4944,6 +4944,9 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, if (model->integrality_[col] != HighsVarType::kInteger) updateRowDualImpliedBounds(col); + // aggregate variable bounds + aggregateVarBounds(col); + return Result::kOk; } @@ -9149,9 +9152,20 @@ void HPresolve::extractVarBounds(HighsInt row) { } } -HPresolve::Result HPresolve::aggregateVarBounds( - HighsPostsolveStack& postsolve_stack) { +void HPresolve::aggregateVarBounds(HighsInt col) { assert(mipsolver != nullptr); + + if (colDeleted[col]) return; + + // get lower bound and upper bound + double lb = model->col_lower_[col]; + double ub = model->col_upper_[col]; + + // skip binary columns — we aggregate VLBs/VUBs on non-binary columns + if (model->integrality_[col] == HighsVarType::kInteger && lb == 0.0 && + ub == 1.0) + return; + HighsImplications& implications = mipsolver->mipdata_->implications; HighsCliqueTable& cliquetable = mipsolver->mipdata_->cliquetable; @@ -9163,145 +9177,110 @@ HPresolve::Result HPresolve::aggregateVarBounds( HighsHashTree vlbsFromRow; HighsHashTree vubsFromRow; - HighsInt overallNumRowsRemoved = 0; - HighsInt overallNumRowsModified = 0; - HighsInt overallNumVarsLifted = 0; - - for (HighsInt col = 0; col < model->num_col_; ++col) { - if (colDeleted[col]) continue; - // get lower bound and upper bound - double lb = model->col_lower_[col]; - double ub = model->col_upper_[col]; - - // skip binary columns — we aggregate VLBs/VUBs on non-binary columns - if (model->integrality_[col] == HighsVarType::kInteger && lb == 0.0 && - ub == 1.0) - continue; - - // collect VLBs and VUBs - implications.getVlbs(col).for_each( - [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { - if (vlb.origin_row >= 0) { - HighsCDouble newCoef = vlb.constant - static_cast(lb); - if (vlb.coef > 0) newCoef += vlb.coef; - vlbsFromRow.insert( - binaryCol, - colImpliedBounds{vlb, HighsImplications::VarBound{ - static_cast(newCoef), lb, - vlb.origin_row}}); - } - }); - implications.getVubs(col).for_each( - [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { - if (vub.origin_row >= 0) { - HighsCDouble newCoef = static_cast(ub) - vub.constant; - if (vub.coef < 0) newCoef -= vub.coef; - vubsFromRow.insert( - binaryCol, - colImpliedBounds{vub, HighsImplications::VarBound{ - static_cast(newCoef), ub, - vub.origin_row}}); - } - }); - - // set up cliques - std::vector vlbsClique; - std::vector vubsClique; - vlbsFromRow.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { - HighsInt val = bounds.originalBound.coef > 0 ? 1 : 0; - vlbsClique.emplace_back(binaryCol, val); - }); - vubsFromRow.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { - HighsInt val = bounds.originalBound.coef < 0 ? 1 : 0; - vubsClique.emplace_back(binaryCol, val); - }); - - // clique partitioning - std::vector vlbsCliquePartitionStart; - std::vector vubsCliquePartitionStart; - cliquetable.cliquePartition(vlbsClique, vlbsCliquePartitionStart); - cliquetable.cliquePartition(vubsClique, vubsCliquePartitionStart); - HighsInt numVlbsCliques = - static_cast(vlbsCliquePartitionStart.size()) - 1; - HighsInt numVubsCliques = - static_cast(vubsCliquePartitionStart.size()) - 1; - - auto mergeCliques = - [&](std::vector& clqVars, - std::vector& partitionStart, - HighsHashTree& boundsMap, - double baseBound, HighsInt direction) { - HighsInt numRowsRemoved = 0; - HighsInt numRowsModified = 0; - HighsInt numVarsLifted = 0; - - HighsInt numCliques = - static_cast(partitionStart.size()) - 1; - - for (HighsInt i = 0; i < numCliques; ++i) { - HighsInt cliqueStart = partitionStart[i]; - HighsInt cliqueEnd = partitionStart[i + 1]; - if (cliqueEnd - cliqueStart < 2) continue; - - HighsInt row = -1; - HighsCDouble rowBound = baseBound; - - for (HighsInt j = cliqueStart; j < cliqueEnd; ++j) { - HighsInt binCol = clqVars[j].col; - HighsInt val = clqVars[j].val; - const auto* bounds = boundsMap.find(binCol); - double a = bounds->standardBound.coef; - HighsInt currentrow = bounds->originalBound.origin_row; - - if (row == -1) { - row = currentrow; - unlinkRow(row); - addToMatrix(row, col, 1.0); - numRowsModified++; - } else { - removeRow(currentrow); - numRowsRemoved++; - } - - numVarsLifted++; + // collect VLBs and VUBs + implications.getVlbs(col).for_each( + [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { + if (vlb.origin_row >= 0) { + HighsCDouble newCoef = vlb.constant - static_cast(lb); + if (vlb.coef > 0) newCoef += vlb.coef; + vlbsFromRow.insert( + binaryCol, colImpliedBounds{vlb, HighsImplications::VarBound{ + static_cast(newCoef), + lb, vlb.origin_row}}); + } + }); + implications.getVubs(col).for_each( + [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { + if (vub.origin_row >= 0) { + HighsCDouble newCoef = static_cast(ub) - vub.constant; + if (vub.coef < 0) newCoef -= vub.coef; + vubsFromRow.insert( + binaryCol, colImpliedBounds{vub, HighsImplications::VarBound{ + static_cast(newCoef), + ub, vub.origin_row}}); + } + }); - if (val == 1) { - addToMatrix(row, binCol, -direction * a); - } else { - addToMatrix(row, binCol, direction * a); - rowBound -= direction * a; - } - } - if (direction > 0) { - model->row_lower_[row] = static_cast(rowBound); - model->row_upper_[row] = kHighsInf; - } else { - model->row_lower_[row] = -kHighsInf; - model->row_upper_[row] = static_cast(rowBound); - } - } - overallNumRowsRemoved += numRowsRemoved; - overallNumRowsModified += numRowsModified; - overallNumVarsLifted += numVarsLifted; - }; + // set up cliques + std::vector vlbsClique; + std::vector vubsClique; + vlbsFromRow.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { + HighsInt val = bounds.originalBound.coef > 0 ? 1 : 0; + vlbsClique.emplace_back(binaryCol, val); + }); + vubsFromRow.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { + HighsInt val = bounds.originalBound.coef < 0 ? 1 : 0; + vubsClique.emplace_back(binaryCol, val); + }); - mergeCliques(vlbsClique, vlbsCliquePartitionStart, vlbsFromRow, lb, - HighsInt{1}); - mergeCliques(vubsClique, vubsCliquePartitionStart, vubsFromRow, ub, - HighsInt{-1}); + // clique partitioning + std::vector vlbsCliquePartitionStart; + std::vector vubsCliquePartitionStart; + cliquetable.cliquePartition(vlbsClique, vlbsCliquePartitionStart); + cliquetable.cliquePartition(vubsClique, vubsCliquePartitionStart); + HighsInt numVlbsCliques = + static_cast(vlbsCliquePartitionStart.size()) - 1; + HighsInt numVubsCliques = + static_cast(vubsCliquePartitionStart.size()) - 1; + + HighsInt numRowsRemoved = 0; + HighsInt numRowsModified = 0; + HighsInt numVarsLifted = 0; + + auto mergeCliques = [&](std::vector& clqVars, + std::vector& partitionStart, + HighsHashTree& boundsMap, + double baseBound, HighsInt direction) { + HighsInt numCliques = static_cast(partitionStart.size()) - 1; + + for (HighsInt i = 0; i < numCliques; ++i) { + HighsInt cliqueStart = partitionStart[i]; + HighsInt cliqueEnd = partitionStart[i + 1]; + if (cliqueEnd - cliqueStart < 2) continue; + + HighsInt row = -1; + HighsCDouble rowBound = baseBound; + + for (HighsInt j = cliqueStart; j < cliqueEnd; ++j) { + HighsInt binCol = clqVars[j].col; + HighsInt val = clqVars[j].val; + const auto* bounds = boundsMap.find(binCol); + double a = bounds->standardBound.coef; + HighsInt currentrow = bounds->originalBound.origin_row; + + if (row == -1) { + row = currentrow; + unlinkRow(row); + addToMatrix(row, col, 1.0); + numRowsModified++; + } else { + removeRow(currentrow); + numRowsRemoved++; + } - // clear vectors - vlbsFromRow.clear(); - vubsFromRow.clear(); - } + numVarsLifted++; - if (overallNumRowsModified > 0) - printf( - "VI aggregation: %d rows modified, %d rows removed, %d vars lifted\n", - (int)overallNumRowsModified, (int)overallNumRowsRemoved, - (int)overallNumVarsLifted); + if (val == 1) { + addToMatrix(row, binCol, -direction * a); + } else { + addToMatrix(row, binCol, direction * a); + rowBound -= direction * a; + } + } + if (direction > 0) { + model->row_lower_[row] = static_cast(rowBound); + model->row_upper_[row] = kHighsInf; + } else { + model->row_lower_[row] = -kHighsInf; + model->row_upper_[row] = static_cast(rowBound); + } + } + }; - return Result::kOk; + mergeCliques(vlbsClique, vlbsCliquePartitionStart, vlbsFromRow, lb, + HighsInt{1}); + mergeCliques(vubsClique, vubsCliquePartitionStart, vubsFromRow, ub, + HighsInt{-1}); } // Not currently called diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 200eb2a7482..9d2544c43a9 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -531,7 +531,7 @@ class HPresolve { void extractVarBounds(HighsInt row); - Result aggregateVarBounds(HighsPostsolveStack& postsolve_stack); + void aggregateVarBounds(HighsInt col); Result sparsify(HighsPostsolveStack& postsolve_stack); From 3272ad6439cb824c451f56bb4a1fbd5f91aa44fb Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 3 Sep 2026 16:55:59 +0200 Subject: [PATCH 11/29] Clean up --- highs/presolve/HPresolve.cpp | 57 +++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 7eae35727bc..7373e9492b3 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9177,29 +9177,36 @@ void HPresolve::aggregateVarBounds(HighsInt col) { HighsHashTree vlbsFromRow; HighsHashTree vubsFromRow; - // collect VLBs and VUBs - implications.getVlbs(col).for_each( - [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { - if (vlb.origin_row >= 0) { - HighsCDouble newCoef = vlb.constant - static_cast(lb); - if (vlb.coef > 0) newCoef += vlb.coef; - vlbsFromRow.insert( - binaryCol, colImpliedBounds{vlb, HighsImplications::VarBound{ - static_cast(newCoef), - lb, vlb.origin_row}}); - } - }); - implications.getVubs(col).for_each( - [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { - if (vub.origin_row >= 0) { - HighsCDouble newCoef = static_cast(ub) - vub.constant; - if (vub.coef < 0) newCoef -= vub.coef; - vubsFromRow.insert( - binaryCol, colImpliedBounds{vub, HighsImplications::VarBound{ - static_cast(newCoef), - ub, vub.origin_row}}); - } - }); + // collect VLBs (standardization needs finite lb) + if (lb != -kHighsInf) { + implications.getVlbs(col).for_each( + [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { + if (vlb.origin_row >= 0) { + HighsCDouble newCoef = vlb.constant - static_cast(lb); + if (vlb.coef > 0) newCoef += vlb.coef; + vlbsFromRow.insert( + binaryCol, + colImpliedBounds{vlb, HighsImplications::VarBound{ + static_cast(newCoef), lb, + vlb.origin_row}}); + } + }); + } + // collect VUBs (standardization needs finite ub) + if (ub != kHighsInf) { + implications.getVubs(col).for_each( + [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { + if (vub.origin_row >= 0) { + HighsCDouble newCoef = static_cast(ub) - vub.constant; + if (vub.coef < 0) newCoef -= vub.coef; + vubsFromRow.insert( + binaryCol, + colImpliedBounds{vub, HighsImplications::VarBound{ + static_cast(newCoef), ub, + vub.origin_row}}); + } + }); + } // set up cliques std::vector vlbsClique; @@ -9218,10 +9225,6 @@ void HPresolve::aggregateVarBounds(HighsInt col) { std::vector vubsCliquePartitionStart; cliquetable.cliquePartition(vlbsClique, vlbsCliquePartitionStart); cliquetable.cliquePartition(vubsClique, vubsCliquePartitionStart); - HighsInt numVlbsCliques = - static_cast(vlbsCliquePartitionStart.size()) - 1; - HighsInt numVubsCliques = - static_cast(vubsCliquePartitionStart.size()) - 1; HighsInt numRowsRemoved = 0; HighsInt numRowsModified = 0; From 998bdb7186de809375e2e25767927a686fc6c914 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 3 Sep 2026 21:00:46 +0200 Subject: [PATCH 12/29] Check if mipsolver != nullptr --- highs/presolve/HPresolve.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 7373e9492b3..3fa896df9da 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9153,9 +9153,7 @@ void HPresolve::extractVarBounds(HighsInt row) { } void HPresolve::aggregateVarBounds(HighsInt col) { - assert(mipsolver != nullptr); - - if (colDeleted[col]) return; + if (mipsolver == nullptr || colDeleted[col]) return; // get lower bound and upper bound double lb = model->col_lower_[col]; @@ -9178,7 +9176,7 @@ void HPresolve::aggregateVarBounds(HighsInt col) { HighsHashTree vubsFromRow; // collect VLBs (standardization needs finite lb) - if (lb != -kHighsInf) { + if (lb > -kHighsInf) { implications.getVlbs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { if (vlb.origin_row >= 0) { @@ -9193,7 +9191,7 @@ void HPresolve::aggregateVarBounds(HighsInt col) { }); } // collect VUBs (standardization needs finite ub) - if (ub != kHighsInf) { + if (ub < kHighsInf) { implications.getVubs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { if (vub.origin_row >= 0) { @@ -9280,10 +9278,12 @@ void HPresolve::aggregateVarBounds(HighsInt col) { } }; - mergeCliques(vlbsClique, vlbsCliquePartitionStart, vlbsFromRow, lb, - HighsInt{1}); - mergeCliques(vubsClique, vubsCliquePartitionStart, vubsFromRow, ub, - HighsInt{-1}); + if (lb > -kHighsInf) + mergeCliques(vlbsClique, vlbsCliquePartitionStart, vlbsFromRow, lb, + HighsInt{1}); + if (ub < kHighsInf) + mergeCliques(vubsClique, vubsCliquePartitionStart, vubsFromRow, ub, + HighsInt{-1}); } // Not currently called From 4807ad15237dd2392aba6abed7e106aaede8d46c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 7 Sep 2026 09:46:01 +0200 Subject: [PATCH 13/29] Add cliqueCover --- highs/mip/HighsCliqueTable.cpp | 94 ++++++++++++++++++++++----- highs/mip/HighsCliqueTable.h | 9 +++ highs/presolve/HPresolve.cpp | 114 +++++++++++++++++---------------- 3 files changed, 148 insertions(+), 69 deletions(-) diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index a054d1867b9..fc1055b7c14 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -987,6 +987,26 @@ void HighsCliqueTable::extractCliques( } } +HighsInt HighsCliqueTable::extendClique( + std::vector& neighbourhoodInds, std::vector& clqVars, + HighsInt seedPos, HighsInt extensionEnd, + const std::function& + candidateOrder) { + for (HighsInt i = seedPos; i < extensionEnd; ++i) { + if (candidateOrder) + pdqsort_branchless(clqVars.begin() + i, clqVars.begin() + extensionEnd, + candidateOrder); + CliqueVar v = clqVars[i]; + HighsInt extensionStart = i + 1; + extensionEnd = + partitionNeighbourhood(neighbourhoodInds, numNeighbourhoodQueries, v, + clqVars.data() + extensionStart, + extensionEnd - extensionStart) + + extensionStart; + } + return extensionEnd; +} + void HighsCliqueTable::cliquePartition(std::vector& clqVars, std::vector& partitionStart) { randgen.shuffle(clqVars.data(), clqVars.size()); @@ -997,22 +1017,12 @@ void HighsCliqueTable::cliquePartition(std::vector& clqVars, HighsInt numClqVars = clqVars.size(); partitionStart.clear(); partitionStart.reserve(clqVars.size()); - HighsInt extensionEnd = numClqVars; - partitionStart.push_back(0); - for (HighsInt i = 0; i < numClqVars; ++i) { - if (i == extensionEnd) { - partitionStart.push_back(i); - extensionEnd = numClqVars; - } - CliqueVar v = clqVars[i]; - HighsInt extensionStart = i + 1; - extensionEnd = - partitionNeighbourhood(neighbourhoodInds, numNeighbourhoodQueries, v, - clqVars.data() + extensionStart, - extensionEnd - extensionStart) + - extensionStart; - } + HighsInt pos = 0; + while (pos < numClqVars) { + partitionStart.push_back(pos); + pos = extendClique(neighbourhoodInds, clqVars, pos, numClqVars); + } partitionStart.push_back(numClqVars); } @@ -1064,6 +1074,60 @@ void HighsCliqueTable::cliquePartition(const std::vector& objective, partitionStart.push_back(numClqVars); } +void HighsCliqueTable::cliqueCover(std::vector& clqVars, + std::vector>& cover) { + cover.clear(); + if (clqVars.empty()) return; + + auto candidateOrder = [&](const CliqueVar& a, const CliqueVar& b) { + return numcliquesvar[a.index()] > numcliquesvar[b.index()]; + }; + + std::vector neighbourhoodInds; + neighbourhoodInds.reserve(clqVars.size()); + + HighsInt numClqVars = clqVars.size(); + HighsInt pos = 0; + + while (pos < numClqVars) { + HighsInt cliqueEnd = extendClique(neighbourhoodInds, clqVars, pos, + numClqVars, candidateOrder); + + std::vector clique(clqVars.begin() + pos, + clqVars.begin() + cliqueEnd); + + for (HighsInt i = 0; i < pos; ++i) { + if (std::all_of(clique.begin(), clique.end(), + [&](const CliqueVar& clqvar) { + return haveCommonClique(clqVars[i], clqvar); + })) + clique.push_back(clqVars[i]); + } + + cover.push_back(std::move(clique)); + pos = cliqueEnd; + } + + // count variable occurrences + HighsHashTable count; + for (const auto& clique : cover) + for (const auto& var : clique) count[var.index()]++; + + // remove cliques where every member appears in at least one other clique + size_t kept = 0; + for (auto& clique : cover) { + if (std::all_of(clique.begin(), clique.end(), [&](const CliqueVar& var) { + return count[var.index()] >= 2; + })) { + for (const auto& var : clique) count[var.index()]--; + } else { + std::swap(cover[kept], clique); + kept++; + } + } + cover.resize(kept); +} + bool HighsCliqueTable::foundCover(HighsDomain& globaldom, CliqueVar v1, CliqueVar v2) { HighsInt commonclique = findCommonCliqueId(v1, v2); diff --git a/highs/mip/HighsCliqueTable.h b/highs/mip/HighsCliqueTable.h index 30dd56e2bf1..1ca9b0542a6 100644 --- a/highs/mip/HighsCliqueTable.h +++ b/highs/mip/HighsCliqueTable.h @@ -160,6 +160,12 @@ class HighsCliqueTable { int64_t& numNeighbourhoodqueries, CliqueVar v, CliqueVar* q, HighsInt N) const; + HighsInt extendClique( + std::vector& neighbourhoodInds, std::vector& clqVars, + HighsInt seedPos, HighsInt extensionEnd, + const std::function& + candidateOrder = nullptr); + public: int64_t numNeighbourhoodQueries; @@ -263,6 +269,9 @@ class HighsCliqueTable { std::vector& clqVars, std::vector& partitionStart); + void cliqueCover(std::vector& clqVars, + std::vector>& cover); + bool foundCover(HighsDomain& globaldom, CliqueVar v1, CliqueVar v2); void extractCliques(HighsMipSolver& mipsolver, bool transformRows = true); diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 3fa896df9da..26a64a13ffd 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9218,72 +9218,78 @@ void HPresolve::aggregateVarBounds(HighsInt col) { vubsClique.emplace_back(binaryCol, val); }); - // clique partitioning - std::vector vlbsCliquePartitionStart; - std::vector vubsCliquePartitionStart; - cliquetable.cliquePartition(vlbsClique, vlbsCliquePartitionStart); - cliquetable.cliquePartition(vubsClique, vubsCliquePartitionStart); + // clique cover + std::vector> vlbsCover; + std::vector> vubsCover; + cliquetable.cliqueCover(vlbsClique, vlbsCover); + cliquetable.cliqueCover(vubsClique, vubsCover); HighsInt numRowsRemoved = 0; HighsInt numRowsModified = 0; HighsInt numVarsLifted = 0; - auto mergeCliques = [&](std::vector& clqVars, - std::vector& partitionStart, - HighsHashTree& boundsMap, - double baseBound, HighsInt direction) { - HighsInt numCliques = static_cast(partitionStart.size()) - 1; - - for (HighsInt i = 0; i < numCliques; ++i) { - HighsInt cliqueStart = partitionStart[i]; - HighsInt cliqueEnd = partitionStart[i + 1]; - if (cliqueEnd - cliqueStart < 2) continue; - - HighsInt row = -1; - HighsCDouble rowBound = baseBound; - - for (HighsInt j = cliqueStart; j < cliqueEnd; ++j) { - HighsInt binCol = clqVars[j].col; - HighsInt val = clqVars[j].val; - const auto* bounds = boundsMap.find(binCol); - double a = bounds->standardBound.coef; - HighsInt currentrow = bounds->originalBound.origin_row; - - if (row == -1) { - row = currentrow; + auto mergeCliques = + [&](std::vector>& cover, + HighsHashTree& boundsMap, + double baseBound, HighsInt direction) { + std::set consumedRows; + + for (const auto& clique : cover) { + if (clique.size() < 2) continue; + + // find an unconsumed row to reuse and remove the rest + HighsInt row = -1; + for (const auto& var : clique) { + const auto* bounds = boundsMap.find(var.col); + HighsInt currentrow = bounds->originalBound.origin_row; + if (consumedRows.insert(currentrow).second) { + if (row == -1) + row = currentrow; + else { + removeRow(currentrow); + numRowsRemoved++; + } + } + } + if (row == -1) continue; + + // rewrite the reused row with the aggregated constraint unlinkRow(row); addToMatrix(row, col, 1.0); numRowsModified++; - } else { - removeRow(currentrow); - numRowsRemoved++; - } - numVarsLifted++; + HighsCDouble rowBound = baseBound; + for (const auto& var : clique) { + const auto* bounds = boundsMap.find(var.col); + double a = bounds->standardBound.coef; + numVarsLifted++; + if (var.val == 1) { + addToMatrix(row, var.col, -direction * a); + } else { + addToMatrix(row, var.col, direction * a); + rowBound -= direction * a; + } + } - if (val == 1) { - addToMatrix(row, binCol, -direction * a); - } else { - addToMatrix(row, binCol, direction * a); - rowBound -= direction * a; + if (direction > 0) { + model->row_lower_[row] = static_cast(rowBound); + model->row_upper_[row] = kHighsInf; + } else { + model->row_lower_[row] = -kHighsInf; + model->row_upper_[row] = static_cast(rowBound); + } } - } - if (direction > 0) { - model->row_lower_[row] = static_cast(rowBound); - model->row_upper_[row] = kHighsInf; - } else { - model->row_lower_[row] = -kHighsInf; - model->row_upper_[row] = static_cast(rowBound); - } - } - }; + }; + + if (lb > -kHighsInf) mergeCliques(vlbsCover, vlbsFromRow, lb, HighsInt{1}); + if (ub < kHighsInf) mergeCliques(vubsCover, vubsFromRow, ub, HighsInt{-1}); - if (lb > -kHighsInf) - mergeCliques(vlbsClique, vlbsCliquePartitionStart, vlbsFromRow, lb, - HighsInt{1}); - if (ub < kHighsInf) - mergeCliques(vubsClique, vubsCliquePartitionStart, vubsFromRow, ub, - HighsInt{-1}); + if (numRowsRemoved > 0 || numRowsModified > 0) + printf( + "aggregateVarBounds col %d: %d rows removed, %d rows modified, %d vars " + "lifted\n", + (int)col, (int)numRowsRemoved, (int)numRowsModified, + (int)numVarsLifted); } // Not currently called From 68e34d03c64d366902d3a7ee7b84cf4058dd15ba Mon Sep 17 00:00:00 2001 From: fwesselm Date: Mon, 7 Sep 2026 22:31:21 +0200 Subject: [PATCH 14/29] Added update of dual row bounds and also a test --- check/TestPresolveRules.cpp | 81 ++++++++++++++++++++++++++++++++ highs/presolve/HPresolve.cpp | 79 +++++++++++++++---------------- highs/presolve/HPresolve.h | 3 ++ highs/presolve/HPresolveTest.cpp | 9 ++++ 4 files changed, 130 insertions(+), 42 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 49d7532f7b8..78fbd37233d 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -3,6 +3,9 @@ #include "HCheckConfig.h" #include "Highs.h" #include "catch.hpp" +#include "mip/HighsMipSolver.h" +#include "mip/HighsMipSolverData.h" +#include "parallel/HighsParallel.h" #include "presolve/HPresolve.h" #include "presolve/HighsPostsolveStack.h" @@ -90,6 +93,84 @@ TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { h.resetGlobalScheduler(true); } +TEST_CASE("test-implied-bound-aggregation", "[highs_test_presolve_rules]") { + // Three VLB rows on a continuous variable z with binary variables + // x0, x1, x2 in a clique (from assignment equation). Probing should + // discover the clique and aggregation should merge the three VLBs + // into one stronger row. + // + // min z + // x0 + x1 + x2 = 1 + // z >= 10*x0 (stored as -z + 10*x0 <= 0) + // z >= 20*x1 (stored as -z + 20*x1 <= 0) + // z >= 30*x2 (stored as -z + 30*x2 <= 0) + // z >= 0, x0,x1,x2 binary + HighsLp lp; + lp.num_col_ = 4; + lp.num_row_ = 4; + lp.sense_ = ObjSense::kMinimize; + lp.col_cost_ = {1, 0, 0, 0}; + lp.col_lower_ = {0, 0, 0, 0}; + lp.col_upper_ = {kHighsInf, 1, 1, 1}; + lp.integrality_ = {HighsVarType::kContinuous, HighsVarType::kInteger, + HighsVarType::kInteger, HighsVarType::kInteger}; + lp.row_lower_ = {1, -kHighsInf, -kHighsInf, -kHighsInf}; + lp.row_upper_ = {1, 0, 0, 0}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.num_col_ = 4; + lp.a_matrix_.num_row_ = 4; + lp.a_matrix_.start_ = {0, 3, 5, 7, 9}; + lp.a_matrix_.index_ = {1, 2, 3, 0, 1, 0, 2, 0, 3}; + lp.a_matrix_.value_ = {-1, -1, -1, 1, 10, 1, 20, 1, 30}; + + highs::parallel::initialize_scheduler(1); + + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.setOptionValue("presolve_rule_test", kPresolveRuleProbing); + highs.passModel(lp); + + HighsCallback callback(&highs); + const HighsOptions& options = highs.getOptions(); + HighsSolution solution; + HighsProfiling profiling; + + HighsMipSolver mipsolver(callback, options, lp, solution); + mipsolver.timer_.start(); + profiling.initialize(mipsolver.timer_, true, true); + mipsolver.setProfiling(&profiling); + mipsolver.mipdata_ = + std::unique_ptr(new HighsMipSolverData(mipsolver)); + mipsolver.mipdata_->init(); + mipsolver.mipdata_->setupDomainPropagation(); + + // Populate VLBs: z >= 10*x0 (row 1), z >= 20*x1 (row 2), z >= 30*x2 (row 3) + HighsImplications& implications = mipsolver.mipdata_->implications; + implications.addVLB(0, 1, 10.0, 0.0, 1); + implications.addVLB(0, 2, 20.0, 0.0, 2); + implications.addVLB(0, 3, 30.0, 0.0, 3); + + presolve::HighsPostsolveStack& postsolve_stack = + mipsolver.mipdata_->postSolveStack; + + presolve::HPresolve presolve; + presolve.setInput(mipsolver, -1); + REQUIRE(presolve.okSetupPresolveDataStructures()); + HighsModelStatus status = presolve.run(postsolve_stack); + REQUIRE(status == HighsModelStatus::kNotset); + + // The three VLBs form a clique, so aggregation merges them into one + // row, removing at least 2 of the 3 VLB rows + HighsInt num_surviving_vlb_rows = 0; + for (HighsInt i = 0; i < mipsolver.numRow(); i++) { + HighsInt orig = postsolve_stack.getOrigRowIndex(i); + if (orig >= 1) num_surviving_vlb_rows++; + } + REQUIRE(num_surviving_vlb_rows <= 1); + + HighsTaskExecutor::shutdown(true); +} + TEST_CASE("test-parallel-rows-cut-ordering", "[highs_test_presolve_rules]") { // Rows 0 and 1 are parallel (both [1, 1]). Row 0 is marked as a // cut. detectParallelRowsAndCols must remove the cut row (0) and diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 26a64a13ffd..ca1c3c6bc64 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -630,15 +630,7 @@ void HPresolve::unlink(HighsInt pos) { impliedRowBounds.remove(Arow[pos], Acol[pos], Avalue[pos]); } - // remove implied bounds on row duals that where implied by this column's dual - // constraint - resetRowDualImpliedBoundsDerivedFromCol(Acol[pos]); - - // remove implied bounds on columns that where implied by this row - resetColImpliedBoundsDerivedFromRow(Arow[pos]); - - // modifications to row invalidate lifting opportunities - clearLiftingOpportunities(Arow[pos]); + matrixNonZeroChanged(Arow[pos], Acol[pos]); // remove non-zero Avalue[pos] = 0; @@ -935,6 +927,21 @@ void HPresolve::resetRowDualImpliedBoundsDerivedFromCol(HighsInt col) { } } +void HPresolve::matrixNonZeroChanged(HighsInt row, HighsInt col) { + // remove implied bounds on row duals that were implied by this column's + // dual constraint + resetRowDualImpliedBoundsDerivedFromCol(col); + + // remove implied bounds on columns that were implied by this row + resetColImpliedBoundsDerivedFromRow(row); + + // modifications to row invalidate lifting opportunities + clearLiftingOpportunities(row); + + // inform implication table about row modification + if (mipsolver != nullptr) mipsolver->mipdata_->implications.rowModified(row); +} + HighsInt HPresolve::findNonzero(HighsInt row, HighsInt col) { if (rowroot[row] == -1) return -1; @@ -1720,6 +1727,9 @@ HPresolve::Result HPresolve::finaliseProbing( HPRESOLVE_CHECKED_CALL( applyConflictGraphSubstitutions(postsolve_stack, numVarsSubstituted)); + // aggregate variable bounds now that the clique table is populated + for (HighsInt i = 0; i != model->num_col_; ++i) aggregateVarBounds(i); + return checkLimits(postsolve_stack); } @@ -2211,8 +2221,6 @@ void HPresolve::addToMatrix(const HighsInt row, const HighsInt col, markChangedRow(row); markChangedCol(col); - if (mipsolver != nullptr) mipsolver->mipdata_->implications.rowModified(row); - if (pos == -1) { if (freeslots.empty()) { pos = static_cast(Avalue.size()); @@ -2234,36 +2242,22 @@ void HPresolve::addToMatrix(const HighsInt row, const HighsInt col, link(pos); - // remove implied bounds on row duals that where implied by this column's - // dual constraint - resetRowDualImpliedBoundsDerivedFromCol(col); - - // remove implied bounds on columns that where implied by this row - resetColImpliedBoundsDerivedFromRow(row); - - // modifications to row invalidate lifting opportunities - clearLiftingOpportunities(row); + matrixNonZeroChanged(row, col); } else { double sum = Avalue[pos] + val; if (std::abs(sum) <= options->small_matrix_value) { unlink(pos); } else { - // remove implied bounds on row duals that where implied by this column's - // dual constraint - resetRowDualImpliedBoundsDerivedFromCol(col); - - // remove implied bounds on columns that where implied by this row - resetColImpliedBoundsDerivedFromRow(row); - - // modifications to row invalidate lifting opportunities - clearLiftingOpportunities(row); + matrixNonZeroChanged(row, col); // remove the locks and contribution to implied (dual) row bounds, then // add then again impliedRowBounds.remove(row, col, Avalue[pos]); impliedDualRowBounds.remove(col, row, Avalue[pos]); + Avalue[pos] = sum; + // value not zero, add new contributions and locks with opposite sign impliedRowBounds.add(row, col, Avalue[pos]); impliedDualRowBounds.add(col, row, Avalue[pos]); @@ -4944,9 +4938,6 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, if (model->integrality_[col] != HighsVarType::kInteger) updateRowDualImpliedBounds(col); - // aggregate variable bounds - aggregateVarBounds(col); - return Result::kOk; } @@ -5721,6 +5712,8 @@ HPresolve::Result HPresolve::zeroCostSingleton( model->row_lower_[row] = newRowLower; model->row_upper_[row] = newRowUpper; + if (newRowLower == -kHighsInf) changeRowDualUpper(row, 0.0); + if (newRowUpper == kHighsInf) changeRowDualLower(row, 0.0); if (was_equation && newRowLower != newRowUpper && eqiters[row] != equations.end()) { equations.erase(eqiters[row]); @@ -7990,11 +7983,6 @@ 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]; } @@ -9271,12 +9259,18 @@ void HPresolve::aggregateVarBounds(HighsInt col) { } } + // update row dual bounds to match the new row type, + // as the original row may have been <= or >= if (direction > 0) { model->row_lower_[row] = static_cast(rowBound); model->row_upper_[row] = kHighsInf; + changeRowDualLower(row, 0.0); + changeRowDualUpper(row, kHighsInf); } else { model->row_lower_[row] = -kHighsInf; model->row_upper_[row] = static_cast(rowBound); + changeRowDualLower(row, -kHighsInf); + changeRowDualUpper(row, 0.0); } } }; @@ -9285,11 +9279,12 @@ void HPresolve::aggregateVarBounds(HighsInt col) { if (ub < kHighsInf) mergeCliques(vubsCover, vubsFromRow, ub, HighsInt{-1}); if (numRowsRemoved > 0 || numRowsModified > 0) - printf( - "aggregateVarBounds col %d: %d rows removed, %d rows modified, %d vars " - "lifted\n", - (int)col, (int)numRowsRemoved, (int)numRowsModified, - (int)numVarsLifted); + highsLogDev( + options->log_options, HighsLogType::kInfo, + "Implied variable bound aggregation for column %" HIGHSINT_FORMAT + ": %" HIGHSINT_FORMAT " rows removed, %" HIGHSINT_FORMAT + " rows modified, %" HIGHSINT_FORMAT " vars lifted\n", + col, numRowsRemoved, numRowsModified, numVarsLifted); } // Not currently called diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 9d2544c43a9..7c0f472171e 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -214,6 +214,8 @@ class HPresolve { void resetRowDualImpliedBoundsDerivedFromCol(HighsInt col); + void matrixNonZeroChanged(HighsInt row, HighsInt col); + bool rowCoefficientsIntegral(HighsInt row, double scale) const; bool isImpliedFree(HighsInt col) const; @@ -552,6 +554,7 @@ class HPresolve { Result presolveRuleTestColStuffing(HighsPostsolveStack& postsolve_stack); Result presolveRuleTestParallelRowsAndCols( HighsPostsolveStack& postsolve_stack); + Result presolveRuleTestProbing(HighsPostsolveStack& postsolve_stack); // Not currently called static void debug(const HighsLp& lp, const HighsOptions& options); diff --git a/highs/presolve/HPresolveTest.cpp b/highs/presolve/HPresolveTest.cpp index ff06d91285a..d5771314b50 100644 --- a/highs/presolve/HPresolveTest.cpp +++ b/highs/presolve/HPresolveTest.cpp @@ -16,6 +16,8 @@ HPresolve::Result HPresolve::presolveRuleTest( return presolveRuleTestColStuffing(postsolve_stack); } else if (options->presolve_rule_test == kPresolveRuleParallelRowsAndCols) { return presolveRuleTestParallelRowsAndCols(postsolve_stack); + } else if (options->presolve_rule_test == kPresolveRuleProbing) { + return presolveRuleTestProbing(postsolve_stack); } return Result::kOk; } @@ -45,4 +47,11 @@ HPresolve::Result HPresolve::presolveRuleTestParallelRowsAndCols( "HPresolve::presolveRuleTestParallelRowsAndCols\n"); return detectParallelRowsAndCols(postsolve_stack); } +HPresolve::Result HPresolve::presolveRuleTestProbing( + HighsPostsolveStack& postsolve_stack) { + assert(options->presolve_rule_test == kPresolveRuleProbing); + highsLogUser(options->log_options, HighsLogType::kInfo, + "HPresolve::presolveRuleTestProbing\n"); + return runProbing(postsolve_stack); +} } // namespace presolve From ae03a762c543c9e559b058b5c4204f1ac45fc167 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 8 Sep 2026 07:40:39 +0200 Subject: [PATCH 15/29] Also use implications without origin rows --- highs/presolve/HPresolve.cpp | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ca1c3c6bc64..725d6b3b61d 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9167,30 +9167,24 @@ void HPresolve::aggregateVarBounds(HighsInt col) { if (lb > -kHighsInf) { implications.getVlbs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { - if (vlb.origin_row >= 0) { - HighsCDouble newCoef = vlb.constant - static_cast(lb); - if (vlb.coef > 0) newCoef += vlb.coef; - vlbsFromRow.insert( - binaryCol, - colImpliedBounds{vlb, HighsImplications::VarBound{ - static_cast(newCoef), lb, - vlb.origin_row}}); - } + HighsCDouble newCoef = vlb.constant - static_cast(lb); + if (vlb.coef > 0) newCoef += vlb.coef; + vlbsFromRow.insert( + binaryCol, colImpliedBounds{vlb, HighsImplications::VarBound{ + static_cast(newCoef), + lb, vlb.origin_row}}); }); } // collect VUBs (standardization needs finite ub) if (ub < kHighsInf) { implications.getVubs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { - if (vub.origin_row >= 0) { - HighsCDouble newCoef = static_cast(ub) - vub.constant; - if (vub.coef < 0) newCoef -= vub.coef; - vubsFromRow.insert( - binaryCol, - colImpliedBounds{vub, HighsImplications::VarBound{ - static_cast(newCoef), ub, - vub.origin_row}}); - } + HighsCDouble newCoef = static_cast(ub) - vub.constant; + if (vub.coef < 0) newCoef -= vub.coef; + vubsFromRow.insert( + binaryCol, colImpliedBounds{vub, HighsImplications::VarBound{ + static_cast(newCoef), + ub, vub.origin_row}}); }); } @@ -9230,6 +9224,7 @@ void HPresolve::aggregateVarBounds(HighsInt col) { for (const auto& var : clique) { const auto* bounds = boundsMap.find(var.col); HighsInt currentrow = bounds->originalBound.origin_row; + if (currentrow < 0) continue; if (consumedRows.insert(currentrow).second) { if (row == -1) row = currentrow; From ec3dc096d8c71816f3ab6acad8fc1d92a6351b8e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 8 Sep 2026 09:15:30 +0200 Subject: [PATCH 16/29] Fix handling of non-model vlbs/vubs --- check/TestPresolveRules.cpp | 126 ++++++++++++++++++++++++++++++-- highs/mip/HighsImplications.cpp | 36 ++++----- highs/mip/HighsImplications.h | 12 ++- highs/presolve/HPresolve.cpp | 39 +++++----- 4 files changed, 165 insertions(+), 48 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 78fbd37233d..736bfedff54 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -160,13 +160,129 @@ TEST_CASE("test-implied-bound-aggregation", "[highs_test_presolve_rules]") { REQUIRE(status == HighsModelStatus::kNotset); // The three VLBs form a clique, so aggregation merges them into one - // row, removing at least 2 of the 3 VLB rows - HighsInt num_surviving_vlb_rows = 0; + // row: z - 10*x0 - 20*x1 - 30*x2 >= 0 + const HighsLp& presolved = *mipsolver.model_; + HighsInt agg_row = -1; for (HighsInt i = 0; i < mipsolver.numRow(); i++) { - HighsInt orig = postsolve_stack.getOrigRowIndex(i); - if (orig >= 1) num_surviving_vlb_rows++; + if (postsolve_stack.getOrigRowIndex(i) == 0) continue; + REQUIRE(agg_row == -1); + agg_row = i; } - REQUIRE(num_surviving_vlb_rows <= 1); + REQUIRE(agg_row >= 0); + REQUIRE(presolved.row_lower_[agg_row] == 0.0); + REQUIRE(presolved.row_upper_[agg_row] == kHighsInf); + + std::vector coeffs(4, 0.0); + for (HighsInt j = 0; j < presolved.num_col_; j++) { + for (HighsInt p = presolved.a_matrix_.start_[j]; + p < presolved.a_matrix_.start_[j + 1]; p++) { + if (presolved.a_matrix_.index_[p] != agg_row) continue; + coeffs[postsolve_stack.getOrigColIndex(j)] = + presolved.a_matrix_.value_[p]; + } + } + REQUIRE(coeffs[0] == 1.0); + REQUIRE(coeffs[1] == -10.0); + REQUIRE(coeffs[2] == -20.0); + REQUIRE(coeffs[3] == -30.0); + + HighsTaskExecutor::shutdown(true); +} + +TEST_CASE("test-implied-bound-aggregation-vub", "[highs_test_presolve_rules]") { + // Three VUB rows on a continuous variable z with binary variables + // x0, x1, x2 where x0+x1+x2 = 2 (so at most one xi can be 0). + // The complement literals (1-x0), (1-x1), (1-x2) form a clique. + // VUBs have positive coefficients so the activating literal is x=0 + // (val=0), exercising the complement offset path in mergeCliques. + // + // max z + // x0 + x1 + x2 = 2 + // z <= 10*x0 + 20 (stored as z - 10*x0 <= 20) + // z <= 20*x1 + 10 (stored as z - 20*x1 <= 10) + // z <= 30*x2 (stored as z - 30*x2 <= 0) + // 0 <= z <= 30, x0,x1,x2 binary + HighsLp lp; + lp.num_col_ = 4; + lp.num_row_ = 4; + lp.sense_ = ObjSense::kMaximize; + lp.col_cost_ = {1, 0, 0, 0}; + lp.col_lower_ = {0, 0, 0, 0}; + lp.col_upper_ = {30, 1, 1, 1}; + lp.integrality_ = {HighsVarType::kContinuous, HighsVarType::kInteger, + HighsVarType::kInteger, HighsVarType::kInteger}; + lp.row_lower_ = {2, -kHighsInf, -kHighsInf, -kHighsInf}; + lp.row_upper_ = {2, 20, 10, 0}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.num_col_ = 4; + lp.a_matrix_.num_row_ = 4; + lp.a_matrix_.start_ = {0, 3, 5, 7, 9}; + lp.a_matrix_.index_ = {1, 2, 3, 0, 1, 0, 2, 0, 3}; + lp.a_matrix_.value_ = {1, 1, 1, 1, -10, 1, -20, 1, -30}; + + highs::parallel::initialize_scheduler(1); + + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.setOptionValue("presolve_rule_test", kPresolveRuleProbing); + highs.passModel(lp); + + HighsCallback callback(&highs); + const HighsOptions& options = highs.getOptions(); + HighsSolution solution; + HighsProfiling profiling; + + HighsMipSolver mipsolver(callback, options, lp, solution); + mipsolver.timer_.start(); + profiling.initialize(mipsolver.timer_, true, true); + mipsolver.setProfiling(&profiling); + mipsolver.mipdata_ = + std::unique_ptr(new HighsMipSolverData(mipsolver)); + mipsolver.mipdata_->init(); + mipsolver.mipdata_->setupDomainPropagation(); + + // Populate VUBs with positive coef (val=0 complement path): + // z <= 10*x0 + 20 (row 1), z <= 20*x1 + 10 (row 2), z <= 30*x2 (row 3) + HighsImplications& implications = mipsolver.mipdata_->implications; + implications.addVUB(0, 1, 10.0, 20.0, 30.0, false, 1); + implications.addVUB(0, 2, 20.0, 10.0, 30.0, false, 2); + implications.addVUB(0, 3, 30.0, 0.0, 30.0, false, 3); + + presolve::HighsPostsolveStack& postsolve_stack = + mipsolver.mipdata_->postSolveStack; + + presolve::HPresolve presolve; + presolve.setInput(mipsolver, -1); + REQUIRE(presolve.okSetupPresolveDataStructures()); + HighsModelStatus status = presolve.run(postsolve_stack); + REQUIRE(status == HighsModelStatus::kNotset); + + // The three VUBs form a clique, so aggregation merges them into one + // row: z - 10*x0 - 20*x1 - 30*x2 <= -30 + const HighsLp& presolved = *mipsolver.model_; + HighsInt agg_row = -1; + for (HighsInt i = 0; i < mipsolver.numRow(); i++) { + if (postsolve_stack.getOrigRowIndex(i) == 0) continue; + REQUIRE(agg_row == -1); + agg_row = i; + } + REQUIRE(agg_row >= 0); + REQUIRE(presolved.row_lower_[agg_row] == -kHighsInf); + REQUIRE(presolved.row_upper_[agg_row] == -30.0); + + std::vector coeffs(4, 0.0); + for (HighsInt j = 0; j < presolved.num_col_; j++) { + for (HighsInt p = presolved.a_matrix_.start_[j]; + p < presolved.a_matrix_.start_[j + 1]; p++) { + if (presolved.a_matrix_.index_[p] != agg_row) continue; + coeffs[postsolve_stack.getOrigColIndex(j)] = + presolved.a_matrix_.value_[p]; + } + } + REQUIRE(coeffs[0] == 1.0); + REQUIRE(coeffs[1] == -10.0); + REQUIRE(coeffs[2] == -20.0); + REQUIRE(coeffs[3] == -30.0); HighsTaskExecutor::shutdown(true); } diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index ae2e543c501..05882e83c90 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -415,29 +415,29 @@ void HighsImplications::strengthenVarBound(VarBound& vbnd, void HighsImplications::rowModified(HighsInt row) { rowToVarBounds[row].for_each([&](HighsInt col, HighsInt binaryCol) { VarBound* vb = vlbs[col].find(binaryCol); - if (vb && vb->origin_row == row) vb->origin_row = -1; + if (vb && vb->origin == row) vb->origin = -1; vb = vubs[col].find(binaryCol); - if (vb && vb->origin_row == row) vb->origin_row = -1; + if (vb && vb->origin == row) vb->origin = -1; }); rowToVarBounds[row].clear(); } void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, - double vubconstant, HighsInt origin_row) { + double vubconstant, HighsInt origin) { addVUB(col, vubcol, vubcoef, vubconstant, mipsolver.mipdata_->getDomain().col_upper_[col], - mipsolver.isColIntegral(col), origin_row); + mipsolver.isColIntegral(col), origin); } void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, double vubconstant, double colupperbound, - bool colisintegral, HighsInt origin_row) { + bool colisintegral, HighsInt origin) { // assume that VUBs do not have infinite coefficients and infinite constant // terms since such VUBs effectively evaluate to NaN. assert(std::abs(vubcoef) != kHighsInf || std::abs(vubconstant) != kHighsInf); if (tooManyVarBounds()) return; - VarBound vub{vubcoef, vubconstant, origin_row}; + VarBound vub{vubcoef, vubconstant, origin}; if (colisintegral) { // try to strengthen VUB @@ -458,30 +458,30 @@ void HighsImplications::addVUB(HighsInt col, HighsInt vubcol, double vubcoef, if (minBound < currentMinBound - mipsolver.mipdata_->feastol) { currentvub.coef = vub.coef; currentvub.constant = vub.constant; - currentvub.origin_row = vub.origin_row; + currentvub.origin = vub.origin; } } else numVarBounds++; - if (origin_row >= 0) rowToVarBounds[origin_row].insert(col, vubcol); + if (origin >= 0) rowToVarBounds[origin].insert(col, vubcol); } void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, - double vlbconstant, HighsInt origin_row) { + double vlbconstant, HighsInt origin) { addVLB(col, vlbcol, vlbcoef, vlbconstant, mipsolver.mipdata_->getDomain().col_lower_[col], - mipsolver.isColIntegral(col), origin_row); + mipsolver.isColIntegral(col), origin); } void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, double vlbconstant, double colllowerbound, - bool colisintegral, HighsInt origin_row) { + bool colisintegral, HighsInt origin) { // assume that VLBs do not have infinite coefficients and infinite constant // terms since such VLBs effectively evaluate to NaN. assert(std::abs(vlbcoef) != kHighsInf || std::abs(vlbconstant) != kHighsInf); if (tooManyVarBounds()) return; - VarBound vlb{vlbcoef, vlbconstant, origin_row}; + VarBound vlb{vlbcoef, vlbconstant, origin}; if (colisintegral) { // try to strengthen VLB @@ -503,12 +503,12 @@ void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, if (maxBound > currentMaxBound + mipsolver.mipdata_->feastol) { currentvlb.coef = vlb.coef; currentvlb.constant = vlb.constant; - currentvlb.origin_row = vlb.origin_row; + currentvlb.origin = vlb.origin; } } else numVarBounds++; - if (origin_row >= 0) rowToVarBounds[origin_row].insert(col, vlbcol); + if (origin >= 0) rowToVarBounds[origin].insert(col, vlbcol); } void HighsImplications::rebuild(HighsInt ncols, @@ -574,7 +574,7 @@ void HighsImplications::rebuild(HighsInt ncols, return; addVUB(newi, newVubCol, vub.coef, vub.constant, - vub.origin_row >= 0 ? orig2reducedrow[vub.origin_row] : -1); + vub.origin >= 0 ? orig2reducedrow[vub.origin] : -1); }); oldvlbs[i].for_each([&](HighsInt vlbCol, VarBound vlb) { @@ -587,7 +587,7 @@ void HighsImplications::rebuild(HighsInt ncols, return; addVLB(newi, newVlbCol, vlb.coef, vlb.constant, - vlb.origin_row >= 0 ? orig2reducedrow[vlb.origin_row] : -1); + vlb.origin >= 0 ? orig2reducedrow[vlb.origin] : -1); }); if (mipsolver.mipdata_->getDomain().isBinary(newi)) { @@ -613,12 +613,12 @@ void HighsImplications::buildFrom(const HighsImplications& init) { for (HighsInt i = 0; i != numcol; ++i) { init.vubs[i].for_each([&](HighsInt vubCol, VarBound vub) { if (!mipsolver.mipdata_->getDomain().isBinary(vubCol)) return; - addVUB(i, vubCol, vub.coef, vub.constant, vub.origin_row); + addVUB(i, vubCol, vub.coef, vub.constant, vub.origin); }); init.vlbs[i].for_each([&](HighsInt vlbCol, VarBound vlb) { if (!mipsolver.mipdata_->getDomain().isBinary(vlbCol)) return; - addVLB(i, vlbCol, vlb.coef, vlb.constant, vlb.origin_row); + addVLB(i, vlbCol, vlb.coef, vlb.constant, vlb.origin); }); if (mipsolver.mipdata_->getDomain().isBinary(i)) { diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 5292b565f8a..bc9480918d2 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -51,7 +51,7 @@ class HighsImplications { struct VarBound { double coef; double constant; - HighsInt origin_row; + HighsInt origin; double minValue() const { return static_cast(static_cast(constant) + @@ -134,18 +134,16 @@ class HighsImplications { void strengthenVarBound(VarBound& vbnd, HighsInt multiplier) const; void addVUB(HighsInt col, HighsInt vubcol, double vubcoef, double vubconstant, - HighsInt origin_row = -1); + HighsInt origin = -1); void addVUB(HighsInt col, HighsInt vubcol, double vubcoef, double vubconstant, - double colupperbound, bool colisinteger, - HighsInt origin_row = -1); + double colupperbound, bool colisinteger, HighsInt origin = -1); void addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, double vlbconstant, - HighsInt origin_row = -1); + HighsInt origin = -1); void addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, double vlbconstant, - double collowerbound, bool colisinteger, - HighsInt origin_row = -1); + double collowerbound, bool colisinteger, HighsInt origin = -1); void columnTransformed(HighsInt col, double scale, double constant) { // Update implications affected by transformation diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 725d6b3b61d..e8d0f50ce66 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9119,13 +9119,16 @@ void HPresolve::extractVarBounds(HighsInt row) { // compute coefficient for binary variable double vbCoef = -binCoef / nonzero.value(); + // only record origin for size-2 one-sided rows that can be safely rewritten + HighsInt origin = rowsize[row] == 2 && !isRanged(row) ? row : -1; + // add VLB if (vlbConstant != -kHighsInf) mipsolver->mipdata_->implications.addVLB( nonzero.index(), binCol, vbCoef, vlbConstant, model->col_lower_[nonzero.index()], model->integrality_[nonzero.index()] != HighsVarType::kContinuous, - row); + origin); // add VUB if (vubConstant != kHighsInf) @@ -9133,7 +9136,7 @@ void HPresolve::extractVarBounds(HighsInt row) { nonzero.index(), binCol, vbCoef, vubConstant, model->col_upper_[nonzero.index()], model->integrality_[nonzero.index()] != HighsVarType::kContinuous, - row); + origin); // stop if no additional variable bounds can be found if (!useLhs && !useRhs) break; @@ -9160,8 +9163,8 @@ void HPresolve::aggregateVarBounds(HighsInt col) { HighsImplications::VarBound standardBound; }; - HighsHashTree vlbsFromRow; - HighsHashTree vubsFromRow; + HighsHashTree vlbs; + HighsHashTree vubs; // collect VLBs (standardization needs finite lb) if (lb > -kHighsInf) { @@ -9169,10 +9172,10 @@ void HPresolve::aggregateVarBounds(HighsInt col) { [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { HighsCDouble newCoef = vlb.constant - static_cast(lb); if (vlb.coef > 0) newCoef += vlb.coef; - vlbsFromRow.insert( - binaryCol, colImpliedBounds{vlb, HighsImplications::VarBound{ - static_cast(newCoef), - lb, vlb.origin_row}}); + vlbs.insert(binaryCol, + colImpliedBounds{vlb, HighsImplications::VarBound{ + static_cast(newCoef), + lb, vlb.origin}}); }); } // collect VUBs (standardization needs finite ub) @@ -9181,21 +9184,21 @@ void HPresolve::aggregateVarBounds(HighsInt col) { [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { HighsCDouble newCoef = static_cast(ub) - vub.constant; if (vub.coef < 0) newCoef -= vub.coef; - vubsFromRow.insert( - binaryCol, colImpliedBounds{vub, HighsImplications::VarBound{ - static_cast(newCoef), - ub, vub.origin_row}}); + vubs.insert(binaryCol, + colImpliedBounds{vub, HighsImplications::VarBound{ + static_cast(newCoef), + ub, vub.origin}}); }); } // set up cliques std::vector vlbsClique; std::vector vubsClique; - vlbsFromRow.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { + vlbs.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { HighsInt val = bounds.originalBound.coef > 0 ? 1 : 0; vlbsClique.emplace_back(binaryCol, val); }); - vubsFromRow.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { + vubs.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { HighsInt val = bounds.originalBound.coef < 0 ? 1 : 0; vubsClique.emplace_back(binaryCol, val); }); @@ -9223,7 +9226,7 @@ void HPresolve::aggregateVarBounds(HighsInt col) { HighsInt row = -1; for (const auto& var : clique) { const auto* bounds = boundsMap.find(var.col); - HighsInt currentrow = bounds->originalBound.origin_row; + HighsInt currentrow = bounds->originalBound.origin; if (currentrow < 0) continue; if (consumedRows.insert(currentrow).second) { if (row == -1) @@ -9250,7 +9253,7 @@ void HPresolve::aggregateVarBounds(HighsInt col) { addToMatrix(row, var.col, -direction * a); } else { addToMatrix(row, var.col, direction * a); - rowBound -= direction * a; + rowBound += direction * a; } } @@ -9270,8 +9273,8 @@ void HPresolve::aggregateVarBounds(HighsInt col) { } }; - if (lb > -kHighsInf) mergeCliques(vlbsCover, vlbsFromRow, lb, HighsInt{1}); - if (ub < kHighsInf) mergeCliques(vubsCover, vubsFromRow, ub, HighsInt{-1}); + if (lb > -kHighsInf) mergeCliques(vlbsCover, vlbs, lb, HighsInt{1}); + if (ub < kHighsInf) mergeCliques(vubsCover, vubs, ub, HighsInt{-1}); if (numRowsRemoved > 0 || numRowsModified > 0) highsLogDev( From b4df112462dca99cd98284117e573a50705aa1e9 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 8 Sep 2026 10:18:41 +0200 Subject: [PATCH 17/29] Use tightening --- highs/mip/HighsImplications.cpp | 76 +++++++++++++++++++++------------ highs/mip/HighsImplications.h | 5 +++ highs/presolve/HPresolve.cpp | 26 ++++++----- 3 files changed, 69 insertions(+), 38 deletions(-) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 05882e83c90..39fbc20e140 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -791,6 +791,44 @@ void HighsImplications::cleanupVarbounds(HighsInt col) { numVarBounds -= delVbds.size(); } +bool HighsImplications::redundantVlb(const VarBound& vlb, double lb) const { + return vlb.maxValue() <= lb + mipsolver.mipdata_->feastol; +} + +bool HighsImplications::redundantVub(const VarBound& vub, double ub) const { + return vub.minValue() >= ub - mipsolver.mipdata_->feastol; +} + +bool HighsImplications::tightenVlb(VarBound& vlb, double lb) const { + if (redundantVlb(vlb, lb) || + vlb.minValue() >= lb - mipsolver.mipdata_->epsilon) + return false; + double newcoef = + static_cast(lb - static_cast(vlb.maxValue())); + if (vlb.coef < 0) + vlb.coef = newcoef; + else { + vlb.constant = lb; + vlb.coef = -newcoef; + } + return true; +} + +bool HighsImplications::tightenVub(VarBound& vub, double ub) const { + if (redundantVub(vub, ub) || + vub.maxValue() <= ub + mipsolver.mipdata_->epsilon) + return false; + double newcoef = + static_cast(ub - static_cast(vub.minValue())); + if (vub.coef > 0) + vub.coef = newcoef; + else { + vub.constant = ub; + vub.coef = -newcoef; + } + return true; +} + void HighsImplications::cleanupVlb(HighsInt col, HighsInt vlbCol, HighsImplications::VarBound& vlb, double lb, bool& redundant, bool& infeasible, @@ -806,27 +844,18 @@ void HighsImplications::cleanupVlb(HighsInt col, HighsInt vlbCol, mipsolver.mipdata_->debugSolution.checkVlb(col, vlbCol, vlb.coef, vlb.constant); - HighsCDouble maxlb = vlb.maxValue(); - HighsCDouble minlb = vlb.minValue(); - - if (maxlb <= lb + mipsolver.mipdata_->feastol) { + if (redundantVlb(vlb, lb)) { // variable bound is redundant redundant = true; - } else if (minlb < lb - mipsolver.mipdata_->epsilon) { + } else if (tightenVlb(vlb, lb)) { // coefficient can be tightened - double newcoef = static_cast(lb - maxlb); - if (vlb.coef < 0) { - vlb.coef = newcoef; - } else { - vlb.constant = lb; - vlb.coef = -newcoef; - } // check tightened variable lower bound mipsolver.mipdata_->debugSolution.checkVlb(col, vlbCol, vlb.coef, vlb.constant); - } else if (allowBoundChanges && minlb > lb + mipsolver.mipdata_->epsilon) { + } else if (allowBoundChanges && + vlb.minValue() > lb + mipsolver.mipdata_->epsilon) { mipsolver.mipdata_->getDomain().changeBound( - HighsBoundType::kLower, col, static_cast(minlb), + HighsBoundType::kLower, col, vlb.minValue(), HighsDomain::Reason::unspecified()); infeasible = mipsolver.mipdata_->getDomain().infeasible(); } @@ -847,27 +876,18 @@ void HighsImplications::cleanupVub(HighsInt col, HighsInt vubCol, mipsolver.mipdata_->debugSolution.checkVub(col, vubCol, vub.coef, vub.constant); - HighsCDouble maxub = vub.maxValue(); - HighsCDouble minub = vub.minValue(); - - if (minub >= ub - mipsolver.mipdata_->feastol) { + if (redundantVub(vub, ub)) { // variable bound is redundant redundant = true; - } else if (maxub > ub + mipsolver.mipdata_->epsilon) { + } else if (tightenVub(vub, ub)) { // coefficient can be tightened - double newcoef = static_cast(ub - minub); - if (vub.coef > 0) { - vub.coef = newcoef; - } else { - vub.constant = ub; - vub.coef = -newcoef; - } // check tightened variable upper bound mipsolver.mipdata_->debugSolution.checkVub(col, vubCol, vub.coef, vub.constant); - } else if (allowBoundChanges && maxub < ub - mipsolver.mipdata_->epsilon) { + } else if (allowBoundChanges && + vub.maxValue() < ub - mipsolver.mipdata_->epsilon) { mipsolver.mipdata_->getDomain().changeBound( - HighsBoundType::kUpper, col, static_cast(maxub), + HighsBoundType::kUpper, col, vub.maxValue(), HighsDomain::Reason::unspecified()); infeasible = mipsolver.mipdata_->getDomain().infeasible(); } diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index bc9480918d2..2740a5378bd 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -220,6 +220,11 @@ class HighsImplications { void cleanupVarbounds(HighsInt col); + bool redundantVlb(const VarBound& vlb, double lb) const; + bool redundantVub(const VarBound& vub, double ub) const; + bool tightenVlb(VarBound& vlb, double lb) const; + bool tightenVub(VarBound& vub, double ub) const; + void cleanupVlb(HighsInt col, HighsInt vlbCol, HighsImplications::VarBound& vlb, double lb, bool& redundant, bool& infeasible, bool allowBoundChanges = true) const; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index e8d0f50ce66..74b8bb1cc7b 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9170,24 +9170,30 @@ void HPresolve::aggregateVarBounds(HighsInt col) { if (lb > -kHighsInf) { implications.getVlbs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { - HighsCDouble newCoef = vlb.constant - static_cast(lb); - if (vlb.coef > 0) newCoef += vlb.coef; + if (implications.redundantVlb(vlb, lb)) return; + HighsImplications::VarBound v = vlb; + implications.tightenVlb(v, lb); + HighsCDouble newCoef = v.constant - static_cast(lb); + if (v.coef > 0) newCoef += v.coef; vlbs.insert(binaryCol, - colImpliedBounds{vlb, HighsImplications::VarBound{ - static_cast(newCoef), - lb, vlb.origin}}); + colImpliedBounds{ + v, HighsImplications::VarBound{ + static_cast(newCoef), lb, v.origin}}); }); } // collect VUBs (standardization needs finite ub) if (ub < kHighsInf) { implications.getVubs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { - HighsCDouble newCoef = static_cast(ub) - vub.constant; - if (vub.coef < 0) newCoef -= vub.coef; + if (implications.redundantVub(vub, ub)) return; + HighsImplications::VarBound v = vub; + implications.tightenVub(v, ub); + HighsCDouble newCoef = static_cast(ub) - v.constant; + if (v.coef < 0) newCoef -= v.coef; vubs.insert(binaryCol, - colImpliedBounds{vub, HighsImplications::VarBound{ - static_cast(newCoef), - ub, vub.origin}}); + colImpliedBounds{ + v, HighsImplications::VarBound{ + static_cast(newCoef), ub, v.origin}}); }); } From 878ec2ddb0769b809a03cfdd2d5094d84bb49c06 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 8 Sep 2026 10:48:24 +0200 Subject: [PATCH 18/29] Add range check --- highs/presolve/HPresolve.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 74b8bb1cc7b..5b8e09ee228 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9166,30 +9166,47 @@ void HPresolve::aggregateVarBounds(HighsInt col) { HighsHashTree vlbs; HighsHashTree vubs; - // collect VLBs (standardization needs finite lb) + // compute range + double range = kHighsInf; + if (lb > -kHighsInf && ub < kHighsInf) + range = static_cast(static_cast(ub) - lb); + + // collect VLBs: y >= coef * x + constant + // standardized form: y >= a * x + lb, where a > 0 if (lb > -kHighsInf) { implications.getVlbs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { + // skip if the VLB is dominated by the global lower bound if (implications.redundantVlb(vlb, lb)) return; + // tighten so that minValue() >= lb HighsImplications::VarBound v = vlb; implications.tightenVlb(v, lb); + // standardize: a = maxValue() - lb HighsCDouble newCoef = v.constant - static_cast(lb); if (v.coef > 0) newCoef += v.coef; + // skip if the standardized coefficient exceeds the variable range + if (newCoef > range + mipsolver->mipdata_->feastol) return; vlbs.insert(binaryCol, colImpliedBounds{ v, HighsImplications::VarBound{ static_cast(newCoef), lb, v.origin}}); }); } - // collect VUBs (standardization needs finite ub) + // collect VUBs: y <= coef * x + constant + // standardized form: y <= ub - a * x, where a > 0 if (ub < kHighsInf) { implications.getVubs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { + // skip if the VUB is dominated by the global upper bound if (implications.redundantVub(vub, ub)) return; + // tighten so that maxValue() <= ub HighsImplications::VarBound v = vub; implications.tightenVub(v, ub); + // standardize: a = ub - minValue() HighsCDouble newCoef = static_cast(ub) - v.constant; if (v.coef < 0) newCoef -= v.coef; + // skip if the standardized coefficient exceeds the variable range + if (newCoef > range + mipsolver->mipdata_->feastol) return; vubs.insert(binaryCol, colImpliedBounds{ v, HighsImplications::VarBound{ From 536a5959503397ea288f2d16b94e173f418c12c7 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 8 Sep 2026 12:01:53 +0200 Subject: [PATCH 19/29] Handle infinite coefficients and bounds in minValue/maxValue --- highs/mip/HighsImplications.h | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 2740a5378bd..c160b28d989 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -54,12 +54,16 @@ class HighsImplications { HighsInt origin; double minValue() const { - return static_cast(static_cast(constant) + - std::min(coef, 0.0)); + double m = std::min(coef, 0.0); + if (std::abs(constant) >= kHighsInf || std::abs(m) >= kHighsInf) + return constant + m; + return static_cast(static_cast(constant) + m); } double maxValue() const { - return static_cast(static_cast(constant) + - std::max(coef, 0.0)); + double m = std::max(coef, 0.0); + if (std::abs(constant) >= kHighsInf || std::abs(m) >= kHighsInf) + return constant + m; + return static_cast(static_cast(constant) + m); } }; From b99640b6f712dd60fdc8673dc780c1a5d7e2c477 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 10 Sep 2026 09:25:35 +0200 Subject: [PATCH 20/29] Use template instead of std::function --- highs/mip/HighsCliqueTable.cpp | 7 +++---- highs/mip/HighsCliqueTable.h | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index fc1055b7c14..d6daf06a983 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -987,13 +987,12 @@ void HighsCliqueTable::extractCliques( } } +template HighsInt HighsCliqueTable::extendClique( std::vector& neighbourhoodInds, std::vector& clqVars, - HighsInt seedPos, HighsInt extensionEnd, - const std::function& - candidateOrder) { + HighsInt seedPos, HighsInt extensionEnd, const Comparator& candidateOrder) { for (HighsInt i = seedPos; i < extensionEnd; ++i) { - if (candidateOrder) + if (Sort) pdqsort_branchless(clqVars.begin() + i, clqVars.begin() + extensionEnd, candidateOrder); CliqueVar v = clqVars[i]; diff --git a/highs/mip/HighsCliqueTable.h b/highs/mip/HighsCliqueTable.h index 1ca9b0542a6..054efd66138 100644 --- a/highs/mip/HighsCliqueTable.h +++ b/highs/mip/HighsCliqueTable.h @@ -160,11 +160,19 @@ class HighsCliqueTable { int64_t& numNeighbourhoodqueries, CliqueVar v, CliqueVar* q, HighsInt N) const; - HighsInt extendClique( - std::vector& neighbourhoodInds, std::vector& clqVars, - HighsInt seedPos, HighsInt extensionEnd, - const std::function& - candidateOrder = nullptr); + template + HighsInt extendClique(std::vector& neighbourhoodInds, + std::vector& clqVars, HighsInt seedPos, + HighsInt extensionEnd, + const Comparator& candidateOrder); + + HighsInt extendClique(std::vector& neighbourhoodInds, + std::vector& clqVars, HighsInt seedPos, + HighsInt extensionEnd) { + auto noop = [](const CliqueVar&, const CliqueVar&) { return false; }; + return extendClique(neighbourhoodInds, clqVars, seedPos, + extensionEnd, noop); + } public: int64_t numNeighbourhoodQueries; From 3ade574b061d19745e23554cbc63f3d6545005e2 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 10 Sep 2026 09:37:26 +0200 Subject: [PATCH 21/29] Move loop to aggregateVarBounds --- highs/presolve/HPresolve.cpp | 301 ++++++++++++++++++----------------- highs/presolve/HPresolve.h | 4 +- 2 files changed, 157 insertions(+), 148 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5b8e09ee228..8ec9dad771c 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -240,6 +240,11 @@ bool HPresolve::isUpperStrictlyImplied(HighsInt col, double* tolerance) const { (tolerance != nullptr ? *tolerance : primal_feastol)); } +bool HPresolve::isBinary(HighsInt col) const { + return model->integrality_[col] == HighsVarType::kInteger && + model->col_lower_[col] == 0.0 && model->col_upper_[col] == 1.0; +} + bool HPresolve::isImpliedFree(HighsInt col) const { return isLowerImplied(col) && isUpperImplied(col); } @@ -1201,11 +1206,6 @@ HPresolve::Result HPresolve::dominatedColumns( size_t numDomChecks = 0; size_t numDomChecksPredBndAnalysis = 0; - auto isBinary = [&](HighsInt i) { - return model->integrality_[i] == HighsVarType::kInteger && - model->col_lower_[i] == 0.0 && model->col_upper_[i] == 1.0; - }; - auto addSignature = [&](HighsInt row, HighsInt col, uint32_t rowLowerFinite, uint32_t rowUpperFinite) { HighsInt rowHashedPos = (HighsHashHelpers::hash(row) >> 59); @@ -1728,7 +1728,7 @@ HPresolve::Result HPresolve::finaliseProbing( applyConflictGraphSubstitutions(postsolve_stack, numVarsSubstituted)); // aggregate variable bounds now that the clique table is populated - for (HighsInt i = 0; i != model->num_col_; ++i) aggregateVarBounds(i); + aggregateVarBounds(); return checkLimits(postsolve_stack); } @@ -9143,17 +9143,8 @@ void HPresolve::extractVarBounds(HighsInt row) { } } -void HPresolve::aggregateVarBounds(HighsInt col) { - if (mipsolver == nullptr || colDeleted[col]) return; - - // get lower bound and upper bound - double lb = model->col_lower_[col]; - double ub = model->col_upper_[col]; - - // skip binary columns — we aggregate VLBs/VUBs on non-binary columns - if (model->integrality_[col] == HighsVarType::kInteger && lb == 0.0 && - ub == 1.0) - return; +void HPresolve::aggregateVarBounds() { + if (mipsolver == nullptr) return; HighsImplications& implications = mipsolver->mipdata_->implications; HighsCliqueTable& cliquetable = mipsolver->mipdata_->cliquetable; @@ -9165,147 +9156,163 @@ void HPresolve::aggregateVarBounds(HighsInt col) { HighsHashTree vlbs; HighsHashTree vubs; - - // compute range - double range = kHighsInf; - if (lb > -kHighsInf && ub < kHighsInf) - range = static_cast(static_cast(ub) - lb); - - // collect VLBs: y >= coef * x + constant - // standardized form: y >= a * x + lb, where a > 0 - if (lb > -kHighsInf) { - implications.getVlbs(col).for_each( - [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { - // skip if the VLB is dominated by the global lower bound - if (implications.redundantVlb(vlb, lb)) return; - // tighten so that minValue() >= lb - HighsImplications::VarBound v = vlb; - implications.tightenVlb(v, lb); - // standardize: a = maxValue() - lb - HighsCDouble newCoef = v.constant - static_cast(lb); - if (v.coef > 0) newCoef += v.coef; - // skip if the standardized coefficient exceeds the variable range - if (newCoef > range + mipsolver->mipdata_->feastol) return; - vlbs.insert(binaryCol, - colImpliedBounds{ - v, HighsImplications::VarBound{ - static_cast(newCoef), lb, v.origin}}); - }); - } - // collect VUBs: y <= coef * x + constant - // standardized form: y <= ub - a * x, where a > 0 - if (ub < kHighsInf) { - implications.getVubs(col).for_each( - [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { - // skip if the VUB is dominated by the global upper bound - if (implications.redundantVub(vub, ub)) return; - // tighten so that maxValue() <= ub - HighsImplications::VarBound v = vub; - implications.tightenVub(v, ub); - // standardize: a = ub - minValue() - HighsCDouble newCoef = static_cast(ub) - v.constant; - if (v.coef < 0) newCoef -= v.coef; - // skip if the standardized coefficient exceeds the variable range - if (newCoef > range + mipsolver->mipdata_->feastol) return; - vubs.insert(binaryCol, - colImpliedBounds{ - v, HighsImplications::VarBound{ - static_cast(newCoef), ub, v.origin}}); - }); - } - - // set up cliques std::vector vlbsClique; std::vector vubsClique; - vlbs.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { - HighsInt val = bounds.originalBound.coef > 0 ? 1 : 0; - vlbsClique.emplace_back(binaryCol, val); - }); - vubs.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { - HighsInt val = bounds.originalBound.coef < 0 ? 1 : 0; - vubsClique.emplace_back(binaryCol, val); - }); - - // clique cover std::vector> vlbsCover; std::vector> vubsCover; - cliquetable.cliqueCover(vlbsClique, vlbsCover); - cliquetable.cliqueCover(vubsClique, vubsCover); - - HighsInt numRowsRemoved = 0; - HighsInt numRowsModified = 0; - HighsInt numVarsLifted = 0; - - auto mergeCliques = - [&](std::vector>& cover, - HighsHashTree& boundsMap, - double baseBound, HighsInt direction) { - std::set consumedRows; - - for (const auto& clique : cover) { - if (clique.size() < 2) continue; - - // find an unconsumed row to reuse and remove the rest - HighsInt row = -1; - for (const auto& var : clique) { - const auto* bounds = boundsMap.find(var.col); - HighsInt currentrow = bounds->originalBound.origin; - if (currentrow < 0) continue; - if (consumedRows.insert(currentrow).second) { - if (row == -1) - row = currentrow; - else { - removeRow(currentrow); - numRowsRemoved++; + + for (HighsInt col = 0; col != model->num_col_; ++col) { + if (colDeleted[col]) continue; + + // get lower bound and upper bound + double lb = model->col_lower_[col]; + double ub = model->col_upper_[col]; + + // skip binary columns — we aggregate VLBs/VUBs on non-binary columns + if (isBinary(col)) continue; + + vlbs.clear(); + vubs.clear(); + vlbsClique.clear(); + vubsClique.clear(); + + // compute range + double range = kHighsInf; + if (lb > -kHighsInf && ub < kHighsInf) + range = static_cast(static_cast(ub) - lb); + + // collect VLBs: y >= coef * x + constant + // standardized form: y >= a * x + lb, where a > 0 + if (lb > -kHighsInf) { + implications.getVlbs(col).for_each( + [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { + // skip if the VLB is dominated by the global lower bound + if (implications.redundantVlb(vlb, lb)) return; + // tighten so that minValue() >= lb + HighsImplications::VarBound v = vlb; + implications.tightenVlb(v, lb); + // standardize: a = maxValue() - lb + HighsCDouble newCoef = v.constant - static_cast(lb); + if (v.coef > 0) newCoef += v.coef; + // skip if the standardized coefficient exceeds the variable range + if (newCoef > range + mipsolver->mipdata_->feastol) return; + vlbs.insert(binaryCol, + colImpliedBounds{v, HighsImplications::VarBound{ + static_cast(newCoef), + lb, v.origin}}); + }); + } + // collect VUBs: y <= coef * x + constant + // standardized form: y <= ub - a * x, where a > 0 + if (ub < kHighsInf) { + implications.getVubs(col).for_each( + [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { + // skip if the VUB is dominated by the global upper bound + if (implications.redundantVub(vub, ub)) return; + // tighten so that maxValue() <= ub + HighsImplications::VarBound v = vub; + implications.tightenVub(v, ub); + // standardize: a = ub - minValue() + HighsCDouble newCoef = static_cast(ub) - v.constant; + if (v.coef < 0) newCoef -= v.coef; + // skip if the standardized coefficient exceeds the variable range + if (newCoef > range + mipsolver->mipdata_->feastol) return; + vubs.insert(binaryCol, + colImpliedBounds{v, HighsImplications::VarBound{ + static_cast(newCoef), + ub, v.origin}}); + }); + } + + // set up cliques + vlbs.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { + HighsInt val = bounds.originalBound.coef > 0 ? 1 : 0; + vlbsClique.emplace_back(binaryCol, val); + }); + vubs.for_each([&](HighsInt binaryCol, colImpliedBounds& bounds) { + HighsInt val = bounds.originalBound.coef < 0 ? 1 : 0; + vubsClique.emplace_back(binaryCol, val); + }); + + // clique cover + cliquetable.cliqueCover(vlbsClique, vlbsCover); + cliquetable.cliqueCover(vubsClique, vubsCover); + + HighsInt numRowsRemoved = 0; + HighsInt numRowsModified = 0; + HighsInt numVarsLifted = 0; + + auto mergeCliques = + [&](std::vector>& cover, + HighsHashTree& boundsMap, + double baseBound, HighsInt direction) { + std::set consumedRows; + + for (const auto& clique : cover) { + if (clique.size() < 2) continue; + + // find an unconsumed row to reuse and remove the rest + HighsInt row = -1; + for (const auto& var : clique) { + const auto* bounds = boundsMap.find(var.col); + HighsInt currentrow = bounds->originalBound.origin; + if (currentrow < 0) continue; + if (consumedRows.insert(currentrow).second) { + if (row == -1) + row = currentrow; + else { + removeRow(currentrow); + numRowsRemoved++; + } } } - } - if (row == -1) continue; - - // rewrite the reused row with the aggregated constraint - unlinkRow(row); - addToMatrix(row, col, 1.0); - numRowsModified++; - - HighsCDouble rowBound = baseBound; - for (const auto& var : clique) { - const auto* bounds = boundsMap.find(var.col); - double a = bounds->standardBound.coef; - numVarsLifted++; - if (var.val == 1) { - addToMatrix(row, var.col, -direction * a); - } else { - addToMatrix(row, var.col, direction * a); - rowBound += direction * a; + if (row == -1) continue; + + // rewrite the reused row with the aggregated constraint + unlinkRow(row); + addToMatrix(row, col, 1.0); + numRowsModified++; + + HighsCDouble rowBound = baseBound; + for (const auto& var : clique) { + const auto* bounds = boundsMap.find(var.col); + double a = bounds->standardBound.coef; + numVarsLifted++; + if (var.val == 1) { + addToMatrix(row, var.col, -direction * a); + } else { + addToMatrix(row, var.col, direction * a); + rowBound += direction * a; + } } - } - // update row dual bounds to match the new row type, - // as the original row may have been <= or >= - if (direction > 0) { - model->row_lower_[row] = static_cast(rowBound); - model->row_upper_[row] = kHighsInf; - changeRowDualLower(row, 0.0); - changeRowDualUpper(row, kHighsInf); - } else { - model->row_lower_[row] = -kHighsInf; - model->row_upper_[row] = static_cast(rowBound); - changeRowDualLower(row, -kHighsInf); - changeRowDualUpper(row, 0.0); + // update row dual bounds to match the new row type, + // as the original row may have been <= or >= + if (direction > 0) { + model->row_lower_[row] = static_cast(rowBound); + model->row_upper_[row] = kHighsInf; + changeRowDualLower(row, 0.0); + changeRowDualUpper(row, kHighsInf); + } else { + model->row_lower_[row] = -kHighsInf; + model->row_upper_[row] = static_cast(rowBound); + changeRowDualLower(row, -kHighsInf); + changeRowDualUpper(row, 0.0); + } } - } - }; + }; - if (lb > -kHighsInf) mergeCliques(vlbsCover, vlbs, lb, HighsInt{1}); - if (ub < kHighsInf) mergeCliques(vubsCover, vubs, ub, HighsInt{-1}); + if (lb > -kHighsInf) mergeCliques(vlbsCover, vlbs, lb, HighsInt{1}); + if (ub < kHighsInf) mergeCliques(vubsCover, vubs, ub, HighsInt{-1}); - if (numRowsRemoved > 0 || numRowsModified > 0) - highsLogDev( - options->log_options, HighsLogType::kInfo, - "Implied variable bound aggregation for column %" HIGHSINT_FORMAT - ": %" HIGHSINT_FORMAT " rows removed, %" HIGHSINT_FORMAT - " rows modified, %" HIGHSINT_FORMAT " vars lifted\n", - col, numRowsRemoved, numRowsModified, numVarsLifted); + if (numRowsRemoved > 0 || numRowsModified > 0) + highsLogDev( + options->log_options, HighsLogType::kInfo, + "Implied variable bound aggregation for column %" HIGHSINT_FORMAT + ": %" HIGHSINT_FORMAT " rows removed, %" HIGHSINT_FORMAT + " rows modified, %" HIGHSINT_FORMAT " vars lifted\n", + col, numRowsRemoved, numRowsModified, numVarsLifted); + } } // Not currently called diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 6ed9ea07ac8..ad5cffbafba 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -218,6 +218,8 @@ class HPresolve { bool rowCoefficientsIntegral(HighsInt row, double scale) const; + bool isBinary(HighsInt col) const; + bool isImpliedFree(HighsInt col) const; bool isDualImpliedFree(HighsInt row) const; @@ -533,7 +535,7 @@ class HPresolve { void extractVarBounds(HighsInt row); - void aggregateVarBounds(HighsInt col); + void aggregateVarBounds(); Result sparsify(HighsPostsolveStack& postsolve_stack); From fb9a6e133c51515b0ede04516087b840657bcaa0 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 10 Sep 2026 10:07:06 +0200 Subject: [PATCH 22/29] Add HighsBool vector to mark rows --- highs/presolve/HPresolve.cpp | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 8ec9dad771c..ffcd42c2046 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9154,12 +9154,19 @@ void HPresolve::aggregateVarBounds() { HighsImplications::VarBound standardBound; }; + // data structures HighsHashTree vlbs; HighsHashTree vubs; std::vector vlbsClique; std::vector vubsClique; std::vector> vlbsCover; std::vector> vubsCover; + std::vector rowConsumed(model->num_row_, false); + + // initialise counters + HighsInt numRowsRemoved = 0; + HighsInt numRowsModified = 0; + HighsInt numVarsLifted = 0; for (HighsInt col = 0; col != model->num_col_; ++col) { if (colDeleted[col]) continue; @@ -9238,16 +9245,10 @@ void HPresolve::aggregateVarBounds() { cliquetable.cliqueCover(vlbsClique, vlbsCover); cliquetable.cliqueCover(vubsClique, vubsCover); - HighsInt numRowsRemoved = 0; - HighsInt numRowsModified = 0; - HighsInt numVarsLifted = 0; - auto mergeCliques = [&](std::vector>& cover, HighsHashTree& boundsMap, double baseBound, HighsInt direction) { - std::set consumedRows; - for (const auto& clique : cover) { if (clique.size() < 2) continue; @@ -9257,7 +9258,8 @@ void HPresolve::aggregateVarBounds() { const auto* bounds = boundsMap.find(var.col); HighsInt currentrow = bounds->originalBound.origin; if (currentrow < 0) continue; - if (consumedRows.insert(currentrow).second) { + if (!rowConsumed[currentrow]) { + rowConsumed[currentrow] = true; if (row == -1) row = currentrow; else { @@ -9304,15 +9306,14 @@ void HPresolve::aggregateVarBounds() { if (lb > -kHighsInf) mergeCliques(vlbsCover, vlbs, lb, HighsInt{1}); if (ub < kHighsInf) mergeCliques(vubsCover, vubs, ub, HighsInt{-1}); - - if (numRowsRemoved > 0 || numRowsModified > 0) - highsLogDev( - options->log_options, HighsLogType::kInfo, - "Implied variable bound aggregation for column %" HIGHSINT_FORMAT - ": %" HIGHSINT_FORMAT " rows removed, %" HIGHSINT_FORMAT - " rows modified, %" HIGHSINT_FORMAT " vars lifted\n", - col, numRowsRemoved, numRowsModified, numVarsLifted); } + + if (numRowsRemoved > 0 || numRowsModified > 0) + highsLogDev(options->log_options, HighsLogType::kInfo, + "Implied variable bound aggregation: %" HIGHSINT_FORMAT + " rows removed, %" HIGHSINT_FORMAT + " rows modified, %" HIGHSINT_FORMAT " vars lifted\n", + numRowsRemoved, numRowsModified, numVarsLifted); } // Not currently called From 1465289c77d540f96d735ffc2e2a8893fb46f730 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Thu, 10 Sep 2026 11:33:42 +0200 Subject: [PATCH 23/29] Fix compiler error --- highs/mip/HighsCliqueTable.cpp | 8 ++++++++ highs/mip/HighsCliqueTable.h | 6 +----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/highs/mip/HighsCliqueTable.cpp b/highs/mip/HighsCliqueTable.cpp index d6daf06a983..ba2f1f3cad9 100644 --- a/highs/mip/HighsCliqueTable.cpp +++ b/highs/mip/HighsCliqueTable.cpp @@ -1006,6 +1006,14 @@ HighsInt HighsCliqueTable::extendClique( return extensionEnd; } +HighsInt HighsCliqueTable::extendClique( + std::vector& neighbourhoodInds, std::vector& clqVars, + HighsInt seedPos, HighsInt extensionEnd) { + auto noop = [](const CliqueVar&, const CliqueVar&) { return false; }; + return extendClique(neighbourhoodInds, clqVars, seedPos, extensionEnd, + noop); +} + void HighsCliqueTable::cliquePartition(std::vector& clqVars, std::vector& partitionStart) { randgen.shuffle(clqVars.data(), clqVars.size()); diff --git a/highs/mip/HighsCliqueTable.h b/highs/mip/HighsCliqueTable.h index 054efd66138..48ba8ae81cb 100644 --- a/highs/mip/HighsCliqueTable.h +++ b/highs/mip/HighsCliqueTable.h @@ -168,11 +168,7 @@ class HighsCliqueTable { HighsInt extendClique(std::vector& neighbourhoodInds, std::vector& clqVars, HighsInt seedPos, - HighsInt extensionEnd) { - auto noop = [](const CliqueVar&, const CliqueVar&) { return false; }; - return extendClique(neighbourhoodInds, clqVars, seedPos, - extensionEnd, noop); - } + HighsInt extensionEnd); public: int64_t numNeighbourhoodQueries; From b5f97b2780fa90f0a235711737ea6317d2b5819e Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 11 Sep 2026 10:31:31 +0200 Subject: [PATCH 24/29] Minor changes --- highs/presolve/HPresolve.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ffcd42c2046..1e42e98c73a 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9169,20 +9169,20 @@ void HPresolve::aggregateVarBounds() { HighsInt numVarsLifted = 0; for (HighsInt col = 0; col != model->num_col_; ++col) { - if (colDeleted[col]) continue; - - // get lower bound and upper bound - double lb = model->col_lower_[col]; - double ub = model->col_upper_[col]; - - // skip binary columns — we aggregate VLBs/VUBs on non-binary columns - if (isBinary(col)) continue; + // skip deleted and binary columns (aggregate VLBs/VUBs on non-binary + // columns) + if (colDeleted[col] || isBinary(col)) continue; + // clear vectors vlbs.clear(); vubs.clear(); vlbsClique.clear(); vubsClique.clear(); + // get lower bound and upper bound + double lb = model->col_lower_[col]; + double ub = model->col_upper_[col]; + // compute range double range = kHighsInf; if (lb > -kHighsInf && ub < kHighsInf) From a33586d7842f071b1f6df226f029c8c39eb600d0 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 11 Sep 2026 16:03:42 +0200 Subject: [PATCH 25/29] Add changeRowLower and changeRowUpper --- highs/presolve/HPresolve.cpp | 90 +++++++++++++++++++++--------------- highs/presolve/HPresolve.h | 6 +++ 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ffcd42c2046..c714c4e9301 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -376,11 +376,8 @@ HPresolve::StatusResult HPresolve::isImpliedIntegral(HighsInt col) { primal_feastol); // check if modification is large enough if (std::abs(model->row_upper_[nz.index()] - rUpper) > - options->small_matrix_value) { - // update right-hand side and mark row as changed - model->row_upper_[nz.index()] = rUpper; - markChangedRow(nz.index()); - } + options->small_matrix_value) + changeRowUpper(nz.index(), rUpper); } if (model->row_lower_[nz.index()] != -kHighsInf) { // left-hand side: scale, round up and unscale again @@ -390,11 +387,8 @@ HPresolve::StatusResult HPresolve::isImpliedIntegral(HighsInt col) { primal_feastol); // check if modification is large enough if (std::abs(model->row_lower_[nz.index()] - rLower) > - options->small_matrix_value) { - // update left-hand side and mark row as changed - model->row_lower_[nz.index()] = rLower; - markChangedRow(nz.index()); - } + options->small_matrix_value) + changeRowLower(nz.index(), rLower); } } @@ -947,6 +941,40 @@ void HPresolve::matrixNonZeroChanged(HighsInt row, HighsInt col) { if (mipsolver != nullptr) mipsolver->mipdata_->implications.rowModified(row); } +void HPresolve::changeRowLower(HighsInt row, double newLower, + bool skipRowDualUpdate) { + double oldLower = model->row_lower_[row]; + model->row_lower_[row] = newLower; + + if (!skipRowDualUpdate) { + if (oldLower == -kHighsInf && newLower != -kHighsInf) + changeRowDualUpper(row, kHighsInf); + else if (oldLower != -kHighsInf && newLower == -kHighsInf) + changeRowDualUpper(row, 0.0); + } + + resetColImpliedBoundsDerivedFromRow(row); + if (mipsolver != nullptr) mipsolver->mipdata_->implications.rowModified(row); + markChangedRow(row); +} + +void HPresolve::changeRowUpper(HighsInt row, double newUpper, + bool skipRowDualUpdate) { + double oldUpper = model->row_upper_[row]; + model->row_upper_[row] = newUpper; + + if (!skipRowDualUpdate) { + if (oldUpper == kHighsInf && newUpper != kHighsInf) + changeRowDualLower(row, -kHighsInf); + else if (oldUpper != kHighsInf && newUpper == kHighsInf) + changeRowDualLower(row, 0.0); + } + + resetColImpliedBoundsDerivedFromRow(row); + if (mipsolver != nullptr) mipsolver->mipdata_->implications.rowModified(row); + markChangedRow(row); +} + HighsInt HPresolve::findNonzero(HighsInt row, HighsInt col) { if (rowroot[row] == -1) return -1; @@ -3924,20 +3952,14 @@ HPresolve::Result HPresolve::rowPresolve(HighsPostsolveStack& postsolve_stack, // Convert to equality constraint and record for dual postsolve if (!isEquation(row)) { if (isImpliedEquationAtLower(row)) { - model->row_upper_[row] = model->row_lower_[row]; + changeRowUpper(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); if (mipsolver == nullptr) HPRESOLVE_CHECKED_CALL( checkRedundantBounds(rowDualLowerSource[row], row)); } else if (isImpliedEquationAtUpper(row)) { - model->row_lower_[row] = model->row_upper_[row]; + changeRowLower(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); if (mipsolver == nullptr) HPRESOLVE_CHECKED_CALL( checkRedundantBounds(rowDualUpperSource[row], row)); @@ -4392,9 +4414,9 @@ HPresolve::Result HPresolve::rowPresolve(HighsPostsolveStack& postsolve_stack, roundedLhs /= intScale; roundedRhs /= intScale; if (roundedRhs < model->row_upper_[row] - primal_feastol) - model->row_upper_[row] = static_cast(roundedRhs); + changeRowUpper(row, static_cast(roundedRhs)); if (roundedLhs > model->row_lower_[row] + primal_feastol) - model->row_lower_[row] = static_cast(roundedLhs); + changeRowLower(row, static_cast(roundedLhs)); } else if ((rhsTightened && fractionRhs < minRhsTightening - primal_feastol) || (lhsTightened && @@ -5710,10 +5732,8 @@ HPresolve::Result HPresolve::zeroCostSingleton( model->row_upper_[row], newRowLower, newRowUpper, lb, ub, coef, getStoredRow()); - model->row_lower_[row] = newRowLower; - model->row_upper_[row] = newRowUpper; - if (newRowLower == -kHighsInf) changeRowDualUpper(row, 0.0); - if (newRowUpper == kHighsInf) changeRowDualLower(row, 0.0); + changeRowLower(row, newRowLower); + changeRowUpper(row, newRowUpper); if (was_equation && newRowLower != newRowUpper && eqiters[row] != equations.end()) { equations.erase(eqiters[row]); @@ -8849,7 +8869,7 @@ HPresolve::Result HPresolve::detectParallelRowsAndCols( rowDualLower[parallelRowCand] = tmp; } - model->row_upper_[parallelRowCand] = newUpper; + changeRowUpper(parallelRowCand, newUpper, true); } } @@ -8883,7 +8903,7 @@ HPresolve::Result HPresolve::detectParallelRowsAndCols( rowDualUpper[parallelRowCand] = tmp; } - model->row_lower_[parallelRowCand] = newLower; + changeRowLower(parallelRowCand, newLower, true); } } // remove implied bounds, since they might in general not be valid @@ -9270,6 +9290,10 @@ void HPresolve::aggregateVarBounds() { } if (row == -1) continue; + // origin is invalidated when row bounds change, so the + // row must still be one-sided + assert(!isRanged(row)); + // rewrite the reused row with the aggregated constraint unlinkRow(row); addToMatrix(row, col, 1.0); @@ -9288,18 +9312,12 @@ void HPresolve::aggregateVarBounds() { } } - // update row dual bounds to match the new row type, - // as the original row may have been <= or >= if (direction > 0) { - model->row_lower_[row] = static_cast(rowBound); - model->row_upper_[row] = kHighsInf; - changeRowDualLower(row, 0.0); - changeRowDualUpper(row, kHighsInf); + changeRowLower(row, static_cast(rowBound)); + changeRowUpper(row, kHighsInf); } else { - model->row_lower_[row] = -kHighsInf; - model->row_upper_[row] = static_cast(rowBound); - changeRowDualLower(row, -kHighsInf); - changeRowDualUpper(row, 0.0); + changeRowLower(row, -kHighsInf); + changeRowUpper(row, static_cast(rowBound)); } } }; diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index ad5cffbafba..48e34ff322b 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -216,6 +216,12 @@ class HPresolve { void matrixNonZeroChanged(HighsInt row, HighsInt col); + void changeRowLower(HighsInt row, double newLower, + bool skipRowDualUpdate = false); + + void changeRowUpper(HighsInt row, double newUpper, + bool skipRowDualUpdate = false); + bool rowCoefficientsIntegral(HighsInt row, double scale) const; bool isBinary(HighsInt col) const; From fe20f523cd46e0510bc24db74176429a2028eeaa Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 25 Sep 2026 10:39:49 +0200 Subject: [PATCH 26/29] Fix copiler warning --- highs/mip/HighsRedcostFixing.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/highs/mip/HighsRedcostFixing.cpp b/highs/mip/HighsRedcostFixing.cpp index cc51452e46e..abf8afc6648 100644 --- a/highs/mip/HighsRedcostFixing.cpp +++ b/highs/mip/HighsRedcostFixing.cpp @@ -292,28 +292,30 @@ void HighsRedcostFixing::addRootRedcost(const HighsMipSolver& mipsolver, double lb = mipsolver.mipdata_->getDomain().col_lower_[col]; double ub = mipsolver.mipdata_->getDomain().col_upper_[col]; - if (lpredcost[col] > mipsolver.mipdata_->feastol && lb > -kHighsIInf) { + if (lpredcost[col] > mipsolver.mipdata_->feastol && + lb > -static_cast(kHighsIInf)) { // col <= (cutoffbound - lpobj)/redcost + lb // so for lurkub = lb to ub - 1 we can compute the necessary cutoff // bound to reach this bound which is: // lurkub = (cutoffbound - lpobj)/redcost + lb // cutoffbound = (lurkub - lb) * redcost + lpobj findLurkingBounds(col, HighsInt{1}, static_cast(lb), - static_cast(ub), ub < kHighsIInf, lpobjective, + static_cast(ub), + ub < static_cast(kHighsIInf), lpobjective, lpredcost[col], maxNumSteps, maxNumStepsExp, lurkingColUpper[col], lurkingColLower[col]); } else if (lpredcost[col] < -mipsolver.mipdata_->feastol && - ub < kHighsIInf) { + ub < static_cast(kHighsIInf)) { // col >= (cutoffbound - lpobj)/redcost + ub // so for lurklb = lb + 1 to ub we can compute the necessary cutoff // bound to reach this bound which is: // lurklb = (cutoffbound - lpobj)/redcost + ub // cutoffbound = (lurklb - ub) * redcost + lpobj findLurkingBounds(col, HighsInt{-1}, static_cast(ub), - static_cast(lb), lb > -kHighsIInf, - lpobjective, lpredcost[col], maxNumSteps, - maxNumStepsExp, lurkingColLower[col], - lurkingColUpper[col]); + static_cast(lb), + lb > -static_cast(kHighsIInf), lpobjective, + lpredcost[col], maxNumSteps, maxNumStepsExp, + lurkingColLower[col], lurkingColUpper[col]); } } } From 61bca4695e5c28b666c6728c14e5ccf09c03d460 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 25 Sep 2026 11:08:51 +0200 Subject: [PATCH 27/29] Skip fixed binary variables --- highs/presolve/HPresolve.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index aefa217c360..5849002fe86 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -9832,6 +9832,8 @@ void HPresolve::aggregateVarBounds() { if (lb > -kHighsInf) { implications.getVlbs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vlb) { + // skip deleted cols + if (colDeleted[binaryCol]) return; // skip if the VLB is dominated by the global lower bound if (implications.redundantVlb(vlb, lb)) return; // tighten so that minValue() >= lb @@ -9853,6 +9855,8 @@ void HPresolve::aggregateVarBounds() { if (ub < kHighsInf) { implications.getVubs(col).for_each( [&](HighsInt binaryCol, const HighsImplications::VarBound& vub) { + // skip deleted cols + if (colDeleted[binaryCol]) return; // skip if the VUB is dominated by the global upper bound if (implications.redundantVub(vub, ub)) return; // tighten so that maxValue() <= ub From 528261b13c52e334a9143483587969686ed4d567 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 25 Sep 2026 13:53:09 +0200 Subject: [PATCH 28/29] Update implications after moving cuts into the cut pool --- highs/mip/HighsImplications.cpp | 20 ++++++++++++++++++++ highs/mip/HighsImplications.h | 2 ++ highs/presolve/HPresolve.cpp | 2 ++ 3 files changed, 24 insertions(+) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 39fbc20e140..8946aaaed41 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -511,6 +511,26 @@ void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, if (origin >= 0) rowToVarBounds[origin].insert(col, vlbcol); } +void HighsImplications::compactRows(const std::vector& newRowIndex) { + for (HighsInt r = 0; r < static_cast(rowToVarBounds.size()); ++r) + rowToVarBounds[r].clear(); + + for (HighsInt col = 0; col < static_cast(vlbs.size()); ++col) { + vlbs[col].for_each([&](HighsInt vlbCol, VarBound& vlb) { + if (vlb.origin >= 0) { + vlb.origin = newRowIndex[vlb.origin]; + if (vlb.origin >= 0) rowToVarBounds[vlb.origin].insert(col, vlbCol); + } + }); + vubs[col].for_each([&](HighsInt vubCol, VarBound& vub) { + if (vub.origin >= 0) { + vub.origin = newRowIndex[vub.origin]; + if (vub.origin >= 0) rowToVarBounds[vub.origin].insert(col, vubCol); + } + }); + } +} + void HighsImplications::rebuild(HighsInt ncols, const std::vector& orig2reducedcol, const std::vector& orig2reducedrow) { diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index c160b28d989..c2d3ba11b27 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -212,6 +212,8 @@ class HighsImplications { bool runProbing(HighsInt col, HighsInt& numReductions); + void compactRows(const std::vector& newRowIndex); + void rebuild(HighsInt ncols, const std::vector& cIndex, const std::vector& rIndex); diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5849002fe86..2298f67cb20 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7103,6 +7103,8 @@ void HPresolve::moveCutsToPool(HighsPostsolveStack& postsolve_stack) { assert(newRowIndex[Arow[i]] != -1); Arow[i] = newRowIndex[Arow[i]]; } + + mipsolver->mipdata_->implications.compactRows(newRowIndex); } HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) { From 157f63c1af07396e363b668efa8378e940cf1536 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 25 Sep 2026 14:41:10 +0200 Subject: [PATCH 29/29] Rename and simplify --- highs/mip/HighsImplications.cpp | 7 ++++--- highs/mip/HighsImplications.h | 2 +- highs/presolve/HPresolve.cpp | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 8946aaaed41..a141983fd89 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -511,9 +511,10 @@ void HighsImplications::addVLB(HighsInt col, HighsInt vlbcol, double vlbcoef, if (origin >= 0) rowToVarBounds[origin].insert(col, vlbcol); } -void HighsImplications::compactRows(const std::vector& newRowIndex) { - for (HighsInt r = 0; r < static_cast(rowToVarBounds.size()); ++r) - rowToVarBounds[r].clear(); +void HighsImplications::reindexRows(HighsInt nrows, + const std::vector& newRowIndex) { + rowToVarBounds.clear(); + rowToVarBounds.resize(nrows); for (HighsInt col = 0; col < static_cast(vlbs.size()); ++col) { vlbs[col].for_each([&](HighsInt vlbCol, VarBound& vlb) { diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index c2d3ba11b27..6ad02807eaa 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -212,7 +212,7 @@ class HighsImplications { bool runProbing(HighsInt col, HighsInt& numReductions); - void compactRows(const std::vector& newRowIndex); + void reindexRows(HighsInt nrows, const std::vector& newRowIndex); void rebuild(HighsInt ncols, const std::vector& cIndex, const std::vector& rIndex); diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 2298f67cb20..3c327e338c5 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -7104,7 +7104,7 @@ void HPresolve::moveCutsToPool(HighsPostsolveStack& postsolve_stack) { Arow[i] = newRowIndex[Arow[i]]; } - mipsolver->mipdata_->implications.compactRows(newRowIndex); + mipsolver->mipdata_->implications.reindexRows(model->num_row_, newRowIndex); } HighsModelStatus HPresolve::run(HighsPostsolveStack& postsolve_stack) {