From b4f0b20c4d70fdc66f586944d5000a437179bf49 Mon Sep 17 00:00:00 2001 From: zwang Date: Tue, 21 Jul 2026 00:13:20 +0800 Subject: [PATCH 01/46] add the dual fixing probing propagator --- highs/mip/HighsDomain.cpp | 73 +++++++++++++++++++++++++++++++++++++++ highs/mip/HighsDomain.h | 51 +++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index f09d0f5b817..4998cbabf3b 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -75,6 +75,7 @@ HighsDomain::HighsDomain(HighsMipSolver& mipsolver) : mipsolver(&mipsolver) { changedcols_.reserve(mipsolver.numCol()); infeasible_reason = Reason::unspecified(); infeasible_ = false; + dfprobingPropagation.domain = this; } void HighsDomain::addCutpool(HighsCutPool& cutpool) { @@ -637,6 +638,78 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange( } } +HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation(const DualfixingProbingPropagation& other) + : zeroCostVarsDirection_(other.zeroCostVarsDirection_), + colLowerLockNum_(other.colLowerLockNum_), + colUpperLockNum_(other.colUpperLockNum_), + redundantPropagateflags_(other.redundantPropagateflags_), + redundantPropagateinds_(other.redundantPropagateinds_), + zeroCostFixedVariables_(other.zeroCostFixedVariables_), + tmpColLoLock_(other.tmpColLoLock_), + tmpColUpLock_(other.tmpColUpLock_), + involvedVars(other.involvedVars), + indsVars(other.indsVars) {;} + + +void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { + redundantPropagateflags_.assign(2 * mipsolver->numRow(), false); + redundantPropagateinds_.clear(); + redundantPropagateinds_.reserve(2 * mipsolver->numRow()); + zeroCostFixedVariables_.clear(); + zeroCostFixedVariables_.reserve(2 * mipsolver->numCol()); + + tmpColLoLock_.assign(mipsolver->numCol(), 0); + tmpColUpLock_.assign(mipsolver->numCol(), 0); + + involvedVars.clear(); + involvedVars.reserve(mipsolver->numCol()); + indsVars.assign(mipsolver->numCol(), false); +} + +bool HighsDomain::DualfixingProbingPropagation::isUpperRedundant(HighsInt row) { + bool upperRedundant; + + upperRedundant = (mipsolver->model_->row_upper_[row] != kHighsInf) && + (domain->getMaxActivity(row) <= mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol); + return upperRedundant; +} + +bool HighsDomain::DualfixingProbingPropagation::isLowerRedundant(HighsInt row) { + bool lowerRedundant; + + lowerRedundant = (mipsolver->model_->row_lower_[row] != -kHighsInf) && + (domain->getMinActivity(row) >= mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol); + return lowerRedundant; +} + +void HighsDomain::DualfixingProbingPropagation::markRedundantPropagate(HighsInt row, bool isUpper) { + assert(row < (int)mipsolver->numRow()); + if (mipsolver->submip) + return; + const HighsInt pos = 2 * row + isUpper; + if (!redundantPropagateflags_[pos]) { + if (isUpper) { + const bool upperRedundant = isUpperRedundant(row); + if (upperRedundant) { + redundantPropagateinds_.push_back(pos); + redundantPropagateflags_[pos] = 1; + } + } + else { + const bool lowerRedundant = isLowerRedundant(row); + if (lowerRedundant) { + redundantPropagateinds_.push_back(pos); + redundantPropagateflags_[pos] = 1; + } + } + } +} + +void HighsDomain::DualfixingProbingPropagation::propagate() { + mipsolver = domain->mipsolver; + +} + namespace highs { template <> struct RbTreeTraits< diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index e69ab43c8c9..3e20aee1363 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -235,6 +235,53 @@ class HighsDomain { void propagateConflict(HighsInt conflict); }; + struct DualfixingProbingPropagation { + HighsDomain* domain; + HighsMipSolver* mipsolver; + std::vector zeroCostVarsDirection_; + vector colLowerLockNum_; + vector colUpperLockNum_; + // row lower and upper, length = 2 * rownum + std::vector redundantPropagateflags_; + std::vector redundantPropagateinds_; + std::vector> zeroCostFixedVariables_; + HighsInt probingStatusSide = 0; + bool startZeroCostFixing; + + std::vector tmpColLoLock_; + std::vector tmpColUpLock_; + std::vector involvedVars; + std::vector indsVars; + + void clearInvolved(HighsInt start) { + for (const auto x : involvedVars) + indsVars[x] = false; + involvedVars.clear(); + } + + void clearRedundant(); + + + + DualfixingProbingPropagation() {}; + + DualfixingProbingPropagation(HighsDomain* domain) : domain(domain) {}; + + DualfixingProbingPropagation(const DualfixingProbingPropagation& other); + + DualfixingProbingPropagation& operator=(const DualfixingProbingPropagation& other); + + ~DualfixingProbingPropagation(); + + void recomputeLocks(); + bool isUpperRedundant(HighsInt row); + bool isLowerRedundant(HighsInt row); + void markRedundantPropagate(HighsInt row, bool isUpper); + + void propagate(); + + }; + private: struct ObjectivePropagation { HighsDomain* domain = nullptr; @@ -320,6 +367,7 @@ class HighsDomain { private: std::deque cutpoolpropagation; std::deque conflictPoolPropagation; + DualfixingProbingPropagation dfprobingPropagation; bool infeasible_ = false; Reason infeasible_reason; @@ -370,6 +418,7 @@ class HighsDomain { mipsolver(other.mipsolver), cutpoolpropagation(other.cutpoolpropagation), conflictPoolPropagation(other.conflictPoolPropagation), + dfprobingPropagation(other.dfprobingPropagation), infeasible_(other.infeasible_), infeasible_reason(other.infeasible_reason), infeasible_pos(other.infeasible_pos), @@ -383,6 +432,7 @@ class HighsDomain { for (ConflictPoolPropagation& conflictprop : conflictPoolPropagation) conflictprop.domain = this; if (objProp_.domain) objProp_.domain = this; + dfprobingPropagation.domain = this; } HighsDomain& operator=(const HighsDomain& other) { @@ -414,6 +464,7 @@ class HighsDomain { for (ConflictPoolPropagation& conflictprop : conflictPoolPropagation) conflictprop.domain = this; if (objProp_.domain) objProp_.domain = this; + dfprobingPropagation.domain = this; return *this; } From 7ee938f19129b25b82896c5b990a3305b47b0038 Mon Sep 17 00:00:00 2001 From: zwang Date: Thu, 23 Jul 2026 16:07:31 +0800 Subject: [PATCH 02/46] add main logic of dual fixing augmented probing --- highs/mip/HighsDomain.cpp | 369 +++++++++++++++++++++++++++++++++----- highs/mip/HighsDomain.h | 91 +++++++--- 2 files changed, 396 insertions(+), 64 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 4998cbabf3b..26c155deefb 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -639,77 +639,342 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange( } HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation(const DualfixingProbingPropagation& other) - : zeroCostVarsDirection_(other.zeroCostVarsDirection_), - colLowerLockNum_(other.colLowerLockNum_), - colUpperLockNum_(other.colUpperLockNum_), - redundantPropagateflags_(other.redundantPropagateflags_), + : redundantPropagateflags_(other.redundantPropagateflags_), redundantPropagateinds_(other.redundantPropagateinds_), + zeroCostVarsDirection_(other.zeroCostVarsDirection_), zeroCostFixedVariables_(other.zeroCostFixedVariables_), - tmpColLoLock_(other.tmpColLoLock_), - tmpColUpLock_(other.tmpColUpLock_), - involvedVars(other.involvedVars), - indsVars(other.indsVars) {;} - + colLowerLockOriginal_(other.colLowerLockOriginal_), + colUpperLockOriginal_(other.colUpperLockOriginal_), + colLowerLockReduced_(other.colLowerLockReduced_), + colUpperLockReduced_(other.colUpperLockReduced_), + candidatesVec_(other.candidatesVec_), + candidatesFlag_(other.candidatesFlag_) {;} void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { + if (!isEnabled()) + return; + + mipsolver = domain->mipsolver; redundantPropagateflags_.assign(2 * mipsolver->numRow(), false); redundantPropagateinds_.clear(); redundantPropagateinds_.reserve(2 * mipsolver->numRow()); + zeroCostVarsDirection_.assign(2 * mipsolver->numCol(), FIXDIRECTION_NOT_DECIDED); zeroCostFixedVariables_.clear(); zeroCostFixedVariables_.reserve(2 * mipsolver->numCol()); - tmpColLoLock_.assign(mipsolver->numCol(), 0); - tmpColUpLock_.assign(mipsolver->numCol(), 0); + startZeroCostFixing_ = false; + previousSize_ = 0; - involvedVars.clear(); - involvedVars.reserve(mipsolver->numCol()); - indsVars.assign(mipsolver->numCol(), false); + colLowerLockOriginal_.assign(mipsolver->numCol(), 0); + colUpperLockOriginal_.assign(mipsolver->numCol(), 0); + colLowerLockReduced_.assign(mipsolver->numCol(), 0); + colUpperLockReduced_.assign(mipsolver->numCol(), 0); + + candidatesVec_.clear(); + candidatesVec_.reserve(mipsolver->numCol()); + candidatesFlag_.assign(mipsolver->numCol(), false); } -bool HighsDomain::DualfixingProbingPropagation::isUpperRedundant(HighsInt row) { - bool upperRedundant; +void HighsDomain::DualfixingProbingPropagation::updateRhsRedundant(HighsInt row) { + if (!isEnabled()) + return; - upperRedundant = (mipsolver->model_->row_upper_[row] != kHighsInf) && - (domain->getMaxActivity(row) <= mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol); - return upperRedundant; + if (domain->activitymaxinf_[row] != 0 || redundantPropagateflags_[2 * row + 1] || mipsolver->model_->row_upper_[row] == kHighsInf) + return; + + if (domain->getMaxActivity(row) <= mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol) { + redundantPropagateinds_.push_back(2 * row + 1); + redundantPropagateflags_[2 * row + 1] = 1; + } } -bool HighsDomain::DualfixingProbingPropagation::isLowerRedundant(HighsInt row) { - bool lowerRedundant; +void HighsDomain::DualfixingProbingPropagation::updateLhsRedundant(HighsInt row) { + if (!isEnabled()) + return; + + if (domain->activitymininf_[row] != 0 || redundantPropagateflags_[2 * row] || mipsolver->model_->row_lower_[row] == -kHighsInf) + return; - lowerRedundant = (mipsolver->model_->row_lower_[row] != -kHighsInf) && - (domain->getMinActivity(row) >= mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol); - return lowerRedundant; + if (domain->getMinActivity(row) >= mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol) { + redundantPropagateinds_.push_back(2 * row); + redundantPropagateflags_[2 * row] = 1; + } } -void HighsDomain::DualfixingProbingPropagation::markRedundantPropagate(HighsInt row, bool isUpper) { - assert(row < (int)mipsolver->numRow()); - if (mipsolver->submip) + +void HighsDomain::DualfixingProbingPropagation::propagate() { + // The boolean variable ``startZeroCostFixing_'' is used to flag if we allow variables with zero cost objective coefficients can be fixed in domain propagation. + // The process of domain propagtion in probing is executed in two phases: + // Phase 1: Apply classic domain propagation, and additionally fix variables with nonzero objective coefficients using dual fixing + // Phase 2: Apply classic domain propagation, and additionally fix variables (including those with zero objective coefficients) using dual fixing + // In Phase 1, ``startZeroCostFixing_'' is set to be ``false'' to exclude variable with zero objective coefficients. + // In Phase 2, ``startZeroCostFixing_'' is set to be ``true''. + // Note that + // (1) For all the bound changes in Phase 1, reductions deduced from them are valid for all optimal solutions; + // (2) For the bound changes in Phase 2, reductions deduced from them can only be used to derive global valid reductions (i.e., variable fixing, global bound tightening, variable substitution). + if (!isEnabled()) return; - const HighsInt pos = 2 * row + isUpper; - if (!redundantPropagateflags_[pos]) { - if (isUpper) { - const bool upperRedundant = isUpperRedundant(row); - if (upperRedundant) { - redundantPropagateinds_.push_back(pos); - redundantPropagateflags_[pos] = 1; + + assert(candidatesVec_.empty()); + vector domainchangeProbing; + + // tool lambda functions + auto addToCandidate = [&](HighsInt k) { + // std::cout << "k = " << k << std::endl; + if (candidatesFlag_[k]) + return; + else { + candidatesVec_.push_back(k); + candidatesFlag_[k] = true; + } + }; + + auto checkVariableLowerLock = [&](HighsInt iCol) { + auto model = mipsolver->model_; + if (ableToFixToLb(iCol)) { + for (HighsInt k = model->a_matrix_.start_[iCol]; k < model->a_matrix_.start_[iCol + 1]; k ++) { + const HighsInt iRow = model->a_matrix_.index_[k]; + const double iValue = model->a_matrix_.value_[k]; + const double blower = model->row_lower_[iRow], bupper = model->row_upper_[iRow]; + const bool lhsOk = iValue > 0 && domain->getMinActivity(iRow) >= blower - domain->feastol(); + const bool rhsOk = iValue < 0 && domain->getMaxActivity(iRow) <= bupper + domain->feastol(); + if (!lhsOk && !rhsOk) { + std::cout << "Lower lock: variable " << iCol << " at row = " << iRow << " coef = " << iValue + << " not redundant at constraint " << iRow << ", minact = " << domain->getMinActivity(iRow) << ", maxact = " << domain->getMaxActivity(iRow) + << " lhs = " << blower << " rhs = " << bupper << std::endl; + } } } - else { - const bool lowerRedundant = isLowerRedundant(row); - if (lowerRedundant) { - redundantPropagateinds_.push_back(pos); - redundantPropagateflags_[pos] = 1; + }; + + auto checkVariableUpperLock = [&](HighsInt iCol) { + auto model = mipsolver->model_; + if (ableToFixToUb(iCol)) { + for (HighsInt k = model->a_matrix_.start_[iCol]; k < model->a_matrix_.start_[iCol + 1]; k ++) { + const HighsInt iRow = model->a_matrix_.index_[k]; + const double iValue = model->a_matrix_.value_[k]; + const double blower = model->row_lower_[iRow], bupper = model->row_upper_[iRow]; + const bool lhsOk = iValue < 0 && domain->getMinActivity(iRow) >= blower - domain->feastol(); + const bool rhsOk = iValue > 0 && domain->getMaxActivity(iRow) <= bupper + domain->feastol(); + if (!lhsOk && !rhsOk) { + std::cout << "Upper lock: variable " << iCol << " at row = " << iRow << " coef = " << iValue + << " not redundant at constraint " << iRow << ", minact = " << domain->getMinActivity(iRow) << ", maxact = " << domain->getMaxActivity(iRow) + << " lhs = " << blower << " rhs = " << bupper << std::endl; + } + } + } + }; + + auto addFixLower = [&](int iCol) { + HighsDomainChange* thisbchg = new HighsDomainChange; + thisbchg->column = iCol; + thisbchg->boundtype = HighsBoundType::kUpper; + thisbchg->boundval = domain->col_lower_[iCol]; + domainchangeProbing.push_back(thisbchg); + // std::cout << "fixing to lower " << iCol << std::endl; + }; + + auto addFixUpper = [&](int iCol) { + HighsDomainChange* thisbchg = new HighsDomainChange; + thisbchg->column = iCol; + thisbchg->boundtype = HighsBoundType::kLower; + thisbchg->boundval = domain->col_upper_[iCol]; + domainchangeProbing.push_back(thisbchg); + // std::cout << "fixing to upper " << iCol << std::endl; + }; + + auto collectFixLower = [&](int iCol) { + zeroCostFixedVariables_.emplace_back(iCol, FIXDIRECTION_LOWER_BOUND); + }; + + auto collectFixUpper = [&](int iCol) { + zeroCostFixedVariables_.emplace_back(iCol, FIXDIRECTION_UPPER_BOUND); + }; + + + + // get candidate + HighsInt maxLockLeft = redundantPropagateinds_.size() - previousSize_; + if (maxLockLeft == 0) + return; + for (; previousSize_ < redundantPropagateinds_.size(); ++ previousSize_, -- maxLockLeft) { + const HighsInt i = redundantPropagateinds_[previousSize_]; + const HighsInt iRow = i / 2; + assert(iRow < mipsolver->numRow()); + + if (i % 2 == 0) { // lower redundant + HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; + HighsInt rend = mipsolver->mipdata_->ARstart_[iRow + 1]; + for (auto k = rstart; k < rend; ++ k) { + const HighsInt iCol = mipsolver->mipdata_->ARindex_[k]; + if (domain->isFixed(iCol)) + continue; + const double iValue = mipsolver->mipdata_->ARvalue_[k]; + const double cost = mipsolver->model_->col_cost_[iCol]; + + bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; + bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; + + if (iValue > 0 && cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { + colLowerLockReduced_[iCol] ++; + lowerNoInsert = lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; + } + else if (iValue < 0 && cost <= mipsolver->options_mip_->dual_feasibility_tolerance) { + colUpperLockReduced_[iCol] ++; + upperNoInsert = upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; + } + + if (!lowerNoInsert || !upperNoInsert) + addToCandidate(iCol); + } + } + else { // upper redundant + HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; + HighsInt rend = mipsolver->mipdata_->ARstart_[iRow + 1]; + for (auto k = rstart; k < rend; k++) { + const HighsInt iCol = mipsolver->mipdata_->ARindex_[k]; + if (domain->isFixed(iCol)) + continue; + const double iValue = mipsolver->mipdata_->ARvalue_[k]; + const double cost = mipsolver->model_->col_cost_[iCol]; + + bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; + bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; + + if (iValue < 0 && cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { + colLowerLockReduced_[iCol] ++; + lowerNoInsert = lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; + } + else if (iValue > 0 && cost <= mipsolver->options_mip_->dual_feasibility_tolerance) { + colUpperLockReduced_[iCol] ++; + upperNoInsert = upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; + } + + if (!lowerNoInsert || !upperNoInsert) + addToCandidate(iCol); } } } -} -void HighsDomain::DualfixingProbingPropagation::propagate() { - mipsolver = domain->mipsolver; - + for (auto iCol : candidatesVec_) { + if (domain->isFixed(iCol)) + continue; + const bool canBeFixedToLower = colLowerLockReduced_[iCol] == colLowerLockOriginal_[iCol]; + const bool canBeFixedToUpper = colUpperLockReduced_[iCol] == colUpperLockOriginal_[iCol]; + if (!canBeFixedToLower && !canBeFixedToUpper) + continue; + + if (fabs(mipsolver->model_->col_cost_[iCol]) <= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (startZeroCostFixing_) { + // not fixed before + if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_NOT_DECIDED) { + // both directions are ok - depending on cost (no tolerance) + if (canBeFixedToLower && canBeFixedToUpper) { + if (mipsolver->model_->col_cost_[iCol] >= 0) { + addFixLower(iCol); + zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; + } + else { + addFixUpper(iCol); + zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; + } + } + // fix depending on the direction + else if (canBeFixedToLower) { + addFixLower(iCol); + zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; + } + else if (canBeFixedToUpper) { + addFixUpper(iCol); + zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; + } + } + // fix to lb + else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_LOWER_BOUND && canBeFixedToLower) + addFixLower(iCol); + // fix to ub + else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_UPPER_BOUND && canBeFixedToUpper) + addFixUpper(iCol); + + continue; + } + // do not perfrom zero cost variable fixing, just collect them and choose directions + else { + // not fixed before + if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_NOT_DECIDED) { + // both directions are ok - depending on cost (no tolerance) + if (canBeFixedToLower && canBeFixedToUpper) { + if (mipsolver->model_->col_cost_[iCol] >= 0) { + collectFixLower(iCol); + zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; + } + else { + collectFixUpper(iCol); + zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; + } + } + else if (canBeFixedToLower) { // fix to lower and set its direction + collectFixLower(iCol); + zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; + } + else if (canBeFixedToUpper) { + collectFixUpper(iCol); + zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; + } + } + else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_UPPER_BOUND && canBeFixedToUpper) { // fix to upper + collectFixUpper(iCol); + } + else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_LOWER_BOUND && canBeFixedToLower) { // fix to lower + collectFixLower(iCol); + } + // we have collected this column + continue; + } + } + + + // if (mipsolver->model_->col_cost_[iCol] >= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (mipsolver->model_->col_cost_[iCol] >= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (canBeFixedToLower) { + checkVariableLowerLock(iCol); + addFixLower(iCol); + continue; + } + } + // if (mipsolver->model_->col_cost_[iCol] <= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (mipsolver->model_->col_cost_[iCol] <= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (canBeFixedToUpper) { + checkVariableUpperLock(iCol); + addFixUpper(iCol); + continue; + } + } + } + + // clear candidate info + for (const auto x : candidatesVec_) + candidatesFlag_[x] = false; + candidatesVec_.clear(); + + // change bound + size_t j = 0; + for (; j != domainchangeProbing.size() && !domain->infeasible_; ++ j) { + domain->changeBound(*domainchangeProbing[j], Reason::unspecified()); + delete domainchangeProbing[j]; + } + + for (j ++; j < domainchangeProbing.size(); ++ j) { + assert(domain->infeasible); + delete domainchangeProbing[j]; + } + + // record the current number of redundant constraints. + previousSize_ = redundantPropagateinds_.size(); } + + namespace highs { template <> struct RbTreeTraits< @@ -1639,6 +1904,9 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); + + if (newbound >= oldbound + mipsolver->mipdata_->feastol) + dfprobingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); if (deltamin <= 0) { updateThresholdLbChange(col, newbound, mip->a_matrix_.value_[i], @@ -1689,6 +1957,9 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); + if (newbound >= oldbound + mipsolver->mipdata_->feastol) + dfprobingPropagation.updateRhsRedundant(mip->a_matrix_.index_[i]); + if (deltamax >= 0) { updateThresholdLbChange(col, newbound, mip->a_matrix_.value_[i], capacityThreshold_[mip->a_matrix_.index_[i]]); @@ -1806,6 +2077,9 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, mip->row_lower_[mip->a_matrix_.index_[i]] == -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); + + if (newbound <= oldbound - mipsolver->mipdata_->feastol) + dfprobingPropagation.updateRhsRedundant(mip->a_matrix_.index_[i]); if (deltamax >= 0) { updateThresholdUbChange(col, newbound, mip->a_matrix_.value_[i], @@ -1858,6 +2132,9 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); + + if (newbound <= oldbound - mipsolver->mipdata_->feastol) + dfprobingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); if (deltamin <= 0) { updateThresholdUbChange(col, newbound, mip->a_matrix_.value_[i], @@ -2452,6 +2729,9 @@ bool HighsDomain::propagate() { if (!conflictprop.propagateConflictInds_.empty()) return true; } + if (dfprobingPropagation.isActive()) + return true; + return false; }; @@ -2628,6 +2908,9 @@ bool HighsDomain::propagate() { propagateinds.clear(); } } + + if (dfprobingPropagation.isActive()) + dfprobingPropagation.propagate(); } return true; diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 3e20aee1363..3bf3c0efac4 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -238,32 +238,79 @@ class HighsDomain { struct DualfixingProbingPropagation { HighsDomain* domain; HighsMipSolver* mipsolver; - std::vector zeroCostVarsDirection_; - vector colLowerLockNum_; - vector colUpperLockNum_; // row lower and upper, length = 2 * rownum std::vector redundantPropagateflags_; std::vector redundantPropagateinds_; + + enum DFPROBING_FIX_DIRECTION { + FIXDIRECTION_NOT_DECIDED = 0, + FIXDIRECTION_LOWER_BOUND = 1, + FIXDIRECTION_UPPER_BOUND = 2, + }; + std::vector zeroCostVarsDirection_; std::vector> zeroCostFixedVariables_; - HighsInt probingStatusSide = 0; - bool startZeroCostFixing; - - std::vector tmpColLoLock_; - std::vector tmpColUpLock_; - std::vector involvedVars; - std::vector indsVars; - - void clearInvolved(HighsInt start) { - for (const auto x : involvedVars) - indsVars[x] = false; - involvedVars.clear(); + bool startZeroCostFixing_; + + bool enabled_ = false; + size_t previousSize_; + + std::vector colLowerLockOriginal_; + std::vector colUpperLockOriginal_; + std::vector colLowerLockReduced_; + std::vector colUpperLockReduced_; + std::vector candidatesVec_; + std::vector candidatesFlag_; + + void enablePropagator() { + enabled_ = true; } - void clearRedundant(); + void disablePropagator() { + enabled_ = false; + } + bool isEnabled() { + return enabled_; + } + bool isActive() { + return enabled_ && redundantPropagateinds_.size() > previousSize_; + } + + void enableZeroObjFixing() { + startZeroCostFixing_ = true; + } - DualfixingProbingPropagation() {}; + void disableZeroObjFixing() { + startZeroCostFixing_ = false; + } + + bool ableToFixToLb(int col) { + return mipsolver->model_->col_cost_[col] >= -mipsolver->options_mip_->dual_feasibility_tolerance + && mipsolver->model_->col_lower_[col] > -kHighsInf; + } + + bool ableToFixToUb(int col) { + return mipsolver->model_->col_cost_[col] <= mipsolver->options_mip_->dual_feasibility_tolerance + && mipsolver->model_->col_upper_[col] < kHighsInf; + } + + + void clearRedundant() { + if (!redundantPropagateinds_.empty()) { // clear buffers + for (auto x : redundantPropagateinds_) + redundantPropagateflags_[x] = false; + + redundantPropagateinds_.clear(); + } + + for (size_t i = 0; i < redundantPropagateflags_.size(); ++ i) + assert(!redundantPropagateflags_[i]); + + zeroCostFixedVariables_.clear(); + } + + DualfixingProbingPropagation() {;}; DualfixingProbingPropagation(HighsDomain* domain) : domain(domain) {}; @@ -271,14 +318,16 @@ class HighsDomain { DualfixingProbingPropagation& operator=(const DualfixingProbingPropagation& other); - ~DualfixingProbingPropagation(); + ~DualfixingProbingPropagation() {;}; void recomputeLocks(); - bool isUpperRedundant(HighsInt row); - bool isLowerRedundant(HighsInt row); - void markRedundantPropagate(HighsInt row, bool isUpper); + void updateRhsRedundant(HighsInt row); + void updateLhsRedundant(HighsInt row); void propagate(); + + + }; From de053a8c1e1a458b0fbe8593112d550f61f021cb Mon Sep 17 00:00:00 2001 From: zwang Date: Thu, 23 Jul 2026 20:44:14 +0800 Subject: [PATCH 03/46] add main logic in HighsImplications --- highs/mip/HighsDomain.h | 15 ++++ highs/mip/HighsImplications.cpp | 131 +++++++++++++++++++++++++++++++- highs/mip/HighsImplications.h | 120 +++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 3bf3c0efac4..b86cc50dcd3 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -250,6 +250,7 @@ class HighsDomain { std::vector zeroCostVarsDirection_; std::vector> zeroCostFixedVariables_; bool startZeroCostFixing_; + size_t zeroCostStartPos_; bool enabled_ = false; size_t previousSize_; @@ -277,6 +278,14 @@ class HighsDomain { return enabled_ && redundantPropagateinds_.size() > previousSize_; } + void setZeroCostFixingPosition(HighsInt v) { + zeroCostStartPos_ = v; + } + + size_t getZeroCostFixingPosition() { + return zeroCostStartPos_; + } + void enableZeroObjFixing() { startZeroCostFixing_ = true; } @@ -448,6 +457,8 @@ class HighsDomain { std::vector col_lower_; std::vector col_upper_; + bool inProbing_ = false; + HighsDomain(HighsMipSolver& mipsolver); HighsDomain(const HighsDomain& other) @@ -769,6 +780,10 @@ class HighsDomain { void setRecordRedundantRows(bool val) { recordRedundantRows_ = val; }; bool isRedundantRow(HighsInt row) const; + + DualfixingProbingPropagation& getDfProbingPropagation() { + return dfprobingPropagation; + } }; #endif diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 906c2349c4e..4a6884503c3 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -27,6 +27,8 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const auto& domchgreason = globaldomain.getDomainChangeReason(); size_t changedend = globaldomain.getChangedCols().size(); + globaldomain.getDfProbingPropagation().clearRedundant(); + HighsInt stackimplicstart = domchgstack.size() + 1; HighsInt numImplications = -stackimplicstart; if (val) @@ -61,7 +63,11 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { if (isInfeasible(col, val)) return true; + if (globaldomain.inProbing_) + globaldomain.getDfProbingPropagation().enablePropagator(); globaldomain.propagate(); + if (globaldomain.inProbing_) + globaldomain.getDfProbingPropagation().disablePropagator(); if (isInfeasible(col, val)) return true; @@ -73,13 +79,27 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { std::vector implics; implics.reserve(numImplications); + // data structure to cache implications for non-binary variables + std::vector implics_tentative; + implics_tentative.reserve(numImplications); + std::vector isTentative(numImplications, false); + HighsInt numEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); HighsInt maxEntries = 100000 + mipsolver.numNonzero(); + const HighsInt tentativeStart = globaldomain.inProbing_ ? globaldomain.getDfProbingPropagation().getZeroCostFixingPosition() : kHighsIInf32; + if (globaldomain.inProbing_) { + implics_tentative.assign(domchgstack.begin() + stackimplicstart, domchgstack.begin() + stackimplicend); + for (int i = 0; i < stackimplicend - stackimplicstart; i ++) + isTentative[i] = (i + stackimplicstart >= tentativeStart); + } for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { if (domchgreason[i].type == HighsDomain::Reason::kCliqueTable && ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) continue; + + if (i >= tentativeStart) // cache tentative implications + continue; implics.push_back(domchgstack[i]); } @@ -90,6 +110,18 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // backtrack doBacktrack(changedend); + if (!implics_tentative.empty()) { + // add the implications of binary variables to the clique table + auto binstart_tmp = std::partition(implics_tentative.begin(), implics_tentative.end(), + [&](const HighsDomainChange& a) { + return !globaldomain.isBinary(a.column); + }); + // Store the tentative bound changes (fixing) of binary variables separately + for (auto i = binstart_tmp; i != implics_tentative.end(); ++ i) + cacheTmpCliques(val, *i); + implics_tentative.erase(binstart_tmp, implics_tentative.end()); + } + // add the implications of binary variables to the clique table auto binstart = std::partition(implics.begin(), implics.end(), [&](const HighsDomainChange& a) { @@ -147,6 +179,11 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { implications[loc].implics = std::move(implics); this->numImplications += implications[loc].implics.size(); } + if (!implics_tentative.empty()) { + pdqsort(implics_tentative.begin(), implics_tentative.end()); + implications[loc].implics_tentative = std::move(implics_tentative); + implications[loc].isTentative = std::move(isTentative); + } return false; } @@ -300,6 +337,11 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (globaldomain.isBinary(col) && !implicationsCached(col, 1) && !implicationsCached(col, 0) && mipsolver.mipdata_->cliquetable.getSubstitution(col) == nullptr) { + + // setup for dfprobingPropagation + clearCacheClique(); + globaldomain.getDfProbingPropagation().setZeroCostFixingPosition(kHighsIInf32); + bool infeasible = computeImplications(col, 1); if (globaldomain.infeasible()) return true; if (infeasible) return true; @@ -312,11 +354,87 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (mipsolver.mipdata_->cliquetable.getSubstitution(col) != nullptr) return true; + if (globaldomain.inProbing_ && !binaryInvolvedInds_.empty()) { + HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; + HighsCliqueTable::CliqueVar clique[2]; + bool haveReduction; + do + { + haveReduction = false; + // Loop over binary variables that are tighened at least once + for (auto k : binaryInvolvedInds_) { + // Skip non-binary variables (being fixed now) or those can be substituted by other binary variables + if (!globaldomain.isBinary(k) || colsubstituted[k]) + continue; + // Return if the whole problem is infeasible + if (globaldomain.infeasible()) + return true; + // Get the information how x[k] is fixed in probing on x[col] = 0 and x[col] = 1 + // For the meaning of ``data'', please see lines 71-82 in HighsImplications.h + uint8_t data = binaryInvolvedFlags_[k]; + if (data == 0) // flag for no reduction + continue; + + if (data == binaryFixType::kGlobalLower) { // x[k] is fixed at 0 under both x[col] = 0 and x[col] = 1 + // fix x[k] = 0 by adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + clique[0] = HighsCliqueTable::CliqueVar(col, 0); + clique[1] = HighsCliqueTable::CliqueVar(k, 1); + cliquetable.addClique(mipsolver, &clique[0], 2); + clique[0] = HighsCliqueTable::CliqueVar(col, 1); + clique[1] = HighsCliqueTable::CliqueVar(k, 1); + cliquetable.addClique(mipsolver, &clique[0], 2); + data = 0; + haveReduction = true; + } + else if (data == binaryFixType::kGlobalUpper) { // x[k] is fixed at 1 under both x[col] = 0 and x[col] = 1 + // fix x[k] = 1 by adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + clique[0] = HighsCliqueTable::CliqueVar(col, 0); + clique[1] = HighsCliqueTable::CliqueVar(k, 0); + cliquetable.addClique(mipsolver, &clique[0], 2); + clique[0] = HighsCliqueTable::CliqueVar(col, 1); + clique[1] = HighsCliqueTable::CliqueVar(k, 0); + cliquetable.addClique(mipsolver, &clique[0], 2); + data = 0; + haveReduction = true; + } + else if (data == binaryFixType::kSubstituteComplement) { // x[k] is fixed at 0 under x[col] = 1, and is fixed at 1 under x[col] = 0; this makes x[col] + x[k] = 1 + // Adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + clique[0] = HighsCliqueTable::CliqueVar(col, 1); + clique[1] = HighsCliqueTable::CliqueVar(k, 1); + cliquetable.addClique(mipsolver, &clique[0], 2); + clique[0] = HighsCliqueTable::CliqueVar(col, 0); + clique[1] = HighsCliqueTable::CliqueVar(k, 0); + cliquetable.addClique(mipsolver, &clique[0], 2); + data = 0; + haveReduction = true; + } + else if (data == binaryFixType::kSubstituteEqual) { // x[k] is fixed at 0 under x[col] = 0, and is fixed at 1 under x[col] = 1; this makes x[col] = x[k] + // Adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + clique[0] = HighsCliqueTable::CliqueVar(col, 1); + clique[1] = HighsCliqueTable::CliqueVar(k, 0); + cliquetable.addClique(mipsolver, &clique[0], 2); + clique[0] = HighsCliqueTable::CliqueVar(col, 0); + clique[1] = HighsCliqueTable::CliqueVar(k, 1); + cliquetable.addClique(mipsolver, &clique[0], 2); + data = 0; + haveReduction = true; + } + } + } while (haveReduction); + + // clear the tentative bound changes for binary variables obtained from probing on x[col] + clearCacheClique(); + } + // analyze implications + // also include the bound changes of non-binary variables here, to derive tighter global bounds and variable substitutions + const bool haveTentativeImplics_zero = !implications[2 * col].implics_tentative.empty(); + const bool haveTentativeImplics_one = !implications[2 * col + 1].implics_tentative.empty(); + const std::vector& implicsdown = - getImplications(col, 0, infeasible); + haveTentativeImplics_zero ? getImplications_tentative(col, 0) : getImplications(col, 0, infeasible); const std::vector& implicsup = - getImplications(col, 1, infeasible); + haveTentativeImplics_one ? getImplications_tentative(col, 1) : getImplications(col, 1, infeasible); HighsInt nimplicsdown = implicsdown.size(); HighsInt nimplicsup = implicsup.size(); HighsInt u = 0; @@ -382,6 +500,15 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { } } + if (haveTentativeImplics_zero) { + implications[2 * col].implics_tentative.clear(); + implications[2 * col].isTentative.clear(); + } + if (haveTentativeImplics_one) { + implications[2 * col + 1].implics_tentative.clear(); + implications[2 * col + 1].isTentative.clear(); + } + return true; } diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index a82cc1d1a9b..7e88f431c2e 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -25,6 +25,15 @@ class HighsImplications { struct Implics { std::vector implics; + /* the "tentative" implications. + A implication of type x_j \ge (\ell^1_j - \ell^0_j) x_k + \ell^0_j is called "tentative", if + (1) c_j = 0 + (2) x_j is fixed by applying dual fixing in probing + These implications can only be used to perform globally valid reductions. + Therefore, special treatment is required. + */ + std::vector implics_tentative; + std::vector isTentative; bool computed = false; }; std::vector implications; @@ -57,6 +66,30 @@ class HighsImplications { const HighsMipSolver& mipsolver; std::vector substitutions; std::vector colsubstituted; + + // if a binary variable x_j is: (1) c_j = 0 (2) x_j is fixed by applying dual fixing in probing + std::vector binaryInvolvedInds_; + enum binaryFixType { + kNoReduction = 0b0000, + kGlobalLower = 0b1010, + kGlobalUpper = 0b0101, + kSubstituteComplement = 0b1001, + kSubstituteEqual = 0b0110, + }; + /* + Possible values for binaryInvolvedFlags_ + 0 (0000, kNoReduction): Not involved + 2 (0010): fixed to 0 in second side probing + 1 (0001): fixed to 1 in second side probing + 8 (1000): fixed to 0 in first side probing + 4 (0100): fixed to 1 in first side probing + 10(1010, kGlobalLower): fixed to 0 in both side probing (global fixing!) + 5 (0101, kGlobalUpper): fixed to 1 in both side probing (global fixing!) + 9 (1001, kSubstituteComplement): substitutation type 1 --- x1 + x2 = 1 + 6 (0110, kSubstituteEqual): substitutation type 2 --- x1 = x2 + */ + std::vector binaryInvolvedFlags_; + HighsImplications(const HighsMipSolver& mipsolver) : mipsolver(mipsolver) { HighsInt numcol = mipsolver.numCol(); implications.resize(2 * static_cast(numcol)); @@ -67,6 +100,9 @@ class HighsImplications { numImplications = 0; numVarBounds = 0; maxVarBounds = calcMaxVarBounds(numcol); + + binaryInvolvedInds_.reserve(numcol); + binaryInvolvedFlags_.assign(numcol, 0b0000); } std::function @@ -92,6 +128,9 @@ class HighsImplications { maxVarBounds = calcMaxVarBounds(numcol); nextCleanupCall = mipsolver.numNonzero(); + binaryInvolvedInds_.reserve(numcol); + binaryInvolvedFlags_.assign(numcol, 0b0000); + } constexpr static int64_t calcMaxVarBounds(HighsInt numcol) { @@ -115,6 +154,13 @@ class HighsImplications { return implications[loc].implics; } + // get the "tentative implications" w.r.t non-binary variables + const std::vector& getImplications_tentative(HighsInt col, bool val) { + HighsInt loc = 2 * col + val; + return implications[loc].implics_tentative; + } + + bool implicationsCached(HighsInt col, bool val) { HighsInt loc = 2 * col + val; return implications[loc].computed; @@ -192,6 +238,80 @@ class HighsImplications { bool& infeasible, bool allowBoundChanges = true) const; void applyImplications(HighsDomain& domain, HighsInt col, HighsInt val); + + // collect tentative binary implications + void cacheTmpCliques(bool val, const HighsDomainChange& bchg) { + const int iCol = bchg.column; + if (val == 0) { // probing x_k = 0 + if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 + if (!isFixedTo1(val, iCol)) { + if (binaryInvolvedFlags_[iCol] == 0) + binaryInvolvedInds_.push_back(iCol); + binaryInvolvedFlags_[iCol] += 0b0001; // 0001 + } + } + else { // fixed to 0 + if (!isFixedTo0(val, iCol)) { + if (binaryInvolvedFlags_[iCol] == 0) + binaryInvolvedInds_.push_back(iCol); + binaryInvolvedFlags_[iCol] += 0b0010; // 0010 + } + } + } + else { + if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 + if (!isFixedTo1(val, iCol)) { + if (binaryInvolvedFlags_[iCol] == 0) + binaryInvolvedInds_.push_back(iCol); + binaryInvolvedFlags_[iCol] += 0b0100; // 0100 + } + } + else { // fixed to 0 + if (!isFixedTo0(val, iCol)) { + if (binaryInvolvedFlags_[iCol] == 0) + binaryInvolvedInds_.push_back(iCol); + binaryInvolvedFlags_[iCol] += 0b1000; // 1000 + } + } + } + } + // clear tentative binary implications + void clearCacheClique() { + for (auto iCol : binaryInvolvedInds_) + binaryInvolvedFlags_[iCol] = binaryFixType::kNoReduction; + binaryInvolvedInds_.clear(); + } + // tools for cacheTmpCliques + bool isFixedTo0(bool val, HighsInt iCol) { + if (binaryInvolvedFlags_[iCol] == 0) + return false; + + uint8_t mask; + if (val == 0) { // x_k = 0, last two digits + mask = 1 << (1); + return (binaryInvolvedFlags_[iCol] & mask) != 0; + } + else { // x_k = 1, first two digits + mask = 1 << (3); + return (binaryInvolvedFlags_[iCol] & mask) != 0; + } + } + // tools for cacheTmpCliques + bool isFixedTo1(bool val, HighsInt iCol) { + if (binaryInvolvedFlags_[iCol] == 0) + return false; + + uint8_t mask; + if (val == 0) { // x_k = 0, last two digits + mask = 1; + return (binaryInvolvedFlags_[iCol] & mask) != 0; + } + else { // x_k = 1, first two digits + mask = 1 << (2); + return (binaryInvolvedFlags_[iCol] & mask) != 0; + } + } + }; #endif From 54d5a49bcbd2ca48070b57241d1bfcfb1ff35ba9 Mon Sep 17 00:00:00 2001 From: zwang Date: Thu, 23 Jul 2026 21:02:00 +0800 Subject: [PATCH 04/46] turn on dfprobing propagator in probing --- highs/mip/HighsDomain.cpp | 18 ++++++++++++++---- highs/mip/HighsImplications.cpp | 12 ++---------- highs/mip/HighsImplications.h | 1 - highs/presolve/HPresolve.cpp | 2 ++ 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 26c155deefb..16392f46cde 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -651,9 +651,6 @@ HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation(const Du candidatesFlag_(other.candidatesFlag_) {;} void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { - if (!isEnabled()) - return; - mipsolver = domain->mipsolver; redundantPropagateflags_.assign(2 * mipsolver->numRow(), false); redundantPropagateinds_.clear(); @@ -673,6 +670,19 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { candidatesVec_.clear(); candidatesVec_.reserve(mipsolver->numCol()); candidatesFlag_.assign(mipsolver->numCol(), false); + + const auto model = mipsolver->model_; + for (HighsInt iCol = 0; iCol < model->a_matrix_.num_col_; iCol ++) { + for (HighsInt k = model->a_matrix_.start_[iCol]; k < model->a_matrix_.start_[iCol + 1]; k ++) { + const HighsInt iRow = model->a_matrix_.index_[k]; + const double iValue = model->a_matrix_.value_[k]; + const double lhs = model->row_lower_[iRow], rhs = model->row_upper_[iRow]; + if ((iValue > 0 && rhs != kHighsInf) || (iValue < 0 && lhs != -kHighsInf)) + colUpperLockOriginal_[iCol] ++; + if ((iValue > 0 && lhs != -kHighsInf) || (iValue < 0 && rhs != kHighsInf)) + colLowerLockOriginal_[iCol] ++; + } + } } void HighsDomain::DualfixingProbingPropagation::updateRhsRedundant(HighsInt row) { @@ -965,7 +975,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } for (j ++; j < domainchangeProbing.size(); ++ j) { - assert(domain->infeasible); + assert(domain->infeasible_); delete domainchangeProbing[j]; } diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 4a6884503c3..eed1394db96 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -82,7 +82,6 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // data structure to cache implications for non-binary variables std::vector implics_tentative; implics_tentative.reserve(numImplications); - std::vector isTentative(numImplications, false); HighsInt numEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); HighsInt maxEntries = 100000 + mipsolver.numNonzero(); @@ -90,8 +89,6 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const HighsInt tentativeStart = globaldomain.inProbing_ ? globaldomain.getDfProbingPropagation().getZeroCostFixingPosition() : kHighsIInf32; if (globaldomain.inProbing_) { implics_tentative.assign(domchgstack.begin() + stackimplicstart, domchgstack.begin() + stackimplicend); - for (int i = 0; i < stackimplicend - stackimplicstart; i ++) - isTentative[i] = (i + stackimplicstart >= tentativeStart); } for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { if (domchgreason[i].type == HighsDomain::Reason::kCliqueTable && @@ -182,7 +179,6 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { if (!implics_tentative.empty()) { pdqsort(implics_tentative.begin(), implics_tentative.end()); implications[loc].implics_tentative = std::move(implics_tentative); - implications[loc].isTentative = std::move(isTentative); } return false; @@ -500,14 +496,10 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { } } - if (haveTentativeImplics_zero) { + if (haveTentativeImplics_zero) implications[2 * col].implics_tentative.clear(); - implications[2 * col].isTentative.clear(); - } - if (haveTentativeImplics_one) { + if (haveTentativeImplics_one) implications[2 * col + 1].implics_tentative.clear(); - implications[2 * col + 1].isTentative.clear(); - } return true; } diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 7e88f431c2e..4e66b7b4bb6 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -33,7 +33,6 @@ class HighsImplications { Therefore, special treatment is required. */ std::vector implics_tentative; - std::vector isTentative; bool computed = false; }; std::vector implications; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ac5b83a74fa..4f88e85013b 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1799,7 +1799,9 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { HighsInt numBoundChgs = 0; HighsInt numNewCliques = -cliquetable.numCliques(); + domain.inProbing_ = true; const bool probing_result = implications.runProbing(i, numBoundChgs); + domain.inProbing_ = false; if (!probing_result) continue; probingContingent += numBoundChgs; numNewCliques += cliquetable.numCliques(); From bff422ffbde6dded65524c23ca67888b6e2bb338 Mon Sep 17 00:00:00 2001 From: zwang Date: Thu, 23 Jul 2026 23:23:14 +0800 Subject: [PATCH 05/46] add initialization --- highs/presolve/HPresolve.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 4f88e85013b..f296ba75925 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1735,6 +1735,8 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { } }; + domain.getDfProbingPropagation().recomputeLocks(); + for (const auto& binvar : binaries) { // Count the binaries considered iBin++; From e9e0148ee97f7d8010fa478e2b7bc1ea6dea8754 Mon Sep 17 00:00:00 2001 From: zwang Date: Fri, 24 Jul 2026 16:07:33 +0800 Subject: [PATCH 06/46] clear lock number when propagation finishes --- highs/mip/HighsDomain.cpp | 18 ++++++++++++++---- highs/mip/HighsDomain.h | 12 ++++++++++-- highs/mip/HighsImplications.cpp | 4 ++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 16392f46cde..9af75dc4b5b 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -648,7 +648,8 @@ HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation(const Du colLowerLockReduced_(other.colLowerLockReduced_), colUpperLockReduced_(other.colUpperLockReduced_), candidatesVec_(other.candidatesVec_), - candidatesFlag_(other.candidatesFlag_) {;} + candidatesFlag_(other.candidatesFlag_), + lockNeedClear_(other.lockNeedClear_) {;} void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { mipsolver = domain->mipsolver; @@ -670,6 +671,7 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { candidatesVec_.clear(); candidatesVec_.reserve(mipsolver->numCol()); candidatesFlag_.assign(mipsolver->numCol(), false); + lockNeedClear_.reserve(mipsolver->numCol()); const auto model = mipsolver->model_; for (HighsInt iCol = 0; iCol < model->a_matrix_.num_col_; iCol ++) { @@ -963,8 +965,10 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } // clear candidate info - for (const auto x : candidatesVec_) - candidatesFlag_[x] = false; + for (const auto x : candidatesVec_) { + candidatesFlag_[x] = false; + lockNeedClear_.insert(x); + } candidatesVec_.clear(); // change bound @@ -2919,8 +2923,14 @@ bool HighsDomain::propagate() { } } - if (dfprobingPropagation.isActive()) + if (dfprobingPropagation.isActive()) { dfprobingPropagation.propagate(); + if (!havePropagationRows() && !dfprobingPropagation.isZeroObjFixingEnabled()) { + dfprobingPropagation.enableZeroObjFixing(); + dfprobingPropagation.setZeroCostFixingPosition(domchgstack_.size()); + dfprobingPropagation.propagate(); + } + } } return true; diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index b86cc50dcd3..288d72e8c3e 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -13,6 +13,7 @@ #include #include #include +#include #include "HighsPseudocost.h" #include "mip/HighsDomainChange.h" @@ -261,6 +262,7 @@ class HighsDomain { std::vector colUpperLockReduced_; std::vector candidatesVec_; std::vector candidatesFlag_; + std::unordered_set lockNeedClear_; void enablePropagator() { enabled_ = true; @@ -294,6 +296,10 @@ class HighsDomain { startZeroCostFixing_ = false; } + bool isZeroObjFixingEnabled() { + return startZeroCostFixing_; + } + bool ableToFixToLb(int col) { return mipsolver->model_->col_cost_[col] >= -mipsolver->options_mip_->dual_feasibility_tolerance && mipsolver->model_->col_lower_[col] > -kHighsInf; @@ -317,6 +323,10 @@ class HighsDomain { assert(!redundantPropagateflags_[i]); zeroCostFixedVariables_.clear(); + + for (const auto x : lockNeedClear_) + colLowerLockReduced_[x] = colUpperLockReduced_[x] = 0; + lockNeedClear_.clear(); } DualfixingProbingPropagation() {;}; @@ -325,8 +335,6 @@ class HighsDomain { DualfixingProbingPropagation(const DualfixingProbingPropagation& other); - DualfixingProbingPropagation& operator=(const DualfixingProbingPropagation& other); - ~DualfixingProbingPropagation() {;}; void recomputeLocks(); diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index eed1394db96..3e2ed8d8a74 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -28,6 +28,8 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { size_t changedend = globaldomain.getChangedCols().size(); globaldomain.getDfProbingPropagation().clearRedundant(); + if (globaldomain.inProbing_) + globaldomain.getDfProbingPropagation().enablePropagator(); HighsInt stackimplicstart = domchgstack.size() + 1; HighsInt numImplications = -stackimplicstart; @@ -63,8 +65,6 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { if (isInfeasible(col, val)) return true; - if (globaldomain.inProbing_) - globaldomain.getDfProbingPropagation().enablePropagator(); globaldomain.propagate(); if (globaldomain.inProbing_) globaldomain.getDfProbingPropagation().disablePropagator(); From 5035bf921e3607244bf2637b384c5296e08f09b7 Mon Sep 17 00:00:00 2001 From: zwang Date: Fri, 24 Jul 2026 16:29:43 +0800 Subject: [PATCH 07/46] add debug info --- highs/mip/HighsDomain.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index da8f524d9cc..c412d929820 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -978,6 +978,8 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { delete domainchangeProbing[j]; } + std::cout << "#Bchg = " << j << std::endl; + for (j ++; j < domainchangeProbing.size(); ++ j) { assert(domain->infeasible_); delete domainchangeProbing[j]; @@ -2916,6 +2918,7 @@ bool HighsDomain::propagate() { } if (dfprobingPropagation.isActive()) { + std::cout << "Activated by nRedundantIndices = " << dfprobingPropagation.redundantPropagateinds_.size() << std::endl; dfprobingPropagation.propagate(); if (!havePropagationRows() && !dfprobingPropagation.isZeroObjFixingEnabled()) { dfprobingPropagation.enableZeroObjFixing(); From 2344dbe4a89340ccaa170322500ee8296b36d615 Mon Sep 17 00:00:00 2001 From: zwang Date: Sat, 25 Jul 2026 20:02:49 +0800 Subject: [PATCH 08/46] add debug output --- highs/mip/HighsDomain.cpp | 48 ++++++++++++++++++++++++++------------- highs/mip/HighsDomain.h | 18 +++++++-------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index c412d929820..7aa3a4d20b7 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -639,8 +639,8 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange( } HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation(const DualfixingProbingPropagation& other) - : redundantPropagateflags_(other.redundantPropagateflags_), - redundantPropagateinds_(other.redundantPropagateinds_), + : redundantPropagateFlag_(other.redundantPropagateFlag_), + redundantPropagateVec_(other.redundantPropagateVec_), zeroCostVarsDirection_(other.zeroCostVarsDirection_), zeroCostFixedVariables_(other.zeroCostFixedVariables_), colLowerLockOriginal_(other.colLowerLockOriginal_), @@ -653,9 +653,9 @@ HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation(const Du void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { mipsolver = domain->mipsolver; - redundantPropagateflags_.assign(2 * mipsolver->numRow(), false); - redundantPropagateinds_.clear(); - redundantPropagateinds_.reserve(2 * mipsolver->numRow()); + redundantPropagateFlag_.assign(2 * mipsolver->numRow(), false); + redundantPropagateVec_.clear(); + redundantPropagateVec_.reserve(2 * mipsolver->numRow()); zeroCostVarsDirection_.assign(2 * mipsolver->numCol(), FIXDIRECTION_NOT_DECIDED); zeroCostFixedVariables_.clear(); zeroCostFixedVariables_.reserve(2 * mipsolver->numCol()); @@ -691,12 +691,12 @@ void HighsDomain::DualfixingProbingPropagation::updateRhsRedundant(HighsInt row) if (!isEnabled()) return; - if (domain->activitymaxinf_[row] != 0 || redundantPropagateflags_[2 * row + 1] || mipsolver->model_->row_upper_[row] == kHighsInf) + if (domain->activitymaxinf_[row] != 0 || redundantPropagateFlag_[2 * row + 1] || mipsolver->model_->row_upper_[row] == kHighsInf) return; if (domain->getMaxActivity(row) <= mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol) { - redundantPropagateinds_.push_back(2 * row + 1); - redundantPropagateflags_[2 * row + 1] = 1; + redundantPropagateVec_.push_back(2 * row + 1); + redundantPropagateFlag_[2 * row + 1] = 1; } } @@ -704,12 +704,12 @@ void HighsDomain::DualfixingProbingPropagation::updateLhsRedundant(HighsInt row) if (!isEnabled()) return; - if (domain->activitymininf_[row] != 0 || redundantPropagateflags_[2 * row] || mipsolver->model_->row_lower_[row] == -kHighsInf) + if (domain->activitymininf_[row] != 0 || redundantPropagateFlag_[2 * row] || mipsolver->model_->row_lower_[row] == -kHighsInf) return; if (domain->getMinActivity(row) >= mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol) { - redundantPropagateinds_.push_back(2 * row); - redundantPropagateflags_[2 * row] = 1; + redundantPropagateVec_.push_back(2 * row); + redundantPropagateFlag_[2 * row] = 1; } } @@ -727,6 +727,17 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { if (!isEnabled()) return; +// #ifndef NDEBUG + for (const HighsInt x : redundantPropagateVec_) { + HighsInt iRow = x / 2; + bool isUpper = x % 2; + if (isUpper && domain->getMaxActivity(iRow) > mipsolver->model_->row_upper_[iRow] + domain->feastol()) + printf("Row %d not rhs redundant, maxAct = %f, rhs = %f.\n", iRow, domain->getMaxActivity(iRow), mipsolver->model_->row_upper_[iRow]); + if (!isUpper && domain->getMinActivity(iRow) < mipsolver->model_->row_lower_[iRow] - domain->feastol()) + printf("Row %d not lhs redundant, minAct = %f, lhs = %f.\n", iRow, domain->getMinActivity(iRow), mipsolver->model_->row_lower_[iRow]); + } +// #endif + assert(candidatesVec_.empty()); vector domainchangeProbing; @@ -754,6 +765,11 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { std::cout << "Lower lock: variable " << iCol << " at row = " << iRow << " coef = " << iValue << " not redundant at constraint " << iRow << ", minact = " << domain->getMinActivity(iRow) << ", maxact = " << domain->getMaxActivity(iRow) << " lhs = " << blower << " rhs = " << bupper << std::endl; + std::cout << "lock rows:\n"; + for (int kk = model->a_matrix_.start_[iCol]; kk < model->a_matrix_.start_[iCol + 1]; kk ++) { + std::cout << kk << " "; + } + std::cout << std::endl; } } } @@ -806,11 +822,11 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // get candidate - HighsInt maxLockLeft = redundantPropagateinds_.size() - previousSize_; + HighsInt maxLockLeft = redundantPropagateVec_.size() - previousSize_; if (maxLockLeft == 0) return; - for (; previousSize_ < redundantPropagateinds_.size(); ++ previousSize_, -- maxLockLeft) { - const HighsInt i = redundantPropagateinds_[previousSize_]; + for (; previousSize_ < redundantPropagateVec_.size(); ++ previousSize_, -- maxLockLeft) { + const HighsInt i = redundantPropagateVec_[previousSize_]; const HighsInt iRow = i / 2; assert(iRow < mipsolver->numRow()); @@ -986,7 +1002,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } // record the current number of redundant constraints. - previousSize_ = redundantPropagateinds_.size(); + previousSize_ = redundantPropagateVec_.size(); } @@ -2918,7 +2934,7 @@ bool HighsDomain::propagate() { } if (dfprobingPropagation.isActive()) { - std::cout << "Activated by nRedundantIndices = " << dfprobingPropagation.redundantPropagateinds_.size() << std::endl; + std::cout << "Activated by nRedundantIndices = " << dfprobingPropagation.redundantPropagateVec_.size() << std::endl; dfprobingPropagation.propagate(); if (!havePropagationRows() && !dfprobingPropagation.isZeroObjFixingEnabled()) { dfprobingPropagation.enableZeroObjFixing(); diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index cd3f517389a..d87b0bbdd36 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -240,8 +240,8 @@ class HighsDomain { HighsDomain* domain; HighsMipSolver* mipsolver; // row lower and upper, length = 2 * rownum - std::vector redundantPropagateflags_; - std::vector redundantPropagateinds_; + std::vector redundantPropagateFlag_; + std::vector redundantPropagateVec_; enum DFPROBING_FIX_DIRECTION { FIXDIRECTION_NOT_DECIDED = 0, @@ -277,7 +277,7 @@ class HighsDomain { } bool isActive() { - return enabled_ && redundantPropagateinds_.size() > previousSize_; + return enabled_ && redundantPropagateVec_.size() > previousSize_; } void setZeroCostFixingPosition(HighsInt v) { @@ -312,15 +312,15 @@ class HighsDomain { void clearRedundant() { - if (!redundantPropagateinds_.empty()) { // clear buffers - for (auto x : redundantPropagateinds_) - redundantPropagateflags_[x] = false; + if (!redundantPropagateVec_.empty()) { // clear buffers + for (auto x : redundantPropagateVec_) + redundantPropagateFlag_[x] = false; - redundantPropagateinds_.clear(); + redundantPropagateVec_.clear(); } - for (size_t i = 0; i < redundantPropagateflags_.size(); ++ i) - assert(!redundantPropagateflags_[i]); + for (size_t i = 0; i < redundantPropagateFlag_.size(); ++ i) + assert(!redundantPropagateFlag_[i]); zeroCostFixedVariables_.clear(); From ac5a926ad01cccab63ea07047d470b0f2cf6c105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=85=86=E7=BB=B4?= Date: Sat, 25 Jul 2026 22:10:59 +0800 Subject: [PATCH 09/46] add to lockNeedClear --- highs/mip/HighsDomain.cpp | 18 +++++++++++------- highs/mip/HighsDomain.h | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 7aa3a4d20b7..4bf9634cdc7 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -727,6 +727,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { if (!isEnabled()) return; + // printf("%f, %f\n", domain->getMaxActivity(1001), domain->getMinActivity(1001)); // #ifndef NDEBUG for (const HighsInt x : redundantPropagateVec_) { HighsInt iRow = x / 2; @@ -765,11 +766,11 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { std::cout << "Lower lock: variable " << iCol << " at row = " << iRow << " coef = " << iValue << " not redundant at constraint " << iRow << ", minact = " << domain->getMinActivity(iRow) << ", maxact = " << domain->getMaxActivity(iRow) << " lhs = " << blower << " rhs = " << bupper << std::endl; - std::cout << "lock rows:\n"; - for (int kk = model->a_matrix_.start_[iCol]; kk < model->a_matrix_.start_[iCol + 1]; kk ++) { - std::cout << kk << " "; - } - std::cout << std::endl; + // std::cout << "lock rows:\n"; + // for (int kk = model->a_matrix_.start_[iCol]; kk < model->a_matrix_.start_[iCol + 1]; kk ++) { + // std::cout << kk << " "; + // } + // std::cout << std::endl; } } } @@ -844,10 +845,12 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; if (iValue > 0 && cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { + lockNeedClear_.insert(iCol); colLowerLockReduced_[iCol] ++; lowerNoInsert = lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; } else if (iValue < 0 && cost <= mipsolver->options_mip_->dual_feasibility_tolerance) { + lockNeedClear_.insert(iCol); colUpperLockReduced_[iCol] ++; upperNoInsert = upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; } @@ -870,10 +873,12 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; if (iValue < 0 && cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { + lockNeedClear_.insert(iCol); colLowerLockReduced_[iCol] ++; lowerNoInsert = lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; } else if (iValue > 0 && cost <= mipsolver->options_mip_->dual_feasibility_tolerance) { + lockNeedClear_.insert(iCol); colUpperLockReduced_[iCol] ++; upperNoInsert = upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; } @@ -983,7 +988,6 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // clear candidate info for (const auto x : candidatesVec_) { candidatesFlag_[x] = false; - lockNeedClear_.insert(x); } candidatesVec_.clear(); @@ -2933,7 +2937,7 @@ bool HighsDomain::propagate() { } } - if (dfprobingPropagation.isActive()) { + if (!infeasible_ && dfprobingPropagation.isActive()) { std::cout << "Activated by nRedundantIndices = " << dfprobingPropagation.redundantPropagateVec_.size() << std::endl; dfprobingPropagation.propagate(); if (!havePropagationRows() && !dfprobingPropagation.isZeroObjFixingEnabled()) { diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index d87b0bbdd36..36a2107ea9f 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -312,6 +312,7 @@ class HighsDomain { void clearRedundant() { + previousSize_ = 0; if (!redundantPropagateVec_.empty()) { // clear buffers for (auto x : redundantPropagateVec_) redundantPropagateFlag_[x] = false; From 5a282ccddef4001f85ff5f38938e7195e87c8a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=85=86=E7=BB=B4?= Date: Sun, 26 Jul 2026 12:08:16 +0800 Subject: [PATCH 10/46] disable dfprobing propagator when probing leads to infeasible binary fixing --- highs/mip/HighsImplications.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 3e2ed8d8a74..f7b957d2a75 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -57,6 +57,8 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { auto isInfeasible = [&](HighsInt col, bool val) { if (!globaldomain.infeasible()) return false; + if (globaldomain.inProbing_) + globaldomain.getDfProbingPropagation().disablePropagator(); storeLiftingOpportunities(col, val); doBacktrack(changedend); cliquetable.vertexInfeasible(globaldomain, col, val); From b7ca8e26026780273e8c295080fa24d58303fb4c Mon Sep 17 00:00:00 2001 From: zwang Date: Sun, 26 Jul 2026 12:17:13 +0800 Subject: [PATCH 11/46] cleanup --- highs/mip/HighsDomain.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 4bf9634cdc7..ef8b5cfdaba 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -998,7 +998,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { delete domainchangeProbing[j]; } - std::cout << "#Bchg = " << j << std::endl; + // std::cout << "#Bchg = " << j << std::endl; for (j ++; j < domainchangeProbing.size(); ++ j) { assert(domain->infeasible_); @@ -2938,7 +2938,7 @@ bool HighsDomain::propagate() { } if (!infeasible_ && dfprobingPropagation.isActive()) { - std::cout << "Activated by nRedundantIndices = " << dfprobingPropagation.redundantPropagateVec_.size() << std::endl; + // std::cout << "Activated by nRedundantIndices = " << dfprobingPropagation.redundantPropagateVec_.size() << std::endl; dfprobingPropagation.propagate(); if (!havePropagationRows() && !dfprobingPropagation.isZeroObjFixingEnabled()) { dfprobingPropagation.enableZeroObjFixing(); From bae50aa85cf326305396218bd7a24d71a7a35b75 Mon Sep 17 00:00:00 2001 From: zwang Date: Sun, 26 Jul 2026 16:04:55 +0800 Subject: [PATCH 12/46] renaming functions --- highs/mip/HighsDomain.h | 6 +++--- highs/mip/HighsImplications.cpp | 8 ++++---- highs/mip/HighsImplications.h | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 36a2107ea9f..7ad32643fbd 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -245,8 +245,8 @@ class HighsDomain { enum DFPROBING_FIX_DIRECTION { FIXDIRECTION_NOT_DECIDED = 0, - FIXDIRECTION_LOWER_BOUND = 1, - FIXDIRECTION_UPPER_BOUND = 2, + FIXDIRECTION_LOWER_BOUND, + FIXDIRECTION_UPPER_BOUND, }; std::vector zeroCostVarsDirection_; std::vector> zeroCostFixedVariables_; @@ -311,7 +311,7 @@ class HighsDomain { } - void clearRedundant() { + void clearRedundantInfo() { previousSize_ = 0; if (!redundantPropagateVec_.empty()) { // clear buffers for (auto x : redundantPropagateVec_) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index f7b957d2a75..1692327b813 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -27,7 +27,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const auto& domchgreason = globaldomain.getDomainChangeReason(); size_t changedend = globaldomain.getChangedCols().size(); - globaldomain.getDfProbingPropagation().clearRedundant(); + globaldomain.getDfProbingPropagation().clearRedundantInfo(); if (globaldomain.inProbing_) globaldomain.getDfProbingPropagation().enablePropagator(); @@ -117,7 +117,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { }); // Store the tentative bound changes (fixing) of binary variables separately for (auto i = binstart_tmp; i != implics_tentative.end(); ++ i) - cacheTmpCliques(val, *i); + recordTentativeCliques(val, *i); implics_tentative.erase(binstart_tmp, implics_tentative.end()); } @@ -337,7 +337,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { mipsolver.mipdata_->cliquetable.getSubstitution(col) == nullptr) { // setup for dfprobingPropagation - clearCacheClique(); + clearTentativeClique(); globaldomain.getDfProbingPropagation().setZeroCostFixingPosition(kHighsIInf32); bool infeasible = computeImplications(col, 1); @@ -421,7 +421,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { } while (haveReduction); // clear the tentative bound changes for binary variables obtained from probing on x[col] - clearCacheClique(); + clearTentativeClique(); } // analyze implications diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 4e66b7b4bb6..c041df32255 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -239,7 +239,7 @@ class HighsImplications { void applyImplications(HighsDomain& domain, HighsInt col, HighsInt val); // collect tentative binary implications - void cacheTmpCliques(bool val, const HighsDomainChange& bchg) { + void recordTentativeCliques(bool val, const HighsDomainChange& bchg) { const int iCol = bchg.column; if (val == 0) { // probing x_k = 0 if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 @@ -275,37 +275,37 @@ class HighsImplications { } } // clear tentative binary implications - void clearCacheClique() { + void clearTentativeClique() { for (auto iCol : binaryInvolvedInds_) binaryInvolvedFlags_[iCol] = binaryFixType::kNoReduction; binaryInvolvedInds_.clear(); } - // tools for cacheTmpCliques + // tools for recordTentativeCliques bool isFixedTo0(bool val, HighsInt iCol) { if (binaryInvolvedFlags_[iCol] == 0) return false; uint8_t mask; - if (val == 0) { // x_k = 0, last two digits + if (val == 0) { // probing at x = 0, last two digits mask = 1 << (1); return (binaryInvolvedFlags_[iCol] & mask) != 0; } - else { // x_k = 1, first two digits + else { // probing at x = 1, first two digits mask = 1 << (3); return (binaryInvolvedFlags_[iCol] & mask) != 0; } } - // tools for cacheTmpCliques + // tools for recordTentativeCliques bool isFixedTo1(bool val, HighsInt iCol) { if (binaryInvolvedFlags_[iCol] == 0) return false; uint8_t mask; - if (val == 0) { // x_k = 0, last two digits + if (val == 0) { // probing at x = 0, last two digits mask = 1; return (binaryInvolvedFlags_[iCol] & mask) != 0; } - else { // x_k = 1, first two digits + else { // probint at x = 1, first two digits mask = 1 << (2); return (binaryInvolvedFlags_[iCol] & mask) != 0; } From 6fd7e6d92c6425d35376f6bfe32137f4c9402f0b Mon Sep 17 00:00:00 2001 From: zwang Date: Mon, 27 Jul 2026 20:31:41 +0800 Subject: [PATCH 13/46] add parameters for DFProbing and GDF --- highs/lp_data/HighsOptions.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index ef09ae76954..55280fc0f86 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -466,6 +466,8 @@ struct HighsOptionsStruct { bool less_infeasible_DSE_check; bool less_infeasible_DSE_choose_row; bool use_original_HFactor_logic; + bool presolve_dfprobing; + bool presolve_gdf; // bool allow_pdlp_cleanup; bool run_centring; HighsInt max_centring_steps; @@ -1724,6 +1726,19 @@ class HighsOptions : public HighsOptionsStruct { advanced, ¢ring_ratio_tolerance, 0, 100, kHighsInf); records.push_back(record_double); + record_bool = + new OptionRecordBool("presolve_dfprobing", + "Use the dual fixing aumgented probing technique in presolve", advanced, + &presolve_dfprobing, true); + records.push_back(record_bool); + + record_bool = + new OptionRecordBool("presolve_gdf", + "Use the generalized dual fixing technique in presolve", advanced, + &presolve_gdf, true); + records.push_back(record_bool); + + // Set up the log_options aliases log_options.clear(); log_options.log_stream = From 35a08f5fb07c7d96f833acb8d31ef806f7e05c71 Mon Sep 17 00:00:00 2001 From: zwang Date: Mon, 27 Jul 2026 20:32:34 +0800 Subject: [PATCH 14/46] Add the functions of GDF --- highs/mip/HighsDomain.cpp | 235 ++++++++++++++++++++++++++++++-- highs/mip/HighsDomain.h | 19 ++- highs/mip/HighsImplications.cpp | 41 ++++-- highs/presolve/HPresolve.cpp | 5 +- 4 files changed, 271 insertions(+), 29 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index ef8b5cfdaba..1c3ad8cd9ab 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -649,7 +649,15 @@ HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation(const Du colUpperLockReduced_(other.colUpperLockReduced_), candidatesVec_(other.candidatesVec_), candidatesFlag_(other.candidatesFlag_), - lockNeedClear_(other.lockNeedClear_) {;} + lockNeedClear_(other.lockNeedClear_), + gdfCandidatesVec_(other.gdfCandidatesVec_), + gdfCandidatesFlag_(other.gdfCandidatesFlag_), + gdfLbReachable0_(other.gdfLbReachable0_), + gdfLbReachable1_(other.gdfLbReachable1_), + gdfUbReachable0_(other.gdfUbReachable0_), + gdfUbReachable1_(other.gdfUbReachable1_), + gdfLbReachable_(other.gdfLbReachable_), + gdfUbReachable_(other.gdfUbReachable_) {;} void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { mipsolver = domain->mipsolver; @@ -673,6 +681,16 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { candidatesFlag_.assign(mipsolver->numCol(), false); lockNeedClear_.reserve(mipsolver->numCol()); + gdfCandidatesVec_.reserve(mipsolver->numCol()); + gdfCandidatesFlag_.assign(mipsolver->numCol(), false); + + gdfLbReachable0_.clear(); + gdfLbReachable1_.clear(); + gdfUbReachable0_.clear(); + gdfUbReachable1_.clear(); + gdfLbReachable_.clear(); + gdfUbReachable_.clear(); + const auto model = mipsolver->model_; for (HighsInt iCol = 0; iCol < model->a_matrix_.num_col_; iCol ++) { for (HighsInt k = model->a_matrix_.start_[iCol]; k < model->a_matrix_.start_[iCol + 1]; k ++) { @@ -740,11 +758,10 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // #endif assert(candidatesVec_.empty()); - vector domainchangeProbing; + vector domainchangeDFProbing; // tool lambda functions auto addToCandidate = [&](HighsInt k) { - // std::cout << "k = " << k << std::endl; if (candidatesFlag_[k]) return; else { @@ -799,8 +816,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { thisbchg->column = iCol; thisbchg->boundtype = HighsBoundType::kUpper; thisbchg->boundval = domain->col_lower_[iCol]; - domainchangeProbing.push_back(thisbchg); - // std::cout << "fixing to lower " << iCol << std::endl; + domainchangeDFProbing.push_back(thisbchg); }; auto addFixUpper = [&](int iCol) { @@ -808,8 +824,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { thisbchg->column = iCol; thisbchg->boundtype = HighsBoundType::kLower; thisbchg->boundval = domain->col_upper_[iCol]; - domainchangeProbing.push_back(thisbchg); - // std::cout << "fixing to upper " << iCol << std::endl; + domainchangeDFProbing.push_back(thisbchg); }; auto collectFixLower = [&](int iCol) { @@ -993,23 +1008,217 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // change bound size_t j = 0; - for (; j != domainchangeProbing.size() && !domain->infeasible_; ++ j) { - domain->changeBound(*domainchangeProbing[j], Reason::unspecified()); - delete domainchangeProbing[j]; + for (; j != domainchangeDFProbing.size() && !domain->infeasible_; ++ j) { + domain->changeBound(*domainchangeDFProbing[j], Reason::unspecified()); + delete domainchangeDFProbing[j]; } // std::cout << "#Bchg = " << j << std::endl; - for (j ++; j < domainchangeProbing.size(); ++ j) { + for (j ++; j < domainchangeDFProbing.size(); ++ j) { assert(domain->infeasible_); - delete domainchangeProbing[j]; + delete domainchangeDFProbing[j]; } // record the current number of redundant constraints. previousSize_ = redundantPropagateVec_.size(); } +void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_variable, bool val) { + // tool lambda functions + auto addToCandidate = [&](HighsInt k) { + if (gdfCandidatesFlag_[k]) + return; + else { + gdfCandidatesVec_.push_back(k); + gdfCandidatesFlag_[k] = true; + } + }; + + for (const auto x : redundantPropagateFlag_) { + const HighsInt iRow = x / 2; + const bool isRhs = x % 2; + HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; + HighsInt rend = mipsolver->mipdata_->ARstart_[iRow + 1]; + + for (auto k = rstart; k < rend; k++) { + const HighsInt iCol = mipsolver->mipdata_->ARindex_[k]; + const double iValue = mipsolver->mipdata_->ARvalue_[k]; + const double cost = mipsolver->model_->col_cost_[iCol]; + bool considered = false; + if (domain->isFixed(iCol) || mipsolver->mipdata_->implications.colsubstituted[iCol]) + continue; + + if (iValue > 0) { + if (isRhs) { // consider upper bound reachable + const double globalUb = mipsolver->model_->col_upper_[iCol]; + const double probingUb = domain->col_upper_[iCol]; + if (!ableToFixToUb(iCol) || domain->getMaxActivity(iRow) == kHighsInf) + continue; + const bool upper_bound_reachable = + domain->getMaxActivity(iRow) + iValue * (globalUb - probingUb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); + if (upper_bound_reachable) { + considered = true; + if (iCol == probing_variable && val == 0) + gdfUbReachable_[iCol].insert(iRow); + else { + if (val == 0) + gdfUbReachable0_[iCol].insert(iRow); + if (val == 1) + gdfUbReachable1_[iCol].insert(iRow); + } + } + } + else { // consider lower bound reachable + const double globalLb = mipsolver->model_->col_lower_[iCol]; + const double probingLb = domain->col_lower_[iCol]; + if (!ableToFixToLb(iCol) || domain->getMinActivity(iRow) == -kHighsInf) + continue; + const bool lower_bound_reachable = + domain->getMinActivity(iRow) + iValue * (globalLb - probingLb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); + if (lower_bound_reachable) { + considered = true; + if (iCol == probing_variable && val == 1) + gdfLbReachable_[iCol].insert(iRow); + else { + if (val == 0) + gdfLbReachable0_[iCol].insert(iRow); + if (val == 1) + gdfLbReachable1_[iCol].insert(iRow); + } + } + } + } + + else { + if (isRhs) { // consider lower bound reachable + const double globalLb = mipsolver->model_->col_lower_[iCol]; + const double probingLb = domain->col_lower_[iCol]; + if (!ableToFixToLb(iCol) || domain->getMaxActivity(iRow) == kHighsInf) + continue; + const bool lower_bound_reachable = + domain->getMaxActivity(iRow) + iValue * (globalLb - probingLb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); + if (lower_bound_reachable) { + considered = true; + if (iCol == probing_variable && val == 1) + gdfLbReachable_[iCol].insert(iRow); + else { + if (val == 0) + gdfLbReachable0_[iCol].insert(iRow); + if (val == 1) + gdfLbReachable1_[iCol].insert(iRow); + } + } + } + else { // consider upper bound reachable + const double globalUb = mipsolver->model_->col_upper_[iCol]; + const double probingUb = domain->col_upper_[iCol]; + if (!ableToFixToUb(iCol) || domain->getMinActivity(iRow) == -kHighsInf) + continue; + const bool upper_bound_reachable = + domain->getMinActivity(iRow) + iValue * (globalUb - probingUb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); + if (upper_bound_reachable) { + considered = true; + if (iCol == probing_variable && val == 0) + gdfUbReachable_[iCol].insert(iRow); + else { + if (val == 0) + gdfUbReachable0_[iCol].insert(iRow); + if (val == 1) + gdfUbReachable1_[iCol].insert(iRow); + } + } + } + } + if (considered) + addToCandidate(iCol); + } + } +} + +HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { + // extract reachable information + auto getIntersection = [&](const std::unordered_set& vec0, + const std::unordered_set& vec1, + std::unordered_set& vReachable) { + if (vec0.empty() || vec1.empty()) + return; + + // always loop in the smaller vector, and search in the larger vector + if (vec0.size() < vec1.size()) { + for (auto it1 = vec0.begin(); it1 != vec0.end(); it1 ++) { + auto it2 = vec1.find(*it1); + if (it2 != vec1.end()) + vReachable.insert(*it1); + } + } + else { + for (auto it1 = vec1.begin(); it1 != vec1.end(); it1 ++) { + auto it2 = vec0.find(*it1); + if (it2 != vec0.end()) + vReachable.insert(*it1); + } + } + }; + + std::vector gdfFixingStack_; + + for (const auto iCol : gdfCandidatesVec_) { + // lower bound reachable + getIntersection(gdfLbReachable0_[iCol], gdfLbReachable1_[iCol], gdfLbReachable_[iCol]); + // upper bound reachable + getIntersection(gdfUbReachable0_[iCol], gdfUbReachable1_[iCol], gdfUbReachable_[iCol]); + // extract fixings + if ((HighsInt)gdfLbReachable_[iCol].size() == colLowerLockOriginal_[iCol]) { + HighsDomainChange* thisbchg = new HighsDomainChange; + thisbchg->column = iCol; + thisbchg->boundtype = HighsBoundType::kUpper; + thisbchg->boundval = mipsolver->model_->col_lower_[iCol]; + gdfFixingStack_.push_back(thisbchg); + } + // a variable cannot be fixed to lb and ub simultaneously + else if ((HighsInt)gdfUbReachable_[iCol].size() == colUpperLockOriginal_[iCol]) { + HighsDomainChange* thisbchg = new HighsDomainChange; + thisbchg->column = iCol; + thisbchg->boundtype = HighsBoundType::kLower; + thisbchg->boundval = mipsolver->model_->col_upper_[iCol]; + gdfFixingStack_.push_back(thisbchg); + } + } + + // apply bound change + size_t j = 0; + for (; j != gdfFixingStack_.size() && !domain->infeasible_; ++ j) { + domain->changeBound(*gdfFixingStack_[j], Reason::unspecified()); + delete gdfFixingStack_[j]; + } + + for (j ++; j < gdfFixingStack_.size(); ++ j) { + assert(domain->infeasible_); + delete gdfFixingStack_[j]; + } + + gdfFixingStack_.clear(); + std::cout << "GDF find " << j << " fixings.\n"; + + return (HighsInt)j; +} + +void HighsDomain::DualfixingProbingPropagation::clearGDFInfo() { + for (const auto x : gdfCandidatesVec_) + gdfCandidatesFlag_[x] = false; + gdfCandidatesVec_.clear(); + + gdfLbReachable0_.clear(); + gdfUbReachable0_.clear(); + gdfLbReachable1_.clear(); + gdfUbReachable1_.clear(); +} + +HighsInt HighsDomain::DualfixingProbingPropagation::finalRoundGDF() { + ; +} namespace highs { template <> @@ -2937,7 +3146,7 @@ bool HighsDomain::propagate() { } } - if (!infeasible_ && dfprobingPropagation.isActive()) { + if (!infeasible_ && dfprobingPropagation.isActive() && mipsolver->options_mip_->presolve_dfprobing) { // std::cout << "Activated by nRedundantIndices = " << dfprobingPropagation.redundantPropagateVec_.size() << std::endl; dfprobingPropagation.propagate(); if (!havePropagationRows() && !dfprobingPropagation.isZeroObjFixingEnabled()) { diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 7ad32643fbd..acc66372a38 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -264,6 +264,15 @@ class HighsDomain { std::vector candidatesFlag_; std::unordered_set lockNeedClear_; + std::vector gdfCandidatesVec_; + std::vector gdfCandidatesFlag_; + std::unordered_map> gdfLbReachable0_; + std::unordered_map> gdfLbReachable1_; + std::unordered_map> gdfUbReachable0_; + std::unordered_map> gdfUbReachable1_; + std::unordered_map> gdfLbReachable_; + std::unordered_map> gdfUbReachable_; + void enablePropagator() { enabled_ = true; } @@ -314,7 +323,7 @@ class HighsDomain { void clearRedundantInfo() { previousSize_ = 0; if (!redundantPropagateVec_.empty()) { // clear buffers - for (auto x : redundantPropagateVec_) + for (const auto x : redundantPropagateVec_) redundantPropagateFlag_[x] = false; redundantPropagateVec_.clear(); @@ -341,12 +350,12 @@ class HighsDomain { void recomputeLocks(); void updateRhsRedundant(HighsInt row); void updateLhsRedundant(HighsInt row); - void propagate(); - - - + void updateGDFInfo(HighsInt probing_variable, bool val); + HighsInt processGDFFixing(); + HighsInt finalRoundGDF(); + void clearGDFInfo(); }; private: diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 1692327b813..a0d25175f79 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -27,9 +27,12 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const auto& domchgreason = globaldomain.getDomainChangeReason(); size_t changedend = globaldomain.getChangedCols().size(); - globaldomain.getDfProbingPropagation().clearRedundantInfo(); - if (globaldomain.inProbing_) + const bool useDFProbing = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_dfprobing; + const bool useGDF = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_gdf; + if (useDFProbing || useGDF) { + globaldomain.getDfProbingPropagation().clearRedundantInfo(); globaldomain.getDfProbingPropagation().enablePropagator(); + } HighsInt stackimplicstart = domchgstack.size() + 1; HighsInt numImplications = -stackimplicstart; @@ -68,7 +71,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { if (isInfeasible(col, val)) return true; globaldomain.propagate(); - if (globaldomain.inProbing_) + if (useDFProbing || useGDF) globaldomain.getDfProbingPropagation().disablePropagator(); if (isInfeasible(col, val)) return true; @@ -88,10 +91,10 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { HighsInt numEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); HighsInt maxEntries = 100000 + mipsolver.numNonzero(); - const HighsInt tentativeStart = globaldomain.inProbing_ ? globaldomain.getDfProbingPropagation().getZeroCostFixingPosition() : kHighsIInf32; - if (globaldomain.inProbing_) { + const HighsInt tentativeStart = useDFProbing ? globaldomain.getDfProbingPropagation().getZeroCostFixingPosition() : kHighsIInf32; + if (useDFProbing) implics_tentative.assign(domchgstack.begin() + stackimplicstart, domchgstack.begin() + stackimplicend); - } + for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { if (domchgreason[i].type == HighsDomain::Reason::kCliqueTable && ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) @@ -106,6 +109,10 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // inform caller about lifting opportunities storeLiftingOpportunities(col, val); + // update information to derive generalized dual fixings + if (useGDF) + globaldomain.getDfProbingPropagation().updateGDFInfo(col, val); + // backtrack doBacktrack(changedend); @@ -336,9 +343,16 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { !implicationsCached(col, 0) && mipsolver.mipdata_->cliquetable.getSubstitution(col) == nullptr) { - // setup for dfprobingPropagation - clearTentativeClique(); - globaldomain.getDfProbingPropagation().setZeroCostFixingPosition(kHighsIInf32); + const bool useDFProbing = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_dfprobing; + const bool useGDF = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_gdf; + // setup for dfprobingPropagation + if (useDFProbing) { + clearTentativeClique(); + globaldomain.getDfProbingPropagation().setZeroCostFixingPosition(kHighsIInf32); + } + if (useGDF) + globaldomain.getDfProbingPropagation().clearGDFInfo(); + bool infeasible = computeImplications(col, 1); if (globaldomain.infeasible()) return true; @@ -352,7 +366,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (mipsolver.mipdata_->cliquetable.getSubstitution(col) != nullptr) return true; - if (globaldomain.inProbing_ && !binaryInvolvedInds_.empty()) { + if (useDFProbing && !binaryInvolvedInds_.empty()) { HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; HighsCliqueTable::CliqueVar clique[2]; bool haveReduction; @@ -503,6 +517,13 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (haveTentativeImplics_one) implications[2 * col + 1].implics_tentative.clear(); + if (useGDF) { + // fix variables using generalized dual fixing + HighsInt nfix = globaldomain.getDfProbingPropagation().processGDFFixing(); + if (nfix > 0) + globaldomain.propagate(); + } + return true; } diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index e8833d7d68f..0b16e0d7875 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1735,7 +1735,8 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { } }; - domain.getDfProbingPropagation().recomputeLocks(); + if (options->presolve_dfprobing || options->presolve_gdf) + domain.getDfProbingPropagation().recomputeLocks(); for (const auto& binvar : binaries) { // Count the binaries considered @@ -1843,6 +1844,8 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { } } + if (options->presolve_gdf) + domain.getDfProbingPropagation().finalRoundGDF(); // finalise probing HighsInt numVarsFixed = 0; HighsInt numBndsTightened = 0; From 0482578389eff36acc51ae8f4dd2e13462e6984e Mon Sep 17 00:00:00 2001 From: zwang Date: Mon, 27 Jul 2026 21:50:59 +0800 Subject: [PATCH 15/46] use global bounds to skip fixed variables --- highs/mip/HighsDomain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 1c3ad8cd9ab..12ee1c0bce1 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -1046,7 +1046,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v const double iValue = mipsolver->mipdata_->ARvalue_[k]; const double cost = mipsolver->model_->col_cost_[iCol]; bool considered = false; - if (domain->isFixed(iCol) || mipsolver->mipdata_->implications.colsubstituted[iCol]) + if (mipsolver->model_->col_lower_[iCol] == mipsolver->model_->col_upper_[iCol] || mipsolver->mipdata_->implications.colsubstituted[iCol]) continue; if (iValue > 0) { From 316852e9dae309e143a7992adbf049f64c2a2427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=85=86=E7=BB=B4?= Date: Mon, 27 Jul 2026 23:15:56 +0800 Subject: [PATCH 16/46] fix typo in GDF --- highs/mip/HighsDomain.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 1c3ad8cd9ab..a27e3de74cf 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -1035,7 +1035,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v } }; - for (const auto x : redundantPropagateFlag_) { + for (const auto x : redundantPropagateVec_) { const HighsInt iRow = x / 2; const bool isRhs = x % 2; HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; @@ -1174,7 +1174,7 @@ HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { HighsDomainChange* thisbchg = new HighsDomainChange; thisbchg->column = iCol; thisbchg->boundtype = HighsBoundType::kUpper; - thisbchg->boundval = mipsolver->model_->col_lower_[iCol]; + thisbchg->boundval = domain->col_lower_[iCol]; gdfFixingStack_.push_back(thisbchg); } // a variable cannot be fixed to lb and ub simultaneously @@ -1182,7 +1182,7 @@ HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { HighsDomainChange* thisbchg = new HighsDomainChange; thisbchg->column = iCol; thisbchg->boundtype = HighsBoundType::kLower; - thisbchg->boundval = mipsolver->model_->col_upper_[iCol]; + thisbchg->boundval = domain->col_upper_[iCol]; gdfFixingStack_.push_back(thisbchg); } } From 01d66d6318982c27f36f680b942fba7a2c25f433 Mon Sep 17 00:00:00 2001 From: zwang Date: Tue, 28 Jul 2026 12:29:58 +0800 Subject: [PATCH 17/46] add debug output --- highs/mip/HighsDomain.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 62a374674ab..615a38ebf35 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -1059,6 +1059,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMaxActivity(iRow) + iValue * (globalUb - probingUb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); if (upper_bound_reachable) { considered = true; + printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); if (iCol == probing_variable && val == 0) gdfUbReachable_[iCol].insert(iRow); else { @@ -1078,6 +1079,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMinActivity(iRow) + iValue * (globalLb - probingLb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); if (lower_bound_reachable) { considered = true; + printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); if (iCol == probing_variable && val == 1) gdfLbReachable_[iCol].insert(iRow); else { @@ -1100,6 +1102,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMaxActivity(iRow) + iValue * (globalLb - probingLb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); if (lower_bound_reachable) { considered = true; + printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); if (iCol == probing_variable && val == 1) gdfLbReachable_[iCol].insert(iRow); else { @@ -1119,6 +1122,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMinActivity(iRow) + iValue * (globalUb - probingUb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); if (upper_bound_reachable) { considered = true; + printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); if (iCol == probing_variable && val == 0) gdfUbReachable_[iCol].insert(iRow); else { From bc31352ad339babcfe208787edd817cb467fbe80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=85=86=E7=BB=B4?= Date: Tue, 28 Jul 2026 13:52:36 +0800 Subject: [PATCH 18/46] better output --- highs/mip/HighsDomain.cpp | 23 ++++--- highs/mip/HighsImplications.cpp | 117 +++++++++++++++----------------- 2 files changed, 66 insertions(+), 74 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 615a38ebf35..f03c4d41fdb 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -985,7 +985,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // if (mipsolver->model_->col_cost_[iCol] >= mipsolver->options_mip_->dual_feasibility_tolerance) { if (mipsolver->model_->col_cost_[iCol] >= mipsolver->options_mip_->dual_feasibility_tolerance) { if (canBeFixedToLower) { - checkVariableLowerLock(iCol); + // checkVariableLowerLock(iCol); addFixLower(iCol); continue; } @@ -993,7 +993,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // if (mipsolver->model_->col_cost_[iCol] <= mipsolver->options_mip_->dual_feasibility_tolerance) { if (mipsolver->model_->col_cost_[iCol] <= mipsolver->options_mip_->dual_feasibility_tolerance) { if (canBeFixedToUpper) { - checkVariableUpperLock(iCol); + // checkVariableUpperLock(iCol); addFixUpper(iCol); continue; } @@ -1059,7 +1059,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMaxActivity(iRow) + iValue * (globalUb - probingUb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); if (upper_bound_reachable) { considered = true; - printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); + // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); if (iCol == probing_variable && val == 0) gdfUbReachable_[iCol].insert(iRow); else { @@ -1079,7 +1079,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMinActivity(iRow) + iValue * (globalLb - probingLb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); if (lower_bound_reachable) { considered = true; - printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); + // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); if (iCol == probing_variable && val == 1) gdfLbReachable_[iCol].insert(iRow); else { @@ -1102,7 +1102,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMaxActivity(iRow) + iValue * (globalLb - probingLb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); if (lower_bound_reachable) { considered = true; - printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); + // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); if (iCol == probing_variable && val == 1) gdfLbReachable_[iCol].insert(iRow); else { @@ -1122,7 +1122,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMinActivity(iRow) + iValue * (globalUb - probingUb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); if (upper_bound_reachable) { considered = true; - printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); + // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); if (iCol == probing_variable && val == 0) gdfUbReachable_[iCol].insert(iRow); else { @@ -1174,7 +1174,7 @@ HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { // upper bound reachable getIntersection(gdfUbReachable0_[iCol], gdfUbReachable1_[iCol], gdfUbReachable_[iCol]); // extract fixings - if ((HighsInt)gdfLbReachable_[iCol].size() == colLowerLockOriginal_[iCol]) { + if (ableToFixToLb(iCol) && (HighsInt)gdfLbReachable_[iCol].size() == colLowerLockOriginal_[iCol]) { HighsDomainChange* thisbchg = new HighsDomainChange; thisbchg->column = iCol; thisbchg->boundtype = HighsBoundType::kUpper; @@ -1182,7 +1182,7 @@ HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { gdfFixingStack_.push_back(thisbchg); } // a variable cannot be fixed to lb and ub simultaneously - else if ((HighsInt)gdfUbReachable_[iCol].size() == colUpperLockOriginal_[iCol]) { + else if (ableToFixToUb(iCol) && (HighsInt)gdfUbReachable_[iCol].size() == colUpperLockOriginal_[iCol]) { HighsDomainChange* thisbchg = new HighsDomainChange; thisbchg->column = iCol; thisbchg->boundtype = HighsBoundType::kLower; @@ -1198,13 +1198,14 @@ HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { delete gdfFixingStack_[j]; } - for (j ++; j < gdfFixingStack_.size(); ++ j) { + for (; j < gdfFixingStack_.size(); ++ j) { assert(domain->infeasible_); delete gdfFixingStack_[j]; } gdfFixingStack_.clear(); - std::cout << "GDF find " << j << " fixings.\n"; + if (j > 0) + std::cout << "GDF find " << j << " fixings.\n"; return (HighsInt)j; } @@ -2971,7 +2972,7 @@ bool HighsDomain::propagate() { if (!conflictprop.propagateConflictInds_.empty()) return true; } - if (dfprobingPropagation.isActive()) + if (dfprobingPropagation.isActive() && mipsolver->options_mip_->presolve_dfprobing) return true; return false; diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index a0d25175f79..b0a29506a35 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -369,70 +369,61 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (useDFProbing && !binaryInvolvedInds_.empty()) { HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; HighsCliqueTable::CliqueVar clique[2]; - bool haveReduction; - do - { - haveReduction = false; - // Loop over binary variables that are tighened at least once - for (auto k : binaryInvolvedInds_) { - // Skip non-binary variables (being fixed now) or those can be substituted by other binary variables - if (!globaldomain.isBinary(k) || colsubstituted[k]) - continue; - // Return if the whole problem is infeasible - if (globaldomain.infeasible()) - return true; - // Get the information how x[k] is fixed in probing on x[col] = 0 and x[col] = 1 - // For the meaning of ``data'', please see lines 71-82 in HighsImplications.h - uint8_t data = binaryInvolvedFlags_[k]; - if (data == 0) // flag for no reduction - continue; - - if (data == binaryFixType::kGlobalLower) { // x[k] is fixed at 0 under both x[col] = 0 and x[col] = 1 - // fix x[k] = 0 by adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) - clique[0] = HighsCliqueTable::CliqueVar(col, 0); - clique[1] = HighsCliqueTable::CliqueVar(k, 1); - cliquetable.addClique(mipsolver, &clique[0], 2); - clique[0] = HighsCliqueTable::CliqueVar(col, 1); - clique[1] = HighsCliqueTable::CliqueVar(k, 1); - cliquetable.addClique(mipsolver, &clique[0], 2); - data = 0; - haveReduction = true; - } - else if (data == binaryFixType::kGlobalUpper) { // x[k] is fixed at 1 under both x[col] = 0 and x[col] = 1 - // fix x[k] = 1 by adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) - clique[0] = HighsCliqueTable::CliqueVar(col, 0); - clique[1] = HighsCliqueTable::CliqueVar(k, 0); - cliquetable.addClique(mipsolver, &clique[0], 2); - clique[0] = HighsCliqueTable::CliqueVar(col, 1); - clique[1] = HighsCliqueTable::CliqueVar(k, 0); - cliquetable.addClique(mipsolver, &clique[0], 2); - data = 0; - haveReduction = true; - } - else if (data == binaryFixType::kSubstituteComplement) { // x[k] is fixed at 0 under x[col] = 1, and is fixed at 1 under x[col] = 0; this makes x[col] + x[k] = 1 - // Adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) - clique[0] = HighsCliqueTable::CliqueVar(col, 1); - clique[1] = HighsCliqueTable::CliqueVar(k, 1); - cliquetable.addClique(mipsolver, &clique[0], 2); - clique[0] = HighsCliqueTable::CliqueVar(col, 0); - clique[1] = HighsCliqueTable::CliqueVar(k, 0); - cliquetable.addClique(mipsolver, &clique[0], 2); - data = 0; - haveReduction = true; - } - else if (data == binaryFixType::kSubstituteEqual) { // x[k] is fixed at 0 under x[col] = 0, and is fixed at 1 under x[col] = 1; this makes x[col] = x[k] - // Adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) - clique[0] = HighsCliqueTable::CliqueVar(col, 1); - clique[1] = HighsCliqueTable::CliqueVar(k, 0); - cliquetable.addClique(mipsolver, &clique[0], 2); - clique[0] = HighsCliqueTable::CliqueVar(col, 0); - clique[1] = HighsCliqueTable::CliqueVar(k, 1); - cliquetable.addClique(mipsolver, &clique[0], 2); - data = 0; - haveReduction = true; - } + // Loop over binary variables that are tighened at least once + for (auto k : binaryInvolvedInds_) { + // Skip non-binary variables (being fixed now) or those can be substituted by other binary variables + if (!globaldomain.isBinary(k) || colsubstituted[k]) + continue; + // Return if the whole problem is infeasible + if (globaldomain.infeasible()) + return true; + // Get the information how x[k] is fixed in probing on x[col] = 0 and x[col] = 1 + // For the meaning of ``data'', please see lines 71-82 in HighsImplications.h + uint8_t data = binaryInvolvedFlags_[k]; + if (data == 0) // flag for no reduction + continue; + + if (data == binaryFixType::kGlobalLower) { // x[k] is fixed at 0 under both x[col] = 0 and x[col] = 1 + // fix x[k] = 0 by adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + clique[0] = HighsCliqueTable::CliqueVar(col, 0); + clique[1] = HighsCliqueTable::CliqueVar(k, 1); + cliquetable.addClique(mipsolver, &clique[0], 2); + clique[0] = HighsCliqueTable::CliqueVar(col, 1); + clique[1] = HighsCliqueTable::CliqueVar(k, 1); + cliquetable.addClique(mipsolver, &clique[0], 2); + data = 0; + } + else if (data == binaryFixType::kGlobalUpper) { // x[k] is fixed at 1 under both x[col] = 0 and x[col] = 1 + // fix x[k] = 1 by adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + clique[0] = HighsCliqueTable::CliqueVar(col, 0); + clique[1] = HighsCliqueTable::CliqueVar(k, 0); + cliquetable.addClique(mipsolver, &clique[0], 2); + clique[0] = HighsCliqueTable::CliqueVar(col, 1); + clique[1] = HighsCliqueTable::CliqueVar(k, 0); + cliquetable.addClique(mipsolver, &clique[0], 2); + data = 0; + } + else if (data == binaryFixType::kSubstituteComplement) { // x[k] is fixed at 0 under x[col] = 1, and is fixed at 1 under x[col] = 0; this makes x[col] + x[k] = 1 + // Adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + clique[0] = HighsCliqueTable::CliqueVar(col, 1); + clique[1] = HighsCliqueTable::CliqueVar(k, 1); + cliquetable.addClique(mipsolver, &clique[0], 2); + clique[0] = HighsCliqueTable::CliqueVar(col, 0); + clique[1] = HighsCliqueTable::CliqueVar(k, 0); + cliquetable.addClique(mipsolver, &clique[0], 2); + data = 0; } - } while (haveReduction); + else if (data == binaryFixType::kSubstituteEqual) { // x[k] is fixed at 0 under x[col] = 0, and is fixed at 1 under x[col] = 1; this makes x[col] = x[k] + // Adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + clique[0] = HighsCliqueTable::CliqueVar(col, 1); + clique[1] = HighsCliqueTable::CliqueVar(k, 0); + cliquetable.addClique(mipsolver, &clique[0], 2); + clique[0] = HighsCliqueTable::CliqueVar(col, 0); + clique[1] = HighsCliqueTable::CliqueVar(k, 1); + cliquetable.addClique(mipsolver, &clique[0], 2); + data = 0; + } + } // clear the tentative bound changes for binary variables obtained from probing on x[col] clearTentativeClique(); From 4cede010e57aa184fe6071ff5534aba2eddfc47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=85=86=E7=BB=B4?= Date: Thu, 30 Jul 2026 22:56:56 +0800 Subject: [PATCH 19/46] abort propagation when infeasibility is detected --- highs/mip/HighsDomain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index f03c4d41fdb..20e2bafac44 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -2972,7 +2972,7 @@ bool HighsDomain::propagate() { if (!conflictprop.propagateConflictInds_.empty()) return true; } - if (dfprobingPropagation.isActive() && mipsolver->options_mip_->presolve_dfprobing) + if (!infeasible_ && dfprobingPropagation.isActive() && mipsolver->options_mip_->presolve_dfprobing) return true; return false; From 89597e4d4f3024bacd4f9ec3eb0dac0e78bdc37c Mon Sep 17 00:00:00 2001 From: Zhaowei-Wang Date: Wed, 5 Aug 2026 21:45:45 +0800 Subject: [PATCH 20/46] change data structure of GDF to vector --- highs/mip/HighsDomain.cpp | 113 +++++++++++++++-------------------- highs/mip/HighsDomain.h | 23 ++++--- highs/presolve/HPresolve.cpp | 2 - 3 files changed, 63 insertions(+), 75 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 20e2bafac44..747e839d422 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -655,9 +655,7 @@ HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation(const Du gdfLbReachable0_(other.gdfLbReachable0_), gdfLbReachable1_(other.gdfLbReachable1_), gdfUbReachable0_(other.gdfUbReachable0_), - gdfUbReachable1_(other.gdfUbReachable1_), - gdfLbReachable_(other.gdfLbReachable_), - gdfUbReachable_(other.gdfUbReachable_) {;} + gdfUbReachable1_(other.gdfUbReachable1_) {;} void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { mipsolver = domain->mipsolver; @@ -684,12 +682,10 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { gdfCandidatesVec_.reserve(mipsolver->numCol()); gdfCandidatesFlag_.assign(mipsolver->numCol(), false); - gdfLbReachable0_.clear(); - gdfLbReachable1_.clear(); - gdfUbReachable0_.clear(); - gdfUbReachable1_.clear(); - gdfLbReachable_.clear(); - gdfUbReachable_.clear(); + gdfLbReachable0_.assign(mipsolver->numCol(), 0); + gdfLbReachable1_.assign(mipsolver->numCol(), 0); + gdfUbReachable0_.assign(mipsolver->numCol(), 0); + gdfUbReachable1_.assign(mipsolver->numCol(), 0); const auto model = mipsolver->model_; for (HighsInt iCol = 0; iCol < model->a_matrix_.num_col_; iCol ++) { @@ -1060,13 +1056,15 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v if (upper_bound_reachable) { considered = true; // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); - if (iCol == probing_variable && val == 0) - gdfUbReachable_[iCol].insert(iRow); + if (iCol == probing_variable && val == 0) { + gdfUbReachable0_[iCol]++; + gdfUbReachable1_[iCol]++; + } else { if (val == 0) - gdfUbReachable0_[iCol].insert(iRow); + gdfUbReachable0_[iCol]++; if (val == 1) - gdfUbReachable1_[iCol].insert(iRow); + gdfUbReachable1_[iCol]++; } } } @@ -1080,13 +1078,15 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v if (lower_bound_reachable) { considered = true; // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); - if (iCol == probing_variable && val == 1) - gdfLbReachable_[iCol].insert(iRow); + if (iCol == probing_variable && val == 1) { + gdfLbReachable0_[iCol]++; + gdfLbReachable1_[iCol]++; + } else { if (val == 0) - gdfLbReachable0_[iCol].insert(iRow); + gdfLbReachable0_[iCol]++; if (val == 1) - gdfLbReachable1_[iCol].insert(iRow); + gdfLbReachable1_[iCol]++; } } } @@ -1103,13 +1103,15 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v if (lower_bound_reachable) { considered = true; // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); - if (iCol == probing_variable && val == 1) - gdfLbReachable_[iCol].insert(iRow); + if (iCol == probing_variable && val == 1) { + gdfLbReachable0_[iCol]++; + gdfLbReachable1_[iCol]++; + } else { if (val == 0) - gdfLbReachable0_[iCol].insert(iRow); + gdfLbReachable0_[iCol]++; if (val == 1) - gdfLbReachable1_[iCol].insert(iRow); + gdfLbReachable1_[iCol]++; } } } @@ -1123,13 +1125,15 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v if (upper_bound_reachable) { considered = true; // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); - if (iCol == probing_variable && val == 0) - gdfUbReachable_[iCol].insert(iRow); + if (iCol == probing_variable && val == 0) { + gdfUbReachable0_[iCol]++; + gdfUbReachable1_[iCol]++; + } else { if (val == 0) - gdfUbReachable0_[iCol].insert(iRow); + gdfUbReachable0_[iCol]++; if (val == 1) - gdfUbReachable1_[iCol].insert(iRow); + gdfUbReachable1_[iCol]++; } } } @@ -1142,39 +1146,14 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v } HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { - // extract reachable information - auto getIntersection = [&](const std::unordered_set& vec0, - const std::unordered_set& vec1, - std::unordered_set& vReachable) { - if (vec0.empty() || vec1.empty()) - return; - - // always loop in the smaller vector, and search in the larger vector - if (vec0.size() < vec1.size()) { - for (auto it1 = vec0.begin(); it1 != vec0.end(); it1 ++) { - auto it2 = vec1.find(*it1); - if (it2 != vec1.end()) - vReachable.insert(*it1); - } - } - else { - for (auto it1 = vec1.begin(); it1 != vec1.end(); it1 ++) { - auto it2 = vec0.find(*it1); - if (it2 != vec0.end()) - vReachable.insert(*it1); - } - } - }; - std::vector gdfFixingStack_; for (const auto iCol : gdfCandidatesVec_) { - // lower bound reachable - getIntersection(gdfLbReachable0_[iCol], gdfLbReachable1_[iCol], gdfLbReachable_[iCol]); - // upper bound reachable - getIntersection(gdfUbReachable0_[iCol], gdfUbReachable1_[iCol], gdfUbReachable_[iCol]); - // extract fixings - if (ableToFixToLb(iCol) && (HighsInt)gdfLbReachable_[iCol].size() == colLowerLockOriginal_[iCol]) { + const HighsInt lowerLock = colLowerLockOriginal_[iCol]; + const HighsInt upperLock = colUpperLockOriginal_[iCol]; + if (ableToFixToLb(iCol) && lowerLock > 0 && + gdfLbReachable0_[iCol] == lowerLock && + gdfLbReachable1_[iCol] == lowerLock) { HighsDomainChange* thisbchg = new HighsDomainChange; thisbchg->column = iCol; thisbchg->boundtype = HighsBoundType::kUpper; @@ -1182,7 +1161,9 @@ HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { gdfFixingStack_.push_back(thisbchg); } // a variable cannot be fixed to lb and ub simultaneously - else if (ableToFixToUb(iCol) && (HighsInt)gdfUbReachable_[iCol].size() == colUpperLockOriginal_[iCol]) { + else if (ableToFixToUb(iCol) && upperLock > 0 && + gdfUbReachable0_[iCol] == upperLock && + gdfUbReachable1_[iCol] == upperLock) { HighsDomainChange* thisbchg = new HighsDomainChange; thisbchg->column = iCol; thisbchg->boundtype = HighsBoundType::kLower; @@ -1211,18 +1192,18 @@ HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { } void HighsDomain::DualfixingProbingPropagation::clearGDFInfo() { - for (const auto x : gdfCandidatesVec_) + // Reset the per-column count vectors for every column that received at + // least one increment this round. The touched set is exactly + // gdfCandidatesVec_ (every counted column is also a candidate), so we + // reset both with one fused loop. + for (const auto x : gdfCandidatesVec_) { + gdfLbReachable0_[x] = 0; + gdfLbReachable1_[x] = 0; + gdfUbReachable0_[x] = 0; + gdfUbReachable1_[x] = 0; gdfCandidatesFlag_[x] = false; + } gdfCandidatesVec_.clear(); - - gdfLbReachable0_.clear(); - gdfUbReachable0_.clear(); - gdfLbReachable1_.clear(); - gdfUbReachable1_.clear(); -} - -HighsInt HighsDomain::DualfixingProbingPropagation::finalRoundGDF() { - ; } namespace highs { diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index acc66372a38..d59ce0fe980 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -266,12 +266,22 @@ class HighsDomain { std::vector gdfCandidatesVec_; std::vector gdfCandidatesFlag_; - std::unordered_map> gdfLbReachable0_; - std::unordered_map> gdfLbReachable1_; - std::unordered_map> gdfUbReachable0_; - std::unordered_map> gdfUbReachable1_; - std::unordered_map> gdfLbReachable_; - std::unordered_map> gdfUbReachable_; + // GDF reachable-row counts, indexed directly by column id. For each + // column touched during GDF, we only need to know how many redundant + // rows make the column's lower/upper bound reachable under probing + // x_probing=0 / x_probing=1. The actual row indices are not needed: + // (a) within a single (map, column) the row ids are unique (each + // redundant row visits each column at most once), so the set of rows + // is fully described by its size; (b) the original intersection check + // |set0 ∩ set1| == |locking rows| is equivalent to + // |set0| == |locking rows| AND |set1| == |locking rows| because both + // sets are subsets of the locking rows. processGDFFixing therefore + // does no intersection at all. Indexed by column id (dense) so a + // flat vector beats an unordered_map here. + std::vector gdfLbReachable0_; + std::vector gdfLbReachable1_; + std::vector gdfUbReachable0_; + std::vector gdfUbReachable1_; void enablePropagator() { enabled_ = true; @@ -354,7 +364,6 @@ class HighsDomain { void updateGDFInfo(HighsInt probing_variable, bool val); HighsInt processGDFFixing(); - HighsInt finalRoundGDF(); void clearGDFInfo(); }; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 0b16e0d7875..1056c02e816 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1844,8 +1844,6 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { } } - if (options->presolve_gdf) - domain.getDfProbingPropagation().finalRoundGDF(); // finalise probing HighsInt numVarsFixed = 0; HighsInt numBndsTightened = 0; From 3e34d3b3a83ee010cdddf68d9a518c2854294000 Mon Sep 17 00:00:00 2001 From: Zhaowei-Wang Date: Mon, 10 Aug 2026 20:01:58 +0800 Subject: [PATCH 21/46] Substitute char with HighsBool --- highs/mip/HighsDomain.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 04d59c7bd34..3b2cfd874d2 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -240,7 +240,7 @@ class HighsDomain { HighsDomain* domain; HighsMipSolver* mipsolver; // row lower and upper, length = 2 * rownum - std::vector redundantPropagateFlag_; + std::vector redundantPropagateFlag_; std::vector redundantPropagateVec_; enum DFPROBING_FIX_DIRECTION { @@ -261,11 +261,11 @@ class HighsDomain { std::vector colLowerLockReduced_; std::vector colUpperLockReduced_; std::vector candidatesVec_; - std::vector candidatesFlag_; + std::vector candidatesFlag_; std::unordered_set lockNeedClear_; std::vector gdfCandidatesVec_; - std::vector gdfCandidatesFlag_; + std::vector gdfCandidatesFlag_; // GDF reachable-row counts, indexed directly by column id. For each // column touched during GDF, we only need to know how many redundant // rows make the column's lower/upper bound reachable under probing From 6e4f70c5c7a05526d80554e9afe14d7760009c9e Mon Sep 17 00:00:00 2001 From: Zhaowei-Wang Date: Mon, 10 Aug 2026 23:11:24 +0800 Subject: [PATCH 22/46] Update comments --- highs/mip/HighsDomain.cpp | 62 ++++++++++++--------------------- highs/mip/HighsDomain.h | 33 ++++++++++-------- highs/mip/HighsImplications.cpp | 12 ++++--- highs/mip/HighsImplications.h | 15 ++++---- highs/presolve/HPresolve.cpp | 1 + 5 files changed, 56 insertions(+), 67 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index df6bb91cd1f..eb8b4acb88e 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -686,7 +686,8 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { gdfLbReachable1_.assign(mipsolver->numCol(), 0); gdfUbReachable0_.assign(mipsolver->numCol(), 0); gdfUbReachable1_.assign(mipsolver->numCol(), 0); - + + // compute the original locks for each variable const auto model = mipsolver->model_; for (HighsInt iCol = 0; iCol < model->a_matrix_.num_col_; iCol ++) { for (HighsInt k = model->a_matrix_.start_[iCol]; k < model->a_matrix_.start_[iCol + 1]; k ++) { @@ -729,30 +730,18 @@ void HighsDomain::DualfixingProbingPropagation::updateLhsRedundant(HighsInt row) void HighsDomain::DualfixingProbingPropagation::propagate() { - // The boolean variable ``startZeroCostFixing_'' is used to flag if we allow variables with zero cost objective coefficients can be fixed in domain propagation. + // The boolean variable ``startZeroCostFixing_'' is used to flag if we allow variables with zero cost can be fixed in domain propagation. // The process of domain propagtion in probing is executed in two phases: - // Phase 1: Apply classic domain propagation, and additionally fix variables with nonzero objective coefficients using dual fixing + // Phase 1: Apply classic domain propagation, and additionally fix variables with non-zero objective coefficients using dual fixing // Phase 2: Apply classic domain propagation, and additionally fix variables (including those with zero objective coefficients) using dual fixing // In Phase 1, ``startZeroCostFixing_'' is set to be ``false'' to exclude variable with zero objective coefficients. // In Phase 2, ``startZeroCostFixing_'' is set to be ``true''. // Note that // (1) For all the bound changes in Phase 1, reductions deduced from them are valid for all optimal solutions; - // (2) For the bound changes in Phase 2, reductions deduced from them can only be used to derive global valid reductions (i.e., variable fixing, global bound tightening, variable substitution). + // (2) For the bound changes in Phase 2, reductions deduced from them can only be used to derive global valid reductions (i.e., variable fixing, global bound tightening, and variable substitution). if (!isEnabled()) return; - // printf("%f, %f\n", domain->getMaxActivity(1001), domain->getMinActivity(1001)); -// #ifndef NDEBUG - for (const HighsInt x : redundantPropagateVec_) { - HighsInt iRow = x / 2; - bool isUpper = x % 2; - if (isUpper && domain->getMaxActivity(iRow) > mipsolver->model_->row_upper_[iRow] + domain->feastol()) - printf("Row %d not rhs redundant, maxAct = %f, rhs = %f.\n", iRow, domain->getMaxActivity(iRow), mipsolver->model_->row_upper_[iRow]); - if (!isUpper && domain->getMinActivity(iRow) < mipsolver->model_->row_lower_[iRow] - domain->feastol()) - printf("Row %d not lhs redundant, minAct = %f, lhs = %f.\n", iRow, domain->getMinActivity(iRow), mipsolver->model_->row_lower_[iRow]); - } -// #endif - assert(candidatesVec_.empty()); vector domainchangeDFProbing; @@ -766,6 +755,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } }; + // debug functions to check locks auto checkVariableLowerLock = [&](HighsInt iCol) { auto model = mipsolver->model_; if (ableToFixToLb(iCol)) { @@ -779,11 +769,6 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { std::cout << "Lower lock: variable " << iCol << " at row = " << iRow << " coef = " << iValue << " not redundant at constraint " << iRow << ", minact = " << domain->getMinActivity(iRow) << ", maxact = " << domain->getMaxActivity(iRow) << " lhs = " << blower << " rhs = " << bupper << std::endl; - // std::cout << "lock rows:\n"; - // for (int kk = model->a_matrix_.start_[iCol]; kk < model->a_matrix_.start_[iCol + 1]; kk ++) { - // std::cout << kk << " "; - // } - // std::cout << std::endl; } } } @@ -823,6 +808,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { domainchangeDFProbing.push_back(thisbchg); }; + // only record - we do not actually fix them now as their objective coefficients are zero auto collectFixLower = [&](int iCol) { zeroCostFixedVariables_.emplace_back(iCol, FIXDIRECTION_LOWER_BOUND); }; @@ -831,12 +817,11 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { zeroCostFixedVariables_.emplace_back(iCol, FIXDIRECTION_UPPER_BOUND); }; - - - // get candidate + // exit if no new redundant constraints are found HighsInt maxLockLeft = redundantPropagateVec_.size() - previousSize_; if (maxLockLeft == 0) return; + for (; previousSize_ < redundantPropagateVec_.size(); ++ previousSize_, -- maxLockLeft) { const HighsInt i = redundantPropagateVec_[previousSize_]; const HighsInt iRow = i / 2; @@ -852,6 +837,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { const double iValue = mipsolver->mipdata_->ARvalue_[k]; const double cost = mipsolver->model_->col_cost_[iCol]; + // do not insert to candidates if the lock is not reduced enough bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; @@ -880,6 +866,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { const double iValue = mipsolver->mipdata_->ARvalue_[k]; const double cost = mipsolver->model_->col_cost_[iCol]; + // do not insert to candidates if the lock is not reduced enough bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; @@ -978,7 +965,6 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } - // if (mipsolver->model_->col_cost_[iCol] >= mipsolver->options_mip_->dual_feasibility_tolerance) { if (mipsolver->model_->col_cost_[iCol] >= mipsolver->options_mip_->dual_feasibility_tolerance) { if (canBeFixedToLower) { // checkVariableLowerLock(iCol); @@ -986,7 +972,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { continue; } } - // if (mipsolver->model_->col_cost_[iCol] <= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (mipsolver->model_->col_cost_[iCol] <= mipsolver->options_mip_->dual_feasibility_tolerance) { if (canBeFixedToUpper) { // checkVariableUpperLock(iCol); @@ -1009,14 +995,13 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { delete domainchangeDFProbing[j]; } - // std::cout << "#Bchg = " << j << std::endl; - + // clear the remaining domain changes if infeasible + assert(domain->infeasible_); for (j ++; j < domainchangeDFProbing.size(); ++ j) { - assert(domain->infeasible_); delete domainchangeDFProbing[j]; } - // record the current number of redundant constraints. + // record the current number of redundant constraints previousSize_ = redundantPropagateVec_.size(); } @@ -1031,6 +1016,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v } }; + // only redundant constraints are useful in GDF for (const auto x : redundantPropagateVec_) { const HighsInt iRow = x / 2; const bool isRhs = x % 2; @@ -1055,7 +1041,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMaxActivity(iRow) + iValue * (globalUb - probingUb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); if (upper_bound_reachable) { considered = true; - // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); + // special treat if the current variable is the probing variable if (iCol == probing_variable && val == 0) { gdfUbReachable0_[iCol]++; gdfUbReachable1_[iCol]++; @@ -1077,7 +1063,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMinActivity(iRow) + iValue * (globalLb - probingLb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); if (lower_bound_reachable) { considered = true; - // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); + // special treat if the current variable is the probing variable if (iCol == probing_variable && val == 1) { gdfLbReachable0_[iCol]++; gdfLbReachable1_[iCol]++; @@ -1102,7 +1088,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMaxActivity(iRow) + iValue * (globalLb - probingLb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); if (lower_bound_reachable) { considered = true; - // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, rhs = %f, demonstrate lb reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_upper_[iRow]); + // special treat if the current variable is the probing variable if (iCol == probing_variable && val == 1) { gdfLbReachable0_[iCol]++; gdfLbReachable1_[iCol]++; @@ -1124,7 +1110,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v domain->getMinActivity(iRow) + iValue * (globalUb - probingUb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); if (upper_bound_reachable) { considered = true; - // printf("Probing on x-%d = %d, for non-zero (%d, %d) = %f, lhs = %f, demonstrate ub reachable.\n", probing_variable, val, iRow, iCol, iValue, mipsolver->model_->row_lower_[iRow]); + // special treat if the current variable is the probing variable if (iCol == probing_variable && val == 0) { gdfUbReachable0_[iCol]++; gdfUbReachable1_[iCol]++; @@ -1148,6 +1134,7 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { std::vector gdfFixingStack_; + // derive global fixings from the GDF information for (const auto iCol : gdfCandidatesVec_) { const HighsInt lowerLock = colLowerLockOriginal_[iCol]; const HighsInt upperLock = colUpperLockOriginal_[iCol]; @@ -1179,23 +1166,18 @@ HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { delete gdfFixingStack_[j]; } + // clear the remaining domain changes if infeasible for (; j < gdfFixingStack_.size(); ++ j) { assert(domain->infeasible_); delete gdfFixingStack_[j]; } gdfFixingStack_.clear(); - if (j > 0) - std::cout << "GDF find " << j << " fixings.\n"; return (HighsInt)j; } void HighsDomain::DualfixingProbingPropagation::clearGDFInfo() { - // Reset the per-column count vectors for every column that received at - // least one increment this round. The touched set is exactly - // gdfCandidatesVec_ (every counted column is also a candidate), so we - // reset both with one fused loop. for (const auto x : gdfCandidatesVec_) { gdfLbReachable0_[x] = 0; gdfLbReachable1_[x] = 0; diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 3b2cfd874d2..c7ad93767b5 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -239,10 +239,12 @@ class HighsDomain { struct DualfixingProbingPropagation { HighsDomain* domain; HighsMipSolver* mipsolver; + // row lower and upper, length = 2 * rownum std::vector redundantPropagateFlag_; std::vector redundantPropagateVec_; - + + // For zero-cost variables, we need to know which direction we can fix them to. enum DFPROBING_FIX_DIRECTION { FIXDIRECTION_NOT_DECIDED = 0, FIXDIRECTION_LOWER_BOUND, @@ -250,34 +252,32 @@ class HighsDomain { }; std::vector zeroCostVarsDirection_; std::vector> zeroCostFixedVariables_; + + // Flag and position in the domchgstack of the first zero-cost variable that can be fixed to its lower or upper bound. bool startZeroCostFixing_; size_t zeroCostStartPos_; bool enabled_ = false; size_t previousSize_; + // Original lower and upper locks, and the reduced locks after propagation. std::vector colLowerLockOriginal_; std::vector colUpperLockOriginal_; std::vector colLowerLockReduced_; std::vector colUpperLockReduced_; + + // temporary buffers for DFProbing std::vector candidatesVec_; std::vector candidatesFlag_; std::unordered_set lockNeedClear_; + // temporary buffers for GDF std::vector gdfCandidatesVec_; std::vector gdfCandidatesFlag_; - // GDF reachable-row counts, indexed directly by column id. For each - // column touched during GDF, we only need to know how many redundant - // rows make the column's lower/upper bound reachable under probing - // x_probing=0 / x_probing=1. The actual row indices are not needed: - // (a) within a single (map, column) the row ids are unique (each - // redundant row visits each column at most once), so the set of rows - // is fully described by its size; (b) the original intersection check - // |set0 ∩ set1| == |locking rows| is equivalent to - // |set0| == |locking rows| AND |set1| == |locking rows| because both - // sets are subsets of the locking rows. processGDFFixing therefore - // does no intersection at all. Indexed by column id (dense) so a - // flat vector beats an unordered_map here. + + // GDF reachable-row counts, indexed by column id. For each + // variable touched during probing, we only need to know how many + // rows make this variable lower/upper bound reachable. std::vector gdfLbReachable0_; std::vector gdfLbReachable1_; std::vector gdfUbReachable0_; @@ -295,10 +295,12 @@ class HighsDomain { return enabled_; } + // active only when new redundant rows are found. bool isActive() { return enabled_ && redundantPropagateVec_.size() > previousSize_; } + // mark the position when the first zero-cost variable can be fixed to its lower or upper bound. void setZeroCostFixingPosition(HighsInt v) { zeroCostStartPos_ = v; } @@ -329,7 +331,7 @@ class HighsDomain { && mipsolver->model_->col_upper_[col] < kHighsInf; } - + // remove redundant information void clearRedundantInfo() { previousSize_ = 0; if (!redundantPropagateVec_.empty()) { // clear buffers @@ -361,7 +363,8 @@ class HighsDomain { void updateRhsRedundant(HighsInt row); void updateLhsRedundant(HighsInt row); void propagate(); - + + // functionalities for GDF void updateGDFInfo(HighsInt probing_variable, bool val); HighsInt processGDFFixing(); void clearGDFInfo(); diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index cf5ed640a01..55b2f358f42 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -27,8 +27,10 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const auto& domchgreason = globaldomain.getDomainChangeReason(); size_t changedend = globaldomain.getChangedCols().size(); + // get two flags const bool useDFProbing = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_dfprobing; const bool useGDF = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_gdf; + // record redundant rows if any of the two flags is true if (useDFProbing || useGDF) { globaldomain.getDfProbingPropagation().clearRedundantInfo(); globaldomain.getDfProbingPropagation().enablePropagator(); @@ -100,7 +102,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) continue; - if (i >= tentativeStart) // cache tentative implications + if (i >= tentativeStart) // record tentative implications continue; implics.push_back(domchgstack[i]); @@ -122,7 +124,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { [&](const HighsDomainChange& a) { return !globaldomain.isBinary(a.column); }); - // Store the tentative bound changes (fixing) of binary variables separately + // store the tentative bound changes of binary variables separately for (auto i = binstart_tmp; i != implics_tentative.end(); ++ i) recordTentativeCliques(val, *i); implics_tentative.erase(binstart_tmp, implics_tentative.end()); @@ -374,11 +376,11 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { // Skip non-binary variables (being fixed now) or those can be substituted by other binary variables if (!globaldomain.isBinary(k) || colsubstituted[k]) continue; - // Return if the whole problem is infeasible + // Return if infeasible if (globaldomain.infeasible()) return true; // Get the information how x[k] is fixed in probing on x[col] = 0 and x[col] = 1 - // For the meaning of ``data'', please see lines 71-82 in HighsImplications.h + // For the meaning of ``data'', please see lines 71-89 in HighsImplications.h uint8_t data = binaryInvolvedFlags_[k]; if (data == 0) // flag for no reduction continue; @@ -503,6 +505,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { } } + // clear tentative implications if (haveTentativeImplics_zero) implications[2 * col].implics_tentative.clear(); if (haveTentativeImplics_one) @@ -511,6 +514,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (useGDF) { // fix variables using generalized dual fixing HighsInt nfix = globaldomain.getDfProbingPropagation().processGDFFixing(); + // propagate if necessary if (nfix > 0) globaldomain.propagate(); } diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 0f42349e08b..424bc2a259e 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -25,7 +25,7 @@ class HighsImplications { struct Implics { std::vector implics; - /* the "tentative" implications. + /* The "tentative" implications: A implication of type x_j \ge (\ell^1_j - \ell^0_j) x_k + \ell^0_j is called "tentative", if (1) c_j = 0 (2) x_j is fixed by applying dual fixing in probing @@ -66,14 +66,14 @@ class HighsImplications { std::vector substitutions; std::vector colsubstituted; - // if a binary variable x_j is: (1) c_j = 0 (2) x_j is fixed by applying dual fixing in probing + // vector used to derive global reductions from dfprobing std::vector binaryInvolvedInds_; enum binaryFixType { - kNoReduction = 0b0000, - kGlobalLower = 0b1010, - kGlobalUpper = 0b0101, + kNoReduction = 0b0000, + kGlobalLower = 0b1010, + kGlobalUpper = 0b0101, kSubstituteComplement = 0b1001, - kSubstituteEqual = 0b0110, + kSubstituteEqual = 0b0110, }; /* Possible values for binaryInvolvedFlags_ @@ -153,7 +153,6 @@ class HighsImplications { return implications[loc].implics; } - // get the "tentative implications" w.r.t non-binary variables const std::vector& getImplications_tentative(HighsInt col, bool val) { HighsInt loc = 2 * col + val; return implications[loc].implics_tentative; @@ -257,7 +256,7 @@ class HighsImplications { } } } - else { + else { // probing x_k = 1 if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 if (!isFixedTo1(val, iCol)) { if (binaryInvolvedFlags_[iCol] == 0) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ee0b1f9d7e6..93201952049 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1863,6 +1863,7 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { } }; + // setup for dfprobing and gdf if (options->presolve_dfprobing || options->presolve_gdf) domain.getDfProbingPropagation().recomputeLocks(); From e87c31a94d53fd2404a132f04d69d93f600e69e9 Mon Sep 17 00:00:00 2001 From: Zhaowei-Wang Date: Mon, 10 Aug 2026 23:32:13 +0800 Subject: [PATCH 23/46] fix assert --- highs/mip/HighsDomain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index eb8b4acb88e..0b0cf871fed 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -996,8 +996,8 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } // clear the remaining domain changes if infeasible - assert(domain->infeasible_); for (j ++; j < domainchangeDFProbing.size(); ++ j) { + assert(domain->infeasible_); delete domainchangeDFProbing[j]; } From 83d2ba05550ba8a1c7b6e494ab76e7fe2a758b92 Mon Sep 17 00:00:00 2001 From: Zhaowei-Wang Date: Tue, 11 Aug 2026 01:28:53 +0800 Subject: [PATCH 24/46] fix test: lifting-for-probing, and clear information in recomputeLocks() --- highs/mip/HighsDomain.cpp | 22 ++++++++++++++-------- highs/mip/HighsDomain.h | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 0b0cf871fed..a8b0eb0082d 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -677,8 +677,10 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { candidatesVec_.clear(); candidatesVec_.reserve(mipsolver->numCol()); candidatesFlag_.assign(mipsolver->numCol(), false); + lockNeedClear_.clear(); lockNeedClear_.reserve(mipsolver->numCol()); + gdfCandidatesVec_.clear(); gdfCandidatesVec_.reserve(mipsolver->numCol()); gdfCandidatesFlag_.assign(mipsolver->numCol(), false); @@ -2105,8 +2107,9 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, assert(tmpinf == activitymininf_[mip->a_matrix_.index_[i]]); } #endif - - if (recordRedundantRows_ && + // If dfprobingPropagation.isZeroObjFixingEnabled() is true, + // then we cannot record redundant rows for lifting, as this bound change could disregarded. + if (recordRedundantRows_ && !dfprobingPropagation.isZeroObjFixingEnabled() && mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); @@ -2157,8 +2160,9 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, assert(tmpinf == activitymaxinf_[mip->a_matrix_.index_[i]]); } #endif - - if (recordRedundantRows_ && + // If dfprobingPropagation.isZeroObjFixingEnabled() is true, + // then we cannot record redundant rows for lifting, as this bound change could disregarded. + if (recordRedundantRows_ && !dfprobingPropagation.isZeroObjFixingEnabled() && mip->row_lower_[mip->a_matrix_.index_[i]] == -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); @@ -2278,8 +2282,9 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, assert(tmpinf == activitymaxinf_[mip->a_matrix_.index_[i]]); } #endif - - if (recordRedundantRows_ && + // If dfprobingPropagation.isZeroObjFixingEnabled() is true, + // then we cannot record redundant rows for lifting, as this bound change could disregarded. + if (recordRedundantRows_ && !dfprobingPropagation.isZeroObjFixingEnabled() && mip->row_lower_[mip->a_matrix_.index_[i]] == -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); @@ -2333,8 +2338,9 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, assert(tmpinf == activitymininf_[mip->a_matrix_.index_[i]]); } #endif - - if (recordRedundantRows_ && + // If dfprobingPropagation.isZeroObjFixingEnabled() is true, + // then we cannot record redundant rows for lifting, as this bound change could disregarded. + if (recordRedundantRows_ && !dfprobingPropagation.isZeroObjFixingEnabled() && mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index c7ad93767b5..49f80546c1f 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -254,7 +254,7 @@ class HighsDomain { std::vector> zeroCostFixedVariables_; // Flag and position in the domchgstack of the first zero-cost variable that can be fixed to its lower or upper bound. - bool startZeroCostFixing_; + bool startZeroCostFixing_ = false; size_t zeroCostStartPos_; bool enabled_ = false; From 0385e82e74c59eb0bdf6a8466227a6d678205b16 Mon Sep 17 00:00:00 2001 From: Zhaowei-Wang Date: Tue, 11 Aug 2026 12:03:56 +0800 Subject: [PATCH 25/46] fix test: clang-format --- highs/lp_data/HighsOptions.h | 16 +- highs/mip/HighsDomain.cpp | 430 ++++++++++++++++++-------------- highs/mip/HighsDomain.h | 74 +++--- highs/mip/HighsImplications.cpp | 123 +++++---- highs/mip/HighsImplications.h | 62 ++--- 5 files changed, 375 insertions(+), 330 deletions(-) diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index 8b8ecd45386..06298d71445 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -1765,19 +1765,17 @@ class HighsOptions : public HighsOptionsStruct { advanced, ¢ring_ratio_tolerance, 0, 100, kHighsInf); records.push_back(record_double); - record_bool = - new OptionRecordBool("presolve_dfprobing", - "Use the dual fixing aumgented probing technique in presolve", advanced, - &presolve_dfprobing, true); + record_bool = new OptionRecordBool( + "presolve_dfprobing", + "Use the dual fixing aumgented probing technique in presolve", advanced, + &presolve_dfprobing, true); records.push_back(record_bool); - record_bool = - new OptionRecordBool("presolve_gdf", - "Use the generalized dual fixing technique in presolve", advanced, - &presolve_gdf, true); + record_bool = new OptionRecordBool( + "presolve_gdf", "Use the generalized dual fixing technique in presolve", + advanced, &presolve_gdf, true); records.push_back(record_bool); - // Set up the log_options aliases log_options.clear(); log_options.log_stream = diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index a8b0eb0082d..ec7c698c27a 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -638,31 +638,35 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange( } } -HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation(const DualfixingProbingPropagation& other) - : redundantPropagateFlag_(other.redundantPropagateFlag_), - redundantPropagateVec_(other.redundantPropagateVec_), - zeroCostVarsDirection_(other.zeroCostVarsDirection_), - zeroCostFixedVariables_(other.zeroCostFixedVariables_), - colLowerLockOriginal_(other.colLowerLockOriginal_), - colUpperLockOriginal_(other.colUpperLockOriginal_), - colLowerLockReduced_(other.colLowerLockReduced_), - colUpperLockReduced_(other.colUpperLockReduced_), - candidatesVec_(other.candidatesVec_), - candidatesFlag_(other.candidatesFlag_), - lockNeedClear_(other.lockNeedClear_), - gdfCandidatesVec_(other.gdfCandidatesVec_), - gdfCandidatesFlag_(other.gdfCandidatesFlag_), - gdfLbReachable0_(other.gdfLbReachable0_), - gdfLbReachable1_(other.gdfLbReachable1_), - gdfUbReachable0_(other.gdfUbReachable0_), - gdfUbReachable1_(other.gdfUbReachable1_) {;} +HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation( + const DualfixingProbingPropagation& other) + : redundantPropagateFlag_(other.redundantPropagateFlag_), + redundantPropagateVec_(other.redundantPropagateVec_), + zeroCostVarsDirection_(other.zeroCostVarsDirection_), + zeroCostFixedVariables_(other.zeroCostFixedVariables_), + colLowerLockOriginal_(other.colLowerLockOriginal_), + colUpperLockOriginal_(other.colUpperLockOriginal_), + colLowerLockReduced_(other.colLowerLockReduced_), + colUpperLockReduced_(other.colUpperLockReduced_), + candidatesVec_(other.candidatesVec_), + candidatesFlag_(other.candidatesFlag_), + lockNeedClear_(other.lockNeedClear_), + gdfCandidatesVec_(other.gdfCandidatesVec_), + gdfCandidatesFlag_(other.gdfCandidatesFlag_), + gdfLbReachable0_(other.gdfLbReachable0_), + gdfLbReachable1_(other.gdfLbReachable1_), + gdfUbReachable0_(other.gdfUbReachable0_), + gdfUbReachable1_(other.gdfUbReachable1_) { + ; +} void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { mipsolver = domain->mipsolver; redundantPropagateFlag_.assign(2 * mipsolver->numRow(), false); redundantPropagateVec_.clear(); redundantPropagateVec_.reserve(2 * mipsolver->numRow()); - zeroCostVarsDirection_.assign(2 * mipsolver->numCol(), FIXDIRECTION_NOT_DECIDED); + zeroCostVarsDirection_.assign(2 * mipsolver->numCol(), + FIXDIRECTION_NOT_DECIDED); zeroCostFixedVariables_.clear(); zeroCostFixedVariables_.reserve(2 * mipsolver->numCol()); @@ -688,61 +692,71 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { gdfLbReachable1_.assign(mipsolver->numCol(), 0); gdfUbReachable0_.assign(mipsolver->numCol(), 0); gdfUbReachable1_.assign(mipsolver->numCol(), 0); - + // compute the original locks for each variable const auto model = mipsolver->model_; - for (HighsInt iCol = 0; iCol < model->a_matrix_.num_col_; iCol ++) { - for (HighsInt k = model->a_matrix_.start_[iCol]; k < model->a_matrix_.start_[iCol + 1]; k ++) { + for (HighsInt iCol = 0; iCol < model->a_matrix_.num_col_; iCol++) { + for (HighsInt k = model->a_matrix_.start_[iCol]; + k < model->a_matrix_.start_[iCol + 1]; k++) { const HighsInt iRow = model->a_matrix_.index_[k]; const double iValue = model->a_matrix_.value_[k]; const double lhs = model->row_lower_[iRow], rhs = model->row_upper_[iRow]; if ((iValue > 0 && rhs != kHighsInf) || (iValue < 0 && lhs != -kHighsInf)) - colUpperLockOriginal_[iCol] ++; + colUpperLockOriginal_[iCol]++; if ((iValue > 0 && lhs != -kHighsInf) || (iValue < 0 && rhs != kHighsInf)) - colLowerLockOriginal_[iCol] ++; + colLowerLockOriginal_[iCol]++; } } } -void HighsDomain::DualfixingProbingPropagation::updateRhsRedundant(HighsInt row) { - if (!isEnabled()) - return; +void HighsDomain::DualfixingProbingPropagation::updateRhsRedundant( + HighsInt row) { + if (!isEnabled()) return; - if (domain->activitymaxinf_[row] != 0 || redundantPropagateFlag_[2 * row + 1] || mipsolver->model_->row_upper_[row] == kHighsInf) + if (domain->activitymaxinf_[row] != 0 || + redundantPropagateFlag_[2 * row + 1] || + mipsolver->model_->row_upper_[row] == kHighsInf) return; - if (domain->getMaxActivity(row) <= mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol) { + if (domain->getMaxActivity(row) <= + mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol) { redundantPropagateVec_.push_back(2 * row + 1); redundantPropagateFlag_[2 * row + 1] = 1; } } -void HighsDomain::DualfixingProbingPropagation::updateLhsRedundant(HighsInt row) { - if (!isEnabled()) - return; +void HighsDomain::DualfixingProbingPropagation::updateLhsRedundant( + HighsInt row) { + if (!isEnabled()) return; - if (domain->activitymininf_[row] != 0 || redundantPropagateFlag_[2 * row] || mipsolver->model_->row_lower_[row] == -kHighsInf) + if (domain->activitymininf_[row] != 0 || redundantPropagateFlag_[2 * row] || + mipsolver->model_->row_lower_[row] == -kHighsInf) return; - if (domain->getMinActivity(row) >= mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol) { + if (domain->getMinActivity(row) >= + mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol) { redundantPropagateVec_.push_back(2 * row); redundantPropagateFlag_[2 * row] = 1; } } - void HighsDomain::DualfixingProbingPropagation::propagate() { - // The boolean variable ``startZeroCostFixing_'' is used to flag if we allow variables with zero cost can be fixed in domain propagation. - // The process of domain propagtion in probing is executed in two phases: - // Phase 1: Apply classic domain propagation, and additionally fix variables with non-zero objective coefficients using dual fixing - // Phase 2: Apply classic domain propagation, and additionally fix variables (including those with zero objective coefficients) using dual fixing - // In Phase 1, ``startZeroCostFixing_'' is set to be ``false'' to exclude variable with zero objective coefficients. - // In Phase 2, ``startZeroCostFixing_'' is set to be ``true''. - // Note that - // (1) For all the bound changes in Phase 1, reductions deduced from them are valid for all optimal solutions; - // (2) For the bound changes in Phase 2, reductions deduced from them can only be used to derive global valid reductions (i.e., variable fixing, global bound tightening, and variable substitution). - if (!isEnabled()) - return; + // The boolean variable ``startZeroCostFixing_'' is used to flag if we allow + // variables with zero cost can be fixed in domain propagation. The process of + // domain propagtion in probing is executed in two phases: + // Phase 1: Apply classic domain propagation, and additionally fix + // variables with non-zero objective coefficients using dual fixing Phase + // 2: Apply classic domain propagation, and additionally fix variables + // (including those with zero objective coefficients) using dual fixing + // In Phase 1, ``startZeroCostFixing_'' is set to be ``false'' to exclude + // variable with zero objective coefficients. In Phase 2, + // ``startZeroCostFixing_'' is set to be ``true''. Note that + // (1) For all the bound changes in Phase 1, reductions deduced from them + // are valid for all optimal solutions; (2) For the bound changes in Phase + // 2, reductions deduced from them can only be used to derive global valid + // reductions (i.e., variable fixing, global bound tightening, and variable + // substitution). + if (!isEnabled()) return; assert(candidatesVec_.empty()); vector domainchangeDFProbing; @@ -761,15 +775,21 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { auto checkVariableLowerLock = [&](HighsInt iCol) { auto model = mipsolver->model_; if (ableToFixToLb(iCol)) { - for (HighsInt k = model->a_matrix_.start_[iCol]; k < model->a_matrix_.start_[iCol + 1]; k ++) { + for (HighsInt k = model->a_matrix_.start_[iCol]; + k < model->a_matrix_.start_[iCol + 1]; k++) { const HighsInt iRow = model->a_matrix_.index_[k]; const double iValue = model->a_matrix_.value_[k]; - const double blower = model->row_lower_[iRow], bupper = model->row_upper_[iRow]; - const bool lhsOk = iValue > 0 && domain->getMinActivity(iRow) >= blower - domain->feastol(); - const bool rhsOk = iValue < 0 && domain->getMaxActivity(iRow) <= bupper + domain->feastol(); + const double blower = model->row_lower_[iRow], + bupper = model->row_upper_[iRow]; + const bool lhsOk = iValue > 0 && domain->getMinActivity(iRow) >= + blower - domain->feastol(); + const bool rhsOk = iValue < 0 && domain->getMaxActivity(iRow) <= + bupper + domain->feastol(); if (!lhsOk && !rhsOk) { - std::cout << "Lower lock: variable " << iCol << " at row = " << iRow << " coef = " << iValue - << " not redundant at constraint " << iRow << ", minact = " << domain->getMinActivity(iRow) << ", maxact = " << domain->getMaxActivity(iRow) + std::cout << "Lower lock: variable " << iCol << " at row = " << iRow + << " coef = " << iValue << " not redundant at constraint " + << iRow << ", minact = " << domain->getMinActivity(iRow) + << ", maxact = " << domain->getMaxActivity(iRow) << " lhs = " << blower << " rhs = " << bupper << std::endl; } } @@ -779,15 +799,21 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { auto checkVariableUpperLock = [&](HighsInt iCol) { auto model = mipsolver->model_; if (ableToFixToUb(iCol)) { - for (HighsInt k = model->a_matrix_.start_[iCol]; k < model->a_matrix_.start_[iCol + 1]; k ++) { + for (HighsInt k = model->a_matrix_.start_[iCol]; + k < model->a_matrix_.start_[iCol + 1]; k++) { const HighsInt iRow = model->a_matrix_.index_[k]; const double iValue = model->a_matrix_.value_[k]; - const double blower = model->row_lower_[iRow], bupper = model->row_upper_[iRow]; - const bool lhsOk = iValue < 0 && domain->getMinActivity(iRow) >= blower - domain->feastol(); - const bool rhsOk = iValue > 0 && domain->getMaxActivity(iRow) <= bupper + domain->feastol(); + const double blower = model->row_lower_[iRow], + bupper = model->row_upper_[iRow]; + const bool lhsOk = iValue < 0 && domain->getMinActivity(iRow) >= + blower - domain->feastol(); + const bool rhsOk = iValue > 0 && domain->getMaxActivity(iRow) <= + bupper + domain->feastol(); if (!lhsOk && !rhsOk) { - std::cout << "Upper lock: variable " << iCol << " at row = " << iRow << " coef = " << iValue - << " not redundant at constraint " << iRow << ", minact = " << domain->getMinActivity(iRow) << ", maxact = " << domain->getMaxActivity(iRow) + std::cout << "Upper lock: variable " << iCol << " at row = " << iRow + << " coef = " << iValue << " not redundant at constraint " + << iRow << ", minact = " << domain->getMinActivity(iRow) + << ", maxact = " << domain->getMaxActivity(iRow) << " lhs = " << blower << " rhs = " << bupper << std::endl; } } @@ -810,7 +836,8 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { domainchangeDFProbing.push_back(thisbchg); }; - // only record - we do not actually fix them now as their objective coefficients are zero + // only record - we do not actually fix them now as their objective + // coefficients are zero auto collectFixLower = [&](int iCol) { zeroCostFixedVariables_.emplace_back(iCol, FIXDIRECTION_LOWER_BOUND); }; @@ -821,83 +848,95 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // exit if no new redundant constraints are found HighsInt maxLockLeft = redundantPropagateVec_.size() - previousSize_; - if (maxLockLeft == 0) - return; + if (maxLockLeft == 0) return; - for (; previousSize_ < redundantPropagateVec_.size(); ++ previousSize_, -- maxLockLeft) { + for (; previousSize_ < redundantPropagateVec_.size(); + ++previousSize_, --maxLockLeft) { const HighsInt i = redundantPropagateVec_[previousSize_]; const HighsInt iRow = i / 2; assert(iRow < mipsolver->numRow()); - if (i % 2 == 0) { // lower redundant + if (i % 2 == 0) { // lower redundant HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; HighsInt rend = mipsolver->mipdata_->ARstart_[iRow + 1]; - for (auto k = rstart; k < rend; ++ k) { + for (auto k = rstart; k < rend; ++k) { const HighsInt iCol = mipsolver->mipdata_->ARindex_[k]; - if (domain->isFixed(iCol)) - continue; + if (domain->isFixed(iCol)) continue; const double iValue = mipsolver->mipdata_->ARvalue_[k]; const double cost = mipsolver->model_->col_cost_[iCol]; // do not insert to candidates if the lock is not reduced enough - bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; - bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; + bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < + colLowerLockOriginal_[iCol]; + bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < + colUpperLockOriginal_[iCol]; - if (iValue > 0 && cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (iValue > 0 && + cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { lockNeedClear_.insert(iCol); - colLowerLockReduced_[iCol] ++; - lowerNoInsert = lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; - } - else if (iValue < 0 && cost <= mipsolver->options_mip_->dual_feasibility_tolerance) { + colLowerLockReduced_[iCol]++; + lowerNoInsert = + lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < + colLowerLockOriginal_[iCol]; + } else if (iValue < 0 && + cost <= + mipsolver->options_mip_->dual_feasibility_tolerance) { lockNeedClear_.insert(iCol); - colUpperLockReduced_[iCol] ++; - upperNoInsert = upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; + colUpperLockReduced_[iCol]++; + upperNoInsert = + upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < + colUpperLockOriginal_[iCol]; } - if (!lowerNoInsert || !upperNoInsert) - addToCandidate(iCol); + if (!lowerNoInsert || !upperNoInsert) addToCandidate(iCol); } - } - else { // upper redundant + } else { // upper redundant HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; HighsInt rend = mipsolver->mipdata_->ARstart_[iRow + 1]; for (auto k = rstart; k < rend; k++) { const HighsInt iCol = mipsolver->mipdata_->ARindex_[k]; - if (domain->isFixed(iCol)) - continue; + if (domain->isFixed(iCol)) continue; const double iValue = mipsolver->mipdata_->ARvalue_[k]; const double cost = mipsolver->model_->col_cost_[iCol]; // do not insert to candidates if the lock is not reduced enough - bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; - bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; + bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < + colLowerLockOriginal_[iCol]; + bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < + colUpperLockOriginal_[iCol]; - if (iValue < 0 && cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (iValue < 0 && + cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { lockNeedClear_.insert(iCol); - colLowerLockReduced_[iCol] ++; - lowerNoInsert = lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < colLowerLockOriginal_[iCol]; - } - else if (iValue > 0 && cost <= mipsolver->options_mip_->dual_feasibility_tolerance) { + colLowerLockReduced_[iCol]++; + lowerNoInsert = + lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < + colLowerLockOriginal_[iCol]; + } else if (iValue > 0 && + cost <= + mipsolver->options_mip_->dual_feasibility_tolerance) { lockNeedClear_.insert(iCol); - colUpperLockReduced_[iCol] ++; - upperNoInsert = upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < colUpperLockOriginal_[iCol]; + colUpperLockReduced_[iCol]++; + upperNoInsert = + upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < + colUpperLockOriginal_[iCol]; } - if (!lowerNoInsert || !upperNoInsert) - addToCandidate(iCol); + if (!lowerNoInsert || !upperNoInsert) addToCandidate(iCol); } } } for (auto iCol : candidatesVec_) { - if (domain->isFixed(iCol)) - continue; - const bool canBeFixedToLower = colLowerLockReduced_[iCol] == colLowerLockOriginal_[iCol]; - const bool canBeFixedToUpper = colUpperLockReduced_[iCol] == colUpperLockOriginal_[iCol]; - if (!canBeFixedToLower && !canBeFixedToUpper) - continue; - - if (fabs(mipsolver->model_->col_cost_[iCol]) <= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (domain->isFixed(iCol)) continue; + const bool canBeFixedToLower = + colLowerLockReduced_[iCol] == colLowerLockOriginal_[iCol]; + const bool canBeFixedToUpper = + colUpperLockReduced_[iCol] == colUpperLockOriginal_[iCol]; + if (!canBeFixedToLower && !canBeFixedToUpper) continue; + + if (fabs(mipsolver->model_->col_cost_[iCol]) <= + mipsolver->options_mip_->dual_feasibility_tolerance) { if (startZeroCostFixing_) { // not fixed before if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_NOT_DECIDED) { @@ -906,8 +945,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { if (mipsolver->model_->col_cost_[iCol] >= 0) { addFixLower(iCol); zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; - } - else { + } else { addFixUpper(iCol); zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; } @@ -916,22 +954,24 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { else if (canBeFixedToLower) { addFixLower(iCol); zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; - } - else if (canBeFixedToUpper) { + } else if (canBeFixedToUpper) { addFixUpper(iCol); zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; } } // fix to lb - else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_LOWER_BOUND && canBeFixedToLower) + else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_LOWER_BOUND && + canBeFixedToLower) addFixLower(iCol); - // fix to ub - else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_UPPER_BOUND && canBeFixedToUpper) + // fix to ub + else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_UPPER_BOUND && + canBeFixedToUpper) addFixUpper(iCol); continue; } - // do not perfrom zero cost variable fixing, just collect them and choose directions + // do not perfrom zero cost variable fixing, just collect them and choose + // directions else { // not fixed before if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_NOT_DECIDED) { @@ -940,25 +980,22 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { if (mipsolver->model_->col_cost_[iCol] >= 0) { collectFixLower(iCol); zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; - } - else { + } else { collectFixUpper(iCol); zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; } - } - else if (canBeFixedToLower) { // fix to lower and set its direction + } else if (canBeFixedToLower) { // fix to lower and set its direction collectFixLower(iCol); zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; - } - else if (canBeFixedToUpper) { + } else if (canBeFixedToUpper) { collectFixUpper(iCol); zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; } - } - else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_UPPER_BOUND && canBeFixedToUpper) { // fix to upper + } else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_UPPER_BOUND && + canBeFixedToUpper) { // fix to upper collectFixUpper(iCol); - } - else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_LOWER_BOUND && canBeFixedToLower) { // fix to lower + } else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_LOWER_BOUND && + canBeFixedToLower) { // fix to lower collectFixLower(iCol); } // we have collected this column @@ -966,8 +1003,8 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } } - - if (mipsolver->model_->col_cost_[iCol] >= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (mipsolver->model_->col_cost_[iCol] >= + mipsolver->options_mip_->dual_feasibility_tolerance) { if (canBeFixedToLower) { // checkVariableLowerLock(iCol); addFixLower(iCol); @@ -975,7 +1012,8 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } } - if (mipsolver->model_->col_cost_[iCol] <= mipsolver->options_mip_->dual_feasibility_tolerance) { + if (mipsolver->model_->col_cost_[iCol] <= + mipsolver->options_mip_->dual_feasibility_tolerance) { if (canBeFixedToUpper) { // checkVariableUpperLock(iCol); addFixUpper(iCol); @@ -992,13 +1030,13 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // change bound size_t j = 0; - for (; j != domainchangeDFProbing.size() && !domain->infeasible_; ++ j) { + for (; j != domainchangeDFProbing.size() && !domain->infeasible_; ++j) { domain->changeBound(*domainchangeDFProbing[j], Reason::unspecified()); delete domainchangeDFProbing[j]; } // clear the remaining domain changes if infeasible - for (j ++; j < domainchangeDFProbing.size(); ++ j) { + for (j++; j < domainchangeDFProbing.size(); ++j) { assert(domain->infeasible_); delete domainchangeDFProbing[j]; } @@ -1007,7 +1045,8 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { previousSize_ = redundantPropagateVec_.size(); } -void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_variable, bool val) { +void HighsDomain::DualfixingProbingPropagation::updateGDFInfo( + HighsInt probing_variable, bool val) { // tool lambda functions auto addToCandidate = [&](HighsInt k) { if (gdfCandidatesFlag_[k]) @@ -1017,118 +1056,111 @@ void HighsDomain::DualfixingProbingPropagation::updateGDFInfo(HighsInt probing_v gdfCandidatesFlag_[k] = true; } }; - + // only redundant constraints are useful in GDF for (const auto x : redundantPropagateVec_) { const HighsInt iRow = x / 2; const bool isRhs = x % 2; HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; HighsInt rend = mipsolver->mipdata_->ARstart_[iRow + 1]; - + for (auto k = rstart; k < rend; k++) { const HighsInt iCol = mipsolver->mipdata_->ARindex_[k]; const double iValue = mipsolver->mipdata_->ARvalue_[k]; const double cost = mipsolver->model_->col_cost_[iCol]; bool considered = false; - if (mipsolver->model_->col_lower_[iCol] == mipsolver->model_->col_upper_[iCol] || mipsolver->mipdata_->implications.colsubstituted[iCol]) + if (mipsolver->model_->col_lower_[iCol] == + mipsolver->model_->col_upper_[iCol] || + mipsolver->mipdata_->implications.colsubstituted[iCol]) continue; - + if (iValue > 0) { - if (isRhs) { // consider upper bound reachable + if (isRhs) { // consider upper bound reachable const double globalUb = mipsolver->model_->col_upper_[iCol]; const double probingUb = domain->col_upper_[iCol]; if (!ableToFixToUb(iCol) || domain->getMaxActivity(iRow) == kHighsInf) continue; - const bool upper_bound_reachable = - domain->getMaxActivity(iRow) + iValue * (globalUb - probingUb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); + const bool upper_bound_reachable = + domain->getMaxActivity(iRow) + iValue * (globalUb - probingUb) <= + mipsolver->model_->row_upper_[iRow] + domain->feastol(); if (upper_bound_reachable) { considered = true; // special treat if the current variable is the probing variable if (iCol == probing_variable && val == 0) { gdfUbReachable0_[iCol]++; gdfUbReachable1_[iCol]++; - } - else { - if (val == 0) - gdfUbReachable0_[iCol]++; - if (val == 1) - gdfUbReachable1_[iCol]++; + } else { + if (val == 0) gdfUbReachable0_[iCol]++; + if (val == 1) gdfUbReachable1_[iCol]++; } } - } - else { // consider lower bound reachable + } else { // consider lower bound reachable const double globalLb = mipsolver->model_->col_lower_[iCol]; const double probingLb = domain->col_lower_[iCol]; - if (!ableToFixToLb(iCol) || domain->getMinActivity(iRow) == -kHighsInf) + if (!ableToFixToLb(iCol) || + domain->getMinActivity(iRow) == -kHighsInf) continue; - const bool lower_bound_reachable = - domain->getMinActivity(iRow) + iValue * (globalLb - probingLb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); + const bool lower_bound_reachable = + domain->getMinActivity(iRow) + iValue * (globalLb - probingLb) >= + mipsolver->model_->row_lower_[iRow] - domain->feastol(); if (lower_bound_reachable) { considered = true; // special treat if the current variable is the probing variable if (iCol == probing_variable && val == 1) { gdfLbReachable0_[iCol]++; gdfLbReachable1_[iCol]++; - } - else { - if (val == 0) - gdfLbReachable0_[iCol]++; - if (val == 1) - gdfLbReachable1_[iCol]++; + } else { + if (val == 0) gdfLbReachable0_[iCol]++; + if (val == 1) gdfLbReachable1_[iCol]++; } } } } else { - if (isRhs) { // consider lower bound reachable + if (isRhs) { // consider lower bound reachable const double globalLb = mipsolver->model_->col_lower_[iCol]; const double probingLb = domain->col_lower_[iCol]; if (!ableToFixToLb(iCol) || domain->getMaxActivity(iRow) == kHighsInf) continue; - const bool lower_bound_reachable = - domain->getMaxActivity(iRow) + iValue * (globalLb - probingLb) <= mipsolver->model_->row_upper_[iRow] + domain->feastol(); + const bool lower_bound_reachable = + domain->getMaxActivity(iRow) + iValue * (globalLb - probingLb) <= + mipsolver->model_->row_upper_[iRow] + domain->feastol(); if (lower_bound_reachable) { considered = true; // special treat if the current variable is the probing variable if (iCol == probing_variable && val == 1) { gdfLbReachable0_[iCol]++; gdfLbReachable1_[iCol]++; - } - else { - if (val == 0) - gdfLbReachable0_[iCol]++; - if (val == 1) - gdfLbReachable1_[iCol]++; + } else { + if (val == 0) gdfLbReachable0_[iCol]++; + if (val == 1) gdfLbReachable1_[iCol]++; } } - } - else { // consider upper bound reachable + } else { // consider upper bound reachable const double globalUb = mipsolver->model_->col_upper_[iCol]; const double probingUb = domain->col_upper_[iCol]; - if (!ableToFixToUb(iCol) || domain->getMinActivity(iRow) == -kHighsInf) + if (!ableToFixToUb(iCol) || + domain->getMinActivity(iRow) == -kHighsInf) continue; - const bool upper_bound_reachable = - domain->getMinActivity(iRow) + iValue * (globalUb - probingUb) >= mipsolver->model_->row_lower_[iRow] - domain->feastol(); + const bool upper_bound_reachable = + domain->getMinActivity(iRow) + iValue * (globalUb - probingUb) >= + mipsolver->model_->row_lower_[iRow] - domain->feastol(); if (upper_bound_reachable) { considered = true; // special treat if the current variable is the probing variable if (iCol == probing_variable && val == 0) { gdfUbReachable0_[iCol]++; gdfUbReachable1_[iCol]++; - } - else { - if (val == 0) - gdfUbReachable0_[iCol]++; - if (val == 1) - gdfUbReachable1_[iCol]++; + } else { + if (val == 0) gdfUbReachable0_[iCol]++; + if (val == 1) gdfUbReachable1_[iCol]++; } } } } - if (considered) - addToCandidate(iCol); + if (considered) addToCandidate(iCol); } } } @@ -1160,16 +1192,16 @@ HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { gdfFixingStack_.push_back(thisbchg); } } - + // apply bound change size_t j = 0; - for (; j != gdfFixingStack_.size() && !domain->infeasible_; ++ j) { + for (; j != gdfFixingStack_.size() && !domain->infeasible_; ++j) { domain->changeBound(*gdfFixingStack_[j], Reason::unspecified()); delete gdfFixingStack_[j]; } // clear the remaining domain changes if infeasible - for (; j < gdfFixingStack_.size(); ++ j) { + for (; j < gdfFixingStack_.size(); ++j) { assert(domain->infeasible_); delete gdfFixingStack_[j]; } @@ -2107,13 +2139,15 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, assert(tmpinf == activitymininf_[mip->a_matrix_.index_[i]]); } #endif - // If dfprobingPropagation.isZeroObjFixingEnabled() is true, - // then we cannot record redundant rows for lifting, as this bound change could disregarded. - if (recordRedundantRows_ && !dfprobingPropagation.isZeroObjFixingEnabled() && + // If dfprobingPropagation.isZeroObjFixingEnabled() is true, + // then we cannot record redundant rows for lifting, as this bound change + // could disregarded. + if (recordRedundantRows_ && + !dfprobingPropagation.isZeroObjFixingEnabled() && mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); - + if (newbound >= oldbound + mipsolver->mipdata_->feastol) dfprobingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); @@ -2160,9 +2194,11 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, assert(tmpinf == activitymaxinf_[mip->a_matrix_.index_[i]]); } #endif - // If dfprobingPropagation.isZeroObjFixingEnabled() is true, - // then we cannot record redundant rows for lifting, as this bound change could disregarded. - if (recordRedundantRows_ && !dfprobingPropagation.isZeroObjFixingEnabled() && + // If dfprobingPropagation.isZeroObjFixingEnabled() is true, + // then we cannot record redundant rows for lifting, as this bound change + // could disregarded. + if (recordRedundantRows_ && + !dfprobingPropagation.isZeroObjFixingEnabled() && mip->row_lower_[mip->a_matrix_.index_[i]] == -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); @@ -2282,13 +2318,15 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, assert(tmpinf == activitymaxinf_[mip->a_matrix_.index_[i]]); } #endif - // If dfprobingPropagation.isZeroObjFixingEnabled() is true, - // then we cannot record redundant rows for lifting, as this bound change could disregarded. - if (recordRedundantRows_ && !dfprobingPropagation.isZeroObjFixingEnabled() && + // If dfprobingPropagation.isZeroObjFixingEnabled() is true, + // then we cannot record redundant rows for lifting, as this bound change + // could disregarded. + if (recordRedundantRows_ && + !dfprobingPropagation.isZeroObjFixingEnabled() && mip->row_lower_[mip->a_matrix_.index_[i]] == -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); - + if (newbound <= oldbound - mipsolver->mipdata_->feastol) dfprobingPropagation.updateRhsRedundant(mip->a_matrix_.index_[i]); @@ -2338,13 +2376,15 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, assert(tmpinf == activitymininf_[mip->a_matrix_.index_[i]]); } #endif - // If dfprobingPropagation.isZeroObjFixingEnabled() is true, - // then we cannot record redundant rows for lifting, as this bound change could disregarded. - if (recordRedundantRows_ && !dfprobingPropagation.isZeroObjFixingEnabled() && + // If dfprobingPropagation.isZeroObjFixingEnabled() is true, + // then we cannot record redundant rows for lifting, as this bound change + // could disregarded. + if (recordRedundantRows_ && + !dfprobingPropagation.isZeroObjFixingEnabled() && mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); - + if (newbound <= oldbound - mipsolver->mipdata_->feastol) dfprobingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); @@ -2941,7 +2981,8 @@ bool HighsDomain::propagate() { if (!conflictprop.propagateConflictInds_.empty()) return true; } - if (!infeasible_ && dfprobingPropagation.isActive() && mipsolver->options_mip_->presolve_dfprobing) + if (!infeasible_ && dfprobingPropagation.isActive() && + mipsolver->options_mip_->presolve_dfprobing) return true; return false; @@ -3119,11 +3160,14 @@ bool HighsDomain::propagate() { propagateinds.clear(); } } - - if (!infeasible_ && dfprobingPropagation.isActive() && mipsolver->options_mip_->presolve_dfprobing) { - // std::cout << "Activated by nRedundantIndices = " << dfprobingPropagation.redundantPropagateVec_.size() << std::endl; + + if (!infeasible_ && dfprobingPropagation.isActive() && + mipsolver->options_mip_->presolve_dfprobing) { + // std::cout << "Activated by nRedundantIndices = " << + // dfprobingPropagation.redundantPropagateVec_.size() << std::endl; dfprobingPropagation.propagate(); - if (!havePropagationRows() && !dfprobingPropagation.isZeroObjFixingEnabled()) { + if (!havePropagationRows() && + !dfprobingPropagation.isZeroObjFixingEnabled()) { dfprobingPropagation.enableZeroObjFixing(); dfprobingPropagation.setZeroCostFixingPosition(domchgstack_.size()); dfprobingPropagation.propagate(); diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 49f80546c1f..978ade04852 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -12,8 +12,8 @@ #include #include #include -#include #include +#include #include "HighsPseudocost.h" #include "mip/HighsDomainChange.h" @@ -239,12 +239,12 @@ class HighsDomain { struct DualfixingProbingPropagation { HighsDomain* domain; HighsMipSolver* mipsolver; - + // row lower and upper, length = 2 * rownum std::vector redundantPropagateFlag_; std::vector redundantPropagateVec_; - - // For zero-cost variables, we need to know which direction we can fix them to. + + // For zero-cost variables, we need to know which direction we can fix them enum DFPROBING_FIX_DIRECTION { FIXDIRECTION_NOT_DECIDED = 0, FIXDIRECTION_LOWER_BOUND, @@ -252,8 +252,9 @@ class HighsDomain { }; std::vector zeroCostVarsDirection_; std::vector> zeroCostFixedVariables_; - - // Flag and position in the domchgstack of the first zero-cost variable that can be fixed to its lower or upper bound. + + // Flag and position in the domchgstack of the first zero-cost variable that + // can be fixed to its lower or upper bound. bool startZeroCostFixing_ = false; size_t zeroCostStartPos_; @@ -283,67 +284,54 @@ class HighsDomain { std::vector gdfUbReachable0_; std::vector gdfUbReachable1_; - void enablePropagator() { - enabled_ = true; - } + void enablePropagator() { enabled_ = true; } - void disablePropagator() { - enabled_ = false; - } + void disablePropagator() { enabled_ = false; } - bool isEnabled() { - return enabled_; - } + bool isEnabled() { return enabled_; } // active only when new redundant rows are found. bool isActive() { return enabled_ && redundantPropagateVec_.size() > previousSize_; } - // mark the position when the first zero-cost variable can be fixed to its lower or upper bound. - void setZeroCostFixingPosition(HighsInt v) { - zeroCostStartPos_ = v; - } + // mark the position when the first zero-cost variable can be fixed to its + // lower or upper bound. + void setZeroCostFixingPosition(HighsInt v) { zeroCostStartPos_ = v; } - size_t getZeroCostFixingPosition() { - return zeroCostStartPos_; - } + size_t getZeroCostFixingPosition() { return zeroCostStartPos_; } - void enableZeroObjFixing() { - startZeroCostFixing_ = true; - } + void enableZeroObjFixing() { startZeroCostFixing_ = true; } - void disableZeroObjFixing() { - startZeroCostFixing_ = false; - } + void disableZeroObjFixing() { startZeroCostFixing_ = false; } - bool isZeroObjFixingEnabled() { - return startZeroCostFixing_; - } + bool isZeroObjFixingEnabled() { return startZeroCostFixing_; } bool ableToFixToLb(int col) { - return mipsolver->model_->col_cost_[col] >= -mipsolver->options_mip_->dual_feasibility_tolerance - && mipsolver->model_->col_lower_[col] > -kHighsInf; + return mipsolver->model_->col_cost_[col] >= + -mipsolver->options_mip_->dual_feasibility_tolerance && + mipsolver->model_->col_lower_[col] > -kHighsInf; } bool ableToFixToUb(int col) { - return mipsolver->model_->col_cost_[col] <= mipsolver->options_mip_->dual_feasibility_tolerance - && mipsolver->model_->col_upper_[col] < kHighsInf; + return mipsolver->model_->col_cost_[col] <= + mipsolver->options_mip_->dual_feasibility_tolerance && + mipsolver->model_->col_upper_[col] < kHighsInf; } // remove redundant information void clearRedundantInfo() { previousSize_ = 0; - if (!redundantPropagateVec_.empty()) { // clear buffers + if (!redundantPropagateVec_.empty()) { // clear buffers for (const auto x : redundantPropagateVec_) redundantPropagateFlag_[x] = false; redundantPropagateVec_.clear(); } - - for (size_t i = 0; i < redundantPropagateFlag_.size(); ++ i) + + for (size_t i = 0; i < redundantPropagateFlag_.size(); ++i) assert(!redundantPropagateFlag_[i]); - + zeroCostFixedVariables_.clear(); for (const auto x : lockNeedClear_) @@ -351,19 +339,19 @@ class HighsDomain { lockNeedClear_.clear(); } - DualfixingProbingPropagation() {;}; - + DualfixingProbingPropagation() { ; }; + DualfixingProbingPropagation(HighsDomain* domain) : domain(domain) {}; DualfixingProbingPropagation(const DualfixingProbingPropagation& other); - ~DualfixingProbingPropagation() {;}; + ~DualfixingProbingPropagation() { ; }; void recomputeLocks(); void updateRhsRedundant(HighsInt row); void updateLhsRedundant(HighsInt row); void propagate(); - + // functionalities for GDF void updateGDFInfo(HighsInt probing_variable, bool val); HighsInt processGDFFixing(); diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 55b2f358f42..787c911c12e 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -28,8 +28,10 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { size_t changedend = globaldomain.getChangedCols().size(); // get two flags - const bool useDFProbing = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_dfprobing; - const bool useGDF = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_gdf; + const bool useDFProbing = + globaldomain.inProbing_ && mipsolver.options_mip_->presolve_dfprobing; + const bool useGDF = + globaldomain.inProbing_ && mipsolver.options_mip_->presolve_gdf; // record redundant rows if any of the two flags is true if (useDFProbing || useGDF) { globaldomain.getDfProbingPropagation().clearRedundantInfo(); @@ -93,17 +95,21 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { HighsInt numEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); HighsInt maxEntries = 100000 + mipsolver.numNonzero(); - const HighsInt tentativeStart = useDFProbing ? globaldomain.getDfProbingPropagation().getZeroCostFixingPosition() : kHighsIInf32; + const HighsInt tentativeStart = + useDFProbing + ? globaldomain.getDfProbingPropagation().getZeroCostFixingPosition() + : kHighsIInf32; if (useDFProbing) - implics_tentative.assign(domchgstack.begin() + stackimplicstart, domchgstack.begin() + stackimplicend); + implics_tentative.assign(domchgstack.begin() + stackimplicstart, + domchgstack.begin() + stackimplicend); for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { if (domchgreason[i].type == HighsDomain::Reason::kCliqueTable && ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) continue; - - if (i >= tentativeStart) // record tentative implications - continue; + + if (i >= tentativeStart) // record tentative implications + continue; implics.push_back(domchgstack[i]); } @@ -112,20 +118,20 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { storeLiftingOpportunities(col, val); // update information to derive generalized dual fixings - if (useGDF) - globaldomain.getDfProbingPropagation().updateGDFInfo(col, val); + if (useGDF) globaldomain.getDfProbingPropagation().updateGDFInfo(col, val); // backtrack doBacktrack(changedend); if (!implics_tentative.empty()) { // add the implications of binary variables to the clique table - auto binstart_tmp = std::partition(implics_tentative.begin(), implics_tentative.end(), - [&](const HighsDomainChange& a) { - return !globaldomain.isBinary(a.column); - }); - // store the tentative bound changes of binary variables separately - for (auto i = binstart_tmp; i != implics_tentative.end(); ++ i) + auto binstart_tmp = + std::partition(implics_tentative.begin(), implics_tentative.end(), + [&](const HighsDomainChange& a) { + return !globaldomain.isBinary(a.column); + }); + // store the tentative bound changes of binary variables separately + for (auto i = binstart_tmp; i != implics_tentative.end(); ++i) recordTentativeCliques(val, *i); implics_tentative.erase(binstart_tmp, implics_tentative.end()); } @@ -344,17 +350,17 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (globaldomain.isBinary(col) && !implicationsCached(col, 1) && !implicationsCached(col, 0) && mipsolver.mipdata_->cliquetable.getSubstitution(col) == nullptr) { - - const bool useDFProbing = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_dfprobing; - const bool useGDF = globaldomain.inProbing_ && mipsolver.options_mip_->presolve_gdf; + const bool useDFProbing = + globaldomain.inProbing_ && mipsolver.options_mip_->presolve_dfprobing; + const bool useGDF = + globaldomain.inProbing_ && mipsolver.options_mip_->presolve_gdf; // setup for dfprobingPropagation if (useDFProbing) { clearTentativeClique(); - globaldomain.getDfProbingPropagation().setZeroCostFixingPosition(kHighsIInf32); + globaldomain.getDfProbingPropagation().setZeroCostFixingPosition( + kHighsIInf32); } - if (useGDF) - globaldomain.getDfProbingPropagation().clearGDFInfo(); - + if (useGDF) globaldomain.getDfProbingPropagation().clearGDFInfo(); bool infeasible = computeImplications(col, 1); if (globaldomain.infeasible()) return true; @@ -373,20 +379,23 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { HighsCliqueTable::CliqueVar clique[2]; // Loop over binary variables that are tighened at least once for (auto k : binaryInvolvedInds_) { - // Skip non-binary variables (being fixed now) or those can be substituted by other binary variables - if (!globaldomain.isBinary(k) || colsubstituted[k]) - continue; + // Skip non-binary variables (being fixed now) or those can be + // substituted by other binary variables + if (!globaldomain.isBinary(k) || colsubstituted[k]) continue; // Return if infeasible - if (globaldomain.infeasible()) - return true; - // Get the information how x[k] is fixed in probing on x[col] = 0 and x[col] = 1 - // For the meaning of ``data'', please see lines 71-89 in HighsImplications.h + if (globaldomain.infeasible()) return true; + // Get the information how x[k] is fixed in probing on x[col] = 0 and + // x[col] = 1 For the meaning of ``data'', please see lines 71-89 in + // HighsImplications.h uint8_t data = binaryInvolvedFlags_[k]; - if (data == 0) // flag for no reduction + if (data == 0) // flag for no reduction continue; - if (data == binaryFixType::kGlobalLower) { // x[k] is fixed at 0 under both x[col] = 0 and x[col] = 1 - // fix x[k] = 0 by adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + if (data == + binaryFixType::kGlobalLower) { // x[k] is fixed at 0 under both + // x[col] = 0 and x[col] = 1 + // fix x[k] = 0 by adding two cliques (i.e., these two cliques should + // be added in computeImplications() to derive global reductions) clique[0] = HighsCliqueTable::CliqueVar(col, 0); clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); @@ -394,9 +403,11 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); data = 0; - } - else if (data == binaryFixType::kGlobalUpper) { // x[k] is fixed at 1 under both x[col] = 0 and x[col] = 1 - // fix x[k] = 1 by adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + } else if (data == binaryFixType::kGlobalUpper) { // x[k] is fixed at 1 + // under both x[col] + // = 0 and x[col] = 1 + // fix x[k] = 1 by adding two cliques (i.e., these two cliques should + // be added in computeImplications() to derive global reductions) clique[0] = HighsCliqueTable::CliqueVar(col, 0); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); @@ -404,9 +415,14 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); data = 0; - } - else if (data == binaryFixType::kSubstituteComplement) { // x[k] is fixed at 0 under x[col] = 1, and is fixed at 1 under x[col] = 0; this makes x[col] + x[k] = 1 - // Adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + } else if (data == + binaryFixType:: + kSubstituteComplement) { // x[k] is fixed at 0 under + // x[col] = 1, and is fixed at + // 1 under x[col] = 0; this + // makes x[col] + x[k] = 1 + // Adding two cliques (i.e., these two cliques should be added in + // computeImplications() to derive global reductions) clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); @@ -414,9 +430,13 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); data = 0; - } - else if (data == binaryFixType::kSubstituteEqual) { // x[k] is fixed at 0 under x[col] = 0, and is fixed at 1 under x[col] = 1; this makes x[col] = x[k] - // Adding two cliques (i.e., these two cliques should be added in computeImplications() to derive global reductions) + } else if (data == + binaryFixType:: + kSubstituteEqual) { // x[k] is fixed at 0 under x[col] = + // 0, and is fixed at 1 under x[col] + // = 1; this makes x[col] = x[k] + // Adding two cliques (i.e., these two cliques should be added in + // computeImplications() to derive global reductions) clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); @@ -427,19 +447,25 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { } } - // clear the tentative bound changes for binary variables obtained from probing on x[col] + // clear the tentative bound changes for binary variables obtained from + // probing on x[col] clearTentativeClique(); } // analyze implications - // also include the bound changes of non-binary variables here, to derive tighter global bounds and variable substitutions - const bool haveTentativeImplics_zero = !implications[2 * col].implics_tentative.empty(); - const bool haveTentativeImplics_one = !implications[2 * col + 1].implics_tentative.empty(); + // also include the bound changes of non-binary variables here, to derive + // tighter global bounds and variable substitutions + const bool haveTentativeImplics_zero = + !implications[2 * col].implics_tentative.empty(); + const bool haveTentativeImplics_one = + !implications[2 * col + 1].implics_tentative.empty(); const std::vector& implicsdown = - haveTentativeImplics_zero ? getImplications_tentative(col, 0) : getImplications(col, 0, infeasible); + haveTentativeImplics_zero ? getImplications_tentative(col, 0) + : getImplications(col, 0, infeasible); const std::vector& implicsup = - haveTentativeImplics_one ? getImplications_tentative(col, 1) : getImplications(col, 1, infeasible); + haveTentativeImplics_one ? getImplications_tentative(col, 1) + : getImplications(col, 1, infeasible); HighsInt nimplicsdown = implicsdown.size(); HighsInt nimplicsup = implicsup.size(); HighsInt u = 0; @@ -515,8 +541,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { // fix variables using generalized dual fixing HighsInt nfix = globaldomain.getDfProbingPropagation().processGDFFixing(); // propagate if necessary - if (nfix > 0) - globaldomain.propagate(); + if (nfix > 0) globaldomain.propagate(); } return true; diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 424bc2a259e..9cdaf6b2bb3 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -26,11 +26,10 @@ class HighsImplications { struct Implics { std::vector implics; /* The "tentative" implications: - A implication of type x_j \ge (\ell^1_j - \ell^0_j) x_k + \ell^0_j is called "tentative", if - (1) c_j = 0 - (2) x_j is fixed by applying dual fixing in probing - These implications can only be used to perform globally valid reductions. - Therefore, special treatment is required. + A implication of type x_j \ge (\ell^1_j - \ell^0_j) x_k + \ell^0_j is + called "tentative", if (1) c_j = 0 (2) x_j is fixed by applying dual + fixing in probing These implications can only be used to perform globally + valid reductions. Therefore, special treatment is required. */ std::vector implics_tentative; bool computed = false; @@ -69,11 +68,11 @@ class HighsImplications { // vector used to derive global reductions from dfprobing std::vector binaryInvolvedInds_; enum binaryFixType { - kNoReduction = 0b0000, - kGlobalLower = 0b1010, - kGlobalUpper = 0b0101, + kNoReduction = 0b0000, + kGlobalLower = 0b1010, + kGlobalUpper = 0b0101, kSubstituteComplement = 0b1001, - kSubstituteEqual = 0b0110, + kSubstituteEqual = 0b0110, }; /* Possible values for binaryInvolvedFlags_ @@ -129,7 +128,6 @@ class HighsImplications { nextCleanupCall = mipsolver.numNonzero(); binaryInvolvedInds_.reserve(numcol); binaryInvolvedFlags_.assign(numcol, 0b0000); - } constexpr static int64_t calcMaxVarBounds(HighsInt numcol) { @@ -153,12 +151,12 @@ class HighsImplications { return implications[loc].implics; } - const std::vector& getImplications_tentative(HighsInt col, bool val) { + const std::vector& getImplications_tentative(HighsInt col, + bool val) { HighsInt loc = 2 * col + val; return implications[loc].implics_tentative; } - bool implicationsCached(HighsInt col, bool val) { HighsInt loc = 2 * col + val; return implications[loc].computed; @@ -240,35 +238,32 @@ class HighsImplications { // collect tentative binary implications void recordTentativeCliques(bool val, const HighsDomainChange& bchg) { const int iCol = bchg.column; - if (val == 0) { // probing x_k = 0 - if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 + if (val == 0) { // probing x_k = 0 + if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 if (!isFixedTo1(val, iCol)) { if (binaryInvolvedFlags_[iCol] == 0) binaryInvolvedInds_.push_back(iCol); - binaryInvolvedFlags_[iCol] += 0b0001; // 0001 + binaryInvolvedFlags_[iCol] += 0b0001; // 0001 } - } - else { // fixed to 0 + } else { // fixed to 0 if (!isFixedTo0(val, iCol)) { if (binaryInvolvedFlags_[iCol] == 0) binaryInvolvedInds_.push_back(iCol); - binaryInvolvedFlags_[iCol] += 0b0010; // 0010 + binaryInvolvedFlags_[iCol] += 0b0010; // 0010 } } - } - else { // probing x_k = 1 - if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 + } else { // probing x_k = 1 + if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 if (!isFixedTo1(val, iCol)) { if (binaryInvolvedFlags_[iCol] == 0) binaryInvolvedInds_.push_back(iCol); - binaryInvolvedFlags_[iCol] += 0b0100; // 0100 + binaryInvolvedFlags_[iCol] += 0b0100; // 0100 } - } - else { // fixed to 0 + } else { // fixed to 0 if (!isFixedTo0(val, iCol)) { if (binaryInvolvedFlags_[iCol] == 0) binaryInvolvedInds_.push_back(iCol); - binaryInvolvedFlags_[iCol] += 0b1000; // 1000 + binaryInvolvedFlags_[iCol] += 0b1000; // 1000 } } } @@ -281,35 +276,30 @@ class HighsImplications { } // tools for recordTentativeCliques bool isFixedTo0(bool val, HighsInt iCol) { - if (binaryInvolvedFlags_[iCol] == 0) - return false; + if (binaryInvolvedFlags_[iCol] == 0) return false; uint8_t mask; - if (val == 0) { // probing at x = 0, last two digits + if (val == 0) { // probing at x = 0, last two digits mask = 1 << (1); return (binaryInvolvedFlags_[iCol] & mask) != 0; - } - else { // probing at x = 1, first two digits + } else { // probing at x = 1, first two digits mask = 1 << (3); return (binaryInvolvedFlags_[iCol] & mask) != 0; } } // tools for recordTentativeCliques bool isFixedTo1(bool val, HighsInt iCol) { - if (binaryInvolvedFlags_[iCol] == 0) - return false; + if (binaryInvolvedFlags_[iCol] == 0) return false; uint8_t mask; - if (val == 0) { // probing at x = 0, last two digits + if (val == 0) { // probing at x = 0, last two digits mask = 1; return (binaryInvolvedFlags_[iCol] & mask) != 0; - } - else { // probint at x = 1, first two digits + } else { // probint at x = 1, first two digits mask = 1 << (2); return (binaryInvolvedFlags_[iCol] & mask) != 0; } } - }; #endif From 3ca6663694113116e29a49e3ec4597b7f4aec2dd Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 17 Aug 2026 16:49:45 +0200 Subject: [PATCH 26/46] Clean up branch a bit --- highs/lp_data/HConst.h | 1 + highs/lp_data/HighsModelUtils.cpp | 2 + highs/lp_data/HighsOptions.h | 13 -- highs/mip/HighsDomain.cpp | 270 ++++-------------------------- highs/mip/HighsDomain.h | 72 ++++---- highs/mip/HighsImplications.cpp | 138 +++++---------- highs/mip/HighsImplications.h | 121 ++++--------- highs/presolve/HPresolve.cpp | 9 +- 8 files changed, 146 insertions(+), 480 deletions(-) diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index e150f7838a3..567f2588674 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -281,6 +281,7 @@ enum PresolveRuleType : int { kPresolveRuleEnumeration, kPresolveRuleDualFixing, kPresolveRuleColStuffing, + kPresolveRuleDfprobing, kPresolveRuleInitialSweep, kPresolveRuleMax = kPresolveRuleInitialSweep, kPresolveRuleLastAllowOff = kPresolveRuleMax, diff --git a/highs/lp_data/HighsModelUtils.cpp b/highs/lp_data/HighsModelUtils.cpp index 1d7f0b9a057..8c47f77789e 100644 --- a/highs/lp_data/HighsModelUtils.cpp +++ b/highs/lp_data/HighsModelUtils.cpp @@ -1519,6 +1519,8 @@ std::string utilPresolveRuleTypeToString(const HighsInt rule_type) { return "Dual fixing"; } else if (rule_type == kPresolveRuleColStuffing) { return "Col stuffing"; + } else if (rule_type == kPresolveRuleDfprobing) { + return "Dual-fixing probing"; } else if (rule_type == kPresolveRuleInitialSweep) { return "Initial sweep"; } diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index 06298d71445..063918be135 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -477,8 +477,6 @@ struct HighsOptionsStruct { bool less_infeasible_DSE_check; bool less_infeasible_DSE_choose_row; bool use_original_HFactor_logic; - bool presolve_dfprobing; - bool presolve_gdf; // bool allow_pdlp_cleanup; bool run_centring; HighsInt max_centring_steps; @@ -1765,17 +1763,6 @@ class HighsOptions : public HighsOptionsStruct { advanced, ¢ring_ratio_tolerance, 0, 100, kHighsInf); records.push_back(record_double); - record_bool = new OptionRecordBool( - "presolve_dfprobing", - "Use the dual fixing aumgented probing technique in presolve", advanced, - &presolve_dfprobing, true); - records.push_back(record_bool); - - record_bool = new OptionRecordBool( - "presolve_gdf", "Use the generalized dual fixing technique in presolve", - advanced, &presolve_gdf, true); - records.push_back(record_bool); - // Set up the log_options aliases log_options.clear(); log_options.log_stream = diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index ec7c698c27a..db53e7c0a24 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -640,9 +640,9 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange( HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation( const DualfixingProbingPropagation& other) - : redundantPropagateFlag_(other.redundantPropagateFlag_), - redundantPropagateVec_(other.redundantPropagateVec_), - zeroCostVarsDirection_(other.zeroCostVarsDirection_), + : redundantPropagateFlags_(other.redundantPropagateFlags_), + redundantPropagateInds_(other.redundantPropagateInds_), + zeroCostDirections_(other.zeroCostDirections_), zeroCostFixedVariables_(other.zeroCostFixedVariables_), colLowerLockOriginal_(other.colLowerLockOriginal_), colUpperLockOriginal_(other.colUpperLockOriginal_), @@ -650,23 +650,16 @@ HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation( colUpperLockReduced_(other.colUpperLockReduced_), candidatesVec_(other.candidatesVec_), candidatesFlag_(other.candidatesFlag_), - lockNeedClear_(other.lockNeedClear_), - gdfCandidatesVec_(other.gdfCandidatesVec_), - gdfCandidatesFlag_(other.gdfCandidatesFlag_), - gdfLbReachable0_(other.gdfLbReachable0_), - gdfLbReachable1_(other.gdfLbReachable1_), - gdfUbReachable0_(other.gdfUbReachable0_), - gdfUbReachable1_(other.gdfUbReachable1_) { + lockNeedClear_(other.lockNeedClear_) { ; } void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { mipsolver = domain->mipsolver; - redundantPropagateFlag_.assign(2 * mipsolver->numRow(), false); - redundantPropagateVec_.clear(); - redundantPropagateVec_.reserve(2 * mipsolver->numRow()); - zeroCostVarsDirection_.assign(2 * mipsolver->numCol(), - FIXDIRECTION_NOT_DECIDED); + redundantPropagateFlags_.assign(2 * mipsolver->numRow(), false); + redundantPropagateInds_.clear(); + redundantPropagateInds_.reserve(2 * mipsolver->numRow()); + zeroCostDirections_.assign(2 * mipsolver->numCol(), FixUnDecided); zeroCostFixedVariables_.clear(); zeroCostFixedVariables_.reserve(2 * mipsolver->numCol()); @@ -684,15 +677,6 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { lockNeedClear_.clear(); lockNeedClear_.reserve(mipsolver->numCol()); - gdfCandidatesVec_.clear(); - gdfCandidatesVec_.reserve(mipsolver->numCol()); - gdfCandidatesFlag_.assign(mipsolver->numCol(), false); - - gdfLbReachable0_.assign(mipsolver->numCol(), 0); - gdfLbReachable1_.assign(mipsolver->numCol(), 0); - gdfUbReachable0_.assign(mipsolver->numCol(), 0); - gdfUbReachable1_.assign(mipsolver->numCol(), 0); - // compute the original locks for each variable const auto model = mipsolver->model_; for (HighsInt iCol = 0; iCol < model->a_matrix_.num_col_; iCol++) { @@ -714,14 +698,14 @@ void HighsDomain::DualfixingProbingPropagation::updateRhsRedundant( if (!isEnabled()) return; if (domain->activitymaxinf_[row] != 0 || - redundantPropagateFlag_[2 * row + 1] || + redundantPropagateFlags_[2 * row + 1] || mipsolver->model_->row_upper_[row] == kHighsInf) return; if (domain->getMaxActivity(row) <= mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol) { - redundantPropagateVec_.push_back(2 * row + 1); - redundantPropagateFlag_[2 * row + 1] = 1; + redundantPropagateInds_.push_back(2 * row + 1); + redundantPropagateFlags_[2 * row + 1] = 1; } } @@ -729,14 +713,14 @@ void HighsDomain::DualfixingProbingPropagation::updateLhsRedundant( HighsInt row) { if (!isEnabled()) return; - if (domain->activitymininf_[row] != 0 || redundantPropagateFlag_[2 * row] || + if (domain->activitymininf_[row] != 0 || redundantPropagateFlags_[2 * row] || mipsolver->model_->row_lower_[row] == -kHighsInf) return; if (domain->getMinActivity(row) >= mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol) { - redundantPropagateVec_.push_back(2 * row); - redundantPropagateFlag_[2 * row] = 1; + redundantPropagateInds_.push_back(2 * row); + redundantPropagateFlags_[2 * row] = 1; } } @@ -839,20 +823,20 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // only record - we do not actually fix them now as their objective // coefficients are zero auto collectFixLower = [&](int iCol) { - zeroCostFixedVariables_.emplace_back(iCol, FIXDIRECTION_LOWER_BOUND); + zeroCostFixedVariables_.emplace_back(iCol, FixLowerBound); }; auto collectFixUpper = [&](int iCol) { - zeroCostFixedVariables_.emplace_back(iCol, FIXDIRECTION_UPPER_BOUND); + zeroCostFixedVariables_.emplace_back(iCol, FixUpperBound); }; // exit if no new redundant constraints are found - HighsInt maxLockLeft = redundantPropagateVec_.size() - previousSize_; + HighsInt maxLockLeft = redundantPropagateInds_.size() - previousSize_; if (maxLockLeft == 0) return; - for (; previousSize_ < redundantPropagateVec_.size(); + for (; previousSize_ < redundantPropagateInds_.size(); ++previousSize_, --maxLockLeft) { - const HighsInt i = redundantPropagateVec_[previousSize_]; + const HighsInt i = redundantPropagateInds_[previousSize_]; const HighsInt iRow = i / 2; assert(iRow < mipsolver->numRow()); @@ -939,32 +923,32 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { mipsolver->options_mip_->dual_feasibility_tolerance) { if (startZeroCostFixing_) { // not fixed before - if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_NOT_DECIDED) { + if (zeroCostDirections_[iCol] == FixUnDecided) { // both directions are ok - depending on cost (no tolerance) if (canBeFixedToLower && canBeFixedToUpper) { if (mipsolver->model_->col_cost_[iCol] >= 0) { addFixLower(iCol); - zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; + zeroCostDirections_[iCol] = FixLowerBound; } else { addFixUpper(iCol); - zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; + zeroCostDirections_[iCol] = FixUpperBound; } } // fix depending on the direction else if (canBeFixedToLower) { addFixLower(iCol); - zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; + zeroCostDirections_[iCol] = FixLowerBound; } else if (canBeFixedToUpper) { addFixUpper(iCol); - zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; + zeroCostDirections_[iCol] = FixUpperBound; } } // fix to lb - else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_LOWER_BOUND && + else if (zeroCostDirections_[iCol] == FixLowerBound && canBeFixedToLower) addFixLower(iCol); // fix to ub - else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_UPPER_BOUND && + else if (zeroCostDirections_[iCol] == FixUpperBound && canBeFixedToUpper) addFixUpper(iCol); @@ -974,27 +958,27 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // directions else { // not fixed before - if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_NOT_DECIDED) { + if (zeroCostDirections_[iCol] == FixUnDecided) { // both directions are ok - depending on cost (no tolerance) if (canBeFixedToLower && canBeFixedToUpper) { if (mipsolver->model_->col_cost_[iCol] >= 0) { collectFixLower(iCol); - zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; + zeroCostDirections_[iCol] = FixLowerBound; } else { collectFixUpper(iCol); - zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; + zeroCostDirections_[iCol] = FixUpperBound; } } else if (canBeFixedToLower) { // fix to lower and set its direction collectFixLower(iCol); - zeroCostVarsDirection_[iCol] = FIXDIRECTION_LOWER_BOUND; + zeroCostDirections_[iCol] = FixLowerBound; } else if (canBeFixedToUpper) { collectFixUpper(iCol); - zeroCostVarsDirection_[iCol] = FIXDIRECTION_UPPER_BOUND; + zeroCostDirections_[iCol] = FixUpperBound; } - } else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_UPPER_BOUND && + } else if (zeroCostDirections_[iCol] == FixUpperBound && canBeFixedToUpper) { // fix to upper collectFixUpper(iCol); - } else if (zeroCostVarsDirection_[iCol] == FIXDIRECTION_LOWER_BOUND && + } else if (zeroCostDirections_[iCol] == FixLowerBound && canBeFixedToLower) { // fix to lower collectFixLower(iCol); } @@ -1042,184 +1026,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } // record the current number of redundant constraints - previousSize_ = redundantPropagateVec_.size(); -} - -void HighsDomain::DualfixingProbingPropagation::updateGDFInfo( - HighsInt probing_variable, bool val) { - // tool lambda functions - auto addToCandidate = [&](HighsInt k) { - if (gdfCandidatesFlag_[k]) - return; - else { - gdfCandidatesVec_.push_back(k); - gdfCandidatesFlag_[k] = true; - } - }; - - // only redundant constraints are useful in GDF - for (const auto x : redundantPropagateVec_) { - const HighsInt iRow = x / 2; - const bool isRhs = x % 2; - HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; - HighsInt rend = mipsolver->mipdata_->ARstart_[iRow + 1]; - - for (auto k = rstart; k < rend; k++) { - const HighsInt iCol = mipsolver->mipdata_->ARindex_[k]; - const double iValue = mipsolver->mipdata_->ARvalue_[k]; - const double cost = mipsolver->model_->col_cost_[iCol]; - bool considered = false; - if (mipsolver->model_->col_lower_[iCol] == - mipsolver->model_->col_upper_[iCol] || - mipsolver->mipdata_->implications.colsubstituted[iCol]) - continue; - - if (iValue > 0) { - if (isRhs) { // consider upper bound reachable - const double globalUb = mipsolver->model_->col_upper_[iCol]; - const double probingUb = domain->col_upper_[iCol]; - if (!ableToFixToUb(iCol) || domain->getMaxActivity(iRow) == kHighsInf) - continue; - const bool upper_bound_reachable = - domain->getMaxActivity(iRow) + iValue * (globalUb - probingUb) <= - mipsolver->model_->row_upper_[iRow] + domain->feastol(); - if (upper_bound_reachable) { - considered = true; - // special treat if the current variable is the probing variable - if (iCol == probing_variable && val == 0) { - gdfUbReachable0_[iCol]++; - gdfUbReachable1_[iCol]++; - } else { - if (val == 0) gdfUbReachable0_[iCol]++; - if (val == 1) gdfUbReachable1_[iCol]++; - } - } - } else { // consider lower bound reachable - const double globalLb = mipsolver->model_->col_lower_[iCol]; - const double probingLb = domain->col_lower_[iCol]; - if (!ableToFixToLb(iCol) || - domain->getMinActivity(iRow) == -kHighsInf) - continue; - const bool lower_bound_reachable = - domain->getMinActivity(iRow) + iValue * (globalLb - probingLb) >= - mipsolver->model_->row_lower_[iRow] - domain->feastol(); - if (lower_bound_reachable) { - considered = true; - // special treat if the current variable is the probing variable - if (iCol == probing_variable && val == 1) { - gdfLbReachable0_[iCol]++; - gdfLbReachable1_[iCol]++; - } else { - if (val == 0) gdfLbReachable0_[iCol]++; - if (val == 1) gdfLbReachable1_[iCol]++; - } - } - } - } - - else { - if (isRhs) { // consider lower bound reachable - const double globalLb = mipsolver->model_->col_lower_[iCol]; - const double probingLb = domain->col_lower_[iCol]; - if (!ableToFixToLb(iCol) || domain->getMaxActivity(iRow) == kHighsInf) - continue; - const bool lower_bound_reachable = - domain->getMaxActivity(iRow) + iValue * (globalLb - probingLb) <= - mipsolver->model_->row_upper_[iRow] + domain->feastol(); - if (lower_bound_reachable) { - considered = true; - // special treat if the current variable is the probing variable - if (iCol == probing_variable && val == 1) { - gdfLbReachable0_[iCol]++; - gdfLbReachable1_[iCol]++; - } else { - if (val == 0) gdfLbReachable0_[iCol]++; - if (val == 1) gdfLbReachable1_[iCol]++; - } - } - } else { // consider upper bound reachable - const double globalUb = mipsolver->model_->col_upper_[iCol]; - const double probingUb = domain->col_upper_[iCol]; - if (!ableToFixToUb(iCol) || - domain->getMinActivity(iRow) == -kHighsInf) - continue; - const bool upper_bound_reachable = - domain->getMinActivity(iRow) + iValue * (globalUb - probingUb) >= - mipsolver->model_->row_lower_[iRow] - domain->feastol(); - if (upper_bound_reachable) { - considered = true; - // special treat if the current variable is the probing variable - if (iCol == probing_variable && val == 0) { - gdfUbReachable0_[iCol]++; - gdfUbReachable1_[iCol]++; - } else { - if (val == 0) gdfUbReachable0_[iCol]++; - if (val == 1) gdfUbReachable1_[iCol]++; - } - } - } - } - - if (considered) addToCandidate(iCol); - } - } -} - -HighsInt HighsDomain::DualfixingProbingPropagation::processGDFFixing() { - std::vector gdfFixingStack_; - - // derive global fixings from the GDF information - for (const auto iCol : gdfCandidatesVec_) { - const HighsInt lowerLock = colLowerLockOriginal_[iCol]; - const HighsInt upperLock = colUpperLockOriginal_[iCol]; - if (ableToFixToLb(iCol) && lowerLock > 0 && - gdfLbReachable0_[iCol] == lowerLock && - gdfLbReachable1_[iCol] == lowerLock) { - HighsDomainChange* thisbchg = new HighsDomainChange; - thisbchg->column = iCol; - thisbchg->boundtype = HighsBoundType::kUpper; - thisbchg->boundval = domain->col_lower_[iCol]; - gdfFixingStack_.push_back(thisbchg); - } - // a variable cannot be fixed to lb and ub simultaneously - else if (ableToFixToUb(iCol) && upperLock > 0 && - gdfUbReachable0_[iCol] == upperLock && - gdfUbReachable1_[iCol] == upperLock) { - HighsDomainChange* thisbchg = new HighsDomainChange; - thisbchg->column = iCol; - thisbchg->boundtype = HighsBoundType::kLower; - thisbchg->boundval = domain->col_upper_[iCol]; - gdfFixingStack_.push_back(thisbchg); - } - } - - // apply bound change - size_t j = 0; - for (; j != gdfFixingStack_.size() && !domain->infeasible_; ++j) { - domain->changeBound(*gdfFixingStack_[j], Reason::unspecified()); - delete gdfFixingStack_[j]; - } - - // clear the remaining domain changes if infeasible - for (; j < gdfFixingStack_.size(); ++j) { - assert(domain->infeasible_); - delete gdfFixingStack_[j]; - } - - gdfFixingStack_.clear(); - - return (HighsInt)j; -} - -void HighsDomain::DualfixingProbingPropagation::clearGDFInfo() { - for (const auto x : gdfCandidatesVec_) { - gdfLbReachable0_[x] = 0; - gdfLbReachable1_[x] = 0; - gdfUbReachable0_[x] = 0; - gdfUbReachable1_[x] = 0; - gdfCandidatesFlag_[x] = false; - } - gdfCandidatesVec_.clear(); + previousSize_ = redundantPropagateInds_.size(); } namespace highs { @@ -2981,9 +2788,7 @@ bool HighsDomain::propagate() { if (!conflictprop.propagateConflictInds_.empty()) return true; } - if (!infeasible_ && dfprobingPropagation.isActive() && - mipsolver->options_mip_->presolve_dfprobing) - return true; + if (!infeasible_ && dfprobingPropagation.isActive()) return true; return false; }; @@ -3161,10 +2966,7 @@ bool HighsDomain::propagate() { } } - if (!infeasible_ && dfprobingPropagation.isActive() && - mipsolver->options_mip_->presolve_dfprobing) { - // std::cout << "Activated by nRedundantIndices = " << - // dfprobingPropagation.redundantPropagateVec_.size() << std::endl; + if (!infeasible_ && dfprobingPropagation.isActive()) { dfprobingPropagation.propagate(); if (!havePropagationRows() && !dfprobingPropagation.isZeroObjFixingEnabled()) { diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 978ade04852..167dd37f6f2 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -240,17 +240,17 @@ class HighsDomain { HighsDomain* domain; HighsMipSolver* mipsolver; - // row lower and upper, length = 2 * rownum - std::vector redundantPropagateFlag_; - std::vector redundantPropagateVec_; - - // For zero-cost variables, we need to know which direction we can fix them - enum DFPROBING_FIX_DIRECTION { - FIXDIRECTION_NOT_DECIDED = 0, - FIXDIRECTION_LOWER_BOUND, - FIXDIRECTION_UPPER_BOUND, + // store row lower and row upper at 2i and 2i + 1 + std::vector redundantPropagateFlags_; + std::vector redundantPropagateInds_; + + // Track direction of zero fixings so we don't store disagreeing results + enum DfprobingFixDirection { + FixUnDecided, + FixLowerBound, + FixUpperBound, }; - std::vector zeroCostVarsDirection_; + std::vector zeroCostDirections_; std::vector> zeroCostFixedVariables_; // Flag and position in the domchgstack of the first zero-cost variable that @@ -272,64 +272,52 @@ class HighsDomain { std::vector candidatesFlag_; std::unordered_set lockNeedClear_; - // temporary buffers for GDF - std::vector gdfCandidatesVec_; - std::vector gdfCandidatesFlag_; - - // GDF reachable-row counts, indexed by column id. For each - // variable touched during probing, we only need to know how many - // rows make this variable lower/upper bound reachable. - std::vector gdfLbReachable0_; - std::vector gdfLbReachable1_; - std::vector gdfUbReachable0_; - std::vector gdfUbReachable1_; - void enablePropagator() { enabled_ = true; } void disablePropagator() { enabled_ = false; } - bool isEnabled() { return enabled_; } + bool isEnabled() const { return enabled_; } // active only when new redundant rows are found. - bool isActive() { - return enabled_ && redundantPropagateVec_.size() > previousSize_; + bool isActive() const { + return enabled_ && redundantPropagateInds_.size() > previousSize_; } // mark the position when the first zero-cost variable can be fixed to its // lower or upper bound. void setZeroCostFixingPosition(HighsInt v) { zeroCostStartPos_ = v; } - size_t getZeroCostFixingPosition() { return zeroCostStartPos_; } + size_t getZeroCostFixingPosition() const { return zeroCostStartPos_; } void enableZeroObjFixing() { startZeroCostFixing_ = true; } void disableZeroObjFixing() { startZeroCostFixing_ = false; } - bool isZeroObjFixingEnabled() { return startZeroCostFixing_; } + bool isZeroObjFixingEnabled() const { return startZeroCostFixing_; } - bool ableToFixToLb(int col) { + bool ableToFixToLb(const HighsInt col) const { return mipsolver->model_->col_cost_[col] >= -mipsolver->options_mip_->dual_feasibility_tolerance && - mipsolver->model_->col_lower_[col] > -kHighsInf; + mipsolver->model_->col_lower_[col] != -kHighsInf; } - bool ableToFixToUb(int col) { + bool ableToFixToUb(const HighsInt col) const { return mipsolver->model_->col_cost_[col] <= mipsolver->options_mip_->dual_feasibility_tolerance && - mipsolver->model_->col_upper_[col] < kHighsInf; + mipsolver->model_->col_upper_[col] != kHighsInf; } // remove redundant information void clearRedundantInfo() { previousSize_ = 0; - if (!redundantPropagateVec_.empty()) { // clear buffers - for (const auto x : redundantPropagateVec_) - redundantPropagateFlag_[x] = false; + if (!redundantPropagateInds_.empty()) { // clear buffers + for (const auto x : redundantPropagateInds_) + redundantPropagateFlags_[x] = false; - redundantPropagateVec_.clear(); + redundantPropagateInds_.clear(); } - for (size_t i = 0; i < redundantPropagateFlag_.size(); ++i) + for (size_t i = 0; i < redundantPropagateFlags_.size(); ++i) assert(!redundantPropagateFlag_[i]); zeroCostFixedVariables_.clear(); @@ -351,11 +339,6 @@ class HighsDomain { void updateRhsRedundant(HighsInt row); void updateLhsRedundant(HighsInt row); void propagate(); - - // functionalities for GDF - void updateGDFInfo(HighsInt probing_variable, bool val); - HighsInt processGDFFixing(); - void clearGDFInfo(); }; private: @@ -470,13 +453,12 @@ class HighsDomain { std::vector branchPos_; HighsHashTable redundantRows_; bool recordRedundantRows_ = false; + bool inPresolveProbing_ = false; public: std::vector col_lower_; std::vector col_upper_; - bool inProbing_ = false; - HighsDomain(HighsMipSolver& mipsolver); HighsDomain(const HighsDomain& other) @@ -819,6 +801,10 @@ class HighsDomain { DualfixingProbingPropagation& getDfProbingPropagation() { return dfprobingPropagation; } + + void setInPresolveProbing(bool val) { inPresolveProbing_ = val; } + + bool getInPresolveProbing() const { return inPresolveProbing_; } }; #endif diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 787c911c12e..d8d7398a00a 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -27,13 +27,8 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const auto& domchgreason = globaldomain.getDomainChangeReason(); size_t changedend = globaldomain.getChangedCols().size(); - // get two flags - const bool useDFProbing = - globaldomain.inProbing_ && mipsolver.options_mip_->presolve_dfprobing; - const bool useGDF = - globaldomain.inProbing_ && mipsolver.options_mip_->presolve_gdf; - // record redundant rows if any of the two flags is true - if (useDFProbing || useGDF) { + bool dfprobingEnabled = globaldomain.getInPresolveProbing(); + if (dfprobingEnabled) { globaldomain.getDfProbingPropagation().clearRedundantInfo(); globaldomain.getDfProbingPropagation().enablePropagator(); } @@ -64,8 +59,9 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { auto isInfeasible = [&](HighsInt col, bool val) { if (!globaldomain.infeasible()) return false; - if (globaldomain.inProbing_) + if (dfprobingEnabled) { globaldomain.getDfProbingPropagation().disablePropagator(); + } storeLiftingOpportunities(col, val); doBacktrack(changedend); cliquetable.vertexInfeasible(globaldomain, col, val); @@ -75,8 +71,9 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { if (isInfeasible(col, val)) return true; globaldomain.propagate(); - if (useDFProbing || useGDF) + if (dfprobingEnabled) { globaldomain.getDfProbingPropagation().disablePropagator(); + } if (isInfeasible(col, val)) return true; @@ -89,26 +86,27 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { implics.reserve(numImplications); // data structure to cache implications for non-binary variables - std::vector implics_tentative; - implics_tentative.reserve(numImplications); + std::vector tentativeImplics; + tentativeImplics.reserve(numImplications); HighsInt numEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); HighsInt maxEntries = 100000 + mipsolver.numNonzero(); const HighsInt tentativeStart = - useDFProbing + dfprobingEnabled ? globaldomain.getDfProbingPropagation().getZeroCostFixingPosition() : kHighsIInf32; - if (useDFProbing) - implics_tentative.assign(domchgstack.begin() + stackimplicstart, + if (dfprobingEnabled) { + tentativeImplics.assign(domchgstack.begin() + stackimplicstart, domchgstack.begin() + stackimplicend); + } for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { if (domchgreason[i].type == HighsDomain::Reason::kCliqueTable && ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) continue; - if (i >= tentativeStart) // record tentative implications + if (i >= tentativeStart) continue; implics.push_back(domchgstack[i]); @@ -117,23 +115,20 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // inform caller about lifting opportunities storeLiftingOpportunities(col, val); - // update information to derive generalized dual fixings - if (useGDF) globaldomain.getDfProbingPropagation().updateGDFInfo(col, val); - // backtrack doBacktrack(changedend); - if (!implics_tentative.empty()) { + if (!tentativeImplics.empty()) { // add the implications of binary variables to the clique table auto binstart_tmp = - std::partition(implics_tentative.begin(), implics_tentative.end(), + std::partition(tentativeImplics.begin(), tentativeImplics.end(), [&](const HighsDomainChange& a) { return !globaldomain.isBinary(a.column); }); // store the tentative bound changes of binary variables separately - for (auto i = binstart_tmp; i != implics_tentative.end(); ++i) + for (auto i = binstart_tmp; i != tentativeImplics.end(); ++i) recordTentativeCliques(val, *i); - implics_tentative.erase(binstart_tmp, implics_tentative.end()); + tentativeImplics.erase(binstart_tmp, tentativeImplics.end()); } // add the implications of binary variables to the clique table @@ -193,9 +188,9 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { implications[loc].implics = std::move(implics); this->numImplications += implications[loc].implics.size(); } - if (!implics_tentative.empty()) { - pdqsort(implics_tentative.begin(), implics_tentative.end()); - implications[loc].implics_tentative = std::move(implics_tentative); + if (!tentativeImplics.empty()) { + pdqsort(tentativeImplics.begin(), tentativeImplics.end()); + implications[loc].tentativeImplics = std::move(tentativeImplics); } return false; @@ -350,17 +345,13 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (globaldomain.isBinary(col) && !implicationsCached(col, 1) && !implicationsCached(col, 0) && mipsolver.mipdata_->cliquetable.getSubstitution(col) == nullptr) { - const bool useDFProbing = - globaldomain.inProbing_ && mipsolver.options_mip_->presolve_dfprobing; - const bool useGDF = - globaldomain.inProbing_ && mipsolver.options_mip_->presolve_gdf; - // setup for dfprobingPropagation - if (useDFProbing) { + + const bool enableDfprobing = globaldomain.getInPresolveProbing(); + if (enableDfprobing) { clearTentativeClique(); globaldomain.getDfProbingPropagation().setZeroCostFixingPosition( kHighsIInf32); } - if (useGDF) globaldomain.getDfProbingPropagation().clearGDFInfo(); bool infeasible = computeImplications(col, 1); if (globaldomain.infeasible()) return true; @@ -374,97 +365,65 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (mipsolver.mipdata_->cliquetable.getSubstitution(col) != nullptr) return true; - if (useDFProbing && !binaryInvolvedInds_.empty()) { + if (enableDfprobing && !binaryInvolvedInds_.empty()) { HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; HighsCliqueTable::CliqueVar clique[2]; - // Loop over binary variables that are tighened at least once - for (auto k : binaryInvolvedInds_) { - // Skip non-binary variables (being fixed now) or those can be - // substituted by other binary variables + for (HighsInt k : binaryInvolvedInds_) { if (!globaldomain.isBinary(k) || colsubstituted[k]) continue; - // Return if infeasible if (globaldomain.infeasible()) return true; - // Get the information how x[k] is fixed in probing on x[col] = 0 and - // x[col] = 1 For the meaning of ``data'', please see lines 71-89 in - // HighsImplications.h - uint8_t data = binaryInvolvedFlags_[k]; - if (data == 0) // flag for no reduction + uint8_t mask = binaryInvolvedFlags_[k]; + if (mask == 0) continue; - if (data == - binaryFixType::kGlobalLower) { // x[k] is fixed at 0 under both - // x[col] = 0 and x[col] = 1 - // fix x[k] = 0 by adding two cliques (i.e., these two cliques should - // be added in computeImplications() to derive global reductions) + if (mask == 10) { clique[0] = HighsCliqueTable::CliqueVar(col, 0); clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); - data = 0; - } else if (data == binaryFixType::kGlobalUpper) { // x[k] is fixed at 1 - // under both x[col] - // = 0 and x[col] = 1 - // fix x[k] = 1 by adding two cliques (i.e., these two cliques should - // be added in computeImplications() to derive global reductions) + mask = 0; + } else if (mask == 5) { clique[0] = HighsCliqueTable::CliqueVar(col, 0); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); - data = 0; - } else if (data == - binaryFixType:: - kSubstituteComplement) { // x[k] is fixed at 0 under - // x[col] = 1, and is fixed at - // 1 under x[col] = 0; this - // makes x[col] + x[k] = 1 - // Adding two cliques (i.e., these two cliques should be added in - // computeImplications() to derive global reductions) + mask = 0; + } else if (mask == 9) { clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); clique[0] = HighsCliqueTable::CliqueVar(col, 0); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); - data = 0; - } else if (data == - binaryFixType:: - kSubstituteEqual) { // x[k] is fixed at 0 under x[col] = - // 0, and is fixed at 1 under x[col] - // = 1; this makes x[col] = x[k] - // Adding two cliques (i.e., these two cliques should be added in - // computeImplications() to derive global reductions) + mask = 0; + } else if (mask == 6) { clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); clique[0] = HighsCliqueTable::CliqueVar(col, 0); clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); - data = 0; + mask = 0; } } - // clear the tentative bound changes for binary variables obtained from - // probing on x[col] clearTentativeClique(); } // analyze implications - // also include the bound changes of non-binary variables here, to derive - // tighter global bounds and variable substitutions - const bool haveTentativeImplics_zero = - !implications[2 * col].implics_tentative.empty(); - const bool haveTentativeImplics_one = - !implications[2 * col + 1].implics_tentative.empty(); + const bool haveTentativeImplicsZeroProbe = + !implications[2 * col].tentativeImplics.empty(); + const bool haveTentativeImplicsOneProbe = + !implications[2 * col + 1].tentativeImplics.empty(); const std::vector& implicsdown = - haveTentativeImplics_zero ? getImplications_tentative(col, 0) + haveTentativeImplicsZeroProbe ? getTentativeImplications(col, 0) : getImplications(col, 0, infeasible); const std::vector& implicsup = - haveTentativeImplics_one ? getImplications_tentative(col, 1) + haveTentativeImplicsOneProbe ? getTentativeImplications(col, 1) : getImplications(col, 1, infeasible); HighsInt nimplicsdown = implicsdown.size(); HighsInt nimplicsup = implicsup.size(); @@ -532,17 +491,10 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { } // clear tentative implications - if (haveTentativeImplics_zero) - implications[2 * col].implics_tentative.clear(); - if (haveTentativeImplics_one) - implications[2 * col + 1].implics_tentative.clear(); - - if (useGDF) { - // fix variables using generalized dual fixing - HighsInt nfix = globaldomain.getDfProbingPropagation().processGDFFixing(); - // propagate if necessary - if (nfix > 0) globaldomain.propagate(); - } + if (haveTentativeImplicsZeroProbe) + implications[2 * col].tentativeImplics.clear(); + if (haveTentativeImplicsOneProbe) + implications[2 * col + 1].tentativeImplics.clear(); return true; } diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 9cdaf6b2bb3..e07ab92891a 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -25,13 +25,7 @@ class HighsImplications { struct Implics { std::vector implics; - /* The "tentative" implications: - A implication of type x_j \ge (\ell^1_j - \ell^0_j) x_k + \ell^0_j is - called "tentative", if (1) c_j = 0 (2) x_j is fixed by applying dual - fixing in probing These implications can only be used to perform globally - valid reductions. Therefore, special treatment is required. - */ - std::vector implics_tentative; + std::vector tentativeImplics; bool computed = false; }; std::vector implications; @@ -65,28 +59,17 @@ class HighsImplications { std::vector substitutions; std::vector colsubstituted; - // vector used to derive global reductions from dfprobing std::vector binaryInvolvedInds_; - enum binaryFixType { - kNoReduction = 0b0000, - kGlobalLower = 0b1010, - kGlobalUpper = 0b0101, - kSubstituteComplement = 0b1001, - kSubstituteEqual = 0b0110, - }; - /* - Possible values for binaryInvolvedFlags_ - 0 (0000, kNoReduction): Not involved - 2 (0010): fixed to 0 in second side probing - 1 (0001): fixed to 1 in second side probing - 8 (1000): fixed to 0 in first side probing - 4 (0100): fixed to 1 in first side probing - 10(1010, kGlobalLower): fixed to 0 in both side probing (global fixing!) - 5 (0101, kGlobalUpper): fixed to 1 in both side probing (global fixing!) - 9 (1001, kSubstituteComplement): substitutation type 1 --- x1 + x2 = 1 - 6 (0110, kSubstituteEqual): substitutation type 2 --- x1 = x2 - */ - std::vector binaryInvolvedFlags_; + // (0000) : Not involved + // (0010) : Fixed to lower in zero-side probing + // (0001) : Fixed to upper in zero-side probing + // (1000) : Fixed to lower in one-side probing + // (0100) : Fixed to upper in one-side probing + // (1010) : Fixed to lower in both sides. Fix to lower. + // (0101) : Fixed to upper in both sides. Fix to upper. + // (1001) : Conclude that x1 + x2 = 1 + // (0110) : Conclude that x1 = x2 + std::vector binaryInvolvedFlags_; HighsImplications(const HighsMipSolver& mipsolver) : mipsolver(mipsolver) { HighsInt numcol = mipsolver.numCol(); @@ -100,7 +83,7 @@ class HighsImplications { maxVarBounds = calcMaxVarBounds(numcol); binaryInvolvedInds_.reserve(numcol); - binaryInvolvedFlags_.assign(numcol, 0b0000); + binaryInvolvedFlags_.assign(numcol, 0); } std::function @@ -127,7 +110,7 @@ class HighsImplications { nextCleanupCall = mipsolver.numNonzero(); binaryInvolvedInds_.reserve(numcol); - binaryInvolvedFlags_.assign(numcol, 0b0000); + binaryInvolvedFlags_.assign(numcol, 0); } constexpr static int64_t calcMaxVarBounds(HighsInt numcol) { @@ -151,10 +134,10 @@ class HighsImplications { return implications[loc].implics; } - const std::vector& getImplications_tentative(HighsInt col, - bool val) { + const std::vector& getTentativeImplications(HighsInt col, + bool val) { HighsInt loc = 2 * col + val; - return implications[loc].implics_tentative; + return implications[loc].tentativeImplics; } bool implicationsCached(HighsInt col, bool val) { @@ -235,71 +218,23 @@ class HighsImplications { void applyImplications(HighsDomain& domain, HighsInt col, HighsInt val); - // collect tentative binary implications - void recordTentativeCliques(bool val, const HighsDomainChange& bchg) { - const int iCol = bchg.column; - if (val == 0) { // probing x_k = 0 - if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 - if (!isFixedTo1(val, iCol)) { - if (binaryInvolvedFlags_[iCol] == 0) - binaryInvolvedInds_.push_back(iCol); - binaryInvolvedFlags_[iCol] += 0b0001; // 0001 - } - } else { // fixed to 0 - if (!isFixedTo0(val, iCol)) { - if (binaryInvolvedFlags_[iCol] == 0) - binaryInvolvedInds_.push_back(iCol); - binaryInvolvedFlags_[iCol] += 0b0010; // 0010 - } - } - } else { // probing x_k = 1 - if (bchg.boundtype == HighsBoundType::kLower) { // fixed to 1 - if (!isFixedTo1(val, iCol)) { - if (binaryInvolvedFlags_[iCol] == 0) - binaryInvolvedInds_.push_back(iCol); - binaryInvolvedFlags_[iCol] += 0b0100; // 0100 - } - } else { // fixed to 0 - if (!isFixedTo0(val, iCol)) { - if (binaryInvolvedFlags_[iCol] == 0) - binaryInvolvedInds_.push_back(iCol); - binaryInvolvedFlags_[iCol] += 0b1000; // 1000 - } - } + void recordTentativeCliques(const HighsInt val, + const HighsDomainChange& domchg) { + const HighsInt col = domchg.column; + const uint8_t mask = + 1 << (2 * val + (domchg.boundtype != HighsBoundType::kLower)); + + if ((binaryInvolvedFlags_[col] & mask) == 0) { + if (binaryInvolvedFlags_[col] == 0) binaryInvolvedInds_.push_back(col); + + binaryInvolvedFlags_[col] |= mask; } } - // clear tentative binary implications + void clearTentativeClique() { - for (auto iCol : binaryInvolvedInds_) - binaryInvolvedFlags_[iCol] = binaryFixType::kNoReduction; + for (HighsInt col : binaryInvolvedInds_) binaryInvolvedFlags_[col] = 0; binaryInvolvedInds_.clear(); } - // tools for recordTentativeCliques - bool isFixedTo0(bool val, HighsInt iCol) { - if (binaryInvolvedFlags_[iCol] == 0) return false; - - uint8_t mask; - if (val == 0) { // probing at x = 0, last two digits - mask = 1 << (1); - return (binaryInvolvedFlags_[iCol] & mask) != 0; - } else { // probing at x = 1, first two digits - mask = 1 << (3); - return (binaryInvolvedFlags_[iCol] & mask) != 0; - } - } - // tools for recordTentativeCliques - bool isFixedTo1(bool val, HighsInt iCol) { - if (binaryInvolvedFlags_[iCol] == 0) return false; - - uint8_t mask; - if (val == 0) { // probing at x = 0, last two digits - mask = 1; - return (binaryInvolvedFlags_[iCol] & mask) != 0; - } else { // probint at x = 1, first two digits - mask = 1 << (2); - return (binaryInvolvedFlags_[iCol] & mask) != 0; - } - } }; #endif diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 93201952049..e3ff401d0ab 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1863,9 +1863,10 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { } }; - // setup for dfprobing and gdf - if (options->presolve_dfprobing || options->presolve_gdf) + const bool enableDfprobing = allow_rule_[kPresolveRuleDfprobing]; + if (enableDfprobing) { domain.getDfProbingPropagation().recomputeLocks(); + } for (const auto& binvar : binaries) { // Count the binaries considered @@ -1932,9 +1933,9 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { HighsInt numBoundChgs = 0; HighsInt numNewCliques = -cliquetable.numCliques(); - domain.inProbing_ = true; + domain.setInPresolveProbing(enableDfprobing); const bool probing_result = implications.runProbing(i, numBoundChgs); - domain.inProbing_ = false; + domain.setInPresolveProbing(false); if (!probing_result) continue; probingContingent += numBoundChgs; numNewCliques += cliquetable.numCliques(); From 98b3cbe8d6a836f55c2f7c453e0367c9ec8f76a8 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 18 Aug 2026 14:54:22 +0200 Subject: [PATCH 27/46] More minor changes --- highs/mip/HighsDomain.cpp | 2 +- highs/mip/HighsDomain.h | 16 +++++++++++----- highs/mip/HighsImplications.cpp | 7 +++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index db53e7c0a24..c05526887ec 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -856,7 +856,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { colUpperLockOriginal_[iCol]; if (iValue > 0 && - cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { + cost >= -mipsolver->options_mip_->dual_feasibility_tolerance) { lockNeedClear_.insert(iCol); colLowerLockReduced_[iCol]++; lowerNoInsert = diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 167dd37f6f2..24d0e2ce6d6 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -272,9 +272,7 @@ class HighsDomain { std::vector candidatesFlag_; std::unordered_set lockNeedClear_; - void enablePropagator() { enabled_ = true; } - - void disablePropagator() { enabled_ = false; } + void setEnabled(const bool val) { enabled_ = val; } bool isEnabled() const { return enabled_; } @@ -307,8 +305,7 @@ class HighsDomain { mipsolver->model_->col_upper_[col] != kHighsInf; } - // remove redundant information - void clearRedundantInfo() { + void beginProbing() { previousSize_ = 0; if (!redundantPropagateInds_.empty()) { // clear buffers for (const auto x : redundantPropagateInds_) @@ -321,12 +318,21 @@ class HighsDomain { assert(!redundantPropagateFlag_[i]); zeroCostFixedVariables_.clear(); + zeroCostStartPos_ = kHighsIInf; + startZeroCostFixing_ = false; + setEnabled(true); for (const auto x : lockNeedClear_) colLowerLockReduced_[x] = colUpperLockReduced_[x] = 0; lockNeedClear_.clear(); } + void endProbing() { + setEnabled(false); + zeroCostFixedVariables_.clear(); + startZeroCostFixing_ = false; + } + DualfixingProbingPropagation() { ; }; DualfixingProbingPropagation(HighsDomain* domain) : domain(domain) {}; diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index d8d7398a00a..5f7323da350 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -29,8 +29,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { bool dfprobingEnabled = globaldomain.getInPresolveProbing(); if (dfprobingEnabled) { - globaldomain.getDfProbingPropagation().clearRedundantInfo(); - globaldomain.getDfProbingPropagation().enablePropagator(); + globaldomain.getDfProbingPropagation().beginProbing(); } HighsInt stackimplicstart = domchgstack.size() + 1; @@ -60,7 +59,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { auto isInfeasible = [&](HighsInt col, bool val) { if (!globaldomain.infeasible()) return false; if (dfprobingEnabled) { - globaldomain.getDfProbingPropagation().disablePropagator(); + globaldomain.getDfProbingPropagation().endProbing(); } storeLiftingOpportunities(col, val); doBacktrack(changedend); @@ -72,7 +71,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { globaldomain.propagate(); if (dfprobingEnabled) { - globaldomain.getDfProbingPropagation().disablePropagator(); + globaldomain.getDfProbingPropagation().endProbing(); } if (isInfeasible(col, val)) return true; From 4e6b530ce8c742fb94a21a0cf8bf55448e9dab5c Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 18 Aug 2026 15:35:12 +0200 Subject: [PATCH 28/46] Separate out zero cost fixings --- highs/mip/HighsDomain.cpp | 42 +++++++++++++++++++++++++++------------ highs/mip/HighsDomain.h | 19 +++++++++--------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index c05526887ec..3937e218186 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -643,7 +643,7 @@ HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation( : redundantPropagateFlags_(other.redundantPropagateFlags_), redundantPropagateInds_(other.redundantPropagateInds_), zeroCostDirections_(other.zeroCostDirections_), - zeroCostFixedVariables_(other.zeroCostFixedVariables_), + fixedZeroCostColumns_(other.fixedZeroCostColumns_), colLowerLockOriginal_(other.colLowerLockOriginal_), colUpperLockOriginal_(other.colUpperLockOriginal_), colLowerLockReduced_(other.colLowerLockReduced_), @@ -659,9 +659,9 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { redundantPropagateFlags_.assign(2 * mipsolver->numRow(), false); redundantPropagateInds_.clear(); redundantPropagateInds_.reserve(2 * mipsolver->numRow()); - zeroCostDirections_.assign(2 * mipsolver->numCol(), FixUnDecided); - zeroCostFixedVariables_.clear(); - zeroCostFixedVariables_.reserve(2 * mipsolver->numCol()); + zeroCostDirections_.assign(mipsolver->numCol(), FixUnDecided); + fixedZeroCostColumns_.clear(); + fixedZeroCostColumns_.reserve(mipsolver->numCol()); startZeroCostFixing_ = false; previousSize_ = 0; @@ -823,11 +823,11 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // only record - we do not actually fix them now as their objective // coefficients are zero auto collectFixLower = [&](int iCol) { - zeroCostFixedVariables_.emplace_back(iCol, FixLowerBound); + fixedZeroCostColumns_.emplace_back(fixedZeroCostColumn{iCol, FixLowerBound}); }; auto collectFixUpper = [&](int iCol) { - zeroCostFixedVariables_.emplace_back(iCol, FixUpperBound); + fixedZeroCostColumns_.emplace_back(fixedZeroCostColumn{iCol, FixUpperBound}); }; // exit if no new redundant constraints are found @@ -890,7 +890,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { colUpperLockOriginal_[iCol]; if (iValue < 0 && - cost >= mipsolver->options_mip_->dual_feasibility_tolerance) { + cost >= -mipsolver->options_mip_->dual_feasibility_tolerance) { lockNeedClear_.insert(iCol); colLowerLockReduced_[iCol]++; lowerNoInsert = @@ -988,7 +988,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { } if (mipsolver->model_->col_cost_[iCol] >= - mipsolver->options_mip_->dual_feasibility_tolerance) { + -mipsolver->options_mip_->dual_feasibility_tolerance) { if (canBeFixedToLower) { // checkVariableLowerLock(iCol); addFixLower(iCol); @@ -1029,6 +1029,25 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { previousSize_ = redundantPropagateInds_.size(); } +void HighsDomain::DualfixingProbingPropagation::propagateZeroCosts() { + if (fixedZeroCostColumns_.empty()) return; + + zeroCostStartPos_ = domain->getDomainChangeStack().size(); + + for (const fixedZeroCostColumn& fixing : fixedZeroCostColumns_) { + if (domain->isFixed(fixing.col)) continue; + if (fixing.direction == FixLowerBound) { + domain->changeBound(HighsBoundType::kUpper, fixing.col, domain->col_lower_[fixing.col], Reason::unspecified()); + } else { + domain->changeBound(HighsBoundType::kLower, fixing.col, domain->col_upper_[fixing.col], Reason::unspecified()); + } + if (domain->infeasible()) break; + } + + fixedZeroCostColumns_.clear(); +} + + namespace highs { template <> struct RbTreeTraits< @@ -2968,11 +2987,8 @@ bool HighsDomain::propagate() { if (!infeasible_ && dfprobingPropagation.isActive()) { dfprobingPropagation.propagate(); - if (!havePropagationRows() && - !dfprobingPropagation.isZeroObjFixingEnabled()) { - dfprobingPropagation.enableZeroObjFixing(); - dfprobingPropagation.setZeroCostFixingPosition(domchgstack_.size()); - dfprobingPropagation.propagate(); + if (!infeasible_ && !havePropagationRows()) { + dfprobingPropagation.propagateZeroCosts(); } } } diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 24d0e2ce6d6..2a58b248594 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -250,8 +250,14 @@ class HighsDomain { FixLowerBound, FixUpperBound, }; + + struct fixedZeroCostColumn { + HighsInt col; + DfprobingFixDirection direction; + }; + std::vector zeroCostDirections_; - std::vector> zeroCostFixedVariables_; + std::vector fixedZeroCostColumns_; // Flag and position in the domchgstack of the first zero-cost variable that // can be fixed to its lower or upper bound. @@ -287,12 +293,6 @@ class HighsDomain { size_t getZeroCostFixingPosition() const { return zeroCostStartPos_; } - void enableZeroObjFixing() { startZeroCostFixing_ = true; } - - void disableZeroObjFixing() { startZeroCostFixing_ = false; } - - bool isZeroObjFixingEnabled() const { return startZeroCostFixing_; } - bool ableToFixToLb(const HighsInt col) const { return mipsolver->model_->col_cost_[col] >= -mipsolver->options_mip_->dual_feasibility_tolerance && @@ -317,7 +317,7 @@ class HighsDomain { for (size_t i = 0; i < redundantPropagateFlags_.size(); ++i) assert(!redundantPropagateFlag_[i]); - zeroCostFixedVariables_.clear(); + fixedZeroCostColumns_.clear(); zeroCostStartPos_ = kHighsIInf; startZeroCostFixing_ = false; setEnabled(true); @@ -329,7 +329,7 @@ class HighsDomain { void endProbing() { setEnabled(false); - zeroCostFixedVariables_.clear(); + fixedZeroCostColumns_.clear(); startZeroCostFixing_ = false; } @@ -345,6 +345,7 @@ class HighsDomain { void updateRhsRedundant(HighsInt row); void updateLhsRedundant(HighsInt row); void propagate(); + void propagateZeroCosts(); }; private: From c90c2eb111a39a8a711faa441a717bce76061440 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 18 Aug 2026 16:29:19 +0200 Subject: [PATCH 29/46] More fixes --- highs/mip/HighsDomain.cpp | 20 +++++++++++--------- highs/mip/HighsDomain.h | 12 ++++++------ highs/mip/HighsImplications.cpp | 3 ++- highs/mip/HighsImplications.h | 2 +- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 3937e218186..1b2075a5628 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -663,7 +663,7 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { fixedZeroCostColumns_.clear(); fixedZeroCostColumns_.reserve(mipsolver->numCol()); - startZeroCostFixing_ = false; + applyingZeroCostFixings_ = false; previousSize_ = 0; colLowerLockOriginal_.assign(mipsolver->numCol(), 0); @@ -913,16 +913,16 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { for (auto iCol : candidatesVec_) { if (domain->isFixed(iCol)) continue; - const bool canBeFixedToLower = + const bool canBeFixedToLower = ableToFixToLb(iCol) && colLowerLockReduced_[iCol] == colLowerLockOriginal_[iCol]; - const bool canBeFixedToUpper = + const bool canBeFixedToUpper = ableToFixToUb(iCol) && colUpperLockReduced_[iCol] == colUpperLockOriginal_[iCol]; if (!canBeFixedToLower && !canBeFixedToUpper) continue; if (fabs(mipsolver->model_->col_cost_[iCol]) <= mipsolver->options_mip_->dual_feasibility_tolerance) { - if (startZeroCostFixing_) { - // not fixed before + if (applyingZeroCostFixings_) { + // not fixed beforei if (zeroCostDirections_[iCol] == FixUnDecided) { // both directions are ok - depending on cost (no tolerance) if (canBeFixedToLower && canBeFixedToUpper) { @@ -1032,6 +1032,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { void HighsDomain::DualfixingProbingPropagation::propagateZeroCosts() { if (fixedZeroCostColumns_.empty()) return; + applyingZeroCostFixings_ = true; zeroCostStartPos_ = domain->getDomainChangeStack().size(); for (const fixedZeroCostColumn& fixing : fixedZeroCostColumns_) { @@ -1043,6 +1044,7 @@ void HighsDomain::DualfixingProbingPropagation::propagateZeroCosts() { } if (domain->infeasible()) break; } + applyingZeroCostFixings_ = false; fixedZeroCostColumns_.clear(); } @@ -1969,7 +1971,7 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, // then we cannot record redundant rows for lifting, as this bound change // could disregarded. if (recordRedundantRows_ && - !dfprobingPropagation.isZeroObjFixingEnabled() && + !dfprobingPropagation.isZeroCostFixingActive() && mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); @@ -2024,7 +2026,7 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, // then we cannot record redundant rows for lifting, as this bound change // could disregarded. if (recordRedundantRows_ && - !dfprobingPropagation.isZeroObjFixingEnabled() && + !dfprobingPropagation.isZeroCostFixingActive() && mip->row_lower_[mip->a_matrix_.index_[i]] == -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); @@ -2148,7 +2150,7 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, // then we cannot record redundant rows for lifting, as this bound change // could disregarded. if (recordRedundantRows_ && - !dfprobingPropagation.isZeroObjFixingEnabled() && + !dfprobingPropagation.isZeroCostFixingActive() && mip->row_lower_[mip->a_matrix_.index_[i]] == -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); @@ -2206,7 +2208,7 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, // then we cannot record redundant rows for lifting, as this bound change // could disregarded. if (recordRedundantRows_ && - !dfprobingPropagation.isZeroObjFixingEnabled() && + !dfprobingPropagation.isZeroCostFixingActive() && mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 2a58b248594..cd84f9ac795 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -259,9 +259,7 @@ class HighsDomain { std::vector zeroCostDirections_; std::vector fixedZeroCostColumns_; - // Flag and position in the domchgstack of the first zero-cost variable that - // can be fixed to its lower or upper bound. - bool startZeroCostFixing_ = false; + bool applyingZeroCostFixings_ = false; size_t zeroCostStartPos_; bool enabled_ = false; @@ -287,6 +285,8 @@ class HighsDomain { return enabled_ && redundantPropagateInds_.size() > previousSize_; } + bool isZeroCostFixingActive() const { return applyingZeroCostFixings_; } + // mark the position when the first zero-cost variable can be fixed to its // lower or upper bound. void setZeroCostFixingPosition(HighsInt v) { zeroCostStartPos_ = v; } @@ -315,11 +315,11 @@ class HighsDomain { } for (size_t i = 0; i < redundantPropagateFlags_.size(); ++i) - assert(!redundantPropagateFlag_[i]); + assert(!redundantPropagateFlags_[i]); fixedZeroCostColumns_.clear(); zeroCostStartPos_ = kHighsIInf; - startZeroCostFixing_ = false; + applyingZeroCostFixings_ = false; setEnabled(true); for (const auto x : lockNeedClear_) @@ -330,7 +330,7 @@ class HighsDomain { void endProbing() { setEnabled(false); fixedZeroCostColumns_.clear(); - startZeroCostFixing_ = false; + applyingZeroCostFixings_ = false; } DualfixingProbingPropagation() { ; }; diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 5f7323da350..71daa3e27e4 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -364,7 +364,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (mipsolver.mipdata_->cliquetable.getSubstitution(col) != nullptr) return true; - if (enableDfprobing && !binaryInvolvedInds_.empty()) { + if (enableDfprobing && !binaryInvolvedInds_.empty() && mipsolver.mipdata_->cliquetable.isFull()) { HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; HighsCliqueTable::CliqueVar clique[2]; for (HighsInt k : binaryInvolvedInds_) { @@ -407,6 +407,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { cliquetable.addClique(mipsolver, &clique[0], 2); mask = 0; } + if (globaldomain.infeasible()) return true; } clearTentativeClique(); diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index e07ab92891a..020d7dd4c36 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -69,7 +69,7 @@ class HighsImplications { // (0101) : Fixed to upper in both sides. Fix to upper. // (1001) : Conclude that x1 + x2 = 1 // (0110) : Conclude that x1 = x2 - std::vector binaryInvolvedFlags_; + std::vector binaryInvolvedFlags_; HighsImplications(const HighsMipSolver& mipsolver) : mipsolver(mipsolver) { HighsInt numcol = mipsolver.numCol(); From 63a0ffdc7049e104a2d90fd9ba5380f3a2e2e275 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 19 Aug 2026 14:45:20 +0200 Subject: [PATCH 30/46] Clean up a bit more --- highs/lp_data/HConst.h | 2 +- highs/mip/HighsDomain.cpp | 49 +++++++++++++++++---------------- highs/mip/HighsDomain.h | 26 ++++++++--------- highs/mip/HighsImplications.cpp | 10 +++---- highs/presolve/HPresolve.cpp | 4 +-- 5 files changed, 46 insertions(+), 45 deletions(-) diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index 567f2588674..067c107f428 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -281,7 +281,7 @@ enum PresolveRuleType : int { kPresolveRuleEnumeration, kPresolveRuleDualFixing, kPresolveRuleColStuffing, - kPresolveRuleDfprobing, + kPresolveRuleDualFixProbing, kPresolveRuleInitialSweep, kPresolveRuleMax = kPresolveRuleInitialSweep, kPresolveRuleLastAllowOff = kPresolveRuleMax, diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 1b2075a5628..0c3e77896f3 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -638,8 +638,8 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange( } } -HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation( - const DualfixingProbingPropagation& other) +HighsDomain::DualFixProbingPropagation::DualFixProbingPropagation( + const DualFixProbingPropagation& other) : redundantPropagateFlags_(other.redundantPropagateFlags_), redundantPropagateInds_(other.redundantPropagateInds_), zeroCostDirections_(other.zeroCostDirections_), @@ -654,12 +654,12 @@ HighsDomain::DualfixingProbingPropagation::DualfixingProbingPropagation( ; } -void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { +void HighsDomain::DualFixProbingPropagation::recomputeLocks() { mipsolver = domain->mipsolver; redundantPropagateFlags_.assign(2 * mipsolver->numRow(), false); redundantPropagateInds_.clear(); redundantPropagateInds_.reserve(2 * mipsolver->numRow()); - zeroCostDirections_.assign(mipsolver->numCol(), FixUnDecided); + zeroCostDirections_.assign(mipsolver->numCol(), FixUndecided); fixedZeroCostColumns_.clear(); fixedZeroCostColumns_.reserve(mipsolver->numCol()); @@ -678,22 +678,23 @@ void HighsDomain::DualfixingProbingPropagation::recomputeLocks() { lockNeedClear_.reserve(mipsolver->numCol()); // compute the original locks for each variable - const auto model = mipsolver->model_; - for (HighsInt iCol = 0; iCol < model->a_matrix_.num_col_; iCol++) { - for (HighsInt k = model->a_matrix_.start_[iCol]; - k < model->a_matrix_.start_[iCol + 1]; k++) { - const HighsInt iRow = model->a_matrix_.index_[k]; - const double iValue = model->a_matrix_.value_[k]; - const double lhs = model->row_lower_[iRow], rhs = model->row_upper_[iRow]; - if ((iValue > 0 && rhs != kHighsInf) || (iValue < 0 && lhs != -kHighsInf)) - colUpperLockOriginal_[iCol]++; - if ((iValue > 0 && lhs != -kHighsInf) || (iValue < 0 && rhs != kHighsInf)) - colLowerLockOriginal_[iCol]++; + const HighsLp* model = mipsolver->model_; + for (HighsInt col = 0; col < model->a_matrix_.num_col_; col++) { + for (HighsInt k = model->a_matrix_.start_[col]; + k < model->a_matrix_.start_[col + 1]; k++) { + const HighsInt row = model->a_matrix_.index_[k]; + const double val = model->a_matrix_.value_[k]; + const double lhs = model->row_lower_[row]; + const double rhs = model->row_upper_[row]; + if ((val > 0 && rhs != kHighsInf) || (val < 0 && lhs != -kHighsInf)) + colUpperLockOriginal_[col]++; + if ((val > 0 && lhs != -kHighsInf) || (val < 0 && rhs != kHighsInf)) + colLowerLockOriginal_[col]++; } } } -void HighsDomain::DualfixingProbingPropagation::updateRhsRedundant( +void HighsDomain::DualFixProbingPropagation::updateRhsRedundant( HighsInt row) { if (!isEnabled()) return; @@ -709,7 +710,7 @@ void HighsDomain::DualfixingProbingPropagation::updateRhsRedundant( } } -void HighsDomain::DualfixingProbingPropagation::updateLhsRedundant( +void HighsDomain::DualFixProbingPropagation::updateLhsRedundant( HighsInt row) { if (!isEnabled()) return; @@ -724,7 +725,7 @@ void HighsDomain::DualfixingProbingPropagation::updateLhsRedundant( } } -void HighsDomain::DualfixingProbingPropagation::propagate() { +void HighsDomain::DualFixProbingPropagation::propagate() { // The boolean variable ``startZeroCostFixing_'' is used to flag if we allow // variables with zero cost can be fixed in domain propagation. The process of // domain propagtion in probing is executed in two phases: @@ -823,11 +824,11 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // only record - we do not actually fix them now as their objective // coefficients are zero auto collectFixLower = [&](int iCol) { - fixedZeroCostColumns_.emplace_back(fixedZeroCostColumn{iCol, FixLowerBound}); + fixedZeroCostColumns_.emplace_back(FixedZeroCostColumn{iCol, FixLowerBound}); }; auto collectFixUpper = [&](int iCol) { - fixedZeroCostColumns_.emplace_back(fixedZeroCostColumn{iCol, FixUpperBound}); + fixedZeroCostColumns_.emplace_back(FixedZeroCostColumn{iCol, FixUpperBound}); }; // exit if no new redundant constraints are found @@ -923,7 +924,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { mipsolver->options_mip_->dual_feasibility_tolerance) { if (applyingZeroCostFixings_) { // not fixed beforei - if (zeroCostDirections_[iCol] == FixUnDecided) { + if (zeroCostDirections_[iCol] == FixUndecided) { // both directions are ok - depending on cost (no tolerance) if (canBeFixedToLower && canBeFixedToUpper) { if (mipsolver->model_->col_cost_[iCol] >= 0) { @@ -958,7 +959,7 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { // directions else { // not fixed before - if (zeroCostDirections_[iCol] == FixUnDecided) { + if (zeroCostDirections_[iCol] == FixUndecided) { // both directions are ok - depending on cost (no tolerance) if (canBeFixedToLower && canBeFixedToUpper) { if (mipsolver->model_->col_cost_[iCol] >= 0) { @@ -1029,13 +1030,13 @@ void HighsDomain::DualfixingProbingPropagation::propagate() { previousSize_ = redundantPropagateInds_.size(); } -void HighsDomain::DualfixingProbingPropagation::propagateZeroCosts() { +void HighsDomain::DualFixProbingPropagation::propagateZeroCosts() { if (fixedZeroCostColumns_.empty()) return; applyingZeroCostFixings_ = true; zeroCostStartPos_ = domain->getDomainChangeStack().size(); - for (const fixedZeroCostColumn& fixing : fixedZeroCostColumns_) { + for (const FixedZeroCostColumn& fixing : fixedZeroCostColumns_) { if (domain->isFixed(fixing.col)) continue; if (fixing.direction == FixLowerBound) { domain->changeBound(HighsBoundType::kUpper, fixing.col, domain->col_lower_[fixing.col], Reason::unspecified()); diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index cd84f9ac795..0df6549c5c7 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -236,7 +236,7 @@ class HighsDomain { void propagateConflict(HighsInt conflict); }; - struct DualfixingProbingPropagation { + struct DualFixProbingPropagation { HighsDomain* domain; HighsMipSolver* mipsolver; @@ -245,19 +245,19 @@ class HighsDomain { std::vector redundantPropagateInds_; // Track direction of zero fixings so we don't store disagreeing results - enum DfprobingFixDirection { - FixUnDecided, + enum DualFixProbingFixDirection { + FixUndecided, FixLowerBound, FixUpperBound, }; - struct fixedZeroCostColumn { + struct FixedZeroCostColumn { HighsInt col; - DfprobingFixDirection direction; + DualFixProbingFixDirection direction; }; - std::vector zeroCostDirections_; - std::vector fixedZeroCostColumns_; + std::vector zeroCostDirections_; + std::vector fixedZeroCostColumns_; bool applyingZeroCostFixings_ = false; size_t zeroCostStartPos_; @@ -333,13 +333,13 @@ class HighsDomain { applyingZeroCostFixings_ = false; } - DualfixingProbingPropagation() { ; }; + DualFixProbingPropagation() { ; }; - DualfixingProbingPropagation(HighsDomain* domain) : domain(domain) {}; + DualFixProbingPropagation(HighsDomain* domain) : domain(domain) {}; - DualfixingProbingPropagation(const DualfixingProbingPropagation& other); + DualFixProbingPropagation(const DualFixProbingPropagation& other); - ~DualfixingProbingPropagation() { ; }; + ~DualFixProbingPropagation() { ; }; void recomputeLocks(); void updateRhsRedundant(HighsInt row); @@ -433,7 +433,7 @@ class HighsDomain { private: std::deque cutpoolpropagation; std::deque conflictPoolPropagation; - DualfixingProbingPropagation dfprobingPropagation; + DualFixProbingPropagation dfprobingPropagation; bool infeasible_ = false; Reason infeasible_reason; @@ -805,7 +805,7 @@ class HighsDomain { bool isRedundantRow(HighsInt row) const; - DualfixingProbingPropagation& getDfProbingPropagation() { + DualFixProbingPropagation& getDualFixProbingPropagation() { return dfprobingPropagation; } diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 71daa3e27e4..f769b079745 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -29,7 +29,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { bool dfprobingEnabled = globaldomain.getInPresolveProbing(); if (dfprobingEnabled) { - globaldomain.getDfProbingPropagation().beginProbing(); + globaldomain.getDualFixProbingPropagation().beginProbing(); } HighsInt stackimplicstart = domchgstack.size() + 1; @@ -59,7 +59,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { auto isInfeasible = [&](HighsInt col, bool val) { if (!globaldomain.infeasible()) return false; if (dfprobingEnabled) { - globaldomain.getDfProbingPropagation().endProbing(); + globaldomain.getDualFixProbingPropagation().endProbing(); } storeLiftingOpportunities(col, val); doBacktrack(changedend); @@ -71,7 +71,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { globaldomain.propagate(); if (dfprobingEnabled) { - globaldomain.getDfProbingPropagation().endProbing(); + globaldomain.getDualFixProbingPropagation().endProbing(); } if (isInfeasible(col, val)) return true; @@ -93,7 +93,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const HighsInt tentativeStart = dfprobingEnabled - ? globaldomain.getDfProbingPropagation().getZeroCostFixingPosition() + ? globaldomain.getDualFixProbingPropagation().getZeroCostFixingPosition() : kHighsIInf32; if (dfprobingEnabled) { tentativeImplics.assign(domchgstack.begin() + stackimplicstart, @@ -348,7 +348,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { const bool enableDfprobing = globaldomain.getInPresolveProbing(); if (enableDfprobing) { clearTentativeClique(); - globaldomain.getDfProbingPropagation().setZeroCostFixingPosition( + globaldomain.getDualFixProbingPropagation().setZeroCostFixingPosition( kHighsIInf32); } diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index e3ff401d0ab..0785365079e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1863,9 +1863,9 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { } }; - const bool enableDfprobing = allow_rule_[kPresolveRuleDfprobing]; + const bool enableDfprobing = allow_rule_[kPresolveRuleDualFixProbing]; if (enableDfprobing) { - domain.getDfProbingPropagation().recomputeLocks(); + domain.getDualFixProbingPropagation().recomputeLocks(); } for (const auto& binvar : binaries) { From 767648b4ce307ae0aa04447ad81d43cd6925d4ed Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 19 Aug 2026 15:43:36 +0200 Subject: [PATCH 31/46] Clean up even more --- highs/mip/HighsDomain.cpp | 102 +++++++++++++++----------------- highs/mip/HighsDomain.h | 35 ++++++----- highs/mip/HighsImplications.cpp | 37 ++++++------ highs/mip/HighsImplications.h | 5 +- highs/presolve/HPresolve.cpp | 8 +-- 5 files changed, 89 insertions(+), 98 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 0c3e77896f3..ef178d27109 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -75,7 +75,7 @@ HighsDomain::HighsDomain(HighsMipSolver& mipsolver) : mipsolver(&mipsolver) { changedcols_.reserve(mipsolver.numCol()); infeasible_reason = Reason::unspecified(); infeasible_ = false; - dfprobingPropagation.domain = this; + dualFixProbingPropagation.domain = this; } void HighsDomain::addCutpool(HighsCutPool& cutpool) { @@ -640,8 +640,8 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange( HighsDomain::DualFixProbingPropagation::DualFixProbingPropagation( const DualFixProbingPropagation& other) - : redundantPropagateFlags_(other.redundantPropagateFlags_), - redundantPropagateInds_(other.redundantPropagateInds_), + : redundantRowFlags_(other.redundantRowFlags_), + redundantRowInds_(other.redundantRowInds_), zeroCostDirections_(other.zeroCostDirections_), fixedZeroCostColumns_(other.fixedZeroCostColumns_), colLowerLockOriginal_(other.colLowerLockOriginal_), @@ -656,9 +656,9 @@ HighsDomain::DualFixProbingPropagation::DualFixProbingPropagation( void HighsDomain::DualFixProbingPropagation::recomputeLocks() { mipsolver = domain->mipsolver; - redundantPropagateFlags_.assign(2 * mipsolver->numRow(), false); - redundantPropagateInds_.clear(); - redundantPropagateInds_.reserve(2 * mipsolver->numRow()); + redundantRowFlags_.assign(2 * mipsolver->numRow(), false); + redundantRowInds_.clear(); + redundantRowInds_.reserve(2 * mipsolver->numRow()); zeroCostDirections_.assign(mipsolver->numCol(), FixUndecided); fixedZeroCostColumns_.clear(); fixedZeroCostColumns_.reserve(mipsolver->numCol()); @@ -677,7 +677,7 @@ void HighsDomain::DualFixProbingPropagation::recomputeLocks() { lockNeedClear_.clear(); lockNeedClear_.reserve(mipsolver->numCol()); - // compute the original locks for each variable + // compute the locks for each variable const HighsLp* model = mipsolver->model_; for (HighsInt col = 0; col < model->a_matrix_.num_col_; col++) { for (HighsInt k = model->a_matrix_.start_[col]; @@ -694,34 +694,32 @@ void HighsDomain::DualFixProbingPropagation::recomputeLocks() { } } -void HighsDomain::DualFixProbingPropagation::updateRhsRedundant( - HighsInt row) { +void HighsDomain::DualFixProbingPropagation::updateRhsRedundant(HighsInt row) { if (!isEnabled()) return; if (domain->activitymaxinf_[row] != 0 || - redundantPropagateFlags_[2 * row + 1] || + redundantRowFlags_[2 * row + 1] || mipsolver->model_->row_upper_[row] == kHighsInf) return; if (domain->getMaxActivity(row) <= mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol) { - redundantPropagateInds_.push_back(2 * row + 1); - redundantPropagateFlags_[2 * row + 1] = 1; + redundantRowInds_.push_back(2 * row + 1); + redundantRowFlags_[2 * row + 1] = 1; } } -void HighsDomain::DualFixProbingPropagation::updateLhsRedundant( - HighsInt row) { +void HighsDomain::DualFixProbingPropagation::updateLhsRedundant(HighsInt row) { if (!isEnabled()) return; - if (domain->activitymininf_[row] != 0 || redundantPropagateFlags_[2 * row] || + if (domain->activitymininf_[row] != 0 || redundantRowFlags_[2 * row] || mipsolver->model_->row_lower_[row] == -kHighsInf) return; if (domain->getMinActivity(row) >= mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol) { - redundantPropagateInds_.push_back(2 * row); - redundantPropagateFlags_[2 * row] = 1; + redundantRowInds_.push_back(2 * row); + redundantRowFlags_[2 * row] = 1; } } @@ -824,20 +822,22 @@ void HighsDomain::DualFixProbingPropagation::propagate() { // only record - we do not actually fix them now as their objective // coefficients are zero auto collectFixLower = [&](int iCol) { - fixedZeroCostColumns_.emplace_back(FixedZeroCostColumn{iCol, FixLowerBound}); + fixedZeroCostColumns_.emplace_back( + FixedZeroCostColumn{iCol, FixLowerBound}); }; auto collectFixUpper = [&](int iCol) { - fixedZeroCostColumns_.emplace_back(FixedZeroCostColumn{iCol, FixUpperBound}); + fixedZeroCostColumns_.emplace_back( + FixedZeroCostColumn{iCol, FixUpperBound}); }; // exit if no new redundant constraints are found - HighsInt maxLockLeft = redundantPropagateInds_.size() - previousSize_; + HighsInt maxLockLeft = redundantRowInds_.size() - previousSize_; if (maxLockLeft == 0) return; - for (; previousSize_ < redundantPropagateInds_.size(); + for (; previousSize_ < redundantRowInds_.size(); ++previousSize_, --maxLockLeft) { - const HighsInt i = redundantPropagateInds_[previousSize_]; + const HighsInt i = redundantRowInds_[previousSize_]; const HighsInt iRow = i / 2; assert(iRow < mipsolver->numRow()); @@ -914,9 +914,11 @@ void HighsDomain::DualFixProbingPropagation::propagate() { for (auto iCol : candidatesVec_) { if (domain->isFixed(iCol)) continue; - const bool canBeFixedToLower = ableToFixToLb(iCol) && + const bool canBeFixedToLower = + ableToFixToLb(iCol) && colLowerLockReduced_[iCol] == colLowerLockOriginal_[iCol]; - const bool canBeFixedToUpper = ableToFixToUb(iCol) && + const bool canBeFixedToUpper = + ableToFixToUb(iCol) && colUpperLockReduced_[iCol] == colUpperLockOriginal_[iCol]; if (!canBeFixedToLower && !canBeFixedToUpper) continue; @@ -1027,30 +1029,34 @@ void HighsDomain::DualFixProbingPropagation::propagate() { } // record the current number of redundant constraints - previousSize_ = redundantPropagateInds_.size(); + previousSize_ = redundantRowInds_.size(); } void HighsDomain::DualFixProbingPropagation::propagateZeroCosts() { if (fixedZeroCostColumns_.empty()) return; applyingZeroCostFixings_ = true; - zeroCostStartPos_ = domain->getDomainChangeStack().size(); + if (zeroCostStartPos_ == kHighsIInf) + zeroCostStartPos_ = + static_cast(domain->getDomainChangeStack().size()); for (const FixedZeroCostColumn& fixing : fixedZeroCostColumns_) { if (domain->isFixed(fixing.col)) continue; if (fixing.direction == FixLowerBound) { - domain->changeBound(HighsBoundType::kUpper, fixing.col, domain->col_lower_[fixing.col], Reason::unspecified()); + domain->changeBound(HighsBoundType::kUpper, fixing.col, + domain->col_lower_[fixing.col], + Reason::unspecified()); } else { - domain->changeBound(HighsBoundType::kLower, fixing.col, domain->col_upper_[fixing.col], Reason::unspecified()); + domain->changeBound(HighsBoundType::kLower, fixing.col, + domain->col_upper_[fixing.col], + Reason::unspecified()); } if (domain->infeasible()) break; } - applyingZeroCostFixings_ = false; fixedZeroCostColumns_.clear(); } - namespace highs { template <> struct RbTreeTraits< @@ -1968,17 +1974,14 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, assert(tmpinf == activitymininf_[mip->a_matrix_.index_[i]]); } #endif - // If dfprobingPropagation.isZeroObjFixingEnabled() is true, - // then we cannot record redundant rows for lifting, as this bound change - // could disregarded. if (recordRedundantRows_ && - !dfprobingPropagation.isZeroCostFixingActive() && + !dualFixProbingPropagation.isZeroCostFixingActive() && mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); if (newbound >= oldbound + mipsolver->mipdata_->feastol) - dfprobingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); + dualFixProbingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); if (deltamin <= 0) { updateThresholdLbChange(col, newbound, mip->a_matrix_.value_[i], @@ -2023,17 +2026,14 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, assert(tmpinf == activitymaxinf_[mip->a_matrix_.index_[i]]); } #endif - // If dfprobingPropagation.isZeroObjFixingEnabled() is true, - // then we cannot record redundant rows for lifting, as this bound change - // could disregarded. if (recordRedundantRows_ && - !dfprobingPropagation.isZeroCostFixingActive() && + !dualFixProbingPropagation.isZeroCostFixingActive() && mip->row_lower_[mip->a_matrix_.index_[i]] == -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); if (newbound >= oldbound + mipsolver->mipdata_->feastol) - dfprobingPropagation.updateRhsRedundant(mip->a_matrix_.index_[i]); + dualFixProbingPropagation.updateRhsRedundant(mip->a_matrix_.index_[i]); if (deltamax >= 0) { updateThresholdLbChange(col, newbound, mip->a_matrix_.value_[i], @@ -2147,17 +2147,14 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, assert(tmpinf == activitymaxinf_[mip->a_matrix_.index_[i]]); } #endif - // If dfprobingPropagation.isZeroObjFixingEnabled() is true, - // then we cannot record redundant rows for lifting, as this bound change - // could disregarded. if (recordRedundantRows_ && - !dfprobingPropagation.isZeroCostFixingActive() && + !dualFixProbingPropagation.isZeroCostFixingActive() && mip->row_lower_[mip->a_matrix_.index_[i]] == -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); if (newbound <= oldbound - mipsolver->mipdata_->feastol) - dfprobingPropagation.updateRhsRedundant(mip->a_matrix_.index_[i]); + dualFixProbingPropagation.updateRhsRedundant(mip->a_matrix_.index_[i]); if (deltamax >= 0) { updateThresholdUbChange(col, newbound, mip->a_matrix_.value_[i], @@ -2205,17 +2202,14 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, assert(tmpinf == activitymininf_[mip->a_matrix_.index_[i]]); } #endif - // If dfprobingPropagation.isZeroObjFixingEnabled() is true, - // then we cannot record redundant rows for lifting, as this bound change - // could disregarded. if (recordRedundantRows_ && - !dfprobingPropagation.isZeroCostFixingActive() && + !dualFixProbingPropagation.isZeroCostFixingActive() && mip->row_lower_[mip->a_matrix_.index_[i]] != -kHighsInf && mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); if (newbound <= oldbound - mipsolver->mipdata_->feastol) - dfprobingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); + dualFixProbingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); if (deltamin <= 0) { updateThresholdUbChange(col, newbound, mip->a_matrix_.value_[i], @@ -2810,7 +2804,7 @@ bool HighsDomain::propagate() { if (!conflictprop.propagateConflictInds_.empty()) return true; } - if (!infeasible_ && dfprobingPropagation.isActive()) return true; + if (!infeasible_ && dualFixProbingPropagation.isActive()) return true; return false; }; @@ -2988,10 +2982,10 @@ bool HighsDomain::propagate() { } } - if (!infeasible_ && dfprobingPropagation.isActive()) { - dfprobingPropagation.propagate(); + if (!infeasible_ && dualFixProbingPropagation.isActive()) { + dualFixProbingPropagation.propagate(); if (!infeasible_ && !havePropagationRows()) { - dfprobingPropagation.propagateZeroCosts(); + dualFixProbingPropagation.propagateZeroCosts(); } } } diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 0df6549c5c7..d44fa8ad1c0 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -241,8 +241,8 @@ class HighsDomain { HighsMipSolver* mipsolver; // store row lower and row upper at 2i and 2i + 1 - std::vector redundantPropagateFlags_; - std::vector redundantPropagateInds_; + std::vector redundantRowFlags_; + std::vector redundantRowInds_; // Track direction of zero fixings so we don't store disagreeing results enum DualFixProbingFixDirection { @@ -260,7 +260,7 @@ class HighsDomain { std::vector fixedZeroCostColumns_; bool applyingZeroCostFixings_ = false; - size_t zeroCostStartPos_; + HighsInt zeroCostStartPos_; bool enabled_ = false; size_t previousSize_; @@ -282,7 +282,7 @@ class HighsDomain { // active only when new redundant rows are found. bool isActive() const { - return enabled_ && redundantPropagateInds_.size() > previousSize_; + return enabled_ && redundantRowInds_.size() > previousSize_; } bool isZeroCostFixingActive() const { return applyingZeroCostFixings_; } @@ -307,15 +307,14 @@ class HighsDomain { void beginProbing() { previousSize_ = 0; - if (!redundantPropagateInds_.empty()) { // clear buffers - for (const auto x : redundantPropagateInds_) - redundantPropagateFlags_[x] = false; + if (!redundantRowInds_.empty()) { + for (const auto x : redundantRowInds_) redundantRowFlags_[x] = false; - redundantPropagateInds_.clear(); + redundantRowInds_.clear(); } - for (size_t i = 0; i < redundantPropagateFlags_.size(); ++i) - assert(!redundantPropagateFlags_[i]); + for (size_t i = 0; i < redundantRowFlags_.size(); ++i) + assert(!redundantRowFlags_[i]); fixedZeroCostColumns_.clear(); zeroCostStartPos_ = kHighsIInf; @@ -433,7 +432,7 @@ class HighsDomain { private: std::deque cutpoolpropagation; std::deque conflictPoolPropagation; - DualFixProbingPropagation dfprobingPropagation; + DualFixProbingPropagation dualFixProbingPropagation; bool infeasible_ = false; Reason infeasible_reason; @@ -460,7 +459,7 @@ class HighsDomain { std::vector branchPos_; HighsHashTable redundantRows_; bool recordRedundantRows_ = false; - bool inPresolveProbing_ = false; + bool dualFixProbingActive_ = false; public: std::vector col_lower_; @@ -485,7 +484,7 @@ class HighsDomain { mipsolver(other.mipsolver), cutpoolpropagation(other.cutpoolpropagation), conflictPoolPropagation(other.conflictPoolPropagation), - dfprobingPropagation(other.dfprobingPropagation), + dualFixProbingPropagation(other.dualFixProbingPropagation), infeasible_(other.infeasible_), infeasible_reason(other.infeasible_reason), infeasible_pos(other.infeasible_pos), @@ -499,7 +498,7 @@ class HighsDomain { for (ConflictPoolPropagation& conflictprop : conflictPoolPropagation) conflictprop.domain = this; if (objProp_.domain) objProp_.domain = this; - dfprobingPropagation.domain = this; + dualFixProbingPropagation.domain = this; } HighsDomain& operator=(const HighsDomain& other) { @@ -531,7 +530,7 @@ class HighsDomain { for (ConflictPoolPropagation& conflictprop : conflictPoolPropagation) conflictprop.domain = this; if (objProp_.domain) objProp_.domain = this; - dfprobingPropagation.domain = this; + dualFixProbingPropagation.domain = this; return *this; } @@ -806,12 +805,12 @@ class HighsDomain { bool isRedundantRow(HighsInt row) const; DualFixProbingPropagation& getDualFixProbingPropagation() { - return dfprobingPropagation; + return dualFixProbingPropagation; } - void setInPresolveProbing(bool val) { inPresolveProbing_ = val; } + void setDualFixProbingActive(const bool val) { dualFixProbingActive_ = val; } - bool getInPresolveProbing() const { return inPresolveProbing_; } + bool getDualFixProbingActive() const { return dualFixProbingActive_; } }; #endif diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index f769b079745..38f26d3afd4 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -27,7 +27,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const auto& domchgreason = globaldomain.getDomainChangeReason(); size_t changedend = globaldomain.getChangedCols().size(); - bool dfprobingEnabled = globaldomain.getInPresolveProbing(); + bool dfprobingEnabled = globaldomain.getDualFixProbingActive(); if (dfprobingEnabled) { globaldomain.getDualFixProbingPropagation().beginProbing(); } @@ -92,12 +92,12 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { HighsInt maxEntries = 100000 + mipsolver.numNonzero(); const HighsInt tentativeStart = - dfprobingEnabled - ? globaldomain.getDualFixProbingPropagation().getZeroCostFixingPosition() - : kHighsIInf32; + dfprobingEnabled ? globaldomain.getDualFixProbingPropagation() + .getZeroCostFixingPosition() + : kHighsIInf32; if (dfprobingEnabled) { tentativeImplics.assign(domchgstack.begin() + stackimplicstart, - domchgstack.begin() + stackimplicend); + domchgstack.begin() + stackimplicend); } for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { @@ -105,8 +105,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) continue; - if (i >= tentativeStart) - continue; + if (i >= tentativeStart) continue; implics.push_back(domchgstack[i]); } @@ -189,7 +188,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { } if (!tentativeImplics.empty()) { pdqsort(tentativeImplics.begin(), tentativeImplics.end()); - implications[loc].tentativeImplics = std::move(tentativeImplics); + tentativeImplications[loc] = std::move(tentativeImplics); } return false; @@ -344,8 +343,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (globaldomain.isBinary(col) && !implicationsCached(col, 1) && !implicationsCached(col, 0) && mipsolver.mipdata_->cliquetable.getSubstitution(col) == nullptr) { - - const bool enableDfprobing = globaldomain.getInPresolveProbing(); + const bool enableDfprobing = globaldomain.getDualFixProbingActive(); if (enableDfprobing) { clearTentativeClique(); globaldomain.getDualFixProbingPropagation().setZeroCostFixingPosition( @@ -364,15 +362,15 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (mipsolver.mipdata_->cliquetable.getSubstitution(col) != nullptr) return true; - if (enableDfprobing && !binaryInvolvedInds_.empty() && mipsolver.mipdata_->cliquetable.isFull()) { + if (enableDfprobing && !binaryInvolvedInds_.empty() && + mipsolver.mipdata_->cliquetable.isFull()) { HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; HighsCliqueTable::CliqueVar clique[2]; for (HighsInt k : binaryInvolvedInds_) { if (!globaldomain.isBinary(k) || colsubstituted[k]) continue; if (globaldomain.infeasible()) return true; uint8_t mask = binaryInvolvedFlags_[k]; - if (mask == 0) - continue; + if (mask == 0) continue; if (mask == 10) { clique[0] = HighsCliqueTable::CliqueVar(col, 0); @@ -415,16 +413,16 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { // analyze implications const bool haveTentativeImplicsZeroProbe = - !implications[2 * col].tentativeImplics.empty(); + !tentativeImplications[2 * col].empty(); const bool haveTentativeImplicsOneProbe = - !implications[2 * col + 1].tentativeImplics.empty(); + !tentativeImplications[2 * col + 1].empty(); const std::vector& implicsdown = haveTentativeImplicsZeroProbe ? getTentativeImplications(col, 0) - : getImplications(col, 0, infeasible); + : getImplications(col, 0, infeasible); const std::vector& implicsup = haveTentativeImplicsOneProbe ? getTentativeImplications(col, 1) - : getImplications(col, 1, infeasible); + : getImplications(col, 1, infeasible); HighsInt nimplicsdown = implicsdown.size(); HighsInt nimplicsup = implicsup.size(); HighsInt u = 0; @@ -491,10 +489,9 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { } // clear tentative implications - if (haveTentativeImplicsZeroProbe) - implications[2 * col].tentativeImplics.clear(); + if (haveTentativeImplicsZeroProbe) tentativeImplications[2 * col].clear(); if (haveTentativeImplicsOneProbe) - implications[2 * col + 1].tentativeImplics.clear(); + tentativeImplications[2 * col + 1].clear(); return true; } diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 020d7dd4c36..d5c3edbd6cc 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -25,10 +25,10 @@ class HighsImplications { struct Implics { std::vector implics; - std::vector tentativeImplics; bool computed = false; }; std::vector implications; + std::vector> tentativeImplications; int64_t numImplications; int64_t numVarBounds; int64_t maxVarBounds; @@ -59,6 +59,7 @@ class HighsImplications { std::vector substitutions; std::vector colsubstituted; + // TODO: Rename these!!! std::vector binaryInvolvedInds_; // (0000) : Not involved // (0010) : Fixed to lower in zero-side probing @@ -137,7 +138,7 @@ class HighsImplications { const std::vector& getTentativeImplications(HighsInt col, bool val) { HighsInt loc = 2 * col + val; - return implications[loc].tentativeImplics; + return tentativeImplications[loc]; } bool implicationsCached(HighsInt col, bool val) { diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 0785365079e..3a394ee853b 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1863,8 +1863,8 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { } }; - const bool enableDfprobing = allow_rule_[kPresolveRuleDualFixProbing]; - if (enableDfprobing) { + const bool dualFixProbingEnabled = allow_rule_[kPresolveRuleDualFixProbing]; + if (dualFixProbingEnabled) { domain.getDualFixProbingPropagation().recomputeLocks(); } @@ -1933,9 +1933,9 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { HighsInt numBoundChgs = 0; HighsInt numNewCliques = -cliquetable.numCliques(); - domain.setInPresolveProbing(enableDfprobing); + domain.setDualFixProbingActive(dualFixProbingEnabled); const bool probing_result = implications.runProbing(i, numBoundChgs); - domain.setInPresolveProbing(false); + domain.setDualFixProbingActive(false); if (!probing_result) continue; probingContingent += numBoundChgs; numNewCliques += cliquetable.numCliques(); From 71457e41d3901284c066a3575ea7c38389b6e567 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 19 Aug 2026 18:08:10 +0200 Subject: [PATCH 32/46] Clean up propagate --- highs/mip/HighsDomain.cpp | 386 +++++++------------------------- highs/mip/HighsDomain.h | 29 ++- highs/mip/HighsImplications.cpp | 28 +-- 3 files changed, 108 insertions(+), 335 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index ef178d27109..ce77853f39a 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -644,13 +644,13 @@ HighsDomain::DualFixProbingPropagation::DualFixProbingPropagation( redundantRowInds_(other.redundantRowInds_), zeroCostDirections_(other.zeroCostDirections_), fixedZeroCostColumns_(other.fixedZeroCostColumns_), - colLowerLockOriginal_(other.colLowerLockOriginal_), - colUpperLockOriginal_(other.colUpperLockOriginal_), - colLowerLockReduced_(other.colLowerLockReduced_), - colUpperLockReduced_(other.colUpperLockReduced_), - candidatesVec_(other.candidatesVec_), - candidatesFlag_(other.candidatesFlag_), - lockNeedClear_(other.lockNeedClear_) { + colLowerLocksOriginal_(other.colLowerLocksOriginal_), + colUpperLocksOriginal_(other.colUpperLocksOriginal_), + colLowerReducedNumLocks_(other.colLowerReducedNumLocks_), + colUpperReducedNumLocks_(other.colUpperReducedNumLocks_), + candidateFixedCols_(other.candidateFixedCols_), + candidateColFixedFlags_(other.candidateColFixedFlags_), + clearColNumReducedLocks_(other.clearColNumReducedLocks_) { ; } @@ -664,18 +664,18 @@ void HighsDomain::DualFixProbingPropagation::recomputeLocks() { fixedZeroCostColumns_.reserve(mipsolver->numCol()); applyingZeroCostFixings_ = false; - previousSize_ = 0; + previousRedundantRowSize = 0; - colLowerLockOriginal_.assign(mipsolver->numCol(), 0); - colUpperLockOriginal_.assign(mipsolver->numCol(), 0); - colLowerLockReduced_.assign(mipsolver->numCol(), 0); - colUpperLockReduced_.assign(mipsolver->numCol(), 0); + colLowerLocksOriginal_.assign(mipsolver->numCol(), 0); + colUpperLocksOriginal_.assign(mipsolver->numCol(), 0); + colLowerReducedNumLocks_.assign(mipsolver->numCol(), 0); + colUpperReducedNumLocks_.assign(mipsolver->numCol(), 0); - candidatesVec_.clear(); - candidatesVec_.reserve(mipsolver->numCol()); - candidatesFlag_.assign(mipsolver->numCol(), false); - lockNeedClear_.clear(); - lockNeedClear_.reserve(mipsolver->numCol()); + candidateFixedCols_.clear(); + candidateFixedCols_.reserve(mipsolver->numCol()); + candidateColFixedFlags_.assign(mipsolver->numCol(), false); + clearColNumReducedLocks_.clear(); + clearColNumReducedLocks_.reserve(mipsolver->numCol()); // compute the locks for each variable const HighsLp* model = mipsolver->model_; @@ -687,9 +687,9 @@ void HighsDomain::DualFixProbingPropagation::recomputeLocks() { const double lhs = model->row_lower_[row]; const double rhs = model->row_upper_[row]; if ((val > 0 && rhs != kHighsInf) || (val < 0 && lhs != -kHighsInf)) - colUpperLockOriginal_[col]++; + colUpperLocksOriginal_[col]++; if ((val > 0 && lhs != -kHighsInf) || (val < 0 && rhs != kHighsInf)) - colLowerLockOriginal_[col]++; + colLowerLocksOriginal_[col]++; } } } @@ -697,8 +697,7 @@ void HighsDomain::DualFixProbingPropagation::recomputeLocks() { void HighsDomain::DualFixProbingPropagation::updateRhsRedundant(HighsInt row) { if (!isEnabled()) return; - if (domain->activitymaxinf_[row] != 0 || - redundantRowFlags_[2 * row + 1] || + if (domain->activitymaxinf_[row] != 0 || redundantRowFlags_[2 * row + 1] || mipsolver->model_->row_upper_[row] == kHighsInf) return; @@ -724,312 +723,97 @@ void HighsDomain::DualFixProbingPropagation::updateLhsRedundant(HighsInt row) { } void HighsDomain::DualFixProbingPropagation::propagate() { - // The boolean variable ``startZeroCostFixing_'' is used to flag if we allow - // variables with zero cost can be fixed in domain propagation. The process of - // domain propagtion in probing is executed in two phases: - // Phase 1: Apply classic domain propagation, and additionally fix - // variables with non-zero objective coefficients using dual fixing Phase - // 2: Apply classic domain propagation, and additionally fix variables - // (including those with zero objective coefficients) using dual fixing - // In Phase 1, ``startZeroCostFixing_'' is set to be ``false'' to exclude - // variable with zero objective coefficients. In Phase 2, - // ``startZeroCostFixing_'' is set to be ``true''. Note that - // (1) For all the bound changes in Phase 1, reductions deduced from them - // are valid for all optimal solutions; (2) For the bound changes in Phase - // 2, reductions deduced from them can only be used to derive global valid - // reductions (i.e., variable fixing, global bound tightening, and variable - // substitution). - if (!isEnabled()) return; + HighsInt numNewRedundantRows = + static_cast(redundantRowInds_.size()) - + previousRedundantRowSize; + if (!isEnabled() || numNewRedundantRows <= 0) return; assert(candidatesVec_.empty()); - vector domainchangeDFProbing; - - // tool lambda functions - auto addToCandidate = [&](HighsInt k) { - if (candidatesFlag_[k]) - return; - else { - candidatesVec_.push_back(k); - candidatesFlag_[k] = true; - } - }; - - // debug functions to check locks - auto checkVariableLowerLock = [&](HighsInt iCol) { - auto model = mipsolver->model_; - if (ableToFixToLb(iCol)) { - for (HighsInt k = model->a_matrix_.start_[iCol]; - k < model->a_matrix_.start_[iCol + 1]; k++) { - const HighsInt iRow = model->a_matrix_.index_[k]; - const double iValue = model->a_matrix_.value_[k]; - const double blower = model->row_lower_[iRow], - bupper = model->row_upper_[iRow]; - const bool lhsOk = iValue > 0 && domain->getMinActivity(iRow) >= - blower - domain->feastol(); - const bool rhsOk = iValue < 0 && domain->getMaxActivity(iRow) <= - bupper + domain->feastol(); - if (!lhsOk && !rhsOk) { - std::cout << "Lower lock: variable " << iCol << " at row = " << iRow - << " coef = " << iValue << " not redundant at constraint " - << iRow << ", minact = " << domain->getMinActivity(iRow) - << ", maxact = " << domain->getMaxActivity(iRow) - << " lhs = " << blower << " rhs = " << bupper << std::endl; - } - } - } - }; - auto checkVariableUpperLock = [&](HighsInt iCol) { - auto model = mipsolver->model_; - if (ableToFixToUb(iCol)) { - for (HighsInt k = model->a_matrix_.start_[iCol]; - k < model->a_matrix_.start_[iCol + 1]; k++) { - const HighsInt iRow = model->a_matrix_.index_[k]; - const double iValue = model->a_matrix_.value_[k]; - const double blower = model->row_lower_[iRow], - bupper = model->row_upper_[iRow]; - const bool lhsOk = iValue < 0 && domain->getMinActivity(iRow) >= - blower - domain->feastol(); - const bool rhsOk = iValue > 0 && domain->getMaxActivity(iRow) <= - bupper + domain->feastol(); - if (!lhsOk && !rhsOk) { - std::cout << "Upper lock: variable " << iCol << " at row = " << iRow - << " coef = " << iValue << " not redundant at constraint " - << iRow << ", minact = " << domain->getMinActivity(iRow) - << ", maxact = " << domain->getMaxActivity(iRow) - << " lhs = " << blower << " rhs = " << bupper << std::endl; - } - } + auto addCandidateFixing = [&](HighsInt col) { + if (!candidateColFixedFlags_[col]) { + candidateFixedCols_.push_back(col); + candidateColFixedFlags_[col] = true; } }; - auto addFixLower = [&](int iCol) { - HighsDomainChange* thisbchg = new HighsDomainChange; - thisbchg->column = iCol; - thisbchg->boundtype = HighsBoundType::kUpper; - thisbchg->boundval = domain->col_lower_[iCol]; - domainchangeDFProbing.push_back(thisbchg); + auto collectZeroCostFixing = [&](const HighsInt col, + const DualFixProbingFixDirection direction) { + fixedZeroCostColumns_.emplace_back(FixedZeroCostColumn{col, direction}); + zeroCostDirections_[col] = direction; }; - auto addFixUpper = [&](int iCol) { - HighsDomainChange* thisbchg = new HighsDomainChange; - thisbchg->column = iCol; - thisbchg->boundtype = HighsBoundType::kLower; - thisbchg->boundval = domain->col_upper_[iCol]; - domainchangeDFProbing.push_back(thisbchg); - }; - - // only record - we do not actually fix them now as their objective - // coefficients are zero - auto collectFixLower = [&](int iCol) { - fixedZeroCostColumns_.emplace_back( - FixedZeroCostColumn{iCol, FixLowerBound}); - }; - - auto collectFixUpper = [&](int iCol) { - fixedZeroCostColumns_.emplace_back( - FixedZeroCostColumn{iCol, FixUpperBound}); - }; - - // exit if no new redundant constraints are found - HighsInt maxLockLeft = redundantRowInds_.size() - previousSize_; - if (maxLockLeft == 0) return; - - for (; previousSize_ < redundantRowInds_.size(); - ++previousSize_, --maxLockLeft) { - const HighsInt i = redundantRowInds_[previousSize_]; - const HighsInt iRow = i / 2; - assert(iRow < mipsolver->numRow()); - - if (i % 2 == 0) { // lower redundant - HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; - HighsInt rend = mipsolver->mipdata_->ARstart_[iRow + 1]; - for (auto k = rstart; k < rend; ++k) { - const HighsInt iCol = mipsolver->mipdata_->ARindex_[k]; - if (domain->isFixed(iCol)) continue; - const double iValue = mipsolver->mipdata_->ARvalue_[k]; - const double cost = mipsolver->model_->col_cost_[iCol]; - - // do not insert to candidates if the lock is not reduced enough - bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < - colLowerLockOriginal_[iCol]; - bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < - colUpperLockOriginal_[iCol]; - - if (iValue > 0 && - cost >= -mipsolver->options_mip_->dual_feasibility_tolerance) { - lockNeedClear_.insert(iCol); - colLowerLockReduced_[iCol]++; - lowerNoInsert = - lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < - colLowerLockOriginal_[iCol]; - } else if (iValue < 0 && - cost <= - mipsolver->options_mip_->dual_feasibility_tolerance) { - lockNeedClear_.insert(iCol); - colUpperLockReduced_[iCol]++; - upperNoInsert = - upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < - colUpperLockOriginal_[iCol]; - } + const double dualTol = mipsolver->options_mip_->dual_feasibility_tolerance; - if (!lowerNoInsert || !upperNoInsert) addToCandidate(iCol); - } - } else { // upper redundant - HighsInt rstart = mipsolver->mipdata_->ARstart_[iRow]; - HighsInt rend = mipsolver->mipdata_->ARstart_[iRow + 1]; - for (auto k = rstart; k < rend; k++) { - const HighsInt iCol = mipsolver->mipdata_->ARindex_[k]; - if (domain->isFixed(iCol)) continue; - const double iValue = mipsolver->mipdata_->ARvalue_[k]; - const double cost = mipsolver->model_->col_cost_[iCol]; - - // do not insert to candidates if the lock is not reduced enough - bool lowerNoInsert = colLowerLockReduced_[iCol] + maxLockLeft < - colLowerLockOriginal_[iCol]; - bool upperNoInsert = colUpperLockReduced_[iCol] + maxLockLeft < - colUpperLockOriginal_[iCol]; - - if (iValue < 0 && - cost >= -mipsolver->options_mip_->dual_feasibility_tolerance) { - lockNeedClear_.insert(iCol); - colLowerLockReduced_[iCol]++; - lowerNoInsert = - lowerNoInsert && colLowerLockReduced_[iCol] + maxLockLeft < - colLowerLockOriginal_[iCol]; - } else if (iValue > 0 && - cost <= - mipsolver->options_mip_->dual_feasibility_tolerance) { - lockNeedClear_.insert(iCol); - colUpperLockReduced_[iCol]++; - upperNoInsert = - upperNoInsert && colUpperLockReduced_[iCol] + maxLockLeft < - colUpperLockOriginal_[iCol]; + for (HighsInt i = previousRedundantRowSize; + i != static_cast(redundantRowInds_.size()); ++i) { + const HighsInt loc = redundantRowInds_[i]; + const HighsInt row = loc / 2; + const bool isLhs = (loc % 2) == 0; + const HighsInt start = mipsolver->mipdata_->ARstart_[row]; + const HighsInt end = mipsolver->mipdata_->ARstart_[row + 1]; + for (HighsInt j = start; j < end; ++j) { + const HighsInt col = mipsolver->mipdata_->ARindex_[j]; + if (domain->isFixed(col)) continue; + const double val = mipsolver->mipdata_->ARvalue_[j]; + const double cost = mipsolver->model_->col_cost_[col]; + if (val != 0.0) { + // if LHS: Positive val removes a lower lock, negative an upper + // if RHS: Negative val removes a lower lock, positive an upper + const bool reducesLowerLock = (val > 0) == isLhs; + if (reducesLowerLock && cost >= -dualTol) { + clearColNumReducedLocks_.insert(col); + ++colLowerReducedNumLocks_[col]; + if (colLowerReducedNumLocks_[col] == colLowerLocksOriginal_[col]) { + addCandidateFixing(col); + } + } else if (!reducesLowerLock && cost <= dualTol) { + clearColNumReducedLocks_.insert(col); + ++colUpperReducedNumLocks_[col]; + if (colUpperReducedNumLocks_[col] == colUpperLocksOriginal_[col]) { + addCandidateFixing(col); + } } - - if (!lowerNoInsert || !upperNoInsert) addToCandidate(iCol); } } } - for (auto iCol : candidatesVec_) { - if (domain->isFixed(iCol)) continue; + for (const HighsInt col : candidateFixedCols_) { + if (domain->isFixed(col)) continue; const bool canBeFixedToLower = - ableToFixToLb(iCol) && - colLowerLockReduced_[iCol] == colLowerLockOriginal_[iCol]; + ableToFixToLb(col) && + colLowerReducedNumLocks_[col] == colLowerLocksOriginal_[col]; const bool canBeFixedToUpper = - ableToFixToUb(iCol) && - colUpperLockReduced_[iCol] == colUpperLockOriginal_[iCol]; + ableToFixToUb(col) && + colUpperReducedNumLocks_[col] == colUpperLocksOriginal_[col]; if (!canBeFixedToLower && !canBeFixedToUpper) continue; - - if (fabs(mipsolver->model_->col_cost_[iCol]) <= - mipsolver->options_mip_->dual_feasibility_tolerance) { - if (applyingZeroCostFixings_) { - // not fixed beforei - if (zeroCostDirections_[iCol] == FixUndecided) { - // both directions are ok - depending on cost (no tolerance) - if (canBeFixedToLower && canBeFixedToUpper) { - if (mipsolver->model_->col_cost_[iCol] >= 0) { - addFixLower(iCol); - zeroCostDirections_[iCol] = FixLowerBound; - } else { - addFixUpper(iCol); - zeroCostDirections_[iCol] = FixUpperBound; - } - } - // fix depending on the direction - else if (canBeFixedToLower) { - addFixLower(iCol); - zeroCostDirections_[iCol] = FixLowerBound; - } else if (canBeFixedToUpper) { - addFixUpper(iCol); - zeroCostDirections_[iCol] = FixUpperBound; - } - } - // fix to lb - else if (zeroCostDirections_[iCol] == FixLowerBound && - canBeFixedToLower) - addFixLower(iCol); - // fix to ub - else if (zeroCostDirections_[iCol] == FixUpperBound && - canBeFixedToUpper) - addFixUpper(iCol); - - continue; - } - // do not perfrom zero cost variable fixing, just collect them and choose - // directions - else { - // not fixed before - if (zeroCostDirections_[iCol] == FixUndecided) { - // both directions are ok - depending on cost (no tolerance) - if (canBeFixedToLower && canBeFixedToUpper) { - if (mipsolver->model_->col_cost_[iCol] >= 0) { - collectFixLower(iCol); - zeroCostDirections_[iCol] = FixLowerBound; - } else { - collectFixUpper(iCol); - zeroCostDirections_[iCol] = FixUpperBound; - } - } else if (canBeFixedToLower) { // fix to lower and set its direction - collectFixLower(iCol); - zeroCostDirections_[iCol] = FixLowerBound; - } else if (canBeFixedToUpper) { - collectFixUpper(iCol); - zeroCostDirections_[iCol] = FixUpperBound; - } - } else if (zeroCostDirections_[iCol] == FixUpperBound && - canBeFixedToUpper) { // fix to upper - collectFixUpper(iCol); - } else if (zeroCostDirections_[iCol] == FixLowerBound && - canBeFixedToLower) { // fix to lower - collectFixLower(iCol); + const double cost = mipsolver->model_->col_cost_[col]; + if (std::abs(cost) <= dualTol) { + if (zeroCostDirections_[col] == FixUndecided) { + if (canBeFixedToLower && (!canBeFixedToUpper || cost > 0)) { + collectZeroCostFixing(col, FixLowerBound); + } else { + collectZeroCostFixing(col, FixUpperBound); } - // we have collected this column - continue; } - } - - if (mipsolver->model_->col_cost_[iCol] >= - -mipsolver->options_mip_->dual_feasibility_tolerance) { + } else { if (canBeFixedToLower) { - // checkVariableLowerLock(iCol); - addFixLower(iCol); - continue; - } - } - - if (mipsolver->model_->col_cost_[iCol] <= - mipsolver->options_mip_->dual_feasibility_tolerance) { - if (canBeFixedToUpper) { - // checkVariableUpperLock(iCol); - addFixUpper(iCol); - continue; + domain->changeBound(HighsBoundType::kUpper, col, + domain->col_lower_[col], Reason::unspecified()); + } else if (canBeFixedToUpper) { + domain->changeBound(HighsBoundType::kLower, col, + domain->col_upper_[col], Reason::unspecified()); } + if (domain->infeasible()) break; } } - // clear candidate info - for (const auto x : candidatesVec_) { - candidatesFlag_[x] = false; - } - candidatesVec_.clear(); - - // change bound - size_t j = 0; - for (; j != domainchangeDFProbing.size() && !domain->infeasible_; ++j) { - domain->changeBound(*domainchangeDFProbing[j], Reason::unspecified()); - delete domainchangeDFProbing[j]; - } - - // clear the remaining domain changes if infeasible - for (j++; j < domainchangeDFProbing.size(); ++j) { - assert(domain->infeasible_); - delete domainchangeDFProbing[j]; + for (const auto x : candidateFixedCols_) { + candidateColFixedFlags_[x] = false; } + candidateFixedCols_.clear(); - // record the current number of redundant constraints - previousSize_ = redundantRowInds_.size(); + previousRedundantRowSize = static_cast(redundantRowInds_.size()); } void HighsDomain::DualFixProbingPropagation::propagateZeroCosts() { diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index d44fa8ad1c0..a2cf59df4ea 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -243,6 +243,7 @@ class HighsDomain { // store row lower and row upper at 2i and 2i + 1 std::vector redundantRowFlags_; std::vector redundantRowInds_; + HighsInt previousRedundantRowSize; // Track direction of zero fixings so we don't store disagreeing results enum DualFixProbingFixDirection { @@ -263,18 +264,16 @@ class HighsDomain { HighsInt zeroCostStartPos_; bool enabled_ = false; - size_t previousSize_; // Original lower and upper locks, and the reduced locks after propagation. - std::vector colLowerLockOriginal_; - std::vector colUpperLockOriginal_; - std::vector colLowerLockReduced_; - std::vector colUpperLockReduced_; + std::vector colLowerLocksOriginal_; + std::vector colUpperLocksOriginal_; + std::vector colLowerReducedNumLocks_; + std::vector colUpperReducedNumLocks_; + std::unordered_set clearColNumReducedLocks_; - // temporary buffers for DFProbing - std::vector candidatesVec_; - std::vector candidatesFlag_; - std::unordered_set lockNeedClear_; + std::vector candidateFixedCols_; + std::vector candidateColFixedFlags_; void setEnabled(const bool val) { enabled_ = val; } @@ -282,7 +281,7 @@ class HighsDomain { // active only when new redundant rows are found. bool isActive() const { - return enabled_ && redundantRowInds_.size() > previousSize_; + return enabled_ && redundantRowInds_.size() > previousRedundantRowSize; } bool isZeroCostFixingActive() const { return applyingZeroCostFixings_; } @@ -291,7 +290,7 @@ class HighsDomain { // lower or upper bound. void setZeroCostFixingPosition(HighsInt v) { zeroCostStartPos_ = v; } - size_t getZeroCostFixingPosition() const { return zeroCostStartPos_; } + HighsInt getZeroCostFixingPosition() const { return zeroCostStartPos_; } bool ableToFixToLb(const HighsInt col) const { return mipsolver->model_->col_cost_[col] >= @@ -306,7 +305,7 @@ class HighsDomain { } void beginProbing() { - previousSize_ = 0; + previousRedundantRowSize = 0; if (!redundantRowInds_.empty()) { for (const auto x : redundantRowInds_) redundantRowFlags_[x] = false; @@ -321,9 +320,9 @@ class HighsDomain { applyingZeroCostFixings_ = false; setEnabled(true); - for (const auto x : lockNeedClear_) - colLowerLockReduced_[x] = colUpperLockReduced_[x] = 0; - lockNeedClear_.clear(); + for (const auto x : clearColNumReducedLocks_) + colLowerReducedNumLocks_[x] = colUpperReducedNumLocks_[x] = 0; + clearColNumReducedLocks_.clear(); } void endProbing() { diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 38f26d3afd4..06e070d7ac6 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -94,9 +94,9 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const HighsInt tentativeStart = dfprobingEnabled ? globaldomain.getDualFixProbingPropagation() .getZeroCostFixingPosition() - : kHighsIInf32; - if (dfprobingEnabled) { - tentativeImplics.assign(domchgstack.begin() + stackimplicstart, + : kHighsIInf; + if (dfprobingEnabled && tentativeStart != kHighsIInf) { + tentativeImplics.assign(domchgstack.begin() + tentativeStart, domchgstack.begin() + stackimplicend); } @@ -118,15 +118,15 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { if (!tentativeImplics.empty()) { // add the implications of binary variables to the clique table - auto binstart_tmp = + auto binstart = std::partition(tentativeImplics.begin(), tentativeImplics.end(), [&](const HighsDomainChange& a) { return !globaldomain.isBinary(a.column); }); // store the tentative bound changes of binary variables separately - for (auto i = binstart_tmp; i != tentativeImplics.end(); ++i) + for (auto i = binstart; i != tentativeImplics.end(); ++i) recordTentativeCliques(val, *i); - tentativeImplics.erase(binstart_tmp, tentativeImplics.end()); + tentativeImplics.erase(binstart, tentativeImplics.end()); } // add the implications of binary variables to the clique table @@ -347,7 +347,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (enableDfprobing) { clearTentativeClique(); globaldomain.getDualFixProbingPropagation().setZeroCostFixingPosition( - kHighsIInf32); + kHighsIInf); } bool infeasible = computeImplications(col, 1); @@ -373,12 +373,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (mask == 0) continue; if (mask == 10) { - clique[0] = HighsCliqueTable::CliqueVar(col, 0); - clique[1] = HighsCliqueTable::CliqueVar(k, 1); - cliquetable.addClique(mipsolver, &clique[0], 2); - clique[0] = HighsCliqueTable::CliqueVar(col, 1); - clique[1] = HighsCliqueTable::CliqueVar(k, 1); - cliquetable.addClique(mipsolver, &clique[0], 2); + globaldomain.fixCol(k, globaldomain.col_lower_[k]); mask = 0; } else if (mask == 5) { clique[0] = HighsCliqueTable::CliqueVar(col, 0); @@ -397,12 +392,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { cliquetable.addClique(mipsolver, &clique[0], 2); mask = 0; } else if (mask == 6) { - clique[0] = HighsCliqueTable::CliqueVar(col, 1); - clique[1] = HighsCliqueTable::CliqueVar(k, 0); - cliquetable.addClique(mipsolver, &clique[0], 2); - clique[0] = HighsCliqueTable::CliqueVar(col, 0); - clique[1] = HighsCliqueTable::CliqueVar(k, 1); - cliquetable.addClique(mipsolver, &clique[0], 2); + globaldomain.fixCol(k, globaldomain.col_upper_[k]); mask = 0; } if (globaldomain.infeasible()) return true; From e29c3a36ebbd1de57b28bd764e8563fa2a66ee51 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 20 Aug 2026 12:39:28 +0200 Subject: [PATCH 33/46] Change how implications are handled --- highs/lp_data/HighsModelUtils.cpp | 2 +- highs/mip/HighsDomain.cpp | 6 +-- highs/mip/HighsDomain.h | 5 +- highs/mip/HighsImplications.cpp | 83 +++++++++++-------------------- 4 files changed, 37 insertions(+), 59 deletions(-) diff --git a/highs/lp_data/HighsModelUtils.cpp b/highs/lp_data/HighsModelUtils.cpp index 8c47f77789e..8fa6abd0502 100644 --- a/highs/lp_data/HighsModelUtils.cpp +++ b/highs/lp_data/HighsModelUtils.cpp @@ -1519,7 +1519,7 @@ std::string utilPresolveRuleTypeToString(const HighsInt rule_type) { return "Dual fixing"; } else if (rule_type == kPresolveRuleColStuffing) { return "Col stuffing"; - } else if (rule_type == kPresolveRuleDfprobing) { + } else if (rule_type == kPresolveRuleDualFixProbing) { return "Dual-fixing probing"; } else if (rule_type == kPresolveRuleInitialSweep) { return "Initial sweep"; diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index ce77853f39a..90b7f0dd745 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -728,7 +728,7 @@ void HighsDomain::DualFixProbingPropagation::propagate() { previousRedundantRowSize; if (!isEnabled() || numNewRedundantRows <= 0) return; - assert(candidatesVec_.empty()); + assert(candidateFixedCols_.empty()); auto addCandidateFixing = [&](HighsInt col) { if (!candidateColFixedFlags_[col]) { @@ -821,8 +821,8 @@ void HighsDomain::DualFixProbingPropagation::propagateZeroCosts() { applyingZeroCostFixings_ = true; if (zeroCostStartPos_ == kHighsIInf) - zeroCostStartPos_ = - static_cast(domain->getDomainChangeStack().size()); + setZeroCostFixingPosition( + static_cast(domain->getDomainChangeStack().size())); for (const FixedZeroCostColumn& fixing : fixedZeroCostColumns_) { if (domain->isFixed(fixing.col)) continue; diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index a2cf59df4ea..25f78cacba7 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -281,7 +281,8 @@ class HighsDomain { // active only when new redundant rows are found. bool isActive() const { - return enabled_ && redundantRowInds_.size() > previousRedundantRowSize; + return enabled_ && static_cast(redundantRowInds_.size()) > + previousRedundantRowSize; } bool isZeroCostFixingActive() const { return applyingZeroCostFixings_; } @@ -316,7 +317,7 @@ class HighsDomain { assert(!redundantRowFlags_[i]); fixedZeroCostColumns_.clear(); - zeroCostStartPos_ = kHighsIInf; + setZeroCostFixingPosition(kHighsIInf); applyingZeroCostFixings_ = false; setEnabled(true); diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 06e070d7ac6..2b11c5db9a9 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -27,8 +27,8 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { const auto& domchgreason = globaldomain.getDomainChangeReason(); size_t changedend = globaldomain.getChangedCols().size(); - bool dfprobingEnabled = globaldomain.getDualFixProbingActive(); - if (dfprobingEnabled) { + const bool dualFixProbingActive = globaldomain.getDualFixProbingActive(); + if (dualFixProbingActive) { globaldomain.getDualFixProbingPropagation().beginProbing(); } @@ -58,7 +58,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { auto isInfeasible = [&](HighsInt col, bool val) { if (!globaldomain.infeasible()) return false; - if (dfprobingEnabled) { + if (dualFixProbingActive) { globaldomain.getDualFixProbingPropagation().endProbing(); } storeLiftingOpportunities(col, val); @@ -70,7 +70,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { if (isInfeasible(col, val)) return true; globaldomain.propagate(); - if (dfprobingEnabled) { + if (dualFixProbingActive) { globaldomain.getDualFixProbingPropagation().endProbing(); } @@ -84,29 +84,19 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { std::vector implics; implics.reserve(numImplications); - // data structure to cache implications for non-binary variables - std::vector tentativeImplics; - tentativeImplics.reserve(numImplications); - HighsInt numEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); HighsInt maxEntries = 100000 + mipsolver.numNonzero(); - const HighsInt tentativeStart = - dfprobingEnabled ? globaldomain.getDualFixProbingPropagation() - .getZeroCostFixingPosition() - : kHighsIInf; - if (dfprobingEnabled && tentativeStart != kHighsIInf) { - tentativeImplics.assign(domchgstack.begin() + tentativeStart, - domchgstack.begin() + stackimplicend); - } + HighsInt unsafeDualFixesStart = + dualFixProbingActive ? globaldomain.getDualFixProbingPropagation() + .getZeroCostFixingPosition() + : static_cast(implics.size()); for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { if (domchgreason[i].type == HighsDomain::Reason::kCliqueTable && ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) continue; - if (i >= tentativeStart) continue; - implics.push_back(domchgstack[i]); } @@ -116,26 +106,25 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // backtrack doBacktrack(changedend); - if (!tentativeImplics.empty()) { - // add the implications of binary variables to the clique table + if (unsafeDualFixesStart < static_cast(implics.size())) { + // add the dualFix implications of binaries to the clique table auto binstart = - std::partition(tentativeImplics.begin(), tentativeImplics.end(), + std::partition(implics.begin() + unsafeDualFixesStart, implics.end(), [&](const HighsDomainChange& a) { return !globaldomain.isBinary(a.column); }); - // store the tentative bound changes of binary variables separately - for (auto i = binstart; i != tentativeImplics.end(); ++i) + // store the tentative bound changes of binaries separately + for (auto i = binstart; i != implics.end(); ++i) recordTentativeCliques(val, *i); - tentativeImplics.erase(binstart, tentativeImplics.end()); + implics.erase(binstart, implics.end()); } // add the implications of binary variables to the clique table - auto binstart = std::partition(implics.begin(), implics.end(), - [&](const HighsDomainChange& a) { - return !globaldomain.isBinary(a.column); - }); - - pdqsort(implics.begin(), binstart); + auto binstart = + std::partition(implics.begin(), implics.begin() + unsafeDualFixesStart, + [&](const HighsDomainChange& a) { + return !globaldomain.isBinary(a.column); + }); std::array clique; clique[0] = HighsCliqueTable::CliqueVar(col, val); @@ -150,8 +139,14 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { if (globaldomain.infeasible() || globaldomain.isFixed(col)) return true; } + HighsInt numErasedBinaries = + static_cast(binstart - implics.begin()); + implics.erase(binstart, implics.begin() + unsafeDualFixesStart); + unsafeDualFixesStart -= numErasedBinaries; + // store variable bounds derived from implications - for (auto i = implics.begin(); i != binstart; ++i) { + for (auto i = implics.begin(); i != implics.begin() + unsafeDualFixesStart; + ++i) { if (i->boundtype == HighsBoundType::kLower) { if (val == 1) { if (globaldomain.col_lower_[i->column] != -kHighsInf) @@ -179,17 +174,14 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { } } + pdqsort(implics.begin(), implics.end()); + HighsInt loc = 2 * col + val; implications[loc].computed = true; - implics.erase(binstart, implics.end()); if (!implics.empty()) { implications[loc].implics = std::move(implics); this->numImplications += implications[loc].implics.size(); } - if (!tentativeImplics.empty()) { - pdqsort(tentativeImplics.begin(), tentativeImplics.end()); - tentativeImplications[loc] = std::move(tentativeImplics); - } return false; } @@ -401,18 +393,8 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { clearTentativeClique(); } - // analyze implications - const bool haveTentativeImplicsZeroProbe = - !tentativeImplications[2 * col].empty(); - const bool haveTentativeImplicsOneProbe = - !tentativeImplications[2 * col + 1].empty(); - - const std::vector& implicsdown = - haveTentativeImplicsZeroProbe ? getTentativeImplications(col, 0) - : getImplications(col, 0, infeasible); - const std::vector& implicsup = - haveTentativeImplicsOneProbe ? getTentativeImplications(col, 1) - : getImplications(col, 1, infeasible); + const std::vector& implicsdown = getImplications(col, 0, infeasible); + const std::vector& implicsup = getImplications(col, 1, infeasible); HighsInt nimplicsdown = implicsdown.size(); HighsInt nimplicsup = implicsup.size(); HighsInt u = 0; @@ -478,11 +460,6 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { } } - // clear tentative implications - if (haveTentativeImplicsZeroProbe) tentativeImplications[2 * col].clear(); - if (haveTentativeImplicsOneProbe) - tentativeImplications[2 * col + 1].clear(); - return true; } From 5bce4f4935d6417375ca9255aea0d5d390de612f Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 20 Aug 2026 17:43:23 +0200 Subject: [PATCH 34/46] Fix self-introduced bugs --- highs/mip/HighsDomain.cpp | 16 +++++++++----- highs/mip/HighsDomain.h | 2 +- highs/mip/HighsImplications.cpp | 39 ++++++++++++++++++++------------- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 90b7f0dd745..ad6f37cabf4 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -789,12 +789,16 @@ void HighsDomain::DualFixProbingPropagation::propagate() { if (!canBeFixedToLower && !canBeFixedToUpper) continue; const double cost = mipsolver->model_->col_cost_[col]; if (std::abs(cost) <= dualTol) { - if (zeroCostDirections_[col] == FixUndecided) { - if (canBeFixedToLower && (!canBeFixedToUpper || cost > 0)) { - collectZeroCostFixing(col, FixLowerBound); - } else { - collectZeroCostFixing(col, FixUpperBound); - } + DualFixProbingFixDirection direction = zeroCostDirections_[col]; + if (direction == FixUndecided) { + direction = canBeFixedToLower && (!canBeFixedToUpper || cost >= 0) + ? FixLowerBound + : FixUpperBound; + } + if (direction == FixLowerBound && canBeFixedToLower) { + collectZeroCostFixing(col, FixLowerBound); + } else if (direction == FixUpperBound && canBeFixedToUpper) { + collectZeroCostFixing(col, FixUpperBound); } } else { if (canBeFixedToLower) { diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 25f78cacba7..c8f60b77cd9 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -261,7 +261,7 @@ class HighsDomain { std::vector fixedZeroCostColumns_; bool applyingZeroCostFixings_ = false; - HighsInt zeroCostStartPos_; + HighsInt zeroCostStartPos_ = kHighsIInf; bool enabled_ = false; diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 2b11c5db9a9..75505b60513 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -88,11 +88,14 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { HighsInt maxEntries = 100000 + mipsolver.numNonzero(); HighsInt unsafeDualFixesStart = - dualFixProbingActive ? globaldomain.getDualFixProbingPropagation() - .getZeroCostFixingPosition() - : static_cast(implics.size()); + globaldomain.getDualFixProbingPropagation().getZeroCostFixingPosition(); + bool foundDualFixPos = true; for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { + if (foundDualFixPos && i == unsafeDualFixesStart) { + unsafeDualFixesStart = static_cast(implics.size()); + foundDualFixPos = false; + } if (domchgreason[i].type == HighsDomain::Reason::kCliqueTable && ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) continue; @@ -100,6 +103,10 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { implics.push_back(domchgstack[i]); } + if (foundDualFixPos) { + unsafeDualFixesStart = static_cast(implics.size()); + } + // inform caller about lifting opportunities storeLiftingOpportunities(col, val); @@ -129,7 +136,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { std::array clique; clique[0] = HighsCliqueTable::CliqueVar(col, val); - for (auto i = binstart; i != implics.end(); ++i) { + for (auto i = binstart; i != implics.begin() + unsafeDualFixesStart; ++i) { if (i->boundtype == HighsBoundType::kLower) clique[1] = HighsCliqueTable::CliqueVar(i->column, 0); else @@ -140,7 +147,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { } HighsInt numErasedBinaries = - static_cast(binstart - implics.begin()); + static_cast(implics.begin() + unsafeDualFixesStart - binstart); implics.erase(binstart, implics.begin() + unsafeDualFixesStart); unsafeDualFixesStart -= numErasedBinaries; @@ -355,7 +362,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { return true; if (enableDfprobing && !binaryInvolvedInds_.empty() && - mipsolver.mipdata_->cliquetable.isFull()) { + !mipsolver.mipdata_->cliquetable.isFull()) { HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; HighsCliqueTable::CliqueVar clique[2]; for (HighsInt k : binaryInvolvedInds_) { @@ -368,12 +375,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { globaldomain.fixCol(k, globaldomain.col_lower_[k]); mask = 0; } else if (mask == 5) { - clique[0] = HighsCliqueTable::CliqueVar(col, 0); - clique[1] = HighsCliqueTable::CliqueVar(k, 0); - cliquetable.addClique(mipsolver, &clique[0], 2); - clique[0] = HighsCliqueTable::CliqueVar(col, 1); - clique[1] = HighsCliqueTable::CliqueVar(k, 0); - cliquetable.addClique(mipsolver, &clique[0], 2); + globaldomain.fixCol(k, globaldomain.col_upper_[k]); mask = 0; } else if (mask == 9) { clique[0] = HighsCliqueTable::CliqueVar(col, 1); @@ -384,7 +386,12 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { cliquetable.addClique(mipsolver, &clique[0], 2); mask = 0; } else if (mask == 6) { - globaldomain.fixCol(k, globaldomain.col_upper_[k]); + clique[0] = HighsCliqueTable::CliqueVar(col, 1); + clique[1] = HighsCliqueTable::CliqueVar(k, 0); + cliquetable.addClique(mipsolver, &clique[0], 2); + clique[0] = HighsCliqueTable::CliqueVar(col, 0); + clique[1] = HighsCliqueTable::CliqueVar(k, 1); + cliquetable.addClique(mipsolver, &clique[0], 2); mask = 0; } if (globaldomain.infeasible()) return true; @@ -393,8 +400,10 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { clearTentativeClique(); } - const std::vector& implicsdown = getImplications(col, 0, infeasible); - const std::vector& implicsup = getImplications(col, 1, infeasible); + const std::vector& implicsdown = + getImplications(col, 0, infeasible); + const std::vector& implicsup = + getImplications(col, 1, infeasible); HighsInt nimplicsdown = implicsdown.size(); HighsInt nimplicsup = implicsup.size(); HighsInt u = 0; From 1adf3610bc35963cff0dceda4a283e14de1bbc36 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 21 Aug 2026 13:47:11 +0200 Subject: [PATCH 35/46] Tidy up counter a bit --- highs/mip/HighsImplications.cpp | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 75505b60513..0aa7b2aa9cc 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -87,24 +87,17 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { HighsInt numEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); HighsInt maxEntries = 100000 + mipsolver.numNonzero(); - HighsInt unsafeDualFixesStart = + HighsInt unsafeStackStart = globaldomain.getDualFixProbingPropagation().getZeroCostFixingPosition(); - bool foundDualFixPos = true; + HighsInt safeImplicsEnd = 0; for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { - if (foundDualFixPos && i == unsafeDualFixesStart) { - unsafeDualFixesStart = static_cast(implics.size()); - foundDualFixPos = false; - } if (domchgreason[i].type == HighsDomain::Reason::kCliqueTable && ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) continue; implics.push_back(domchgstack[i]); - } - - if (foundDualFixPos) { - unsafeDualFixesStart = static_cast(implics.size()); + if (i < unsafeStackStart) safeImplicsEnd++; } // inform caller about lifting opportunities @@ -113,10 +106,10 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // backtrack doBacktrack(changedend); - if (unsafeDualFixesStart < static_cast(implics.size())) { + if (safeImplicsEnd < static_cast(implics.size())) { // add the dualFix implications of binaries to the clique table auto binstart = - std::partition(implics.begin() + unsafeDualFixesStart, implics.end(), + std::partition(implics.begin() + safeImplicsEnd, implics.end(), [&](const HighsDomainChange& a) { return !globaldomain.isBinary(a.column); }); @@ -128,7 +121,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // add the implications of binary variables to the clique table auto binstart = - std::partition(implics.begin(), implics.begin() + unsafeDualFixesStart, + std::partition(implics.begin(), implics.begin() + safeImplicsEnd, [&](const HighsDomainChange& a) { return !globaldomain.isBinary(a.column); }); @@ -136,7 +129,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { std::array clique; clique[0] = HighsCliqueTable::CliqueVar(col, val); - for (auto i = binstart; i != implics.begin() + unsafeDualFixesStart; ++i) { + for (auto i = binstart; i != implics.begin() + safeImplicsEnd; ++i) { if (i->boundtype == HighsBoundType::kLower) clique[1] = HighsCliqueTable::CliqueVar(i->column, 0); else @@ -147,13 +140,12 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { } HighsInt numErasedBinaries = - static_cast(implics.begin() + unsafeDualFixesStart - binstart); - implics.erase(binstart, implics.begin() + unsafeDualFixesStart); - unsafeDualFixesStart -= numErasedBinaries; + static_cast(implics.begin() + safeImplicsEnd - binstart); + implics.erase(binstart, implics.begin() + safeImplicsEnd); + safeImplicsEnd -= numErasedBinaries; // store variable bounds derived from implications - for (auto i = implics.begin(); i != implics.begin() + unsafeDualFixesStart; - ++i) { + for (auto i = implics.begin(); i != implics.begin() + safeImplicsEnd; ++i) { if (i->boundtype == HighsBoundType::kLower) { if (val == 1) { if (globaldomain.col_lower_[i->column] != -kHighsInf) From 8d22ef6dc38241fe8382f7e2689865e99a5399ab Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 25 Aug 2026 16:52:41 +0200 Subject: [PATCH 36/46] Fix bugs introduced by merge --- highs/mip/HighsDomain.cpp | 7 ++++--- highs/mip/HighsImplications.cpp | 32 +++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 4d484d614e1..5a7db394b8f 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -2772,9 +2772,10 @@ bool HighsDomain::propagate() { if (!infeasible_ && dualFixProbingPropagation.isActive()) { dualFixProbingPropagation.propagate(); - if (!infeasible_ && !havePropagationRows()) { - dualFixProbingPropagation.propagateZeroCosts(); - } + } + if (!infeasible_ && dualFixProbingPropagation.isEnabled() && + !havePropagationRows()) { + dualFixProbingPropagation.propagateZeroCosts(); } } diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index b8b7c9bf4e2..1a35271fb91 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -87,8 +87,10 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { HighsInt numEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); HighsInt maxEntries = 100000 + mipsolver.numNonzero(); - HighsInt unsafeStackStart = - globaldomain.getDualFixProbingPropagation().getZeroCostFixingPosition(); + HighsInt unsafeStackStart = dualFixProbingActive + ? globaldomain.getDualFixProbingPropagation() + .getZeroCostFixingPosition() + : stackimplicend; HighsInt safeImplicsEnd = 0; for (HighsInt i = stackimplicstart; i < stackimplicend; ++i) { @@ -424,31 +426,39 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { HighsInt implcol = implicationsUp[u].domchg.column; double lbDown = globaldomain.col_lower_[implcol]; double ubDown = globaldomain.col_upper_[implcol]; + bool safeDown = implicationsDown[d].dualSafe; double lbUp = lbDown; double ubUp = ubDown; + bool safeUp = implicationsUp[u].dualSafe; do { - if (implicationsDown[d].domchg.boundtype == HighsBoundType::kLower) + if (implicationsDown[d].domchg.boundtype == HighsBoundType::kLower) { lbDown = std::max(lbDown, implicationsDown[d].domchg.boundval); - else + safeDown &= implicationsDown[d].dualSafe; + } else { ubDown = std::min(ubDown, implicationsDown[d].domchg.boundval); + safeDown &= implicationsDown[d].dualSafe; + } ++d; } while (d < nimplicsdown && implicationsDown[d].domchg.column == implcol); do { - if (implicationsUp[u].domchg.boundtype == HighsBoundType::kLower) + if (implicationsUp[u].domchg.boundtype == HighsBoundType::kLower) { lbUp = std::max(lbUp, implicationsUp[u].domchg.boundval); - else + safeUp &= implicationsUp[u].dualSafe; + } else { ubUp = std::min(ubUp, implicationsUp[u].domchg.boundval); + safeUp &= implicationsUp[u].dualSafe; + } ++u; } while (u < nimplicsup && implicationsUp[u].domchg.column == implcol); if (colsubstituted[implcol] || globaldomain.isFixed(implcol)) continue; if (lbDown == ubDown && lbUp == ubUp && - std::abs(lbDown - lbUp) > mipsolver.mipdata_->feastol && - implicationsUp[u].dualSafe && implicationsDown[d].dualSafe) { + std::abs(lbDown - lbUp) > mipsolver.mipdata_->feastol && safeUp && + safeDown) { HighsSubstitution substitution; substitution.substcol = implcol; substitution.staycol = col; @@ -465,21 +475,21 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { globaldomain.changeBound(HighsBoundType::kLower, implcol, lb, HighsDomain::Reason::unspecified()); ++numReductions; + if (globaldomain.infeasible()) return true; } if (ub < globaldomain.col_upper_[implcol]) { globaldomain.changeBound(HighsBoundType::kUpper, implcol, ub, HighsDomain::Reason::unspecified()); ++numReductions; + if (globaldomain.infeasible()) return true; } } } - return globaldomain.infeasible(); - }; + } hasProbed[ImplIdx{col, 0}] = true; hasProbed[ImplIdx{col, 1}] = true; - return true; } From 94732520e9e4bdd703d476ac8a6df71daf82fb4f Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 25 Aug 2026 17:16:54 +0200 Subject: [PATCH 37/46] Fix warnings during build --- highs/mip/HighsDomain.cpp | 4 ++-- highs/mip/HighsDomain.h | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 5a7db394b8f..8c3aa2e71e5 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -648,9 +648,9 @@ HighsDomain::DualFixProbingPropagation::DualFixProbingPropagation( colUpperLocksOriginal_(other.colUpperLocksOriginal_), colLowerReducedNumLocks_(other.colLowerReducedNumLocks_), colUpperReducedNumLocks_(other.colUpperReducedNumLocks_), + clearColNumReducedLocks_(other.clearColNumReducedLocks_), candidateFixedCols_(other.candidateFixedCols_), - candidateColFixedFlags_(other.candidateColFixedFlags_), - clearColNumReducedLocks_(other.clearColNumReducedLocks_) { + candidateColFixedFlags_(other.candidateColFixedFlags_) { ; } diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index c8f60b77cd9..936c50a301e 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -237,13 +237,13 @@ class HighsDomain { }; struct DualFixProbingPropagation { - HighsDomain* domain; - HighsMipSolver* mipsolver; + HighsDomain* domain = nullptr; + HighsMipSolver* mipsolver = nullptr; // store row lower and row upper at 2i and 2i + 1 std::vector redundantRowFlags_; std::vector redundantRowInds_; - HighsInt previousRedundantRowSize; + HighsInt previousRedundantRowSize = 0; // Track direction of zero fixings so we don't store disagreeing results enum DualFixProbingFixDirection { @@ -332,14 +332,10 @@ class HighsDomain { applyingZeroCostFixings_ = false; } - DualFixProbingPropagation() { ; }; - - DualFixProbingPropagation(HighsDomain* domain) : domain(domain) {}; + DualFixProbingPropagation() = default; DualFixProbingPropagation(const DualFixProbingPropagation& other); - ~DualFixProbingPropagation() { ; }; - void recomputeLocks(); void updateRhsRedundant(HighsInt row); void updateLhsRedundant(HighsInt row); From 9a1093ea5915fd884a413584cee633833051e8e5 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 27 Aug 2026 16:05:32 +0200 Subject: [PATCH 38/46] Remove unordered set. Try and remove redundant copy --- highs/mip/HighsDomain.cpp | 42 ++++++++++++++++----------------- highs/mip/HighsDomain.h | 20 +++++++++++----- highs/mip/HighsImplications.cpp | 28 +++++++++++----------- highs/mip/HighsImplications.h | 22 ++++++++--------- 4 files changed, 59 insertions(+), 53 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 8c3aa2e71e5..a9a1282f8d8 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -638,22 +638,6 @@ void HighsDomain::CutpoolPropagation::updateActivityUbChange( } } -HighsDomain::DualFixProbingPropagation::DualFixProbingPropagation( - const DualFixProbingPropagation& other) - : redundantRowFlags_(other.redundantRowFlags_), - redundantRowInds_(other.redundantRowInds_), - zeroCostDirections_(other.zeroCostDirections_), - fixedZeroCostColumns_(other.fixedZeroCostColumns_), - colLowerLocksOriginal_(other.colLowerLocksOriginal_), - colUpperLocksOriginal_(other.colUpperLocksOriginal_), - colLowerReducedNumLocks_(other.colLowerReducedNumLocks_), - colUpperReducedNumLocks_(other.colUpperReducedNumLocks_), - clearColNumReducedLocks_(other.clearColNumReducedLocks_), - candidateFixedCols_(other.candidateFixedCols_), - candidateColFixedFlags_(other.candidateColFixedFlags_) { - ; -} - void HighsDomain::DualFixProbingPropagation::recomputeLocks() { mipsolver = domain->mipsolver; redundantRowFlags_.assign(2 * mipsolver->numRow(), false); @@ -762,13 +746,19 @@ void HighsDomain::DualFixProbingPropagation::propagate() { // if RHS: Negative val removes a lower lock, positive an upper const bool reducesLowerLock = (val > 0) == isLhs; if (reducesLowerLock && cost >= -dualTol) { - clearColNumReducedLocks_.insert(col); + if (colLowerReducedNumLocks_[col] == 0 && + colUpperReducedNumLocks_[col] == 0) { + clearColNumReducedLocks_.push_back(col); + } ++colLowerReducedNumLocks_[col]; if (colLowerReducedNumLocks_[col] == colLowerLocksOriginal_[col]) { addCandidateFixing(col); } } else if (!reducesLowerLock && cost <= dualTol) { - clearColNumReducedLocks_.insert(col); + if (colLowerReducedNumLocks_[col] == 0 && + colUpperReducedNumLocks_[col] == 0) { + clearColNumReducedLocks_.push_back(col); + } ++colUpperReducedNumLocks_[col]; if (colUpperReducedNumLocks_[col] == colUpperLocksOriginal_[col]) { addCandidateFixing(col); @@ -1741,6 +1731,10 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, if (infeasible_) return; } + const bool trackRedundancy = + newbound >= oldbound + mipsolver->mipdata_->feastol && + dualFixProbingPropagation.isEnabled(); + for (HighsInt i = start; i != end; ++i) { if (mip->a_matrix_.value_[i] > 0) { HighsCDouble deltamin = @@ -1768,7 +1762,7 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); - if (newbound >= oldbound + mipsolver->mipdata_->feastol) + if (trackRedundancy) dualFixProbingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); if (deltamin <= 0) { @@ -1820,7 +1814,7 @@ void HighsDomain::updateActivityLbChange(HighsInt col, double oldbound, mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); - if (newbound >= oldbound + mipsolver->mipdata_->feastol) + if (trackRedundancy) dualFixProbingPropagation.updateRhsRedundant(mip->a_matrix_.index_[i]); if (deltamax >= 0) { @@ -1914,6 +1908,10 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, if (infeasible_) return; } + const bool trackRedundancy = + newbound <= oldbound - mipsolver->mipdata_->feastol && + dualFixProbingPropagation.isEnabled(); + for (HighsInt i = start; i != end; ++i) { if (mip->a_matrix_.value_[i] > 0) { HighsCDouble deltamax = @@ -1941,7 +1939,7 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, mip->row_upper_[mip->a_matrix_.index_[i]] != kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); - if (newbound <= oldbound - mipsolver->mipdata_->feastol) + if (trackRedundancy) dualFixProbingPropagation.updateRhsRedundant(mip->a_matrix_.index_[i]); if (deltamax >= 0) { @@ -1996,7 +1994,7 @@ void HighsDomain::updateActivityUbChange(HighsInt col, double oldbound, mip->row_upper_[mip->a_matrix_.index_[i]] == kHighsInf) updateRedundantRows(mip->a_matrix_.index_[i]); - if (newbound <= oldbound - mipsolver->mipdata_->feastol) + if (trackRedundancy) dualFixProbingPropagation.updateLhsRedundant(mip->a_matrix_.index_[i]); if (deltamin <= 0) { diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 936c50a301e..1f18d803337 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -12,7 +12,6 @@ #include #include #include -#include #include #include "HighsPseudocost.h" @@ -270,7 +269,7 @@ class HighsDomain { std::vector colUpperLocksOriginal_; std::vector colLowerReducedNumLocks_; std::vector colUpperReducedNumLocks_; - std::unordered_set clearColNumReducedLocks_; + std::vector clearColNumReducedLocks_; std::vector candidateFixedCols_; std::vector candidateColFixedFlags_; @@ -321,8 +320,10 @@ class HighsDomain { applyingZeroCostFixings_ = false; setEnabled(true); - for (const auto x : clearColNumReducedLocks_) - colLowerReducedNumLocks_[x] = colUpperReducedNumLocks_[x] = 0; + for (const auto col : clearColNumReducedLocks_) { + colLowerReducedNumLocks_[col] = 0; + colUpperReducedNumLocks_[col] = 0; + } clearColNumReducedLocks_.clear(); } @@ -334,7 +335,10 @@ class HighsDomain { DualFixProbingPropagation() = default; - DualFixProbingPropagation(const DualFixProbingPropagation& other); + DualFixProbingPropagation(const DualFixProbingPropagation& other) = delete; + + DualFixProbingPropagation& operator=( + const DualFixProbingPropagation& other) = delete; void recomputeLocks(); void updateRhsRedundant(HighsInt row); @@ -480,7 +484,6 @@ class HighsDomain { mipsolver(other.mipsolver), cutpoolpropagation(other.cutpoolpropagation), conflictPoolPropagation(other.conflictPoolPropagation), - dualFixProbingPropagation(other.dualFixProbingPropagation), infeasible_(other.infeasible_), infeasible_reason(other.infeasible_reason), infeasible_pos(other.infeasible_pos), @@ -495,9 +498,11 @@ class HighsDomain { conflictprop.domain = this; if (objProp_.domain) objProp_.domain = this; dualFixProbingPropagation.domain = this; + dualFixProbingPropagation.mipsolver = mipsolver; } HighsDomain& operator=(const HighsDomain& other) { + if (this == &other) return *this; changedcolsflags_ = other.changedcolsflags_; changedcols_ = other.changedcols_; domchgstack_ = other.domchgstack_; @@ -527,6 +532,9 @@ class HighsDomain { conflictprop.domain = this; if (objProp_.domain) objProp_.domain = this; dualFixProbingPropagation.domain = this; + dualFixProbingActive_ = false; + dualFixProbingPropagation.mipsolver = mipsolver; + dualFixProbingPropagation.endProbing(); return *this; } diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 1a35271fb91..5c3aed2fd62 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -81,7 +81,9 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { mipsolver.mipdata_->getPseudoCost().addInferenceObservation( col, numImplications, val); - std::vector implics; + std::vector& implics = + val ? implicationsUp : implicationsDown; + implics.clear(); implics.reserve(numImplications); HighsInt numEntries = mipsolver.mipdata_->cliquetable.getNumEntries(); @@ -112,7 +114,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // add the dualFix implications of binaries to the clique table auto binstart = std::partition(implics.begin() + safeImplicsEnd, implics.end(), - [&](const tentativeImplication& a) { + [&](const TentativeImplication& a) { return !globaldomain.isBinary(a.domchg.column); }); // store the tentative bound changes of binaries separately @@ -124,7 +126,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // add the implications of binary variables to the clique table auto binstart = std::partition(implics.begin(), implics.begin() + safeImplicsEnd, - [&](const tentativeImplication& a) { + [&](const TentativeImplication& a) { return !globaldomain.isBinary(a.domchg.column); }); @@ -190,14 +192,9 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { } pdqsort(implics.begin(), implics.end(), - [](const tentativeImplication& a, const tentativeImplication& b) { + [](const TentativeImplication& a, const TentativeImplication& b) { return a.domchg.column < b.domchg.column; }); - if (val) { - implicationsUp = std::move(implics); - } else { - implicationsDown = std::move(implics); - } return false; } @@ -351,9 +348,9 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (globaldomain.isBinary(col) && !probedBefore(col, 1) && !probedBefore(col, 0) && mipsolver.mipdata_->cliquetable.getSubstitution(col) == nullptr) { - const bool enableDfprobing = globaldomain.getDualFixProbingActive(); - if (enableDfprobing) { - clearTentativeClique(); + const bool dualFixProbingActive = globaldomain.getDualFixProbingActive(); + if (dualFixProbingActive) { + clearTentativeCliques(); globaldomain.getDualFixProbingPropagation().setZeroCostFixingPosition( kHighsIInf); } @@ -370,7 +367,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (mipsolver.mipdata_->cliquetable.getSubstitution(col) != nullptr) return true; - if (enableDfprobing && !dualFixProbingBinInds_.empty() && + if (dualFixProbingActive && !dualFixProbingBinInds_.empty() && !mipsolver.mipdata_->cliquetable.isFull()) { HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; HighsCliqueTable::CliqueVar clique[2]; @@ -406,7 +403,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (globaldomain.infeasible()) return true; } - clearTentativeClique(); + clearTentativeCliques(); } HighsInt nimplicsdown = implicationsDown.size(); @@ -650,6 +647,9 @@ void HighsImplications::rebuild(HighsInt ncols, vlbs.clear(); vlbs.shrink_to_fit(); vlbs.resize(ncols); + dualFixProbingBinInds_.clear(); + dualFixProbingBinInds_.reserve(ncols); + dualFixProbingBinFlags_.assign(ncols, 0); numImplications = 0; numVarBounds = 0; HighsInt oldncols = oldvubs.size(); diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 0c12a1dacfc..4401209275c 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -28,13 +28,13 @@ class HighsImplications { double ub = kHighsInf; }; - struct tentativeImplication { + struct TentativeImplication { HighsDomainChange domchg; bool dualSafe; }; - std::vector implicationsDown; - std::vector implicationsUp; + std::vector implicationsDown; + std::vector implicationsUp; std::vector> implications; std::vector> reverseImplications; std::vector hasProbed; @@ -72,12 +72,6 @@ class HighsImplications { private: std::vector> vubs; std::vector> vlbs; - - public: - const HighsMipSolver& mipsolver; - std::vector substitutions; - std::vector colsubstituted; - std::vector dualFixProbingBinInds_; // (0000) : Not involved // (0010) : Fixed to lower in zero-side probing @@ -90,6 +84,11 @@ class HighsImplications { // (0110) : Conclude that x1 = x2 std::vector dualFixProbingBinFlags_; + public: + const HighsMipSolver& mipsolver; + std::vector substitutions; + std::vector colsubstituted; + HighsImplications(const HighsMipSolver& mipsolver) : mipsolver(mipsolver) { nextCleanupCall = mipsolver.numNonzero(); numImplications = 0; @@ -114,6 +113,7 @@ class HighsImplications { vubs.shrink_to_fit(); vlbs.clear(); vlbs.shrink_to_fit(); + dualFixProbingBinInds_.clear(); resize(mipsolver.numCol()); numVarBounds = 0; nextCleanupCall = mipsolver.numNonzero(); @@ -247,8 +247,8 @@ class HighsImplications { } } - void clearTentativeClique() { - for (HighsInt col : dualFixProbingBinInds_) + void clearTentativeCliques() { + for (const HighsInt col : dualFixProbingBinInds_) dualFixProbingBinFlags_[col] = 0; dualFixProbingBinInds_.clear(); } From a7581316edfe99e42c49c20b3e900d7e9874543e Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 27 Aug 2026 17:45:28 +0200 Subject: [PATCH 39/46] Don't try and lift a zero --- highs/presolve/HPresolve.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index d0745268915..95e14f8871f 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -1816,6 +1816,7 @@ HPresolve::Result HPresolve::runProbing(HighsPostsolveStack& postsolve_stack) { // store lifting opportunities implications.storeLiftingOpportunity = [&](HighsInt row, HighsInt col, HighsInt val, double coef) { + if (coef == 0.0) return; // find lifting opportunities for row auto& htree = liftingOpportunities[row]; // add element From 47deb3f8799ed724efb8c2d3d327204b7f9229fd Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 27 Aug 2026 18:00:26 +0200 Subject: [PATCH 40/46] Update incorrect descriptions --- highs/mip/HighsDomain.h | 2 +- highs/mip/HighsImplications.cpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 1f18d803337..0b437a43ed5 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -264,7 +264,7 @@ class HighsDomain { bool enabled_ = false; - // Original lower and upper locks, and the reduced locks after propagation. + // Original lower / upper locks + number of removed locks after propagation. std::vector colLowerLocksOriginal_; std::vector colUpperLocksOriginal_; std::vector colLowerReducedNumLocks_; diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 5c3aed2fd62..8e724a4d849 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -111,7 +111,6 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { doBacktrack(changedend); if (safeImplicsEnd < static_cast(implics.size())) { - // add the dualFix implications of binaries to the clique table auto binstart = std::partition(implics.begin() + safeImplicsEnd, implics.end(), [&](const TentativeImplication& a) { From 89bfaf341fd3390112d6189eec4b23dba8936ca9 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 31 Aug 2026 11:45:13 +0200 Subject: [PATCH 41/46] Protect lifting opportunities from zero cost fixings --- highs/mip/HighsDomain.cpp | 5 +++++ highs/mip/HighsDomain.h | 2 ++ highs/mip/HighsImplications.cpp | 5 +++++ 3 files changed, 12 insertions(+) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index a9a1282f8d8..5f3492637c3 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -813,6 +813,11 @@ void HighsDomain::DualFixProbingPropagation::propagate() { void HighsDomain::DualFixProbingPropagation::propagateZeroCosts() { if (fixedZeroCostColumns_.empty()) return; + if (storeLiftingOpportunity != nullptr) { + storeLiftingOpportunity(); + storeLiftingOpportunity = nullptr; + } + applyingZeroCostFixings_ = true; if (zeroCostStartPos_ == kHighsIInf) setZeroCostFixingPosition( diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index 0b437a43ed5..d5538e2371d 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -261,6 +261,7 @@ class HighsDomain { bool applyingZeroCostFixings_ = false; HighsInt zeroCostStartPos_ = kHighsIInf; + std::function storeLiftingOpportunity; bool enabled_ = false; @@ -331,6 +332,7 @@ class HighsDomain { setEnabled(false); fixedZeroCostColumns_.clear(); applyingZeroCostFixings_ = false; + storeLiftingOpportunity = nullptr; } DualFixProbingPropagation() = default; diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 8e724a4d849..b03d577aff0 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -51,6 +51,11 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { } }; + if (dualFixProbingActive && storeLiftingOpportunity != nullptr) { + globaldomain.getDualFixProbingPropagation().storeLiftingOpportunity = + [&]() { storeLiftingOpportunities(col, val); }; + } + auto doBacktrack = [&](size_t changedend) { globaldomain.backtrack(); globaldomain.clearChangedCols(changedend); From 2efc6eed989ba10acc5debed22ffc4ad0196ffc6 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 31 Aug 2026 12:01:40 +0200 Subject: [PATCH 42/46] No need to safe guard substitutions from zero-cost fixes --- highs/mip/HighsDomain.h | 1 + highs/mip/HighsImplications.cpp | 104 ++++++++++++++------------------ highs/mip/HighsImplications.h | 9 +-- 3 files changed, 48 insertions(+), 66 deletions(-) diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index d5538e2371d..b9cdbed648e 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index b03d577aff0..5d1d8e8fcda 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -86,7 +86,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { mipsolver.mipdata_->getPseudoCost().addInferenceObservation( col, numImplications, val); - std::vector& implics = + std::vector& implics = val ? implicationsUp : implicationsDown; implics.clear(); implics.reserve(numImplications); @@ -105,7 +105,7 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { ((domchgreason[i].index >> 1) == col || numEntries >= maxEntries)) continue; - implics.push_back({domchgstack[i], i < unsafeStackStart}); + implics.push_back(domchgstack[i]); if (i < unsafeStackStart) safeImplicsEnd++; } @@ -118,30 +118,30 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { if (safeImplicsEnd < static_cast(implics.size())) { auto binstart = std::partition(implics.begin() + safeImplicsEnd, implics.end(), - [&](const TentativeImplication& a) { - return !globaldomain.isBinary(a.domchg.column); + [&](const HighsDomainChange& a) { + return !globaldomain.isBinary(a.column); }); // store the tentative bound changes of binaries separately for (auto i = binstart; i != implics.end(); ++i) - recordTentativeCliques(val, i->domchg); + recordTentativeCliques(val, *i); implics.erase(binstart, implics.end()); } // add the implications of binary variables to the clique table auto binstart = std::partition(implics.begin(), implics.begin() + safeImplicsEnd, - [&](const TentativeImplication& a) { - return !globaldomain.isBinary(a.domchg.column); + [&](const HighsDomainChange& a) { + return !globaldomain.isBinary(a.column); }); std::array clique; clique[0] = HighsCliqueTable::CliqueVar(col, val); for (auto i = binstart; i != implics.begin() + safeImplicsEnd; ++i) { - if (i->domchg.boundtype == HighsBoundType::kLower) - clique[1] = HighsCliqueTable::CliqueVar(i->domchg.column, 0); + if (i->boundtype == HighsBoundType::kLower) + clique[1] = HighsCliqueTable::CliqueVar(i->column, 0); else - clique[1] = HighsCliqueTable::CliqueVar(i->domchg.column, 1); + clique[1] = HighsCliqueTable::CliqueVar(i->column, 1); cliquetable.addClique(mipsolver, clique.data(), 2); if (globaldomain.infeasible() || globaldomain.isFixed(col)) return true; @@ -154,32 +154,30 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // store variable bounds derived from implications for (auto i = implics.begin(); i != implics.begin() + safeImplicsEnd; ++i) { - if (i->domchg.boundtype == HighsBoundType::kLower) { + if (i->boundtype == HighsBoundType::kLower) { if (val == 1) { - if (globaldomain.col_lower_[i->domchg.column] != -kHighsInf) - addVLB(i->domchg.column, col, - i->domchg.boundval - globaldomain.col_lower_[i->domchg.column], - globaldomain.col_lower_[i->domchg.column]); + if (globaldomain.col_lower_[i->column] != -kHighsInf) + addVLB(i->column, col, + i->boundval - globaldomain.col_lower_[i->column], + globaldomain.col_lower_[i->column]); } else - addVLB(i->domchg.column, + addVLB(i->column, col, // in case the lower bound is infinite the varbound can // still be tightened as soon as a finite upper bound is // known because the offset is finite - globaldomain.col_lower_[i->domchg.column] - i->domchg.boundval, - i->domchg.boundval); + globaldomain.col_lower_[i->column] - i->boundval, i->boundval); } else { if (val == 1) { - if (globaldomain.col_upper_[i->domchg.column] != kHighsInf) - addVUB(i->domchg.column, col, - i->domchg.boundval - globaldomain.col_upper_[i->domchg.column], - globaldomain.col_upper_[i->domchg.column]); + if (globaldomain.col_upper_[i->column] != kHighsInf) + addVUB(i->column, col, + i->boundval - globaldomain.col_upper_[i->column], + globaldomain.col_upper_[i->column]); } else - addVUB(i->domchg.column, + addVUB(i->column, col, // in case the upper bound is infinite the varbound can // still be tightened as soon as a finite upper bound is // known because the offset is finite - globaldomain.col_upper_[i->domchg.column] - i->domchg.boundval, - i->domchg.boundval); + globaldomain.col_upper_[i->column] - i->boundval, i->boundval); } } @@ -187,17 +185,17 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { hasProbed[idx] = true; for (auto i = implics.begin(); i != implics.begin() + safeImplicsEnd; ++i) { Implication implication; - if (i->domchg.boundtype == HighsBoundType::kLower) { - implication.lb = i->domchg.boundval; + if (i->boundtype == HighsBoundType::kLower) { + implication.lb = i->boundval; } else { - implication.ub = i->domchg.boundval; + implication.ub = i->boundval; } - addImplication(idx, i->domchg.column, implication); + addImplication(idx, i->column, implication); } pdqsort(implics.begin(), implics.end(), - [](const TentativeImplication& a, const TentativeImplication& b) { - return a.domchg.column < b.domchg.column; + [](const HighsDomainChange& a, const HighsDomainChange& b) { + return a.column < b.column; }); return false; @@ -371,8 +369,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (mipsolver.mipdata_->cliquetable.getSubstitution(col) != nullptr) return true; - if (dualFixProbingActive && !dualFixProbingBinInds_.empty() && - !mipsolver.mipdata_->cliquetable.isFull()) { + if (dualFixProbingActive && !dualFixProbingBinInds_.empty()) { HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; HighsCliqueTable::CliqueVar clique[2]; for (HighsInt k : dualFixProbingBinInds_) { @@ -380,14 +377,13 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { if (globaldomain.infeasible()) return true; uint8_t mask = dualFixProbingBinFlags_[k]; if (mask == 0) continue; - if (mask == 10) { globaldomain.fixCol(k, globaldomain.col_lower_[k]); mask = 0; } else if (mask == 5) { globaldomain.fixCol(k, globaldomain.col_upper_[k]); mask = 0; - } else if (mask == 9) { + } else if (mask == 9 && !mipsolver.mipdata_->cliquetable.isFull()) { clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); @@ -395,7 +391,7 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); mask = 0; - } else if (mask == 6) { + } else if (mask == 6 && !mipsolver.mipdata_->cliquetable.isFull()) { clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); @@ -416,50 +412,40 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { HighsInt d = 0; while (u < nimplicsup && d < nimplicsdown) { - if (implicationsUp[u].domchg.column < implicationsDown[d].domchg.column) + if (implicationsUp[u].column < implicationsDown[d].column) ++u; - else if (implicationsDown[d].domchg.column < - implicationsUp[u].domchg.column) + else if (implicationsDown[d].column < implicationsUp[u].column) ++d; else { - assert(implicationsUp[u].domchg.column == - implicationsDown[d].domchg.column); - HighsInt implcol = implicationsUp[u].domchg.column; + assert(implicationsUp[u].column == implicationsDown[d].column); + HighsInt implcol = implicationsUp[u].column; double lbDown = globaldomain.col_lower_[implcol]; double ubDown = globaldomain.col_upper_[implcol]; - bool safeDown = implicationsDown[d].dualSafe; double lbUp = lbDown; double ubUp = ubDown; - bool safeUp = implicationsUp[u].dualSafe; do { - if (implicationsDown[d].domchg.boundtype == HighsBoundType::kLower) { - lbDown = std::max(lbDown, implicationsDown[d].domchg.boundval); - safeDown &= implicationsDown[d].dualSafe; + if (implicationsDown[d].boundtype == HighsBoundType::kLower) { + lbDown = std::max(lbDown, implicationsDown[d].boundval); } else { - ubDown = std::min(ubDown, implicationsDown[d].domchg.boundval); - safeDown &= implicationsDown[d].dualSafe; + ubDown = std::min(ubDown, implicationsDown[d].boundval); } ++d; - } while (d < nimplicsdown && - implicationsDown[d].domchg.column == implcol); + } while (d < nimplicsdown && implicationsDown[d].column == implcol); do { - if (implicationsUp[u].domchg.boundtype == HighsBoundType::kLower) { - lbUp = std::max(lbUp, implicationsUp[u].domchg.boundval); - safeUp &= implicationsUp[u].dualSafe; + if (implicationsUp[u].boundtype == HighsBoundType::kLower) { + lbUp = std::max(lbUp, implicationsUp[u].boundval); } else { - ubUp = std::min(ubUp, implicationsUp[u].domchg.boundval); - safeUp &= implicationsUp[u].dualSafe; + ubUp = std::min(ubUp, implicationsUp[u].boundval); } ++u; - } while (u < nimplicsup && implicationsUp[u].domchg.column == implcol); + } while (u < nimplicsup && implicationsUp[u].column == implcol); if (colsubstituted[implcol] || globaldomain.isFixed(implcol)) continue; if (lbDown == ubDown && lbUp == ubUp && - std::abs(lbDown - lbUp) > mipsolver.mipdata_->feastol && safeUp && - safeDown) { + std::abs(lbDown - lbUp) > mipsolver.mipdata_->feastol) { HighsSubstitution substitution; substitution.substcol = implcol; substitution.staycol = col; diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 4401209275c..37469092b9d 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -28,13 +28,8 @@ class HighsImplications { double ub = kHighsInf; }; - struct TentativeImplication { - HighsDomainChange domchg; - bool dualSafe; - }; - - std::vector implicationsDown; - std::vector implicationsUp; + std::vector implicationsDown; + std::vector implicationsUp; std::vector> implications; std::vector> reverseImplications; std::vector hasProbed; From c98bdbbff437afd48ecc4ee3ca5382a0d62dcb18 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 31 Aug 2026 15:51:12 +0200 Subject: [PATCH 43/46] Store implications from safe binary implications too --- highs/mip/HighsImplications.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 5d1d8e8fcda..33e581db4b3 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -116,14 +116,16 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { doBacktrack(changedend); if (safeImplicsEnd < static_cast(implics.size())) { + for (const HighsDomainChange& implic : implics) { + if (globaldomain.isBinary(implic.column)) { + recordTentativeCliques(val, implic); + } + } auto binstart = std::partition(implics.begin() + safeImplicsEnd, implics.end(), [&](const HighsDomainChange& a) { return !globaldomain.isBinary(a.column); }); - // store the tentative bound changes of binaries separately - for (auto i = binstart; i != implics.end(); ++i) - recordTentativeCliques(val, *i); implics.erase(binstart, implics.end()); } From 99b568bba776687cf0bd0d0d89b0eeaae788a2b5 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 31 Aug 2026 16:59:46 +0200 Subject: [PATCH 44/46] Copy entire stack for tentative cliques. Fix direction only when applying fixing --- highs/mip/HighsDomain.cpp | 9 +++++++-- highs/mip/HighsImplications.cpp | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 5f3492637c3..6e0f19d94b7 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -724,7 +724,6 @@ void HighsDomain::DualFixProbingPropagation::propagate() { auto collectZeroCostFixing = [&](const HighsInt col, const DualFixProbingFixDirection direction) { fixedZeroCostColumns_.emplace_back(FixedZeroCostColumn{col, direction}); - zeroCostDirections_[col] = direction; }; const double dualTol = mipsolver->options_mip_->dual_feasibility_tolerance; @@ -825,6 +824,8 @@ void HighsDomain::DualFixProbingPropagation::propagateZeroCosts() { for (const FixedZeroCostColumn& fixing : fixedZeroCostColumns_) { if (domain->isFixed(fixing.col)) continue; + assert(zeroCostDirections_[fixing.col] == FixUndecided || + zeroCostDirections_[fixing.col] == fixing.direction); if (fixing.direction == FixLowerBound) { domain->changeBound(HighsBoundType::kUpper, fixing.col, domain->col_lower_[fixing.col], @@ -834,7 +835,11 @@ void HighsDomain::DualFixProbingPropagation::propagateZeroCosts() { domain->col_upper_[fixing.col], Reason::unspecified()); } - if (domain->infeasible()) break; + if (!domain->infeasible()) { + zeroCostDirections_[fixing.col] = fixing.direction; + } else { + break; + } } fixedZeroCostColumns_.clear(); diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 33e581db4b3..7f2afa1eb72 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -86,6 +86,12 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { mipsolver.mipdata_->getPseudoCost().addInferenceObservation( col, numImplications, val); + std::vector origStackCopy; + if (dualFixProbingActive) { + origStackCopy.assign(domchgstack.begin() + stackimplicstart, + domchgstack.begin() + stackimplicend); + } + std::vector& implics = val ? implicationsUp : implicationsDown; implics.clear(); @@ -115,12 +121,13 @@ bool HighsImplications::computeImplications(HighsInt col, bool val) { // backtrack doBacktrack(changedend); - if (safeImplicsEnd < static_cast(implics.size())) { - for (const HighsDomainChange& implic : implics) { - if (globaldomain.isBinary(implic.column)) { - recordTentativeCliques(val, implic); - } + for (const HighsDomainChange& implic : origStackCopy) { + if (globaldomain.isBinary(implic.column)) { + recordTentativeCliques(val, implic); } + } + + if (safeImplicsEnd < static_cast(implics.size())) { auto binstart = std::partition(implics.begin() + safeImplicsEnd, implics.end(), [&](const HighsDomainChange& a) { From 93c702404719bb6dd36cfa97a2c766bd907c4edd Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 11 Sep 2026 10:18:00 +0200 Subject: [PATCH 45/46] Add struct to simplify --- highs/mip/HighsDomain.cpp | 20 +++++++------- highs/mip/HighsDomain.h | 12 ++++++--- highs/mip/HighsImplications.cpp | 24 +++++++++-------- highs/mip/HighsImplications.h | 47 ++++++++++++++++++--------------- 4 files changed, 58 insertions(+), 45 deletions(-) diff --git a/highs/mip/HighsDomain.cpp b/highs/mip/HighsDomain.cpp index 6e0f19d94b7..32cc330508a 100644 --- a/highs/mip/HighsDomain.cpp +++ b/highs/mip/HighsDomain.cpp @@ -681,28 +681,30 @@ void HighsDomain::DualFixProbingPropagation::recomputeLocks() { void HighsDomain::DualFixProbingPropagation::updateRhsRedundant(HighsInt row) { if (!isEnabled()) return; - if (domain->activitymaxinf_[row] != 0 || redundantRowFlags_[2 * row + 1] || + RowSide idx{row, true}; + if (domain->activitymaxinf_[row] != 0 || redundantRowFlags_[idx] || mipsolver->model_->row_upper_[row] == kHighsInf) return; if (domain->getMaxActivity(row) <= mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol) { - redundantRowInds_.push_back(2 * row + 1); - redundantRowFlags_[2 * row + 1] = 1; + redundantRowInds_.push_back(idx); + redundantRowFlags_[idx] = 1; } } void HighsDomain::DualFixProbingPropagation::updateLhsRedundant(HighsInt row) { if (!isEnabled()) return; - if (domain->activitymininf_[row] != 0 || redundantRowFlags_[2 * row] || + RowSide idx{row, false}; + if (domain->activitymininf_[row] != 0 || redundantRowFlags_[idx] || mipsolver->model_->row_lower_[row] == -kHighsInf) return; if (domain->getMinActivity(row) >= mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol) { - redundantRowInds_.push_back(2 * row); - redundantRowFlags_[2 * row] = 1; + redundantRowInds_.push_back(idx); + redundantRowFlags_[idx] = 1; } } @@ -730,9 +732,9 @@ void HighsDomain::DualFixProbingPropagation::propagate() { for (HighsInt i = previousRedundantRowSize; i != static_cast(redundantRowInds_.size()); ++i) { - const HighsInt loc = redundantRowInds_[i]; - const HighsInt row = loc / 2; - const bool isLhs = (loc % 2) == 0; + const RowSide& idx = redundantRowInds_[i]; + const HighsInt row = idx.row; + const bool isLhs = !idx.isRhs; const HighsInt start = mipsolver->mipdata_->ARstart_[row]; const HighsInt end = mipsolver->mipdata_->ARstart_[row + 1]; for (HighsInt j = start; j < end; ++j) { diff --git a/highs/mip/HighsDomain.h b/highs/mip/HighsDomain.h index b9cdbed648e..d660d1597e6 100644 --- a/highs/mip/HighsDomain.h +++ b/highs/mip/HighsDomain.h @@ -240,9 +240,14 @@ class HighsDomain { HighsDomain* domain = nullptr; HighsMipSolver* mipsolver = nullptr; - // store row lower and row upper at 2i and 2i + 1 + struct RowSide { + HighsInt row; + bool isRhs; + operator size_t() const { return 2 * row + isRhs; } + }; + std::vector redundantRowFlags_; - std::vector redundantRowInds_; + std::vector redundantRowInds_; HighsInt previousRedundantRowSize = 0; // Track direction of zero fixings so we don't store disagreeing results @@ -314,8 +319,7 @@ class HighsDomain { redundantRowInds_.clear(); } - for (size_t i = 0; i < redundantRowFlags_.size(); ++i) - assert(!redundantRowFlags_[i]); + for (const auto& flag : redundantRowFlags_) assert(!flag); fixedZeroCostColumns_.clear(); setZeroCostFixingPosition(kHighsIInf); diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 7f2afa1eb72..567852ed21e 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -384,30 +384,32 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { for (HighsInt k : dualFixProbingBinInds_) { if (!globaldomain.isBinary(k) || colsubstituted[k]) continue; if (globaldomain.infeasible()) return true; - uint8_t mask = dualFixProbingBinFlags_[k]; - if (mask == 0) continue; - if (mask == 10) { + const TentativeFixing& f = dualFixProbingBinFlags_[k]; + if (f.isUndecided()) continue; + if (f.downProbe == TentativeFixing::FixLower && + f.upProbe == TentativeFixing::FixLower) { globaldomain.fixCol(k, globaldomain.col_lower_[k]); - mask = 0; - } else if (mask == 5) { + } else if (f.downProbe == TentativeFixing::FixUpper && + f.upProbe == TentativeFixing::FixUpper) { globaldomain.fixCol(k, globaldomain.col_upper_[k]); - mask = 0; - } else if (mask == 9 && !mipsolver.mipdata_->cliquetable.isFull()) { + } else if (f.downProbe == TentativeFixing::FixLower && + f.upProbe == TentativeFixing::FixUpper && + !cliquetable.isFull()) { clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); clique[0] = HighsCliqueTable::CliqueVar(col, 0); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); - mask = 0; - } else if (mask == 6 && !mipsolver.mipdata_->cliquetable.isFull()) { + } else if (f.downProbe == TentativeFixing::FixUpper && + f.upProbe == TentativeFixing::FixLower && + !cliquetable.isFull()) { clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); clique[0] = HighsCliqueTable::CliqueVar(col, 0); clique[1] = HighsCliqueTable::CliqueVar(k, 1); cliquetable.addClique(mipsolver, &clique[0], 2); - mask = 0; } if (globaldomain.infeasible()) return true; } @@ -648,7 +650,7 @@ void HighsImplications::rebuild(HighsInt ncols, vlbs.resize(ncols); dualFixProbingBinInds_.clear(); dualFixProbingBinInds_.reserve(ncols); - dualFixProbingBinFlags_.assign(ncols, 0); + dualFixProbingBinFlags_.assign(ncols, TentativeFixing{}); numImplications = 0; numVarBounds = 0; HighsInt oldncols = oldvubs.size(); diff --git a/highs/mip/HighsImplications.h b/highs/mip/HighsImplications.h index 37469092b9d..658797a4a84 100644 --- a/highs/mip/HighsImplications.h +++ b/highs/mip/HighsImplications.h @@ -67,17 +67,28 @@ class HighsImplications { private: std::vector> vubs; std::vector> vlbs; + struct TentativeFixing { + enum Direction : uint8_t { Undecided, FixLower, FixUpper }; + Direction downProbe = Undecided; + Direction upProbe = Undecided; + + bool isUndecided() const { + return downProbe == Undecided && upProbe == Undecided; + } + + void record(bool upProbing, HighsBoundType boundtype) { + Direction& probe = upProbing ? upProbe : downProbe; + if (probe != Undecided) return; + probe = boundtype == HighsBoundType::kLower ? FixUpper : FixLower; + } + + void clear() { + downProbe = Undecided; + upProbe = Undecided; + } + }; std::vector dualFixProbingBinInds_; - // (0000) : Not involved - // (0010) : Fixed to lower in zero-side probing - // (0001) : Fixed to upper in zero-side probing - // (1000) : Fixed to lower in one-side probing - // (0100) : Fixed to upper in one-side probing - // (1010) : Fixed to lower in both sides. Fix to lower. - // (0101) : Fixed to upper in both sides. Fix to upper. - // (1001) : Conclude that x1 + x2 = 1 - // (0110) : Conclude that x1 = x2 - std::vector dualFixProbingBinFlags_; + std::vector dualFixProbingBinFlags_; public: const HighsMipSolver& mipsolver; @@ -123,7 +134,7 @@ class HighsImplications { vlbs.resize(ncols); maxVarBounds = calcMaxVarBounds(ncols); dualFixProbingBinInds_.reserve(ncols); - dualFixProbingBinFlags_.assign(ncols, 0); + dualFixProbingBinFlags_.assign(ncols, TentativeFixing{}); } constexpr static int64_t calcMaxVarBounds(HighsInt numcol) { @@ -231,20 +242,14 @@ class HighsImplications { void recordTentativeCliques(const HighsInt val, const HighsDomainChange& domchg) { const HighsInt col = domchg.column; - const uint8_t mask = - 1 << (2 * val + (domchg.boundtype != HighsBoundType::kLower)); - - if ((dualFixProbingBinFlags_[col] & mask) == 0) { - if (dualFixProbingBinFlags_[col] == 0) - dualFixProbingBinInds_.push_back(col); - - dualFixProbingBinFlags_[col] |= mask; - } + TentativeFixing& fixing = dualFixProbingBinFlags_[col]; + if (fixing.isUndecided()) dualFixProbingBinInds_.push_back(col); + fixing.record(val == 1, domchg.boundtype); } void clearTentativeCliques() { for (const HighsInt col : dualFixProbingBinInds_) - dualFixProbingBinFlags_[col] = 0; + dualFixProbingBinFlags_[col].clear(); dualFixProbingBinInds_.clear(); } }; From e66477017404f9341c99b7179a27280c45c9719c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 11 Sep 2026 14:59:11 +0200 Subject: [PATCH 46/46] Add tests --- check/TestMipSolver.cpp | 269 ++++++++++++++++++++++++++++++++ highs/mip/HighsImplications.cpp | 8 +- 2 files changed, 273 insertions(+), 4 deletions(-) diff --git a/check/TestMipSolver.cpp b/check/TestMipSolver.cpp index 27b56e10a3d..9f3eebd8bfb 100644 --- a/check/TestMipSolver.cpp +++ b/check/TestMipSolver.cpp @@ -3,6 +3,7 @@ #include "SpecialLps.h" #include "catch.hpp" #include "mip/HighsCliqueTable.h" +#include "mip/HighsImplications.h" #include "mip/HighsMipSolver.h" #include "mip/HighsMipSolverData.h" @@ -1782,3 +1783,271 @@ TEST_CASE("redcost-fixing-large-bounds", "[highs_test_mip_solver]") { mipsolver, mipsolver.mipdata_->getDomain()); REQUIRE(!lurkingBounds.empty()); } + +TEST_CASE("dual-fix-probing-nonzero-cost", "[highs_test_mip_solver]") { + // Probing x=1 makes rows 0,1 redundant, removing all lower locks on y. + // Since y has positive cost, dual fix probing fixes y=0 during x=1 probe. + // Probing x=0 propagates y=2 from rows 0,1,2. Merging gives y=2-2x + // substitution, incrementing numReductions. + // + // Rows: x + y >= 1 (lower lock on y) + // 2x + y >= 2 (lower lock on y) + // -2x + y <= 2 (upper lock on y) + HighsLp lp; + lp.num_col_ = 2; + lp.num_row_ = 3; + lp.sense_ = ObjSense::kMinimize; + lp.col_cost_ = {0.0, 1.0}; + lp.col_lower_ = {0.0, 0.0}; + lp.col_upper_ = {1.0, 10.0}; + lp.row_lower_ = {1.0, 2.0, -kHighsInf}; + lp.row_upper_ = {kHighsInf, kHighsInf, 2.0}; + lp.integrality_ = {HighsVarType::kInteger, HighsVarType::kContinuous}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.num_col_ = 2; + lp.a_matrix_.num_row_ = 3; + lp.a_matrix_.start_ = {0, 3, 6}; + lp.a_matrix_.index_ = {0, 1, 2, 0, 1, 2}; + lp.a_matrix_.value_ = {1.0, 2.0, -2.0, 1.0, 1.0, 1.0}; + + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.passModel(lp); + + HighsCallback callback(&highs); + const HighsOptions& options = highs.getOptions(); + HighsSolution solution; + HighsMipSolver mipsolver(callback, options, lp, solution); + mipsolver.mipdata_ = + std::unique_ptr(new HighsMipSolverData(mipsolver)); + mipsolver.mipdata_->feastol = 1e-6; + mipsolver.mipdata_->setupDomainPropagation(); + + HighsDomain& domain = mipsolver.mipdata_->getDomain(); + HighsImplications& implications = mipsolver.mipdata_->implications; + + domain.getDualFixProbingPropagation().recomputeLocks(); + domain.setDualFixProbingActive(true); + + HighsInt numBoundChgs = 0; + implications.runProbing(0, numBoundChgs); + + domain.setDualFixProbingActive(false); + domain.propagate(); + + REQUIRE(!domain.infeasible()); + // Merging up/down implications yields substitution y = 2 - 2x + REQUIRE(numBoundChgs > 0); + REQUIRE(implications.substitutions.size() == 1); + REQUIRE(implications.substitutions[0].substcol == 1); + REQUIRE(implications.substitutions[0].staycol == 0); + REQUIRE(implications.substitutions[0].offset == 2.0); + REQUIRE(implications.substitutions[0].scale == -2.0); + + highs.resetGlobalScheduler(true); +} + +TEST_CASE("dual-fix-probing-tentative-global-fix", "[highs_test_mip_solver]") { + // Probing x in either direction forces k=0 via propagation. + // The tentative clique mechanism should detect this and globally fix k=0. + // + // Rows: x + k + z <= 1 (x=1 forces k+z <= 0 => k=0) + // -x + k + z <= 0 (x=0 forces k+z <= 0 => k=0) + HighsLp lp; + lp.num_col_ = 3; + lp.num_row_ = 2; + lp.sense_ = ObjSense::kMinimize; + lp.col_cost_ = {0.0, 0.0, 0.0}; + lp.col_lower_ = {0.0, 0.0, 0.0}; + lp.col_upper_ = {1.0, 1.0, 1.0}; + lp.row_lower_ = {-kHighsInf, -kHighsInf}; + lp.row_upper_ = {1.0, 0.0}; + lp.integrality_ = {HighsVarType::kInteger, HighsVarType::kInteger, + HighsVarType::kContinuous}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.num_col_ = 3; + lp.a_matrix_.num_row_ = 2; + // col 0 (x): row 0 coeff 1, row 1 coeff -1 + // col 1 (k): row 0 coeff 1, row 1 coeff 1 + // col 2 (z): row 0 coeff 1, row 1 coeff 1 + lp.a_matrix_.start_ = {0, 2, 4, 6}; + lp.a_matrix_.index_ = {0, 1, 0, 1, 0, 1}; + lp.a_matrix_.value_ = {1.0, -1.0, 1.0, 1.0, 1.0, 1.0}; + + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.passModel(lp); + + HighsCallback callback(&highs); + const HighsOptions& options = highs.getOptions(); + HighsSolution solution; + HighsMipSolver mipsolver(callback, options, lp, solution); + mipsolver.mipdata_ = + std::unique_ptr(new HighsMipSolverData(mipsolver)); + mipsolver.mipdata_->feastol = 1e-6; + mipsolver.mipdata_->setupDomainPropagation(); + + HighsDomain& domain = mipsolver.mipdata_->getDomain(); + HighsImplications& implications = mipsolver.mipdata_->implications; + + domain.getDualFixProbingPropagation().recomputeLocks(); + domain.setDualFixProbingActive(true); + + HighsInt numBoundChgs = 0; + implications.runProbing(0, numBoundChgs); + + domain.setDualFixProbingActive(false); + domain.propagate(); + + REQUIRE(!domain.infeasible()); + // k (col 1) globally fixed to 0 by tentative clique mechanism + REQUIRE(domain.isFixed(1)); + REQUIRE(domain.col_lower_[1] == 0.0); + REQUIRE(domain.col_upper_[1] == 0.0); + + highs.resetGlobalScheduler(true); +} + +TEST_CASE("dual-fix-probing-tentative-clique-complement", + "[highs_test_mip_solver]") { + // Complement case: x=1 fixes k=0 via propagation (safe implication), + // x=0 fixes k=1 via zero-cost lock removal (unsafe, only tentative). + // The tentative clique mechanism discovers x + k = 1. + // + // Cols: x binary (0), k binary (1, cost 0), y continuous [0,10] (2) + // Rows: x + k <= 1 (upper lock on k; propagation: x=1 => k=0) + // k + y >= 0.5 (lower lock on k; not redundant before zero-cost + // fix) + // + // x=1: propagation from R0 fixes k=0 (safe). + // x=0: R0 max activity = 0+1 = 1 <= 1 => RHS redundant, upper lock removed. + // All upper locks gone, cost=0 => zero-cost fix k=1 (unsafe). + HighsLp lp; + lp.num_col_ = 3; + lp.num_row_ = 2; + lp.sense_ = ObjSense::kMinimize; + lp.col_cost_ = {0.0, 0.0, 0.0}; + lp.col_lower_ = {0.0, 0.0, 0.0}; + lp.col_upper_ = {1.0, 1.0, 10.0}; + lp.row_lower_ = {-kHighsInf, 0.5}; + lp.row_upper_ = {1.0, kHighsInf}; + lp.integrality_ = {HighsVarType::kInteger, HighsVarType::kInteger, + HighsVarType::kContinuous}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.num_col_ = 3; + lp.a_matrix_.num_row_ = 2; + // col 0 (x): row 0 coeff 1 + // col 1 (k): row 0 coeff 1, row 1 coeff 1 + // col 2 (y): row 1 coeff 1 + lp.a_matrix_.start_ = {0, 1, 3, 4}; + lp.a_matrix_.index_ = {0, 0, 1, 1}; + lp.a_matrix_.value_ = {1.0, 1.0, 1.0, 1.0}; + + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.passModel(lp); + + HighsCallback callback(&highs); + const HighsOptions& options = highs.getOptions(); + HighsSolution solution; + HighsMipSolver mipsolver(callback, options, lp, solution); + mipsolver.mipdata_ = + std::unique_ptr(new HighsMipSolverData(mipsolver)); + mipsolver.mipdata_->feastol = 1e-6; + mipsolver.mipdata_->setupDomainPropagation(); + + HighsDomain& domain = mipsolver.mipdata_->getDomain(); + HighsImplications& implications = mipsolver.mipdata_->implications; + HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; + + domain.getDualFixProbingPropagation().recomputeLocks(); + domain.setDualFixProbingActive(true); + + HighsInt numBoundChgs = 0; + implications.runProbing(0, numBoundChgs); + + domain.setDualFixProbingActive(false); + domain.propagate(); + + REQUIRE(!domain.infeasible()); + // k (col 1) substituted by complement of x: k = 1 - x + const auto* subst = cliquetable.getSubstitution(1); + REQUIRE(subst != nullptr); + REQUIRE(subst->substcol == 1); + REQUIRE(subst->replace.col == 0); + REQUIRE(subst->replace.val == 0); + + highs.resetGlobalScheduler(true); +} + +TEST_CASE("dual-fix-probing-tentative-clique-equivalence", + "[highs_test_mip_solver]") { + // Equivalence case: x=1 fixes k=1 via propagation (safe implication), + // x=0 fixes k=0 via zero-cost lock removal (unsafe, only tentative). + // The tentative clique mechanism discovers x = k. + // + // Cols: x binary (0), k binary (1, cost 0), y continuous [0,10] (2) + // Rows: -x + k >= 0 (lower lock on k; propagation: x=1 => k>=1 => k=1) + // k + y <= 10.5 (upper lock on k; not redundant before zero-cost + // fix) + // + // x=1: propagation from R0 fixes k=1 (safe). + // x=0: R0 min activity = 0+0 = 0 >= 0 => LHS redundant, lower lock removed. + // All lower locks gone, cost=0 => zero-cost fix k=0 (unsafe). + HighsLp lp; + lp.num_col_ = 3; + lp.num_row_ = 2; + lp.sense_ = ObjSense::kMinimize; + lp.col_cost_ = {0.0, 0.0, 0.0}; + lp.col_lower_ = {0.0, 0.0, 0.0}; + lp.col_upper_ = {1.0, 1.0, 10.0}; + lp.row_lower_ = {0.0, -kHighsInf}; + lp.row_upper_ = {kHighsInf, 10.5}; + lp.integrality_ = {HighsVarType::kInteger, HighsVarType::kInteger, + HighsVarType::kContinuous}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.num_col_ = 3; + lp.a_matrix_.num_row_ = 2; + // col 0 (x): row 0 coeff -1 + // col 1 (k): row 0 coeff 1, row 1 coeff 1 + // col 2 (y): row 1 coeff 1 + lp.a_matrix_.start_ = {0, 1, 3, 4}; + lp.a_matrix_.index_ = {0, 0, 1, 1}; + lp.a_matrix_.value_ = {-1.0, 1.0, 1.0, 1.0}; + + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.passModel(lp); + + HighsCallback callback(&highs); + const HighsOptions& options = highs.getOptions(); + HighsSolution solution; + HighsMipSolver mipsolver(callback, options, lp, solution); + mipsolver.mipdata_ = + std::unique_ptr(new HighsMipSolverData(mipsolver)); + mipsolver.mipdata_->feastol = 1e-6; + mipsolver.mipdata_->setupDomainPropagation(); + + HighsDomain& domain = mipsolver.mipdata_->getDomain(); + HighsImplications& implications = mipsolver.mipdata_->implications; + HighsCliqueTable& cliquetable = mipsolver.mipdata_->cliquetable; + + domain.getDualFixProbingPropagation().recomputeLocks(); + domain.setDualFixProbingActive(true); + + HighsInt numBoundChgs = 0; + implications.runProbing(0, numBoundChgs); + + domain.setDualFixProbingActive(false); + domain.propagate(); + + REQUIRE(!domain.infeasible()); + // k (col 1) substituted by x: k = x + const auto* subst = cliquetable.getSubstitution(1); + REQUIRE(subst != nullptr); + REQUIRE(subst->substcol == 1); + REQUIRE(subst->replace.col == 0); + REQUIRE(subst->replace.val == 1); + + highs.resetGlobalScheduler(true); +} diff --git a/highs/mip/HighsImplications.cpp b/highs/mip/HighsImplications.cpp index 567852ed21e..79f79f989eb 100644 --- a/highs/mip/HighsImplications.cpp +++ b/highs/mip/HighsImplications.cpp @@ -392,8 +392,8 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { } else if (f.downProbe == TentativeFixing::FixUpper && f.upProbe == TentativeFixing::FixUpper) { globaldomain.fixCol(k, globaldomain.col_upper_[k]); - } else if (f.downProbe == TentativeFixing::FixLower && - f.upProbe == TentativeFixing::FixUpper && + } else if (f.downProbe == TentativeFixing::FixUpper && + f.upProbe == TentativeFixing::FixLower && !cliquetable.isFull()) { clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 1); @@ -401,8 +401,8 @@ bool HighsImplications::runProbing(HighsInt col, HighsInt& numReductions) { clique[0] = HighsCliqueTable::CliqueVar(col, 0); clique[1] = HighsCliqueTable::CliqueVar(k, 0); cliquetable.addClique(mipsolver, &clique[0], 2); - } else if (f.downProbe == TentativeFixing::FixUpper && - f.upProbe == TentativeFixing::FixLower && + } else if (f.downProbe == TentativeFixing::FixLower && + f.upProbe == TentativeFixing::FixUpper && !cliquetable.isFull()) { clique[0] = HighsCliqueTable::CliqueVar(col, 1); clique[1] = HighsCliqueTable::CliqueVar(k, 0);