From 5691e569b1d856ee0f1e47597e83234f7b14a43b Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 23 Jul 2025 12:41:12 +0200 Subject: [PATCH 01/64] Add half-way-draft of flow-cover-cuts --- highs/mip/HighsCutGeneration.cpp | 32 +++ highs/mip/HighsCutGeneration.h | 31 +++ highs/mip/HighsTransformedLp.cpp | 396 +++++++++++++++++++++++++++++++ highs/mip/HighsTransformedLp.h | 7 + 4 files changed, 466 insertions(+) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index f518b7d800..2ebdc6793f 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1110,6 +1110,10 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } #endif + // TODO: Add attempt at flow cover generation + initSNFRelaxation(static_cast(inds_.size())); + transLp.; + bool intsPositive = true; if (!transLp.transform(vals_, upper, solval, inds_, rhs_, intsPositive)) return false; @@ -1299,6 +1303,34 @@ bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, return cutindex != -1; } +void HighsCutGeneration::initSNFRelaxation(HighsInt numNonZero) { + HighsInt numTransformedCol = lpRelaxation.numCols() + lpRelaxation.numRows(); + if (snfr.bincolused.size() > numTransformedCol) { + snfr.bincolused.resize(numTransformedCol, false); + snfr.origbincolcoef.resize(numTransformedCol, 0); + } else { + for (HighsInt i = 0; i != snfr.bincolused.size(); ++i) { + snfr.bincolused[i] = false; + snfr.origbincolcoef[i] = 0; + } + } + + if (snfr.coef.size() < numNonZero) { + snfr.origbincols.resize(numNonZero); + snfr.origcontcols.resize(numNonZero); + snfr.binsolval.resize(numNonZero); + snfr.contsolval.resize(numNonZero); + snfr.coef.resize(numNonZero); + snfr.vubcoef.resize(numNonZero); + snfr.aggrconstant.resize(numNonZero); + snfr.aggrbincoef.resize(numNonZero); + snfr.aggrcontcoef.resize(numNonZero); + snfr.rhs = 0; + snfr.lambda = 0; + snfr.nnzs = 0; + } +} + bool HighsCutGeneration::finalizeAndAddCut(std::vector& inds_, std::vector& vals_, double& rhs_) { diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index cfe01b3a8c..1d9743fc38 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -20,6 +20,7 @@ #include "util/HighsCDouble.h" #include "util/HighsInt.h" #include "util/HighsRandom.h" +#include "util/HighsSparseVectorSum.h" class HighsLpRelaxation; class HighsTransformedLp; @@ -103,6 +104,36 @@ class HighsCutGeneration { /// cutpool if it is violated enough bool finalizeAndAddCut(std::vector& inds, std::vector& vals, double& rhs); + + /// Single Node Flow Relaxation for flow cover cuts + struct SNFRelaxation { + std::vector bincolused; // has col been used in a vub + std::vector origbincolcoef; // original bin col coef + + HighsInt nnzs; // number of nonzeros + std::vector coef; // coefficients of cols in SNFR + std::vector vubcoef; // coefficients in vub of cols in SNFR + std::vector binsolval; // vub bin col sol in SNFR + std::vector contsolval; // real sol in SNFR + std::vector origbincols; // orig bin col used in SNFR + std::vector origcontcols; // orig cont cols used in SNFR + std::vector aggrbincoef; // aggr coef of orignal bin-col in SNFR + std::vector aggrcontcoef; // aggr coef of original cont-col in SNFR + std::vector aggrconstant; // aggr original constant in SNFR + + std::vector flowCoverStatus; // (+1) in fcover (-1) not in fcover + double rhs; + double lambda; + std::vector boundTypes; + HighsSparseVectorSum vectorsum; + }; + + private: + SNFRelaxation snfr; + void initSNFRelaxation(HighsInt numNonZero); + + public: + SNFRelaxation& getSNFRelaxation() { return snfr; } }; #endif diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 703b377e09..6a237881c9 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -6,8 +6,10 @@ /* */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #include "mip/HighsTransformedLp.h" +#include "../extern/pdqsort/pdqsort.h" #include "mip/HighsMipSolverData.h" #include "util/HighsCDouble.h" #include "util/HighsIntegers.h" @@ -561,3 +563,397 @@ bool HighsTransformedLp::untransform(std::vector& vals, return true; } + +// Create a single node flow relaxation (SNFR) from an aggregated +// mixed-integer row and find a valid flow cover. +bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, + std::vector& upper, + std::vector& solval, + std::vector& inds, + double& rhs, + bool& integersPositive, + HighsCutGeneration::SNFRelaxation& snfr) { + + // Turn \sum c_i x_i + \sum a_i y_i <= a_0 (x_i binary, y_i real non-neg) + // into \sum_{j \in N1} y_j - \sum_{j \in N2} y_j <= b, where y_j <= u_j x_j + + // vector sum should be empty + assert(snfr.vectorsum.getNonzeros().empty()); + const HighsSolution& lpSolution = lprelaxation.getLpSolver().getSolution(); + + HighsCDouble tmpRhs = rhs; + HighsCDouble tmpSnfrRhs = rhs; + + const HighsMipSolver& mip = lprelaxation.getMipSolver(); + const HighsInt slackOffset = lprelaxation.numCols(); + + HighsInt numNz = inds.size(); + + auto getLb = [&](HighsInt col) { + return (col < slackOffset ? mip.mipdata_->domain.col_lower_[col] + : lprelaxation.slackLower(col - slackOffset)); + }; + + auto getUb = [&](HighsInt col) { + return (col < slackOffset ? mip.mipdata_->domain.col_upper_[col] + : lprelaxation.slackUpper(col - slackOffset)); + }; + + auto remove = [&](HighsInt position) { + numNz--; + inds[position] = inds[numNz]; + vals[position] = vals[numNz]; + inds[numNz] = 0; + vals[numNz] = 0; + }; + + auto checkValidityVB = [&](HighsInt bincol, HighsImplications::VarBound vb, + double coef, double origbincoef, double lb, + double ub, bool isVub) { + if (bincol == -1) return false; + if (snfr.bincolused[bincol]) return false; + const double sign = coef >= 0 ? 1 : -1; + if (isVub) { + double val = sign * ((coef * vb.coef) + origbincoef); + if (val < 0 || val > kHighsInf) return false; + val = sign * ((coef * (lb - vb.constant)) + origbincoef); + if (val < 0) return false; + } else { + double val = sign * ((coef * vb.coef) + origbincoef); + if (val > 0 || val > kHighsInf) return false; + val = sign * ((coef * (ub - vb.constant)) + origbincoef); + if (val > 0) return false; + } + return true; + }; + + auto addSNFRentry = [&](HighsInt origbincol, HighsInt origcontcol, + double binsolval, double contsolval, HighsInt coef, + double vubcoef, double aggrconstant, + double aggrbincoef, double aggrcontcoef) { + snfr.origbincols[snfr.nnzs] = origbincol; + snfr.origcontcols[snfr.nnzs] = origcontcol; + snfr.binsolval[snfr.nnzs] = binsolval; + snfr.contsolval[snfr.nnzs] = contsolval; + snfr.coef[snfr.nnzs] = coef; + snfr.vubcoef[snfr.nnzs] = vubcoef; + snfr.aggrconstant[snfr.nnzs] = aggrconstant; + snfr.aggrbincoef[snfr.nnzs] = aggrbincoef; + snfr.aggrcontcoef[snfr.nnzs] = aggrcontcoef; + snfr.nnzs++; + }; + + // Place the non-binary variables to the front (all general ints relaxed) + HighsInt nbincols = 0; + for (HighsInt i = 0; i < numNz - nbincols; ++i) { + HighsInt col = inds[i]; + double lb = getLb(col); + double ub = getUb(col); + if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 0) { + nbincols++; + snfr.origbincolcoef[col] = vals[i]; + std::swap(inds[i], inds[numNz - nbincols]); + std::swap(vals[i], vals[numNz - nbincols]); + } + } + + HighsInt i = 0; + while (i < numNz) { + HighsInt col = inds[i]; + + double lb = getLb(col); + double ub = getUb(col); + + if (ub - lb < mip.options_mip_->small_matrix_value) { + tmpRhs -= std::min(lb, ub) * vals[i]; + tmpSnfrRhs -= std::min(lb, ub) * vals[i]; + if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 0) { + snfr.origbincolcoef[col] = 0; + } + remove(i); + continue; + } + + if (lb == -kHighsInf && ub == kHighsInf) { + vectorsum.clear(); + return false; + } + + // the code below uses the difference between the column upper and lower + // bounds as the upper bound for the slack from the variable upper bound + // constraint (upper[j] = ub - lb) and thus assumes that the variable upper + // bound constraints are tight. this assumption may not be satisfied when + // new bound changes were derived during cut generation and, therefore, we + // tighten the best variable upper bound. + if (bestVub[col].first != -1 && + bestVub[col].second.maxValue() > ub + mip.mipdata_->feastol) { + bool redundant = false; + bool infeasible = false; + mip.mipdata_->implications.cleanupVub(col, bestVub[col].first, + bestVub[col].second, ub, redundant, + infeasible, false); + } + + // the code below uses the difference between the column upper and lower + // bounds as the upper bound for the slack from the variable lower bound + // constraint (upper[j] = ub - lb) and thus assumes that the variable lower + // bound constraints are tight. this assumption may not be satisfied when + // new bound changes were derived during cut generation and, therefore, we + // tighten the best variable lower bound. + if (bestVlb[col].first != -1 && + bestVlb[col].second.minValue() < lb - mip.mipdata_->feastol) { + bool redundant = false; + bool infeasible = false; + mip.mipdata_->implications.cleanupVlb(col, bestVlb[col].first, + bestVlb[col].second, lb, redundant, + infeasible, false); + } + + // store the old bound type so that we can restore it if the continuous + // column is relaxed out anyways. This allows to correctly transform and + // then untransform multiple base rows which is useful to compute cuts based + // on several transformed base rows. It could otherwise lead to bugs if a + // column is first transformed with a simple bound and not relaxed but for + // another base row is transformed and relaxed with a variable bound. Should + // the non-relaxed column now be untransformed we would wrongly use the + // variable bound even though this is not the correct way to untransform the + // column. + BoundType oldBoundType = boundTypes[col]; + + // Transform entry into the SNFR + if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 0) { + if (snfr.bincolused[col] != 1) { + if (vals[i] >= 0) { + addSNFRentry(col, -1, lpSolution.col_value[col], + lpSolution.col_value[col] * vals[i], 1, vals[i], 0, + vals[i], 0); + } else { + addSNFRentry(col, -1, lpSolution.col_value[col], + -lpSolution.col_value[col] * vals[i], -1, -vals[i], 0, + -vals[i], 0); + } + } + } else { + if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { + if (!checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], + snfr.origbincolcoef[bestVlb[col].first], + lb, ub, false)) { + boundTypes[col] = BoundType::kSimpleLb; + } else if (vals[i] > 0 || + simpleLbDist[col] > lbDist[col] + mip.mipdata_->feastol) { + boundTypes[col] = BoundType::kVariableLb; + snfr.bincolused[bestVlb[col].first] = true; + } else + boundTypes[col] = BoundType::kSimpleLb; + } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol) { + if (!checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], + snfr.origbincolcoef[bestVub[col].first], + lb, ub, true)) { + boundTypes[col] = BoundType::kSimpleUb; + } else if (vals[i] < 0 || + simpleUbDist[col] > ubDist[col] + mip.mipdata_->feastol) { + boundTypes[col] = BoundType::kVariableUb; + snfr.bincolused[bestVub[col].first] = true; + } else { + boundTypes[col] = BoundType::kSimpleUb; + } + } else if (vals[i] > 0) { + if (checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], + snfr.origbincolcoef[bestVlb[col].first], + lb, ub, false)) { + snfr.bincolused[bestVlb[col].first] = true; + boundTypes[col] = BoundType::kVariableLb; + } else { + boundTypes[col] = BoundType::kSimpleLb; + } + } else { + if (checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], + snfr.origbincolcoef[bestVub[col].first], + lb, ub, true)) { + snfr.bincolused[bestVub[col].first] = true; + boundTypes[col] = BoundType::kVariableUb; + } else { + boundTypes[col] = BoundType::kSimpleUb; + } + } + + switch (boundTypes[col]) { + case BoundType::kSimpleLb: + double substsolval = static_cast( + vals[i] * (HighsCDouble(lpSolution.col_value[col]) - ub)); + double vubcoef = + static_cast(vals[i] * (HighsCDouble(ub) - lb)); + double valtimesub = static_cast(HighsCDouble(vals[i]) * ub); + if (vals[i] >= 0) { + addSNFRentry(-1, col, 1.0, -substsolval, -1, vubcoef, valtimesub, 0, + -vals[i]); + } else { + addSNFRentry(-1, col, 1, substsolval, 1, -vubcoef, -valtimesub, 0, + vals[i]); + } + tmpSnfrRhs -= valtimesub; + break; + case BoundType::kSimpleUb: + double substsolval = static_cast( + vals[i] * (HighsCDouble(lpSolution.col_value[col]) - lb)); + double vubcoef = + static_cast(vals[i] * (HighsCDouble(ub) - lb)); + double valtimeslb = static_cast(HighsCDouble(vals[i]) * lb); + if (vals[i] >= 0) { + addSNFRentry(-1, col, 1, substsolval, 1, vubcoef, -valtimeslb, 0, + vals[i]); + } else { + addSNFRentry(-1, col, 1, -substsolval, -1, -vubcoef, valtimeslb, 0, + -vals[i]); + } + tmpSnfrRhs -= valtimeslb; + break; + case BoundType::kVariableLb: + HighsInt vlbcol = bestVlb[col].first; + double substsolval = static_cast( + vals[i] * (HighsCDouble(lpSolution.col_value[col]) - + bestVlb[col].second.constant) + + (HighsCDouble(lpSolution.col_value[vlbcol]) * + snfr.origbincolcoef[vlbcol])); + double vlbcoef = static_cast( + HighsCDouble(vals[i]) * bestVlb[col].second.coef + + snfr.origbincolcoef[vlbcol]); + double valtimesvlbconst = static_cast( + HighsCDouble(vals[i]) * bestVlb[col].second.constant); + if (vals[i] >= 0) { + addSNFRentry(vlbcol, col, lpSolution.col_value[vlbcol], + -substsolval, -1, -vlbcoef, valtimesvlbconst, + -snfr.origbincolcoef[vlbcol], -vals[i]); + } else { + addSNFRentry(vlbcol, col, lpSolution.col_value[vlbcol], substsolval, + 1, vlbcoef, -valtimesvlbconst, + snfr.origbincolcoef[vlbcol], vals[i]); + } + tmpSnfrRhs -= valtimesvlbconst; + break; + case BoundType::kVariableUb: + HighsInt vubcol = bestVub[col].first; + double substsolval = static_cast( + vals[i] * (HighsCDouble(lpSolution.col_value[col]) - + bestVub[col].second.constant) + + (HighsCDouble(lpSolution.col_value[vubcol]) * + snfr.origbincolcoef[vubcol])); + double vubcoef = static_cast( + HighsCDouble(vals[i]) * bestVub[col].second.coef + + snfr.origbincolcoef[vubcol]); + double valtimesvubconst = static_cast( + HighsCDouble(vals[i]) * bestVub[col].second.constant); + if (vals[i] >= 0) { + addSNFRentry(vubcol, col, lpSolution.col_value[vubcol], substsolval, + 1, vubcoef, -valtimesvubconst, + snfr.origbincolcoef[vubcol], vals[i]); + } else { + addSNFRentry(vubcol, col, lpSolution.col_value[vubcol], + -substsolval, -1, -vubcoef, valtimesvubconst, + -snfr.origbincolcoef[vubcol], -vals[i]); + } + tmpSnfrRhs -= valtimesvubconst; + break; + } + } + // move to next element + i++; + } + + snfr.rhs = double(tmpSnfrRhs); + if (numNz == 0 && rhs >= -mip.mipdata_->feastol) return false; + + // Compute the flow cover, i.e., get sets C1 subset N1 and C2 subset N2 + // with sum_{j in C1} u_j - sum_{j in C2} u_j = b + lambda, lambda > 0 + if (snfr.flowCoverStatus.size() < snfr.nnzs) { + snfr.flowCoverStatus.resize(snfr.nnzs); + } + std::vector items(snfr.nnzs, -1); + HighsInt nNonFlowCover = 0; + HighsInt nFlowCover = 0; + HighsInt nitems = 0; + double n1itemsWeight = 0; + HighsCDouble flowCoverWeight = 0; + for (i = 0; i < snfr.nnzs; ++i) { + assert(snfr.coef[i] == 1 || snfr.coef[i] == -1); + if (abs(snfr.binsolval[i]) < mip.mipdata_->feastol) { + snfr.flowCoverStatus[i] = -1; + nNonFlowCover++; + continue; + } + if (fractionality(snfr.binsolval[i]) > mip.mipdata_->feastol) { + items[nitems] = i; + nitems++; + if (snfr.coef[i] == 1) { + n1itemsWeight += snfr.vubcoef[i]; + } + } else if (snfr.coef[i] == 1 && snfr.binsolval[i] < 0.5) { + snfr.flowCoverStatus[i] = -1; + nNonFlowCover++; + } else if (snfr.coef[i] == 1 && snfr.binsolval[i] > 0.5) { + snfr.flowCoverStatus[i] = 1; + nFlowCover++; + flowCoverWeight += snfr.vubcoef[i]; + } else if (snfr.coef[i] == -1 && snfr.binsolval[i] > 0.5) { + snfr.flowCoverStatus[i] = 1; + nNonFlowCover++; + flowCoverWeight -= snfr.vubcoef[i]; + } else { + assert(snfr.coef[i] == -1 && snfr.binsolval[i] < 0.5); + snfr.flowCoverStatus[i] = -1; + nNonFlowCover++; + } + } + assert(nNonFlowCover + nFlowCover + nitems == snfr.nnzs); + + double capacity = -snfr.rhs + static_cast(flowCoverWeight) + n1itemsWeight; + // There is no flow cover if capacity is less than zero after fixing + if (capacity < mip.mipdata_->feastol) return false; + // Solve a knapsack greedily to assign items to C1, C2, N1\C1, N2\C2 + double knapsackWeight = 0; + std::vector weights(nitems); + std::vector profits(nitems); + std::vector perm(nitems); + std::iota(perm.begin(), perm.end(), 0); + for (i = 0; i < nitems; ++i) { + weights[i] = snfr.vubcoef[items[i]]; + if (snfr.coef[items[i]] == 1) { + profits[i] = 1 - snfr.binsolval[items[i]]; + } else { + profits[i] = snfr.binsolval[items[i]]; + } + } + pdqsort(perm.begin(), perm.end(), [&](const HighsInt a, const HighsInt b) { + return profits[a] / weights[a] > profits[b] / weights[b]; + }); + // Greedily add items to knapsack + for (i = 0; i < nitems; ++i) { + HighsInt j = perm[i]; + HighsInt k = items[j]; + if (knapsackWeight + weights[j] < capacity) { + knapsackWeight += weights[j]; + if (snfr.coef[k] == 1) { + snfr.flowCoverStatus[k] = -1; + nNonFlowCover++; + } else { + snfr.flowCoverStatus[k] = 1; + nFlowCover++; + flowCoverWeight -= snfr.vubcoef[k]; + } + } else { + if (snfr.coef[k] == 1) { + snfr.flowCoverStatus[k] = 1; + nFlowCover++; + flowCoverWeight += snfr.vubcoef[k]; + } else { + snfr.flowCoverStatus[k] = -1; + nNonFlowCover++; + } + } + } + + snfr.lambda = static_cast(flowCoverWeight) - snfr.rhs; + if (snfr.lambda < mip.mipdata_->feastol) return false; + rhs = static_cast(tmpRhs); + return true; +} diff --git a/highs/mip/HighsTransformedLp.h b/highs/mip/HighsTransformedLp.h index 5c3d2d01db..aab0ce41dd 100644 --- a/highs/mip/HighsTransformedLp.h +++ b/highs/mip/HighsTransformedLp.h @@ -16,12 +16,14 @@ #include +#include "HighsCutGeneration.h" #include "lp_data/HConst.h" #include "mip/HighsImplications.h" #include "util/HighsCDouble.h" #include "util/HighsInt.h" #include "util/HighsSparseVectorSum.h" +class HighsCutGeneration; class HighsLpRelaxation; /// Helper class to compute single-row relaxations from the current LP @@ -58,6 +60,11 @@ class HighsTransformedLp { bool untransform(std::vector& vals, std::vector& inds, double& rhs, bool integral = false); + + bool transformSNFR(std::vector& vals, std::vector& upper, + std::vector& solval, std::vector& inds, + double& rhs, bool& integralPositive, + HighsCutGeneration::SNFRelaxation& snfr); }; #endif From 677c731f6efce80cd236ae4fe92c2a12df3a3fa1 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 23 Jul 2025 18:24:36 +0200 Subject: [PATCH 02/64] Add complete cutgen. Definitely buggy --- highs/mip/HighsCutGeneration.cpp | 345 +++++++++++++++++++++++++++++-- highs/mip/HighsCutGeneration.h | 27 +-- highs/mip/HighsTransformedLp.cpp | 239 +++++++-------------- highs/mip/HighsTransformedLp.h | 9 +- 4 files changed, 417 insertions(+), 203 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 2ebdc6793f..7889dc20ef 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -500,6 +500,207 @@ bool HighsCutGeneration::separateLiftedMixedIntegerCover() { return true; } +bool HighsCutGeneration::separateLiftedFlowCover() { + + // Compute the lifting data (ld) first + std::vector ldm(snfr.numNnzs); + HighsInt ldr = 0; + double ldmp = kHighsInf; + HighsCDouble sumN2mC2LE = 0.0; + HighsCDouble sumC1LE = 0.0; + HighsCDouble sumN2mC2GT = 0.0; + HighsCDouble sumC2 = 0.0; + + for (HighsInt i = 0; i != snfr.numNnzs; ++i) { + // col is in N2 \ C2 + if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == -1) { + assert(snfr.vubCoef[i] >= 0); + if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { + sumN2mC2GT += snfr.vubCoef[i]; + ldm[ldr] = snfr.vubCoef[i]; + ++ldr; + } else { + sumN2mC2LE += snfr.vubCoef[i]; + } + } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == -1) { + // col is in C2 + assert(snfr.vubCoef[i] > 0); + sumC2 += snfr.vubCoef[i]; + } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == 1) { + // col is in C1 + assert(snfr.vubCoef[i] > 0); + if ( snfr.vubCoef[i] > snfr.lambda + 1e-8) { + ldm[ldr] = snfr.vubCoef[i]; + ++ldr; + ldmp = std::min(ldmp, snfr.vubCoef[i]); + } else { + sumC1LE += snfr.vubCoef[i]; + } + } else { + // col is in N1 \ C1 + assert(snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == 1); + } + } + + if (ldmp == kHighsInf) return false; + + std::vector ldM(ldr + 1); + HighsCDouble ldml = std::min(snfr.lambda, static_cast(sumC1LE * sumN2mC2LE)); + HighsCDouble ldd1 = sumC2 * snfr.rhs; + HighsCDouble ldd2 = ldd1 + sumN2mC2GT + sumN2mC2LE; + + // Sort into non-increasing order (TODO: Remove comment after checking) + pdqsort_branchless(ldm.begin(), ldm.end(), [&](const HighsCDouble a, const HighsCDouble b) { + return a > b; + }); + + for (HighsInt i = 0; i != ldr + 1; ++i) { + if (i == 0) { + ldM[i] = ldm[i]; + } else { + ldM[i] = ldM[i - 1] + ldm[i]; + } + } + + HighsInt ldt = 0; + for (HighsInt i = ldr - 1; i != -1; --i) { + if (ldm[i] >= ldmp) { + ldt = i; + break; + } + } + + auto getAlphaBeta = [&](double vubcoef) { + HighsInt alpha; + HighsCDouble beta; + double vubcoefpluslambda = vubcoef + snfr.lambda; + + HighsInt i = 0; + for (i = 0; i < ldr && vubcoefpluslambda >= ldM[i + 1] + 1e-8; ++i) {} + + if (vubcoef <= ldM[i] - 1e-8) { + assert(ldM[i] < vubcoefpluslambda); + alpha = 1; + beta = -i * HighsCDouble(snfr.lambda) - ldM[i]; + } else { + alpha = 0; + beta = 0; + } + return std::make_pair(alpha, beta); + }; + + auto evaluateLiftingFunction = [&](double vubcoef) { + HighsInt i = 0; + for (i = 0; i < ldr && vubcoef + snfr.lambda >= ldM[i + 1] + 1e-8; ++i) {} + if (i < ldt) { + HighsCDouble liftedcoef = i * HighsCDouble(snfr.lambda); + if (ldM[i] < vubcoef - 1e-8) { + return static_cast(liftedcoef); + } + assert(i > 0 && ldM[i] < vubcoef + snfr.lambda - 1e-8 && vubcoef <= ldM[i]); + liftedcoef += vubcoef; + liftedcoef -= ldM[i]; + return static_cast(liftedcoef); + } + if (i < ldr) { + HighsCDouble tmp = HighsCDouble(ldm[i]) - ldmp - ldml + snfr.lambda; + if (tmp < 0) tmp = 0; + tmp += ldM[i] + ldml; + if (tmp < vubcoef + snfr.lambda - 1e-8) { + return static_cast(i * HighsCDouble(snfr.lambda)); + } + assert(ldM[i] <= vubcoef + snfr.lambda + 1e-8 && snfr.lambda + vuebcoef <= ldM[i] + ldml + std::max(0, ldm[i] - (ldmp - snfr.lambda) - ldml)); + return static_cast(i * HighsCDouble(snfr.lambda) + vubcoef - + ldM[i]); + } + assert(i == ldr && ldM[i] <= vubcoef + snfr.lambda + 1e-8); + return static_cast(ldr * HighsCDouble(snfr.lambda) + vubcoef - + ldM[i]); + }; + + // Lift the flow cover cut + HighsCDouble rhs = ldd1; + for (HighsInt i = 0; i != snfr.numNnzs; ++i) { + // col is in N2 \ C2 + if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == -1) { + if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { + if (snfr.origBinCols[i] != -1) { + // col is in L- + assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); + snfr.vectorsum.add(snfr.origBinCols[i], -snfr.lambda); + } else { + rhs += snfr.lambda; + } + } else { + // col is in L-- + if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { + assert(snfr.vectorsum.getValue(snfr.origContCols[i]) == 0); + snfr.vectorsum.add(snfr.origContCols[i], -snfr.aggrContCoef[i]); + } + if (snfr.origBinCols[i] != -1 && snfr.aggrBinCoef[i] != 0) { + assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); + snfr.vectorsum.add(snfr.origContCols[i], -snfr.aggrBinCoef[i]); + } + } + rhs += snfr.aggrConstant[i]; + } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == -1) { + // col is in C2 + if (snfr.origBinCols[i] != -1) { + assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); + double liftedbincoef = evaluateLiftingFunction(snfr.vubCoef[i]); + if (liftedbincoef != 0) { + snfr.vectorsum.add(snfr.origBinCols[i], -liftedbincoef); + rhs -= liftedbincoef; + } + } + } else if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == 1) { + // col is in N1 \ C1 + std::pair alphabeta = getAlphaBeta(snfr.vubCoef[i]); + if (alphabeta.first == 1) { + assert(alphabeta.second > 0); + if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { + assert(snfr.vectorsum.getValue(snfr.origContCols[i]) == 0); + snfr.vectorsum.add(snfr.origContCols[i], snfr.aggrContCoef[i]); + } + HighsCDouble binvarcoef = snfr.aggrBinCoef[i] - alphabeta.second; + if (snfr.origBinCols[i] != -1) { + assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); + if (binvarcoef != 0) { + snfr.vectorsum.add(snfr.origBinCols[i], binvarcoef); + } + } else { + rhs -= binvarcoef; + } + rhs -= snfr.aggrConstant[i]; + } + } else { + // col is in C1 + assert(snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == 1); + HighsCDouble bincoef = snfr.aggrBinCoef[i]; + HighsCDouble constant = snfr.aggrConstant[i]; + if (snfr.origBinCols[i] != -1 && snfr.vubCoef[i] >= snfr.lambda + 1e-8) { + // col is in C++ + HighsCDouble tmp = HighsCDouble(snfr.vubCoef[i]) - snfr.lambda; + constant += tmp; + bincoef -= tmp; + } + if (snfr.origBinCols[i] != -1 && bincoef != 0) { + assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); + snfr.vectorsum.add(snfr.origBinCols[i], bincoef); + } + // TODO: Should this value be constant instead of aggrconstant? + if (snfr.origContCols[i] != -1 && snfr.aggrConstant[i] != 0) { + assert(snfr.vectorsum.getValue(snfr.origContCols[i]) == 0); + snfr.vectorsum.add(snfr.origContCols[i], snfr.aggrConstant[i]); + } + rhs -= constant; + } + } + + // TODO: Transform out the slack variables + +} + static double fast_floor(double x) { return (int64_t)x - (x < (int64_t)x); } bool HighsCutGeneration::cmirCutGenerationHeuristic(double minEfficacy, @@ -1112,7 +1313,12 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, // TODO: Add attempt at flow cover generation initSNFRelaxation(static_cast(inds_.size())); - transLp.; + this->vals = vals_.data(); + this->inds = inds_.data(); + this->rhs = rhs_; + transLp.transformSNFRelaxation(vals_, upper, solval, inds_, rhs_, snfr); + computeFlowCover(); + separateLiftedFlowCover(); bool intsPositive = true; if (!transLp.transform(vals_, upper, solval, inds_, rhs_, intsPositive)) @@ -1305,30 +1511,127 @@ bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, void HighsCutGeneration::initSNFRelaxation(HighsInt numNonZero) { HighsInt numTransformedCol = lpRelaxation.numCols() + lpRelaxation.numRows(); - if (snfr.bincolused.size() > numTransformedCol) { - snfr.bincolused.resize(numTransformedCol, false); - snfr.origbincolcoef.resize(numTransformedCol, 0); - } else { - for (HighsInt i = 0; i != snfr.bincolused.size(); ++i) { - snfr.bincolused[i] = false; - snfr.origbincolcoef[i] = 0; - } + if (static_cast(snfr.binColUsed.size()) < numTransformedCol) { + snfr.binColUsed.resize(numTransformedCol); + snfr.origBinColCoef.resize(numTransformedCol); + snfr.vectorsum.setDimension(numTransformedCol); + } + for (HighsInt i = 0; i != static_cast(snfr.binColUsed.size()); ++i) { + snfr.binColUsed[i] = false; + snfr.origBinColCoef[i] = 0; } - if (snfr.coef.size() < numNonZero) { - snfr.origbincols.resize(numNonZero); - snfr.origcontcols.resize(numNonZero); - snfr.binsolval.resize(numNonZero); - snfr.contsolval.resize(numNonZero); + if (static_cast(snfr.coef.size()) < numNonZero) { + snfr.origBinCols.resize(numNonZero); + snfr.origContCols.resize(numNonZero); + snfr.binSolval.resize(numNonZero); + snfr.contSolval.resize(numNonZero); snfr.coef.resize(numNonZero); - snfr.vubcoef.resize(numNonZero); - snfr.aggrconstant.resize(numNonZero); - snfr.aggrbincoef.resize(numNonZero); - snfr.aggrcontcoef.resize(numNonZero); - snfr.rhs = 0; - snfr.lambda = 0; - snfr.nnzs = 0; + snfr.vubCoef.resize(numNonZero); + snfr.aggrConstant.resize(numNonZero); + snfr.aggrBinCoef.resize(numNonZero); + snfr.aggrContCoef.resize(numNonZero); } + snfr.rhs = 0; + snfr.lambda = 0; + snfr.numNnzs = 0; +} + +bool HighsCutGeneration::computeFlowCover() { + // Compute the flow cover, i.e., get sets C1 subset N1 and C2 subset N2 + // with sum_{j in C1} u_j - sum_{j in C2} u_j = b + lambda, lambda > 0 + if (static_cast(snfr.flowCoverStatus.size()) < snfr.numNnzs) { + snfr.flowCoverStatus.resize(snfr.numNnzs); + } + std::vector items(snfr.numNnzs, -1); + HighsInt nNonFlowCover = 0; + HighsInt nFlowCover = 0; + HighsInt nitems = 0; + double n1itemsWeight = 0; + HighsCDouble flowCoverWeight = 0; + for (HighsInt i = 0; i < snfr.numNnzs; ++i) { + assert(snfr.coef[i] == 1 || snfr.coef[i] == -1); + if (abs(snfr.binSolval[i]) < feastol) { + snfr.flowCoverStatus[i] = -1; + nNonFlowCover++; + continue; + } + if (fractionality(snfr.binSolval[i]) > feastol) { + items[nitems] = i; + nitems++; + if (snfr.coef[i] == 1) { + n1itemsWeight += snfr.vubCoef[i]; + } + } else if (snfr.coef[i] == 1 && snfr.binSolval[i] < 0.5) { + snfr.flowCoverStatus[i] = -1; + nNonFlowCover++; + } else if (snfr.coef[i] == 1 && snfr.binSolval[i] > 0.5) { + snfr.flowCoverStatus[i] = 1; + nFlowCover++; + flowCoverWeight += snfr.vubCoef[i]; + } else if (snfr.coef[i] == -1 && snfr.binSolval[i] > 0.5) { + snfr.flowCoverStatus[i] = 1; + nNonFlowCover++; + flowCoverWeight -= snfr.vubCoef[i]; + } else { + assert(snfr.coef[i] == -1 && snfr.binsolval[i] < 0.5); + snfr.flowCoverStatus[i] = -1; + nNonFlowCover++; + } + } + assert(nNonFlowCover + nFlowCover + nitems == snfr.nnzs); + + double capacity = -snfr.rhs + static_cast(flowCoverWeight) + n1itemsWeight; + // There is no flow cover if capacity is less than zero after fixing + if (capacity < feastol) return false; + // Solve a knapsack greedily to assign items to C1, C2, N1\C1, N2\C2 + double knapsackWeight = 0; + std::vector weights(nitems); + std::vector profits(nitems); + std::vector perm(nitems); + std::iota(perm.begin(), perm.end(), 0); + for (HighsInt i = 0; i < nitems; ++i) { + weights[i] = snfr.vubCoef[items[i]]; + if (snfr.coef[items[i]] == 1) { + profits[i] = 1 - snfr.binSolval[items[i]]; + } else { + profits[i] = snfr.binSolval[items[i]]; + } + } + pdqsort_branchless(perm.begin(), perm.end(), [&](const HighsInt a, const HighsInt b) { + return profits[a] / weights[a] > profits[b] / weights[b]; + }); + // Greedily add items to knapsack + for (HighsInt i = 0; i < nitems; ++i) { + const HighsInt j = perm[i]; + const HighsInt k = items[j]; + if (knapsackWeight + weights[j] < capacity) { + knapsackWeight += weights[j]; + if (snfr.coef[k] == 1) { + snfr.flowCoverStatus[k] = -1; + nNonFlowCover++; + } else { + snfr.flowCoverStatus[k] = 1; + nFlowCover++; + flowCoverWeight -= snfr.vubCoef[k]; + } + } else { + if (snfr.coef[k] == 1) { + snfr.flowCoverStatus[k] = 1; + nFlowCover++; + flowCoverWeight += snfr.vubCoef[k]; + } else { + snfr.flowCoverStatus[k] = -1; + nNonFlowCover++; + } + } + } + + assert(nFlowCover + nNonFlowCover == snfr.nnzs); + + snfr.lambda = static_cast(flowCoverWeight - snfr.rhs); + if (snfr.lambda < feastol) return false; + return true; } bool HighsCutGeneration::finalizeAndAddCut(std::vector& inds_, diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index 1d9743fc38..ec9c15f8c8 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -66,6 +66,10 @@ class HighsCutGeneration { bool cmirCutGenerationHeuristic(double minEfficacy, bool onlyInitialCMIRScale = false); + bool computeFlowCover(); + + bool separateLiftedFlowCover(); + double scale(double val); bool postprocessCut(); @@ -107,24 +111,23 @@ class HighsCutGeneration { /// Single Node Flow Relaxation for flow cover cuts struct SNFRelaxation { - std::vector bincolused; // has col been used in a vub - std::vector origbincolcoef; // original bin col coef + std::vector binColUsed; // has col been used in a vub + std::vector origBinColCoef; // original bin col coef - HighsInt nnzs; // number of nonzeros + HighsInt numNnzs; // number of nonzeros std::vector coef; // coefficients of cols in SNFR - std::vector vubcoef; // coefficients in vub of cols in SNFR - std::vector binsolval; // vub bin col sol in SNFR - std::vector contsolval; // real sol in SNFR - std::vector origbincols; // orig bin col used in SNFR - std::vector origcontcols; // orig cont cols used in SNFR - std::vector aggrbincoef; // aggr coef of orignal bin-col in SNFR - std::vector aggrcontcoef; // aggr coef of original cont-col in SNFR - std::vector aggrconstant; // aggr original constant in SNFR + std::vector vubCoef; // coefficients in vub of cols in SNFR + std::vector binSolval; // vub bin col sol in SNFR + std::vector contSolval; // real sol in SNFR + std::vector origBinCols; // orig bin col used in SNFR + std::vector origContCols; // orig cont cols used in SNFR + std::vector aggrBinCoef; // aggr coef of orignal bin-col in SNFR + std::vector aggrContCoef; // aggr coef of original cont-col in SNFR + std::vector aggrConstant; // aggr original constant in SNFR std::vector flowCoverStatus; // (+1) in fcover (-1) not in fcover double rhs; double lambda; - std::vector boundTypes; HighsSparseVectorSum vectorsum; }; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 6a237881c9..0b64800c63 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -9,7 +9,6 @@ #include "mip/HighsTransformedLp.h" -#include "../extern/pdqsort/pdqsort.h" #include "mip/HighsMipSolverData.h" #include "util/HighsCDouble.h" #include "util/HighsIntegers.h" @@ -566,12 +565,9 @@ bool HighsTransformedLp::untransform(std::vector& vals, // Create a single node flow relaxation (SNFR) from an aggregated // mixed-integer row and find a valid flow cover. -bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, - std::vector& upper, - std::vector& solval, - std::vector& inds, - double& rhs, - bool& integersPositive, +bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, + std::vector inds, + double rhs, HighsCutGeneration::SNFRelaxation& snfr) { // Turn \sum c_i x_i + \sum a_i y_i <= a_0 (x_i binary, y_i real non-neg) @@ -581,7 +577,6 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, assert(snfr.vectorsum.getNonzeros().empty()); const HighsSolution& lpSolution = lprelaxation.getLpSolver().getSolution(); - HighsCDouble tmpRhs = rhs; HighsCDouble tmpSnfrRhs = rhs; const HighsMipSolver& mip = lprelaxation.getMipSolver(); @@ -611,7 +606,8 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, double coef, double origbincoef, double lb, double ub, bool isVub) { if (bincol == -1) return false; - if (snfr.bincolused[bincol]) return false; + if (snfr.binColUsed[bincol]) return false; + if (abs(vb.coef) >= 1e+6) return false; const double sign = coef >= 0 ? 1 : -1; if (isVub) { double val = sign * ((coef * vb.coef) + origbincoef); @@ -631,16 +627,16 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, double binsolval, double contsolval, HighsInt coef, double vubcoef, double aggrconstant, double aggrbincoef, double aggrcontcoef) { - snfr.origbincols[snfr.nnzs] = origbincol; - snfr.origcontcols[snfr.nnzs] = origcontcol; - snfr.binsolval[snfr.nnzs] = binsolval; - snfr.contsolval[snfr.nnzs] = contsolval; - snfr.coef[snfr.nnzs] = coef; - snfr.vubcoef[snfr.nnzs] = vubcoef; - snfr.aggrconstant[snfr.nnzs] = aggrconstant; - snfr.aggrbincoef[snfr.nnzs] = aggrbincoef; - snfr.aggrcontcoef[snfr.nnzs] = aggrcontcoef; - snfr.nnzs++; + snfr.origBinCols[snfr.numNnzs] = origbincol; + snfr.origContCols[snfr.numNnzs] = origcontcol; + snfr.binSolval[snfr.numNnzs] = binsolval; + snfr.contSolval[snfr.numNnzs] = contsolval; + snfr.coef[snfr.numNnzs] = coef; + snfr.vubCoef[snfr.numNnzs] = vubcoef; + snfr.aggrConstant[snfr.numNnzs] = aggrconstant; + snfr.aggrBinCoef[snfr.numNnzs] = aggrbincoef; + snfr.aggrContCoef[snfr.numNnzs] = aggrcontcoef; + snfr.numNnzs++; }; // Place the non-binary variables to the front (all general ints relaxed) @@ -651,7 +647,7 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, double ub = getUb(col); if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 0) { nbincols++; - snfr.origbincolcoef[col] = vals[i]; + snfr.origBinColCoef[col] = vals[i]; std::swap(inds[i], inds[numNz - nbincols]); std::swap(vals[i], vals[numNz - nbincols]); } @@ -665,10 +661,10 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, double ub = getUb(col); if (ub - lb < mip.options_mip_->small_matrix_value) { - tmpRhs -= std::min(lb, ub) * vals[i]; + rhs -= std::min(lb, ub) * vals[i]; tmpSnfrRhs -= std::min(lb, ub) * vals[i]; if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 0) { - snfr.origbincolcoef[col] = 0; + snfr.origBinColCoef[col] = 0; } remove(i); continue; @@ -718,11 +714,11 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, // the non-relaxed column now be untransformed we would wrongly use the // variable bound even though this is not the correct way to untransform the // column. - BoundType oldBoundType = boundTypes[col]; + // BoundType oldBoundType = boundTypes[col]; // Transform entry into the SNFR if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 0) { - if (snfr.bincolused[col] != 1) { + if (snfr.binColUsed[col] != 1) { if (vals[i] >= 0) { addSNFRentry(col, -1, lpSolution.col_value[col], lpSolution.col_value[col] * vals[i], 1, vals[i], 0, @@ -736,123 +732,127 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, } else { if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { if (!checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - snfr.origbincolcoef[bestVlb[col].first], + snfr.origBinColCoef[bestVlb[col].first], lb, ub, false)) { boundTypes[col] = BoundType::kSimpleLb; } else if (vals[i] > 0 || simpleLbDist[col] > lbDist[col] + mip.mipdata_->feastol) { boundTypes[col] = BoundType::kVariableLb; - snfr.bincolused[bestVlb[col].first] = true; + snfr.binColUsed[bestVlb[col].first] = true; } else boundTypes[col] = BoundType::kSimpleLb; } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol) { if (!checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - snfr.origbincolcoef[bestVub[col].first], + snfr.origBinColCoef[bestVub[col].first], lb, ub, true)) { boundTypes[col] = BoundType::kSimpleUb; } else if (vals[i] < 0 || simpleUbDist[col] > ubDist[col] + mip.mipdata_->feastol) { boundTypes[col] = BoundType::kVariableUb; - snfr.bincolused[bestVub[col].first] = true; + snfr.binColUsed[bestVub[col].first] = true; } else { boundTypes[col] = BoundType::kSimpleUb; } } else if (vals[i] > 0) { if (checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - snfr.origbincolcoef[bestVlb[col].first], + snfr.origBinColCoef[bestVlb[col].first], lb, ub, false)) { - snfr.bincolused[bestVlb[col].first] = true; + snfr.binColUsed[bestVlb[col].first] = true; boundTypes[col] = BoundType::kVariableLb; } else { boundTypes[col] = BoundType::kSimpleLb; } } else { if (checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - snfr.origbincolcoef[bestVub[col].first], + snfr.origBinColCoef[bestVub[col].first], lb, ub, true)) { - snfr.bincolused[bestVub[col].first] = true; + snfr.binColUsed[bestVub[col].first] = true; boundTypes[col] = BoundType::kVariableUb; } else { boundTypes[col] = BoundType::kSimpleUb; } } + double vbcoef; + double substsolval; + double aggrconstant; + HighsInt vbcol; switch (boundTypes[col]) { case BoundType::kSimpleLb: - double substsolval = static_cast( + substsolval = static_cast( vals[i] * (HighsCDouble(lpSolution.col_value[col]) - ub)); - double vubcoef = + vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); - double valtimesub = static_cast(HighsCDouble(vals[i]) * ub); + aggrconstant = static_cast(HighsCDouble(vals[i]) * ub); if (vals[i] >= 0) { - addSNFRentry(-1, col, 1.0, -substsolval, -1, vubcoef, valtimesub, 0, + addSNFRentry(-1, col, 1.0, -substsolval, -1, vbcoef, aggrconstant, 0, -vals[i]); } else { - addSNFRentry(-1, col, 1, substsolval, 1, -vubcoef, -valtimesub, 0, + addSNFRentry(-1, col, 1, substsolval, 1, -vbcoef, -aggrconstant, 0, vals[i]); } - tmpSnfrRhs -= valtimesub; + tmpSnfrRhs -= aggrconstant; break; case BoundType::kSimpleUb: - double substsolval = static_cast( + substsolval = static_cast( vals[i] * (HighsCDouble(lpSolution.col_value[col]) - lb)); - double vubcoef = + vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); - double valtimeslb = static_cast(HighsCDouble(vals[i]) * lb); + aggrconstant = static_cast(HighsCDouble(vals[i]) * lb); if (vals[i] >= 0) { - addSNFRentry(-1, col, 1, substsolval, 1, vubcoef, -valtimeslb, 0, + addSNFRentry(-1, col, 1, substsolval, 1, vbcoef, -aggrconstant, 0, vals[i]); } else { - addSNFRentry(-1, col, 1, -substsolval, -1, -vubcoef, valtimeslb, 0, + addSNFRentry(-1, col, 1, -substsolval, -1, -vbcoef, aggrconstant, 0, -vals[i]); } - tmpSnfrRhs -= valtimeslb; + tmpSnfrRhs -= aggrconstant; break; case BoundType::kVariableLb: - HighsInt vlbcol = bestVlb[col].first; - double substsolval = static_cast( + vbcol = bestVlb[col].first; + substsolval = static_cast( vals[i] * (HighsCDouble(lpSolution.col_value[col]) - bestVlb[col].second.constant) + - (HighsCDouble(lpSolution.col_value[vlbcol]) * - snfr.origbincolcoef[vlbcol])); - double vlbcoef = static_cast( + (HighsCDouble(lpSolution.col_value[vbcol]) * + snfr.origBinColCoef[vbcol])); + vbcoef = static_cast( HighsCDouble(vals[i]) * bestVlb[col].second.coef + - snfr.origbincolcoef[vlbcol]); - double valtimesvlbconst = static_cast( + snfr.origBinColCoef[vbcol]); + aggrconstant = static_cast( HighsCDouble(vals[i]) * bestVlb[col].second.constant); if (vals[i] >= 0) { - addSNFRentry(vlbcol, col, lpSolution.col_value[vlbcol], - -substsolval, -1, -vlbcoef, valtimesvlbconst, - -snfr.origbincolcoef[vlbcol], -vals[i]); + addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], + -substsolval, -1, -vbcoef, aggrconstant, + -snfr.origBinColCoef[vbcol], -vals[i]); } else { - addSNFRentry(vlbcol, col, lpSolution.col_value[vlbcol], substsolval, - 1, vlbcoef, -valtimesvlbconst, - snfr.origbincolcoef[vlbcol], vals[i]); + addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], substsolval, + 1, vbcoef, -aggrconstant, + snfr.origBinColCoef[vbcol], vals[i]); } - tmpSnfrRhs -= valtimesvlbconst; + tmpSnfrRhs -= aggrconstant; break; case BoundType::kVariableUb: - HighsInt vubcol = bestVub[col].first; - double substsolval = static_cast( + vbcol = bestVub[col].first; + substsolval = static_cast( vals[i] * (HighsCDouble(lpSolution.col_value[col]) - bestVub[col].second.constant) + - (HighsCDouble(lpSolution.col_value[vubcol]) * - snfr.origbincolcoef[vubcol])); - double vubcoef = static_cast( + (HighsCDouble(lpSolution.col_value[vbcol]) * + snfr.origBinColCoef[vbcol])); + vbcoef = static_cast( HighsCDouble(vals[i]) * bestVub[col].second.coef + - snfr.origbincolcoef[vubcol]); - double valtimesvubconst = static_cast( + snfr.origBinColCoef[vbcol]); + aggrconstant = static_cast( HighsCDouble(vals[i]) * bestVub[col].second.constant); if (vals[i] >= 0) { - addSNFRentry(vubcol, col, lpSolution.col_value[vubcol], substsolval, - 1, vubcoef, -valtimesvubconst, - snfr.origbincolcoef[vubcol], vals[i]); + addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], substsolval, + 1, vbcoef, -aggrconstant, + snfr.origBinColCoef[vbcol], vals[i]); } else { - addSNFRentry(vubcol, col, lpSolution.col_value[vubcol], - -substsolval, -1, -vubcoef, valtimesvubconst, - -snfr.origbincolcoef[vubcol], -vals[i]); + addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], + -substsolval, -1, -vbcoef, aggrconstant, + -snfr.origBinColCoef[vbcol], -vals[i]); } - tmpSnfrRhs -= valtimesvubconst; + tmpSnfrRhs -= aggrconstant; break; } } @@ -860,100 +860,7 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector& vals, i++; } - snfr.rhs = double(tmpSnfrRhs); + snfr.rhs = static_cast(tmpSnfrRhs); if (numNz == 0 && rhs >= -mip.mipdata_->feastol) return false; - - // Compute the flow cover, i.e., get sets C1 subset N1 and C2 subset N2 - // with sum_{j in C1} u_j - sum_{j in C2} u_j = b + lambda, lambda > 0 - if (snfr.flowCoverStatus.size() < snfr.nnzs) { - snfr.flowCoverStatus.resize(snfr.nnzs); - } - std::vector items(snfr.nnzs, -1); - HighsInt nNonFlowCover = 0; - HighsInt nFlowCover = 0; - HighsInt nitems = 0; - double n1itemsWeight = 0; - HighsCDouble flowCoverWeight = 0; - for (i = 0; i < snfr.nnzs; ++i) { - assert(snfr.coef[i] == 1 || snfr.coef[i] == -1); - if (abs(snfr.binsolval[i]) < mip.mipdata_->feastol) { - snfr.flowCoverStatus[i] = -1; - nNonFlowCover++; - continue; - } - if (fractionality(snfr.binsolval[i]) > mip.mipdata_->feastol) { - items[nitems] = i; - nitems++; - if (snfr.coef[i] == 1) { - n1itemsWeight += snfr.vubcoef[i]; - } - } else if (snfr.coef[i] == 1 && snfr.binsolval[i] < 0.5) { - snfr.flowCoverStatus[i] = -1; - nNonFlowCover++; - } else if (snfr.coef[i] == 1 && snfr.binsolval[i] > 0.5) { - snfr.flowCoverStatus[i] = 1; - nFlowCover++; - flowCoverWeight += snfr.vubcoef[i]; - } else if (snfr.coef[i] == -1 && snfr.binsolval[i] > 0.5) { - snfr.flowCoverStatus[i] = 1; - nNonFlowCover++; - flowCoverWeight -= snfr.vubcoef[i]; - } else { - assert(snfr.coef[i] == -1 && snfr.binsolval[i] < 0.5); - snfr.flowCoverStatus[i] = -1; - nNonFlowCover++; - } - } - assert(nNonFlowCover + nFlowCover + nitems == snfr.nnzs); - - double capacity = -snfr.rhs + static_cast(flowCoverWeight) + n1itemsWeight; - // There is no flow cover if capacity is less than zero after fixing - if (capacity < mip.mipdata_->feastol) return false; - // Solve a knapsack greedily to assign items to C1, C2, N1\C1, N2\C2 - double knapsackWeight = 0; - std::vector weights(nitems); - std::vector profits(nitems); - std::vector perm(nitems); - std::iota(perm.begin(), perm.end(), 0); - for (i = 0; i < nitems; ++i) { - weights[i] = snfr.vubcoef[items[i]]; - if (snfr.coef[items[i]] == 1) { - profits[i] = 1 - snfr.binsolval[items[i]]; - } else { - profits[i] = snfr.binsolval[items[i]]; - } - } - pdqsort(perm.begin(), perm.end(), [&](const HighsInt a, const HighsInt b) { - return profits[a] / weights[a] > profits[b] / weights[b]; - }); - // Greedily add items to knapsack - for (i = 0; i < nitems; ++i) { - HighsInt j = perm[i]; - HighsInt k = items[j]; - if (knapsackWeight + weights[j] < capacity) { - knapsackWeight += weights[j]; - if (snfr.coef[k] == 1) { - snfr.flowCoverStatus[k] = -1; - nNonFlowCover++; - } else { - snfr.flowCoverStatus[k] = 1; - nFlowCover++; - flowCoverWeight -= snfr.vubcoef[k]; - } - } else { - if (snfr.coef[k] == 1) { - snfr.flowCoverStatus[k] = 1; - nFlowCover++; - flowCoverWeight += snfr.vubcoef[k]; - } else { - snfr.flowCoverStatus[k] = -1; - nNonFlowCover++; - } - } - } - - snfr.lambda = static_cast(flowCoverWeight) - snfr.rhs; - if (snfr.lambda < mip.mipdata_->feastol) return false; - rhs = static_cast(tmpRhs); return true; } diff --git a/highs/mip/HighsTransformedLp.h b/highs/mip/HighsTransformedLp.h index aab0ce41dd..688d24d3be 100644 --- a/highs/mip/HighsTransformedLp.h +++ b/highs/mip/HighsTransformedLp.h @@ -61,10 +61,11 @@ class HighsTransformedLp { bool untransform(std::vector& vals, std::vector& inds, double& rhs, bool integral = false); - bool transformSNFR(std::vector& vals, std::vector& upper, - std::vector& solval, std::vector& inds, - double& rhs, bool& integralPositive, - HighsCutGeneration::SNFRelaxation& snfr); + bool transformSNFRelaxation(std::vector& vals, + std::vector& upper, + std::vector& solval, + std::vector& inds, double& rhs, + HighsCutGeneration::SNFRelaxation& snfr); }; #endif From ad07c98a04331e1c0683d709b7a1786a7fe885fd Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 25 Jul 2025 12:09:38 +0200 Subject: [PATCH 03/64] Further draft of flow cover cuts --- highs/mip/HighsCutGeneration.cpp | 219 ++++++++++++++++++++----------- highs/mip/HighsCutGeneration.h | 5 +- highs/mip/HighsTransformedLp.cpp | 178 ++++++++++++++++++++++--- highs/mip/HighsTransformedLp.h | 10 +- 4 files changed, 315 insertions(+), 97 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 7889dc20ef..be250b7f92 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -500,12 +500,28 @@ bool HighsCutGeneration::separateLiftedMixedIntegerCover() { return true; } -bool HighsCutGeneration::separateLiftedFlowCover() { +// Calculates the lifted simple generalized flow cover cut out of +// Gu, Z., Nemhauser, G. L., & Savelsbergh, M. W. (1999). +// Lifted flow cover inequalities for mixed 0-1 integer programs. +// Mathematical Programming, 85(3), 439-467. +bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, + std::vector& inds, + double& rhs, + double& efficacy) { // Compute the lifting data (ld) first - std::vector ldm(snfr.numNnzs); - HighsInt ldr = 0; - double ldmp = kHighsInf; + struct LiftingData { + std::vector m; + std::vector M; + HighsInt r = 0; + HighsInt t = 0; + double mp = kHighsInf; + HighsCDouble ml = 0; + HighsCDouble d1 = 0; + HighsCDouble d2 = 0; + }; + LiftingData ld; + ld.m.resize(snfr.numNnzs); HighsCDouble sumN2mC2LE = 0.0; HighsCDouble sumC1LE = 0.0; HighsCDouble sumN2mC2GT = 0.0; @@ -517,8 +533,8 @@ bool HighsCutGeneration::separateLiftedFlowCover() { assert(snfr.vubCoef[i] >= 0); if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { sumN2mC2GT += snfr.vubCoef[i]; - ldm[ldr] = snfr.vubCoef[i]; - ++ldr; + ld.m[ld.r] = snfr.vubCoef[i]; + ++ld.r; } else { sumN2mC2LE += snfr.vubCoef[i]; } @@ -530,9 +546,9 @@ bool HighsCutGeneration::separateLiftedFlowCover() { // col is in C1 assert(snfr.vubCoef[i] > 0); if ( snfr.vubCoef[i] > snfr.lambda + 1e-8) { - ldm[ldr] = snfr.vubCoef[i]; - ++ldr; - ldmp = std::min(ldmp, snfr.vubCoef[i]); + ld.m[ld.r] = snfr.vubCoef[i]; + ++ld.r; + ld.mp = std::min(ld.mp, snfr.vubCoef[i]); } else { sumC1LE += snfr.vubCoef[i]; } @@ -542,30 +558,29 @@ bool HighsCutGeneration::separateLiftedFlowCover() { } } - if (ldmp == kHighsInf) return false; + if (ld.mp == kHighsInf) return false; - std::vector ldM(ldr + 1); - HighsCDouble ldml = std::min(snfr.lambda, static_cast(sumC1LE * sumN2mC2LE)); - HighsCDouble ldd1 = sumC2 * snfr.rhs; - HighsCDouble ldd2 = ldd1 + sumN2mC2GT + sumN2mC2LE; + ld.M.resize(ld.r + 1); + ld.ml = std::min(snfr.lambda, static_cast(sumC1LE + sumN2mC2LE)); + ld.d1 = sumC2 * snfr.rhs; + ld.d2 = ld.d1 + sumN2mC2GT + sumN2mC2LE; // Sort into non-increasing order (TODO: Remove comment after checking) - pdqsort_branchless(ldm.begin(), ldm.end(), [&](const HighsCDouble a, const HighsCDouble b) { + pdqsort_branchless(ld.m.begin(), ld.m.end(), [&](const HighsCDouble a, const HighsCDouble b) { return a > b; }); - for (HighsInt i = 0; i != ldr + 1; ++i) { + for (HighsInt i = 0; i != ld.r + 1; ++i) { if (i == 0) { - ldM[i] = ldm[i]; + ld.M[i] = 0; } else { - ldM[i] = ldM[i - 1] + ldm[i]; + ld.M[i] = ld.M[i - 1] + ld.m[i - 1]; } } - HighsInt ldt = 0; - for (HighsInt i = ldr - 1; i != -1; --i) { - if (ldm[i] >= ldmp) { - ldt = i; + for (HighsInt i = ld.r - 1; i != -1; --i) { + if (ld.m[i] >= ld.mp) { + ld.t = i; break; } } @@ -576,12 +591,14 @@ bool HighsCutGeneration::separateLiftedFlowCover() { double vubcoefpluslambda = vubcoef + snfr.lambda; HighsInt i = 0; - for (i = 0; i < ldr && vubcoefpluslambda >= ldM[i + 1] + 1e-8; ++i) {} + while (i < ld.r && vubcoefpluslambda >= ld.M[i + 1] + 1e-8) { + ++i; + } - if (vubcoef <= ldM[i] - 1e-8) { - assert(ldM[i] < vubcoefpluslambda); + if (vubcoef <= ld.M[i] - 1e-8) { + assert(ld.M[i] < vubcoefpluslambda); alpha = 1; - beta = -i * HighsCDouble(snfr.lambda) - ldM[i]; + beta = -i * HighsCDouble(snfr.lambda) + ld.M[i]; } else { alpha = 0; beta = 0; @@ -591,35 +608,39 @@ bool HighsCutGeneration::separateLiftedFlowCover() { auto evaluateLiftingFunction = [&](double vubcoef) { HighsInt i = 0; - for (i = 0; i < ldr && vubcoef + snfr.lambda >= ldM[i + 1] + 1e-8; ++i) {} - if (i < ldt) { + while (i < ld.r && vubcoef + snfr.lambda >= ld.M[i + 1] + 1e-8) { + ++i; + } + if (i < ld.t) { HighsCDouble liftedcoef = i * HighsCDouble(snfr.lambda); - if (ldM[i] < vubcoef - 1e-8) { + if (ld.M[i] < vubcoef - 1e-8) { return static_cast(liftedcoef); } - assert(i > 0 && ldM[i] < vubcoef + snfr.lambda - 1e-8 && vubcoef <= ldM[i]); + assert(i > 0 && ld.M[i] < vubcoef + snfr.lambda - 1e-8 && vubcoef <= ld.M[i]); liftedcoef += vubcoef; - liftedcoef -= ldM[i]; + liftedcoef -= ld.M[i]; return static_cast(liftedcoef); } - if (i < ldr) { - HighsCDouble tmp = HighsCDouble(ldm[i]) - ldmp - ldml + snfr.lambda; + if (i < ld.r) { + HighsCDouble tmp = HighsCDouble(ld.m[i]) - ld.mp - ld.ml + snfr.lambda; if (tmp < 0) tmp = 0; - tmp += ldM[i] + ldml; + tmp += ld.M[i] + ld.ml; if (tmp < vubcoef + snfr.lambda - 1e-8) { return static_cast(i * HighsCDouble(snfr.lambda)); } - assert(ldM[i] <= vubcoef + snfr.lambda + 1e-8 && snfr.lambda + vuebcoef <= ldM[i] + ldml + std::max(0, ldm[i] - (ldmp - snfr.lambda) - ldml)); + assert(ld.M[i] <= vubcoef + snfr.lambda + 1e-8 && snfr.lambda + vubcoef <= ld.M[i] + ld.ml + std::max(0.0, static_cast( + ld.m[i] - (ld.mp - snfr.lambda) - ld.ml))); return static_cast(i * HighsCDouble(snfr.lambda) + vubcoef - - ldM[i]); + ld.M[i]); } - assert(i == ldr && ldM[i] <= vubcoef + snfr.lambda + 1e-8); - return static_cast(ldr * HighsCDouble(snfr.lambda) + vubcoef - - ldM[i]); + assert(i == ld.r && ld.M[i] <= vubcoef + snfr.lambda + 1e-8); + return static_cast(ld.r * HighsCDouble(snfr.lambda) + vubcoef - + ld.M[i]); }; // Lift the flow cover cut - HighsCDouble rhs = ldd1; + assert(snfr.vectorsum.getNonzeros().empty()); + HighsCDouble tmpRhs = ld.d1; for (HighsInt i = 0; i != snfr.numNnzs; ++i) { // col is in N2 \ C2 if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == -1) { @@ -629,7 +650,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); snfr.vectorsum.add(snfr.origBinCols[i], -snfr.lambda); } else { - rhs += snfr.lambda; + tmpRhs += snfr.lambda; } } else { // col is in L-- @@ -639,10 +660,10 @@ bool HighsCutGeneration::separateLiftedFlowCover() { } if (snfr.origBinCols[i] != -1 && snfr.aggrBinCoef[i] != 0) { assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); - snfr.vectorsum.add(snfr.origContCols[i], -snfr.aggrBinCoef[i]); + snfr.vectorsum.add(snfr.origBinCols[i], -snfr.aggrBinCoef[i]); } } - rhs += snfr.aggrConstant[i]; + tmpRhs += snfr.aggrConstant[i]; } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == -1) { // col is in C2 if (snfr.origBinCols[i] != -1) { @@ -650,12 +671,13 @@ bool HighsCutGeneration::separateLiftedFlowCover() { double liftedbincoef = evaluateLiftingFunction(snfr.vubCoef[i]); if (liftedbincoef != 0) { snfr.vectorsum.add(snfr.origBinCols[i], -liftedbincoef); - rhs -= liftedbincoef; + tmpRhs -= liftedbincoef; } } } else if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == 1) { // col is in N1 \ C1 - std::pair alphabeta = getAlphaBeta(snfr.vubCoef[i]); + std::pair alphabeta = getAlphaBeta(snfr.vubCoef[i]); + assert(alphabeta.first == 0 || alphabeta.first == 1); if (alphabeta.first == 1) { assert(alphabeta.second > 0); if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { @@ -669,9 +691,9 @@ bool HighsCutGeneration::separateLiftedFlowCover() { snfr.vectorsum.add(snfr.origBinCols[i], binvarcoef); } } else { - rhs -= binvarcoef; + tmpRhs -= binvarcoef; } - rhs -= snfr.aggrConstant[i]; + tmpRhs -= snfr.aggrConstant[i]; } } else { // col is in C1 @@ -691,14 +713,32 @@ bool HighsCutGeneration::separateLiftedFlowCover() { // TODO: Should this value be constant instead of aggrconstant? if (snfr.origContCols[i] != -1 && snfr.aggrConstant[i] != 0) { assert(snfr.vectorsum.getValue(snfr.origContCols[i]) == 0); - snfr.vectorsum.add(snfr.origContCols[i], snfr.aggrConstant[i]); + snfr.vectorsum.add(snfr.origContCols[i], snfr.aggrContCoef[i]); } - rhs -= constant; + tmpRhs -= constant; } } - // TODO: Transform out the slack variables + // substitute the slack out of the cut + snfr.vectorsum.clear(); + return true; + // Now create the cut and calculate the efficacy + inds = snfr.vectorsum.getNonzeros(); + auto numNz = static_cast(inds.size()); + vals.resize(numNz); + for (HighsInt j = 0; j != numNz; ++j) vals[j] = snfr.vectorsum.getValue(inds[j]); + rhs = static_cast(tmpRhs); + snfr.vectorsum.clear(); + double viol = -rhs; + double norm = 0.0; + for (HighsInt j = 0; j != numNz; ++j) { + if (vals[j] == 0.0) continue; + norm += vals[j] * vals[j]; + // add viol + } + efficacy = viol / sqrt(norm); + return true; } static double fast_floor(double x) { return (int64_t)x - (x < (int64_t)x); } @@ -1312,29 +1352,44 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, #endif // TODO: Add attempt at flow cover generation - initSNFRelaxation(static_cast(inds_.size())); - this->vals = vals_.data(); - this->inds = inds_.data(); - this->rhs = rhs_; - transLp.transformSNFRelaxation(vals_, upper, solval, inds_, rhs_, snfr); - computeFlowCover(); - separateLiftedFlowCover(); + bool flowcoversuccess = false; + std::vector flowcovervals; + std::vector flowcoverinds; + double flowcoverrhs = 0; + double flowcoverefficacy = 0; + if (!onlyInitialCMIRScale) { + initSNFRelaxation(static_cast(inds_.size())); + flowcoversuccess = transLp.transformSNFRelaxation(vals_, inds_, rhs_, snfr); + printf("%d\n", std::rand()); + if (!flowcoversuccess) goto cmir; + flowcoversuccess = computeFlowCover(); // TODO: When should this return false? + if (!flowcoversuccess) goto cmir; + flowcoversuccess = separateLiftedFlowCover(flowcovervals, flowcoverinds, flowcoverrhs, flowcoverefficacy); + flowcoversuccess = false; + // TODO: Make sure to untransform at the end + } +cmir: + bool cmirsuccess = false; bool intsPositive = true; - if (!transLp.transform(vals_, upper, solval, inds_, rhs_, intsPositive)) - return false; + bool hasUnboundedInts = false; + bool hasGeneralInts = false; + bool hasContinuous = false; + if (!transLp.transform(vals_, upper, solval, inds_, rhs_, intsPositive)) { + cmirsuccess = false; + goto untransform; + } rowlen = inds_.size(); this->inds = inds_.data(); this->vals = vals_.data(); this->rhs = rhs_; complementation.clear(); - bool hasUnboundedInts = false; - bool hasGeneralInts = false; - bool hasContinuous = false; if (!preprocessBaseInequality(hasUnboundedInts, hasGeneralInts, - hasContinuous)) - return false; + hasContinuous)) { + cmirsuccess = false; + goto untransform; + } // it can happen that there is an unbounded integer variable during the // transform call so that the integers are not transformed to positive values. @@ -1355,8 +1410,10 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, // try to generate a cut if (!tryGenerateCut(inds_, vals_, hasUnboundedInts, hasGeneralInts, - hasContinuous, 10 * feastol, onlyInitialCMIRScale)) - return false; + hasContinuous, 10 * feastol, onlyInitialCMIRScale)) { + cmirsuccess = false; + goto untransform; + } // remove the complementation if exists removeComplementation(); @@ -1370,12 +1427,17 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } } +untransform: + if (!cmirsuccess && !flowcoversuccess) return false; + // transform the cut back into the original space, i.e. remove the bound // substitution and replace implicit slack variables - rhs_ = (double)rhs; - vals_.resize(rowlen); - inds_.resize(rowlen); - if (!transLp.untransform(vals_, inds_, rhs_)) return false; + if (cmirsuccess) { + rhs_ = (double)rhs; + vals_.resize(rowlen); + inds_.resize(rowlen); + if (!transLp.untransform(vals_, inds_, rhs_)) return false; + } rowlen = inds_.size(); inds = inds_.data(); @@ -1551,11 +1613,14 @@ bool HighsCutGeneration::computeFlowCover() { HighsCDouble flowCoverWeight = 0; for (HighsInt i = 0; i < snfr.numNnzs; ++i) { assert(snfr.coef[i] == 1 || snfr.coef[i] == -1); - if (abs(snfr.binSolval[i]) < feastol) { + assert(snfr.binSolval[i] >= -feastol && snfr.binSolval[i] <= 1 + feastol); + // if u_i = 0 put i into N1 \ C1 or N2 \ C2, i.e., not the cover + if (abs(snfr.vubCoef[i]) < feastol) { snfr.flowCoverStatus[i] = -1; nNonFlowCover++; continue; } + // x_i is fractional -> becomes an item in knapsack (decides if in cover) if (fractionality(snfr.binSolval[i]) > feastol) { items[nitems] = i; nitems++; @@ -1563,23 +1628,27 @@ bool HighsCutGeneration::computeFlowCover() { n1itemsWeight += snfr.vubCoef[i]; } } else if (snfr.coef[i] == 1 && snfr.binSolval[i] < 0.5) { + // i is in N1 and x_i = 0 -> don't put in cover snfr.flowCoverStatus[i] = -1; nNonFlowCover++; } else if (snfr.coef[i] == 1 && snfr.binSolval[i] > 0.5) { + // i is in N1 and x_i = 1 -> put in cover snfr.flowCoverStatus[i] = 1; nFlowCover++; flowCoverWeight += snfr.vubCoef[i]; } else if (snfr.coef[i] == -1 && snfr.binSolval[i] > 0.5) { + // i is in N2 and x_i = 1 -> put in cover snfr.flowCoverStatus[i] = 1; - nNonFlowCover++; + nFlowCover++; flowCoverWeight -= snfr.vubCoef[i]; } else { - assert(snfr.coef[i] == -1 && snfr.binsolval[i] < 0.5); + // i is in N2 and x_i = 0 -> don't put in cover + assert(snfr.coef[i] == -1 && snfr.binSolval[i] < 0.5); snfr.flowCoverStatus[i] = -1; nNonFlowCover++; } } - assert(nNonFlowCover + nFlowCover + nitems == snfr.nnzs); + assert(nNonFlowCover + nFlowCover + nitems == snfr.numNnzs); double capacity = -snfr.rhs + static_cast(flowCoverWeight) + n1itemsWeight; // There is no flow cover if capacity is less than zero after fixing @@ -1627,7 +1696,7 @@ bool HighsCutGeneration::computeFlowCover() { } } - assert(nFlowCover + nNonFlowCover == snfr.nnzs); + assert(nFlowCover + nNonFlowCover == snfr.numNnzs); snfr.lambda = static_cast(flowCoverWeight - snfr.rhs); if (snfr.lambda < feastol) return false; diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index ec9c15f8c8..00fa04937c 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -68,7 +68,10 @@ class HighsCutGeneration { bool computeFlowCover(); - bool separateLiftedFlowCover(); + bool separateLiftedFlowCover(std::vector& vals, + std::vector& inds, + double& rhs, + double& efficacy); double scale(double val); diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 0b64800c63..24e32db681 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -565,14 +565,13 @@ bool HighsTransformedLp::untransform(std::vector& vals, // Create a single node flow relaxation (SNFR) from an aggregated // mixed-integer row and find a valid flow cover. +// Turn \sum c_i x_i + \sum a_i y_i <= a_0 (x_i binary, y_i real non-neg) +// into \sum_{j \in N1} y_j - \sum_{j \in N2} y_j <= b, where y_j <= u_j x_j bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, std::vector inds, double rhs, HighsCutGeneration::SNFRelaxation& snfr) { - // Turn \sum c_i x_i + \sum a_i y_i <= a_0 (x_i binary, y_i real non-neg) - // into \sum_{j \in N1} y_j - \sum_{j \in N2} y_j <= b, where y_j <= u_j x_j - // vector sum should be empty assert(snfr.vectorsum.getNonzeros().empty()); const HighsSolution& lpSolution = lprelaxation.getLpSolver().getSolution(); @@ -583,6 +582,7 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, const HighsInt slackOffset = lprelaxation.numCols(); HighsInt numNz = inds.size(); + HighsInt numBinCols = 0; auto getLb = [&](HighsInt col) { return (col < slackOffset ? mip.mipdata_->domain.col_lower_[col] @@ -596,10 +596,18 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, auto remove = [&](HighsInt position) { numNz--; - inds[position] = inds[numNz]; - vals[position] = vals[numNz]; + if (position < numNz - numBinCols - 1) { + std::swap(vals[position], vals[numNz - numBinCols]); + std::swap(vals[numNz - numBinCols], vals[numNz]); + std::swap(inds[position], inds[numNz - numBinCols]); + std::swap(inds[numNz - numBinCols], inds[numNz]); + } else { + inds[position] = inds[numNz]; + vals[position] = vals[numNz]; + } inds[numNz] = 0; vals[numNz] = 0; + numNz--; }; auto checkValidityVB = [&](HighsInt bincol, HighsImplications::VarBound vb, @@ -616,7 +624,7 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, if (val < 0) return false; } else { double val = sign * ((coef * vb.coef) + origbincoef); - if (val > 0 || val > kHighsInf) return false; + if (val > 0 || -val > kHighsInf) return false; val = sign * ((coef * (ub - vb.constant)) + origbincoef); if (val > 0) return false; } @@ -627,6 +635,7 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, double binsolval, double contsolval, HighsInt coef, double vubcoef, double aggrconstant, double aggrbincoef, double aggrcontcoef) { + assert(binsolval >= -1e-6 && binsolval <= 1 + 1e-6); snfr.origBinCols[snfr.numNnzs] = origbincol; snfr.origContCols[snfr.numNnzs] = origcontcol; snfr.binSolval[snfr.numNnzs] = binsolval; @@ -640,20 +649,22 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, }; // Place the non-binary variables to the front (all general ints relaxed) - HighsInt nbincols = 0; - for (HighsInt i = 0; i < numNz - nbincols; ++i) { + HighsInt i = 0; + while (i < numNz - numBinCols) { HighsInt col = inds[i]; double lb = getLb(col); double ub = getUb(col); - if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 0) { - nbincols++; + if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 1) { + numBinCols++; snfr.origBinColCoef[col] = vals[i]; - std::swap(inds[i], inds[numNz - nbincols]); - std::swap(vals[i], vals[numNz - nbincols]); + std::swap(inds[i], inds[numNz - numBinCols]); + std::swap(vals[i], vals[numNz - numBinCols]); + continue; } + ++i; } - HighsInt i = 0; + i = 0; while (i < numNz) { HighsInt col = inds[i]; @@ -663,7 +674,7 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, if (ub - lb < mip.options_mip_->small_matrix_value) { rhs -= std::min(lb, ub) * vals[i]; tmpSnfrRhs -= std::min(lb, ub) * vals[i]; - if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 0) { + if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 1) { snfr.origBinColCoef[col] = 0; } remove(i); @@ -671,7 +682,6 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, } if (lb == -kHighsInf && ub == kHighsInf) { - vectorsum.clear(); return false; } @@ -717,8 +727,8 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, // BoundType oldBoundType = boundTypes[col]; // Transform entry into the SNFR - if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 0) { - if (snfr.binColUsed[col] != 1) { + if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 1) { + if (snfr.binColUsed[col] == false) { if (vals[i] >= 0) { addSNFRentry(col, -1, lpSolution.col_value[col], lpSolution.col_value[col] * vals[i], 1, vals[i], 0, @@ -728,6 +738,7 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, -lpSolution.col_value[col] * vals[i], -1, -vals[i], 0, -vals[i], 0); } + snfr.binColUsed[col] = true; } } else { if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { @@ -864,3 +875,136 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, if (numNz == 0 && rhs >= -mip.mipdata_->feastol) return false; return true; } + +bool HighsTransformedLp::untransformSNFRelaxation(std::vector& vals, + std::vector& inds, double& rhs, + bool integral) { + HighsCDouble tmpRhs = rhs; + const HighsMipSolver& mip = lprelaxation.getMipSolver(); + const HighsInt slackOffset = mip.numCol(); + + HighsInt numNz = static_cast(inds.size()); + + for (HighsInt i = 0; i != numNz; ++i) { + if (vals[i] == 0.0) continue; + HighsInt col = inds[i]; + if (col < slackOffset) { + vectorsum.add(col, vals[i]); + } else { + + } + } + + for (HighsInt i = 0; i != numNz; ++i) { + if (vals[i] == 0.0) continue; + HighsInt col = inds[i]; + + switch (boundTypes[col]) { + case BoundType::kVariableLb: { + tmpRhs += bestVlb[col].second.constant * vals[i]; + vectorsum.add(bestVlb[col].first, -vals[i] * bestVlb[col].second.coef); + vectorsum.add(col, vals[i]); + break; + } + case BoundType::kVariableUb: { + tmpRhs -= bestVub[col].second.constant * vals[i]; + vectorsum.add(bestVub[col].first, vals[i] * bestVub[col].second.coef); + vectorsum.add(col, -vals[i]); + break; + } + case BoundType::kSimpleLb: { + if (col < slackOffset) { + tmpRhs += vals[i] * mip.mipdata_->domain.col_lower_[col]; + vectorsum.add(col, vals[i]); + } else { + HighsInt row = col - slackOffset; + tmpRhs += vals[i] * lprelaxation.slackLower(row); + + HighsInt rowlen; + const HighsInt* rowinds; + const double* rowvals; + lprelaxation.getRow(row, rowlen, rowinds, rowvals); + + for (HighsInt j = 0; j != rowlen; ++j) + vectorsum.add(rowinds[j], vals[i] * rowvals[j]); + } + break; + } + case BoundType::kSimpleUb: { + if (col < slackOffset) { + tmpRhs -= vals[i] * mip.mipdata_->domain.col_upper_[col]; + vectorsum.add(col, -vals[i]); + } else { + HighsInt row = col - slackOffset; + tmpRhs -= vals[i] * lprelaxation.slackUpper(row); + vals[i] = -vals[i]; + + HighsInt rowlen; + const HighsInt* rowinds; + const double* rowvals; + lprelaxation.getRow(row, rowlen, rowinds, rowvals); + + for (HighsInt j = 0; j != rowlen; ++j) + vectorsum.add(rowinds[j], vals[i] * rowvals[j]); + } + } + } + } + + if (integral) { + // if the cut is integral, we just round all coefficient values and the + // right hand side to the nearest integral value, as small deviation + // only come from numerical errors during resubstitution of slack variables + + auto IsZero = [&](HighsInt col, double val) { + assert(col < mip.numCol()); + return fabs(val) < 0.5; + }; + + vectorsum.cleanup(IsZero); + rhs = std::round(double(tmpRhs)); + } else { + bool abort = false; + auto IsZero = [&](HighsInt col, double val) { + assert(col < mip.numCol()); + double absval = std::abs(val); + if (absval <= mip.options_mip_->small_matrix_value) return true; + + if (absval <= mip.mipdata_->feastol) { + if (val > 0) { + if (mip.mipdata_->domain.col_lower_[col] == -kHighsInf) + abort = true; + else + tmpRhs -= val * mip.mipdata_->domain.col_lower_[col]; + } else { + if (mip.mipdata_->domain.col_upper_[col] == kHighsInf) + abort = true; + else + tmpRhs -= val * mip.mipdata_->domain.col_upper_[col]; + } + return true; + } + return false; + }; + + vectorsum.cleanup(IsZero); + if (abort) { + vectorsum.clear(); + return false; + } + rhs = double(tmpRhs); + } + + inds = vectorsum.getNonzeros(); + numNz = inds.size(); + vals.resize(numNz); + + if (integral) + for (HighsInt i = 0; i != numNz; ++i) + vals[i] = std::round(vectorsum.getValue(inds[i])); + else + for (HighsInt i = 0; i != numNz; ++i) vals[i] = vectorsum.getValue(inds[i]); + vectorsum.clear(); + + return true; +} diff --git a/highs/mip/HighsTransformedLp.h b/highs/mip/HighsTransformedLp.h index 688d24d3be..403bcc7bfc 100644 --- a/highs/mip/HighsTransformedLp.h +++ b/highs/mip/HighsTransformedLp.h @@ -61,11 +61,13 @@ class HighsTransformedLp { bool untransform(std::vector& vals, std::vector& inds, double& rhs, bool integral = false); - bool transformSNFRelaxation(std::vector& vals, - std::vector& upper, - std::vector& solval, - std::vector& inds, double& rhs, + bool transformSNFRelaxation(std::vector vals, + std::vector inds, double rhs, HighsCutGeneration::SNFRelaxation& snfr); + + bool untransformSNFRelaxation(std::vector& vals, + std::vector& inds, double& rhs, + bool integral); }; #endif From 1f3de3a95068bc7eb266919b5943d2f65914b9b2 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 25 Jul 2025 17:51:06 +0200 Subject: [PATCH 04/64] Add comments. Still need to replace slack --- highs/mip/HighsCutGeneration.cpp | 17 ++++- highs/mip/HighsTransformedLp.cpp | 106 ++++++++++++++++--------------- 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index be250b7f92..d64d85f0d7 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -719,6 +719,21 @@ bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, } } + // shift all variables back to their original bounds (this was + // implicitly done to make everything >= 0). + // Then substitute the slack out additionally. + // This can be done by directly replacing it via its + // representation. + // TODO: All originally shiftings have been already made? (those are the + // changes to tmpRhs by some constant) + + // TODO: Why not transform the cut back in the standard way? + // TODO: Change the definition of snfr.aggrConstant (remove val multiplier) + // TODO: Go through snfr.vectorsum and shift everything back: + // TODO: vectorsum.getval * constant + // TODO: For slack variables also substitute back the original variables + // TODO: Be careful of coefficients changing when substituting + // substitute the slack out of the cut snfr.vectorsum.clear(); return true; @@ -1360,7 +1375,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, if (!onlyInitialCMIRScale) { initSNFRelaxation(static_cast(inds_.size())); flowcoversuccess = transLp.transformSNFRelaxation(vals_, inds_, rhs_, snfr); - printf("%d\n", std::rand()); + // printf("%d\n", std::rand()); if (!flowcoversuccess) goto cmir; flowcoversuccess = computeFlowCover(); // TODO: When should this return false? if (!flowcoversuccess) goto cmir; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 24e32db681..0288032f4b 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -6,7 +6,6 @@ /* */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ - #include "mip/HighsTransformedLp.h" #include "mip/HighsMipSolverData.h" @@ -567,11 +566,9 @@ bool HighsTransformedLp::untransform(std::vector& vals, // mixed-integer row and find a valid flow cover. // Turn \sum c_i x_i + \sum a_i y_i <= a_0 (x_i binary, y_i real non-neg) // into \sum_{j \in N1} y_j - \sum_{j \in N2} y_j <= b, where y_j <= u_j x_j -bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, - std::vector inds, - double rhs, - HighsCutGeneration::SNFRelaxation& snfr) { - +bool HighsTransformedLp::transformSNFRelaxation( + std::vector vals, std::vector inds, double rhs, + HighsCutGeneration::SNFRelaxation& snfr) { // vector sum should be empty assert(snfr.vectorsum.getNonzeros().empty()); const HighsSolution& lpSolution = lprelaxation.getLpSolver().getSolution(); @@ -594,6 +591,15 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, : lprelaxation.slackUpper(col - slackOffset)); }; + auto getLpSolution = [&](HighsInt col) { + HighsInt numCols = lprelaxation.numCols(); + if (col < numCols) { + return lpSolution.col_value[col]; + } else { + return lpSolution.row_value[col - numCols]; + } + }; + auto remove = [&](HighsInt position) { numNz--; if (position < numNz - numBinCols - 1) { @@ -635,13 +641,15 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, double binsolval, double contsolval, HighsInt coef, double vubcoef, double aggrconstant, double aggrbincoef, double aggrcontcoef) { - assert(binsolval >= -1e-6 && binsolval <= 1 + 1e-6); + assert(binsolval >= -lprelaxation.getMipSolver().mipdata_->feastol && + binsolval <= 1 + lprelaxation.getMipSolver().mipdata_->feastol); + assert(vubcoef >= -1e-10); snfr.origBinCols[snfr.numNnzs] = origbincol; snfr.origContCols[snfr.numNnzs] = origcontcol; snfr.binSolval[snfr.numNnzs] = binsolval; snfr.contSolval[snfr.numNnzs] = contsolval; snfr.coef[snfr.numNnzs] = coef; - snfr.vubCoef[snfr.numNnzs] = vubcoef; + snfr.vubCoef[snfr.numNnzs] = std::max(vubcoef, 0.0); snfr.aggrConstant[snfr.numNnzs] = aggrconstant; snfr.aggrBinCoef[snfr.numNnzs] = aggrbincoef; snfr.aggrContCoef[snfr.numNnzs] = aggrcontcoef; @@ -730,21 +738,20 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 1) { if (snfr.binColUsed[col] == false) { if (vals[i] >= 0) { - addSNFRentry(col, -1, lpSolution.col_value[col], - lpSolution.col_value[col] * vals[i], 1, vals[i], 0, - vals[i], 0); + addSNFRentry(col, -1, getLpSolution(col), + getLpSolution(col) * vals[i], 1, vals[i], 0, vals[i], 0); } else { - addSNFRentry(col, -1, lpSolution.col_value[col], - -lpSolution.col_value[col] * vals[i], -1, -vals[i], 0, - -vals[i], 0); + addSNFRentry(col, -1, getLpSolution(col), + -getLpSolution(col) * vals[i], -1, -vals[i], 0, -vals[i], + 0); } snfr.binColUsed[col] = true; } } else { if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { if (!checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - snfr.origBinColCoef[bestVlb[col].first], - lb, ub, false)) { + snfr.origBinColCoef[bestVlb[col].first], lb, ub, + false)) { boundTypes[col] = BoundType::kSimpleLb; } else if (vals[i] > 0 || simpleLbDist[col] > lbDist[col] + mip.mipdata_->feastol) { @@ -754,8 +761,8 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, boundTypes[col] = BoundType::kSimpleLb; } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol) { if (!checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - snfr.origBinColCoef[bestVub[col].first], - lb, ub, true)) { + snfr.origBinColCoef[bestVub[col].first], lb, ub, + true)) { boundTypes[col] = BoundType::kSimpleUb; } else if (vals[i] < 0 || simpleUbDist[col] > ubDist[col] + mip.mipdata_->feastol) { @@ -766,8 +773,8 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, } } else if (vals[i] > 0) { if (checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - snfr.origBinColCoef[bestVlb[col].first], - lb, ub, false)) { + snfr.origBinColCoef[bestVlb[col].first], lb, ub, + false)) { snfr.binColUsed[bestVlb[col].first] = true; boundTypes[col] = BoundType::kVariableLb; } else { @@ -775,8 +782,8 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, } } else { if (checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - snfr.origBinColCoef[bestVub[col].first], - lb, ub, true)) { + snfr.origBinColCoef[bestVub[col].first], lb, ub, + true)) { snfr.binColUsed[bestVub[col].first] = true; boundTypes[col] = BoundType::kVariableUb; } else { @@ -792,12 +799,11 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, case BoundType::kSimpleLb: substsolval = static_cast( vals[i] * (HighsCDouble(lpSolution.col_value[col]) - ub)); - vbcoef = - static_cast(vals[i] * (HighsCDouble(ub) - lb)); + vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * ub); if (vals[i] >= 0) { - addSNFRentry(-1, col, 1.0, -substsolval, -1, vbcoef, aggrconstant, 0, - -vals[i]); + addSNFRentry(-1, col, 1.0, -substsolval, -1, vbcoef, aggrconstant, + 0, -vals[i]); } else { addSNFRentry(-1, col, 1, substsolval, 1, -vbcoef, -aggrconstant, 0, vals[i]); @@ -807,8 +813,7 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, case BoundType::kSimpleUb: substsolval = static_cast( vals[i] * (HighsCDouble(lpSolution.col_value[col]) - lb)); - vbcoef = - static_cast(vals[i] * (HighsCDouble(ub) - lb)); + vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * lb); if (vals[i] >= 0) { addSNFRentry(-1, col, 1, substsolval, 1, vbcoef, -aggrconstant, 0, @@ -826,19 +831,19 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, bestVlb[col].second.constant) + (HighsCDouble(lpSolution.col_value[vbcol]) * snfr.origBinColCoef[vbcol])); - vbcoef = static_cast( - HighsCDouble(vals[i]) * bestVlb[col].second.coef + - snfr.origBinColCoef[vbcol]); - aggrconstant = static_cast( - HighsCDouble(vals[i]) * bestVlb[col].second.constant); + vbcoef = static_cast(HighsCDouble(vals[i]) * + bestVlb[col].second.coef + + snfr.origBinColCoef[vbcol]); + aggrconstant = static_cast(HighsCDouble(vals[i]) * + bestVlb[col].second.constant); if (vals[i] >= 0) { - addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], - -substsolval, -1, -vbcoef, aggrconstant, - -snfr.origBinColCoef[vbcol], -vals[i]); + addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], -substsolval, + -1, -vbcoef, aggrconstant, -snfr.origBinColCoef[vbcol], + -vals[i]); } else { addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], substsolval, - 1, vbcoef, -aggrconstant, - snfr.origBinColCoef[vbcol], vals[i]); + 1, vbcoef, -aggrconstant, snfr.origBinColCoef[vbcol], + vals[i]); } tmpSnfrRhs -= aggrconstant; break; @@ -849,19 +854,19 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, bestVub[col].second.constant) + (HighsCDouble(lpSolution.col_value[vbcol]) * snfr.origBinColCoef[vbcol])); - vbcoef = static_cast( - HighsCDouble(vals[i]) * bestVub[col].second.coef + - snfr.origBinColCoef[vbcol]); - aggrconstant = static_cast( - HighsCDouble(vals[i]) * bestVub[col].second.constant); + vbcoef = static_cast(HighsCDouble(vals[i]) * + bestVub[col].second.coef + + snfr.origBinColCoef[vbcol]); + aggrconstant = static_cast(HighsCDouble(vals[i]) * + bestVub[col].second.constant); if (vals[i] >= 0) { addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], substsolval, - 1, vbcoef, -aggrconstant, - snfr.origBinColCoef[vbcol], vals[i]); + 1, vbcoef, -aggrconstant, snfr.origBinColCoef[vbcol], + vals[i]); } else { - addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], - -substsolval, -1, -vbcoef, aggrconstant, - -snfr.origBinColCoef[vbcol], -vals[i]); + addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], -substsolval, + -1, -vbcoef, aggrconstant, -snfr.origBinColCoef[vbcol], + -vals[i]); } tmpSnfrRhs -= aggrconstant; break; @@ -877,8 +882,8 @@ bool HighsTransformedLp::transformSNFRelaxation(std::vector vals, } bool HighsTransformedLp::untransformSNFRelaxation(std::vector& vals, - std::vector& inds, double& rhs, - bool integral) { + std::vector& inds, + double& rhs, bool integral) { HighsCDouble tmpRhs = rhs; const HighsMipSolver& mip = lprelaxation.getMipSolver(); const HighsInt slackOffset = mip.numCol(); @@ -891,7 +896,6 @@ bool HighsTransformedLp::untransformSNFRelaxation(std::vector& vals, if (col < slackOffset) { vectorsum.add(col, vals[i]); } else { - } } From b97863e74fcf7849367ca8c454870c8e630ce602 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Jul 2025 16:48:56 +0200 Subject: [PATCH 05/64] First flow cover draft --- highs/mip/HighsCutGeneration.cpp | 316 ++++++++++++++++++++----------- highs/mip/HighsCutGeneration.h | 10 +- highs/mip/HighsTransformedLp.cpp | 170 ++++++----------- highs/mip/HighsTransformedLp.h | 9 +- 4 files changed, 272 insertions(+), 233 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index d64d85f0d7..1532f9bc03 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -504,11 +504,7 @@ bool HighsCutGeneration::separateLiftedMixedIntegerCover() { // Gu, Z., Nemhauser, G. L., & Savelsbergh, M. W. (1999). // Lifted flow cover inequalities for mixed 0-1 integer programs. // Mathematical Programming, 85(3), 439-467. -bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, - std::vector& inds, - double& rhs, - double& efficacy) { - +bool HighsCutGeneration::separateLiftedFlowCover() { // Compute the lifting data (ld) first struct LiftingData { std::vector m; @@ -545,7 +541,7 @@ bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == 1) { // col is in C1 assert(snfr.vubCoef[i] > 0); - if ( snfr.vubCoef[i] > snfr.lambda + 1e-8) { + if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { ld.m[ld.r] = snfr.vubCoef[i]; ++ld.r; ld.mp = std::min(ld.mp, snfr.vubCoef[i]); @@ -562,13 +558,13 @@ bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, ld.M.resize(ld.r + 1); ld.ml = std::min(snfr.lambda, static_cast(sumC1LE + sumN2mC2LE)); - ld.d1 = sumC2 * snfr.rhs; + ld.d1 = sumC2 + snfr.rhs; ld.d2 = ld.d1 + sumN2mC2GT + sumN2mC2LE; // Sort into non-increasing order (TODO: Remove comment after checking) - pdqsort_branchless(ld.m.begin(), ld.m.end(), [&](const HighsCDouble a, const HighsCDouble b) { - return a > b; - }); + pdqsort_branchless( + ld.m.begin(), ld.m.end(), + [&](const HighsCDouble a, const HighsCDouble b) { return a > b; }); for (HighsInt i = 0; i != ld.r + 1; ++i) { if (i == 0) { @@ -587,7 +583,7 @@ bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, auto getAlphaBeta = [&](double vubcoef) { HighsInt alpha; - HighsCDouble beta; + HighsCDouble beta{}; double vubcoefpluslambda = vubcoef + snfr.lambda; HighsInt i = 0; @@ -613,10 +609,11 @@ bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, } if (i < ld.t) { HighsCDouble liftedcoef = i * HighsCDouble(snfr.lambda); - if (ld.M[i] < vubcoef - 1e-8) { + if (ld.M[i] < vubcoef + 1e-8) { return static_cast(liftedcoef); } - assert(i > 0 && ld.M[i] < vubcoef + snfr.lambda - 1e-8 && vubcoef <= ld.M[i]); + assert(i > 0 && ld.M[i] < vubcoef + snfr.lambda - 1e-8 && + vubcoef <= ld.M[i]); liftedcoef += vubcoef; liftedcoef -= ld.M[i]; return static_cast(liftedcoef); @@ -628,8 +625,12 @@ bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, if (tmp < vubcoef + snfr.lambda - 1e-8) { return static_cast(i * HighsCDouble(snfr.lambda)); } - assert(ld.M[i] <= vubcoef + snfr.lambda + 1e-8 && snfr.lambda + vubcoef <= ld.M[i] + ld.ml + std::max(0.0, static_cast( - ld.m[i] - (ld.mp - snfr.lambda) - ld.ml))); + assert( + ld.M[i] <= vubcoef + snfr.lambda + feastol && + snfr.lambda + vubcoef <= + ld.M[i] + ld.ml + feastol + + std::max(0.0, static_cast( + ld.m[i] - (ld.mp - snfr.lambda) - ld.ml))); return static_cast(i * HighsCDouble(snfr.lambda) + vubcoef - ld.M[i]); } @@ -639,56 +640,63 @@ bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, }; // Lift the flow cover cut - assert(snfr.vectorsum.getNonzeros().empty()); HighsCDouble tmpRhs = ld.d1; + rowlen = 0; for (HighsInt i = 0; i != snfr.numNnzs; ++i) { // col is in N2 \ C2 if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == -1) { if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { if (snfr.origBinCols[i] != -1) { // col is in L- - assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); - snfr.vectorsum.add(snfr.origBinCols[i], -snfr.lambda); + vals[rowlen] = -snfr.lambda; + inds[rowlen] = snfr.origBinCols[i]; + rowlen++; } else { tmpRhs += snfr.lambda; } } else { // col is in L-- if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { - assert(snfr.vectorsum.getValue(snfr.origContCols[i]) == 0); - snfr.vectorsum.add(snfr.origContCols[i], -snfr.aggrContCoef[i]); + vals[rowlen] = -snfr.aggrContCoef[i]; + inds[rowlen] = snfr.origContCols[i]; + rowlen++; } if (snfr.origBinCols[i] != -1 && snfr.aggrBinCoef[i] != 0) { - assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); - snfr.vectorsum.add(snfr.origBinCols[i], -snfr.aggrBinCoef[i]); + vals[rowlen] = -snfr.aggrBinCoef[i]; + inds[rowlen] = snfr.origBinCols[i]; + rowlen++; } + tmpRhs += snfr.aggrConstant[i]; } - tmpRhs += snfr.aggrConstant[i]; } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == -1) { // col is in C2 if (snfr.origBinCols[i] != -1) { - assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); double liftedbincoef = evaluateLiftingFunction(snfr.vubCoef[i]); if (liftedbincoef != 0) { - snfr.vectorsum.add(snfr.origBinCols[i], -liftedbincoef); + vals[rowlen] = -liftedbincoef; + inds[rowlen] = snfr.origBinCols[i]; + rowlen++; tmpRhs -= liftedbincoef; } } } else if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == 1) { // col is in N1 \ C1 - std::pair alphabeta = getAlphaBeta(snfr.vubCoef[i]); + std::pair alphabeta = + getAlphaBeta(snfr.vubCoef[i]); assert(alphabeta.first == 0 || alphabeta.first == 1); if (alphabeta.first == 1) { assert(alphabeta.second > 0); if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { - assert(snfr.vectorsum.getValue(snfr.origContCols[i]) == 0); - snfr.vectorsum.add(snfr.origContCols[i], snfr.aggrContCoef[i]); + vals[rowlen] = snfr.aggrContCoef[i]; + inds[rowlen] = snfr.origContCols[i]; + rowlen++; } HighsCDouble binvarcoef = snfr.aggrBinCoef[i] - alphabeta.second; if (snfr.origBinCols[i] != -1) { - assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); if (binvarcoef != 0) { - snfr.vectorsum.add(snfr.origBinCols[i], binvarcoef); + vals[rowlen] = static_cast(binvarcoef); + inds[rowlen] = snfr.origBinCols[i]; + rowlen++; } } else { tmpRhs -= binvarcoef; @@ -702,57 +710,32 @@ bool HighsCutGeneration::separateLiftedFlowCover(std::vector& vals, HighsCDouble constant = snfr.aggrConstant[i]; if (snfr.origBinCols[i] != -1 && snfr.vubCoef[i] >= snfr.lambda + 1e-8) { // col is in C++ - HighsCDouble tmp = HighsCDouble(snfr.vubCoef[i]) - snfr.lambda; - constant += tmp; - bincoef -= tmp; + constant += HighsCDouble(snfr.vubCoef[i]) - snfr.lambda; + bincoef -= HighsCDouble(snfr.vubCoef[i]) - snfr.lambda; } if (snfr.origBinCols[i] != -1 && bincoef != 0) { - assert(snfr.vectorsum.getValue(snfr.origBinCols[i]) == 0); - snfr.vectorsum.add(snfr.origBinCols[i], bincoef); + vals[rowlen] = static_cast(bincoef); + inds[rowlen] = snfr.origBinCols[i]; + rowlen++; } - // TODO: Should this value be constant instead of aggrconstant? - if (snfr.origContCols[i] != -1 && snfr.aggrConstant[i] != 0) { - assert(snfr.vectorsum.getValue(snfr.origContCols[i]) == 0); - snfr.vectorsum.add(snfr.origContCols[i], snfr.aggrContCoef[i]); + if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { + vals[rowlen] = snfr.aggrContCoef[i]; + inds[rowlen] = snfr.origContCols[i]; + rowlen++; } tmpRhs -= constant; } } - // shift all variables back to their original bounds (this was - // implicitly done to make everything >= 0). - // Then substitute the slack out additionally. - // This can be done by directly replacing it via its - // representation. - // TODO: All originally shiftings have been already made? (those are the - // changes to tmpRhs by some constant) - - // TODO: Why not transform the cut back in the standard way? - // TODO: Change the definition of snfr.aggrConstant (remove val multiplier) - // TODO: Go through snfr.vectorsum and shift everything back: - // TODO: vectorsum.getval * constant - // TODO: For slack variables also substitute back the original variables - // TODO: Be careful of coefficients changing when substituting - - // substitute the slack out of the cut - snfr.vectorsum.clear(); - return true; - // Now create the cut and calculate the efficacy - inds = snfr.vectorsum.getNonzeros(); - auto numNz = static_cast(inds.size()); - vals.resize(numNz); - for (HighsInt j = 0; j != numNz; ++j) vals[j] = snfr.vectorsum.getValue(inds[j]); - rhs = static_cast(tmpRhs); - snfr.vectorsum.clear(); - double viol = -rhs; - double norm = 0.0; - for (HighsInt j = 0; j != numNz; ++j) { - if (vals[j] == 0.0) continue; - norm += vals[j] * vals[j]; - // add viol +#ifndef NDEBUG + // TODO: Remove this if algorithm changes to allow multiple bin col usages + for (HighsInt i = 0; i < rowlen; i++) { + for (HighsInt j = i + 1; j < rowlen; j++) + assert(inds[i] != inds[j]); } - efficacy = viol / sqrt(norm); +#endif + rhs = static_cast(tmpRhs); return true; } @@ -1366,32 +1349,56 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } #endif - // TODO: Add attempt at flow cover generation - bool flowcoversuccess = false; - std::vector flowcovervals; - std::vector flowcoverinds; - double flowcoverrhs = 0; - double flowcoverefficacy = 0; + bool flowCoverSuccess = false; + std::vector flowCoverVals; + std::vector flowCoverInds; + double flowCoverRhs = rhs_; + double flowCoverEfficacy = 0; if (!onlyInitialCMIRScale) { - initSNFRelaxation(static_cast(inds_.size())); - flowcoversuccess = transLp.transformSNFRelaxation(vals_, inds_, rhs_, snfr); - // printf("%d\n", std::rand()); - if (!flowcoversuccess) goto cmir; - flowcoversuccess = computeFlowCover(); // TODO: When should this return false? - if (!flowcoversuccess) goto cmir; - flowcoversuccess = separateLiftedFlowCover(flowcovervals, flowcoverinds, flowcoverrhs, flowcoverefficacy); - flowcoversuccess = false; - // TODO: Make sure to untransform at the end + flowCoverVals = vals_; + flowCoverInds = inds_; + rowlen = static_cast(flowCoverInds.size()); + this->inds = flowCoverInds.data(); + this->vals = flowCoverVals.data(); + this->rhs = rhs_; + flowCoverSuccess = + preprocessSNFRelaxation(); + if (!flowCoverSuccess) goto cmir; + initSNFRelaxation(); + assert(rowlen <= static_cast(flowCoverVals.size())); + flowCoverVals.resize(rowlen); + flowCoverInds.resize(rowlen); + flowCoverRhs = static_cast(rhs); + flowCoverSuccess = transLp.transformSNFRelaxation( + flowCoverInds, flowCoverVals, flowCoverRhs, snfr); + if (!flowCoverSuccess) goto cmir; + // Array resized as each continuous col may create a non-zero bin coef + flowCoverVals.resize(2 * snfr.numNnzs); + flowCoverInds.resize(2 * snfr.numNnzs); + this->inds = flowCoverInds.data(); + this->vals = flowCoverVals.data(); + this->rhs = flowCoverRhs; + flowCoverSuccess = computeFlowCover(); + if (!flowCoverSuccess) goto cmir; + flowCoverSuccess = + separateLiftedFlowCover(); + if (!flowCoverSuccess) goto cmir; + flowCoverInds.resize(rowlen); + flowCoverVals.resize(rowlen); + flowCoverRhs = static_cast(rhs); + flowCoverSuccess = transLp.cleanup(flowCoverInds, flowCoverVals, + flowCoverRhs, flowCoverEfficacy); + if (flowCoverEfficacy < 10 * feastol) flowCoverSuccess = false; } cmir: - bool cmirsuccess = false; + bool cmirSuccess = false; bool intsPositive = true; bool hasUnboundedInts = false; bool hasGeneralInts = false; bool hasContinuous = false; if (!transLp.transform(vals_, upper, solval, inds_, rhs_, intsPositive)) { - cmirsuccess = false; + cmirSuccess = false; goto untransform; } @@ -1402,7 +1409,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, complementation.clear(); if (!preprocessBaseInequality(hasUnboundedInts, hasGeneralInts, hasContinuous)) { - cmirsuccess = false; + cmirSuccess = false; goto untransform; } @@ -1425,8 +1432,9 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, // try to generate a cut if (!tryGenerateCut(inds_, vals_, hasUnboundedInts, hasGeneralInts, - hasContinuous, 10 * feastol, onlyInitialCMIRScale)) { - cmirsuccess = false; + hasContinuous, std::max(flowCoverEfficacy, 10 * feastol), + onlyInitialCMIRScale)) { + cmirSuccess = false; goto untransform; } @@ -1443,15 +1451,19 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } untransform: - if (!cmirsuccess && !flowcoversuccess) return false; + if (!cmirSuccess && !flowCoverSuccess) return false; // transform the cut back into the original space, i.e. remove the bound // substitution and replace implicit slack variables - if (cmirsuccess) { + if (cmirSuccess) { rhs_ = (double)rhs; vals_.resize(rowlen); inds_.resize(rowlen); if (!transLp.untransform(vals_, inds_, rhs_)) return false; + } else { + rhs_ = flowCoverRhs; + std::swap(vals_, flowCoverVals); + std::swap(inds_, flowCoverInds); } rowlen = inds_.size(); @@ -1586,34 +1598,112 @@ bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, return cutindex != -1; } -void HighsCutGeneration::initSNFRelaxation(HighsInt numNonZero) { +void HighsCutGeneration::initSNFRelaxation() { HighsInt numTransformedCol = lpRelaxation.numCols() + lpRelaxation.numRows(); if (static_cast(snfr.binColUsed.size()) < numTransformedCol) { snfr.binColUsed.resize(numTransformedCol); snfr.origBinColCoef.resize(numTransformedCol); - snfr.vectorsum.setDimension(numTransformedCol); } - for (HighsInt i = 0; i != static_cast(snfr.binColUsed.size()); ++i) { - snfr.binColUsed[i] = false; - snfr.origBinColCoef[i] = 0; + for (HighsInt i = 0; i != static_cast(snfr.binColUsed.size()); + ++i) { + snfr.binColUsed[i] = false; + snfr.origBinColCoef[i] = 0; } - if (static_cast(snfr.coef.size()) < numNonZero) { - snfr.origBinCols.resize(numNonZero); - snfr.origContCols.resize(numNonZero); - snfr.binSolval.resize(numNonZero); - snfr.contSolval.resize(numNonZero); - snfr.coef.resize(numNonZero); - snfr.vubCoef.resize(numNonZero); - snfr.aggrConstant.resize(numNonZero); - snfr.aggrBinCoef.resize(numNonZero); - snfr.aggrContCoef.resize(numNonZero); + if (static_cast(snfr.coef.size()) < rowlen) { + snfr.origBinCols.resize(rowlen); + snfr.origContCols.resize(rowlen); + snfr.binSolval.resize(rowlen); + snfr.contSolval.resize(rowlen); + snfr.coef.resize(rowlen); + snfr.vubCoef.resize(rowlen); + snfr.aggrConstant.resize(rowlen); + snfr.aggrBinCoef.resize(rowlen); + snfr.aggrContCoef.resize(rowlen); } snfr.rhs = 0; snfr.lambda = 0; snfr.numNnzs = 0; } +bool HighsCutGeneration::preprocessSNFRelaxation() { + // preprocess the inequality before generating a single node flow relaxation. + // 1. Determine the maximal activity to check for trivial redundancy + // 2. Check for presence of unbounded variables (the SNFR construction + // requires finite bounds on all non-zeros) + // 3. Remove coefficients that are below the feasibility tolerance to avoid + // numerical troubles, use bound constraints to cancel them and + // reject base inequalities where that is not possible due to unbounded + // variables + + HighsInt numZeros = 0; + double maxact = -feastol; + double maxAbsVal = 0; + HighsInt slackOffset = lpRelaxation.getMipSolver().numCol(); + const HighsDomain& domain = lpRelaxation.getMipSolver().mipdata_->domain; + + auto getLb = [&](HighsInt col) { + return (col < slackOffset ? domain.col_lower_[col] + : lpRelaxation.slackLower(col - slackOffset)); + }; + + auto getUb = [&](HighsInt col) { + return (col < slackOffset ? domain.col_upper_[col] + : lpRelaxation.slackUpper(col - slackOffset)); + }; + + for (HighsInt i = 0; i < rowlen; ++i) { + maxAbsVal = std::max(std::abs(vals[i]), maxAbsVal); + if (getLb(inds[i]) == -kHighsInf || getUb(inds[i]) == kHighsInf) { + return false; + } + } + + scale(maxAbsVal); + + for (HighsInt i = 0; i != rowlen; ++i) { + HighsInt col = inds[i]; + + // relax variables with small contributions if possible + if (std::abs(vals[i]) * (getUb(col) - getLb(col)) <= 10 * feastol) { + if (vals[i] < 0) { + if (getUb(col) == kHighsInf) return false; + rhs -= vals[i] * getUb(col); + ++numZeros; + vals[i] = 0.0; + } else if (vals[i] > 0) { + if (getLb(col) == -kHighsInf) return false; + rhs -= vals[i] * getLb(col); + ++numZeros; + vals[i] = 0.0; + } + } + + if (vals[i] > 0) { + maxact += vals[i] * getUb(col); + } else if (vals[i] < 0) { + maxact += vals[i] * getLb(col); + } + } + + HighsInt maxLen = 100 + 0.15 * (lpRelaxation.numCols()); + if (rowlen - numZeros > maxLen) return false; + + if (numZeros != 0) { + // remove zeros in place + for (HighsInt i = rowlen - 1; i >= 0; --i) { + if (vals[i] == 0.0) { + --rowlen; + inds[i] = inds[rowlen]; + vals[i] = vals[rowlen]; + if (--numZeros == 0) break; + } + } + } + + return maxact > rhs; +} + bool HighsCutGeneration::computeFlowCover() { // Compute the flow cover, i.e., get sets C1 subset N1 and C2 subset N2 // with sum_{j in C1} u_j - sum_{j in C2} u_j = b + lambda, lambda > 0 @@ -1665,7 +1755,8 @@ bool HighsCutGeneration::computeFlowCover() { } assert(nNonFlowCover + nFlowCover + nitems == snfr.numNnzs); - double capacity = -snfr.rhs + static_cast(flowCoverWeight) + n1itemsWeight; + double capacity = + static_cast(-snfr.rhs + flowCoverWeight + n1itemsWeight); // There is no flow cover if capacity is less than zero after fixing if (capacity < feastol) return false; // Solve a knapsack greedily to assign items to C1, C2, N1\C1, N2\C2 @@ -1682,9 +1773,10 @@ bool HighsCutGeneration::computeFlowCover() { profits[i] = snfr.binSolval[items[i]]; } } - pdqsort_branchless(perm.begin(), perm.end(), [&](const HighsInt a, const HighsInt b) { - return profits[a] / weights[a] > profits[b] / weights[b]; - }); + pdqsort_branchless(perm.begin(), perm.end(), + [&](const HighsInt a, const HighsInt b) { + return profits[a] / weights[a] > profits[b] / weights[b]; + }); // Greedily add items to knapsack for (HighsInt i = 0; i < nitems; ++i) { const HighsInt j = perm[i]; diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index 00fa04937c..a178095c48 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -68,10 +68,9 @@ class HighsCutGeneration { bool computeFlowCover(); - bool separateLiftedFlowCover(std::vector& vals, - std::vector& inds, - double& rhs, - double& efficacy); + bool separateLiftedFlowCover(); + + bool preprocessSNFRelaxation(); double scale(double val); @@ -131,12 +130,11 @@ class HighsCutGeneration { std::vector flowCoverStatus; // (+1) in fcover (-1) not in fcover double rhs; double lambda; - HighsSparseVectorSum vectorsum; }; private: SNFRelaxation snfr; - void initSNFRelaxation(HighsInt numNonZero); + void initSNFRelaxation(); public: SNFRelaxation& getSNFRelaxation() { return snfr; } diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 0288032f4b..8b41fc8942 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -567,10 +567,8 @@ bool HighsTransformedLp::untransform(std::vector& vals, // Turn \sum c_i x_i + \sum a_i y_i <= a_0 (x_i binary, y_i real non-neg) // into \sum_{j \in N1} y_j - \sum_{j \in N2} y_j <= b, where y_j <= u_j x_j bool HighsTransformedLp::transformSNFRelaxation( - std::vector vals, std::vector inds, double rhs, + std::vector& inds, std::vector& vals, double& rhs, HighsCutGeneration::SNFRelaxation& snfr) { - // vector sum should be empty - assert(snfr.vectorsum.getNonzeros().empty()); const HighsSolution& lpSolution = lprelaxation.getLpSolver().getSolution(); HighsCDouble tmpSnfrRhs = rhs; @@ -644,6 +642,10 @@ bool HighsTransformedLp::transformSNFRelaxation( assert(binsolval >= -lprelaxation.getMipSolver().mipdata_->feastol && binsolval <= 1 + lprelaxation.getMipSolver().mipdata_->feastol); assert(vubcoef >= -1e-10); + for (HighsInt j = 0; j < snfr.numNnzs; j++) { + assert(snfr.origBinCols[j] == -1 || snfr.origBinCols[j] != origbincol); + assert(snfr.origContCols[j] == -1 || snfr.origContCols[j] != origcontcol); + } snfr.origBinCols[snfr.numNnzs] = origbincol; snfr.origContCols[snfr.numNnzs] = origcontcol; snfr.binSolval[snfr.numNnzs] = binsolval; @@ -689,7 +691,7 @@ bool HighsTransformedLp::transformSNFRelaxation( continue; } - if (lb == -kHighsInf && ub == kHighsInf) { + if (lb == -kHighsInf || ub == kHighsInf) { return false; } @@ -881,134 +883,82 @@ bool HighsTransformedLp::transformSNFRelaxation( return true; } -bool HighsTransformedLp::untransformSNFRelaxation(std::vector& vals, - std::vector& inds, - double& rhs, bool integral) { +bool HighsTransformedLp::cleanup(std::vector& inds, + std::vector& vals, + double& rhs, + double& efficacy) { HighsCDouble tmpRhs = rhs; const HighsMipSolver& mip = lprelaxation.getMipSolver(); const HighsInt slackOffset = mip.numCol(); - HighsInt numNz = static_cast(inds.size()); + auto numNz = static_cast(inds.size()); for (HighsInt i = 0; i != numNz; ++i) { if (vals[i] == 0.0) continue; - HighsInt col = inds[i]; + const HighsInt col = inds[i]; if (col < slackOffset) { vectorsum.add(col, vals[i]); } else { + const HighsInt row = col - slackOffset; + HighsInt rowlen; + const HighsInt* rowinds; + const double* rowvals; + lprelaxation.getRow(row, rowlen, rowinds, rowvals); + + for (HighsInt j = 0; j != rowlen; ++j) + vectorsum.add(rowinds[j], vals[i] * rowvals[j]); } } - for (HighsInt i = 0; i != numNz; ++i) { - if (vals[i] == 0.0) continue; - HighsInt col = inds[i]; - - switch (boundTypes[col]) { - case BoundType::kVariableLb: { - tmpRhs += bestVlb[col].second.constant * vals[i]; - vectorsum.add(bestVlb[col].first, -vals[i] * bestVlb[col].second.coef); - vectorsum.add(col, vals[i]); - break; - } - case BoundType::kVariableUb: { - tmpRhs -= bestVub[col].second.constant * vals[i]; - vectorsum.add(bestVub[col].first, vals[i] * bestVub[col].second.coef); - vectorsum.add(col, -vals[i]); - break; - } - case BoundType::kSimpleLb: { - if (col < slackOffset) { - tmpRhs += vals[i] * mip.mipdata_->domain.col_lower_[col]; - vectorsum.add(col, vals[i]); - } else { - HighsInt row = col - slackOffset; - tmpRhs += vals[i] * lprelaxation.slackLower(row); - - HighsInt rowlen; - const HighsInt* rowinds; - const double* rowvals; - lprelaxation.getRow(row, rowlen, rowinds, rowvals); - - for (HighsInt j = 0; j != rowlen; ++j) - vectorsum.add(rowinds[j], vals[i] * rowvals[j]); - } - break; - } - case BoundType::kSimpleUb: { - if (col < slackOffset) { - tmpRhs -= vals[i] * mip.mipdata_->domain.col_upper_[col]; - vectorsum.add(col, -vals[i]); - } else { - HighsInt row = col - slackOffset; - tmpRhs -= vals[i] * lprelaxation.slackUpper(row); - vals[i] = -vals[i]; - - HighsInt rowlen; - const HighsInt* rowinds; - const double* rowvals; - lprelaxation.getRow(row, rowlen, rowinds, rowvals); - - for (HighsInt j = 0; j != rowlen; ++j) - vectorsum.add(rowinds[j], vals[i] * rowvals[j]); - } - } + bool abort = false; + auto IsZero = [&](HighsInt col, double val) { + assert(col < mip.numCol()); + double absval = std::abs(val); + if (absval <= mip.options_mip_->small_matrix_value) { + return true; } - } - if (integral) { - // if the cut is integral, we just round all coefficient values and the - // right hand side to the nearest integral value, as small deviation - // only come from numerical errors during resubstitution of slack variables - - auto IsZero = [&](HighsInt col, double val) { - assert(col < mip.numCol()); - return fabs(val) < 0.5; - }; - - vectorsum.cleanup(IsZero); - rhs = std::round(double(tmpRhs)); - } else { - bool abort = false; - auto IsZero = [&](HighsInt col, double val) { - assert(col < mip.numCol()); - double absval = std::abs(val); - if (absval <= mip.options_mip_->small_matrix_value) return true; - - if (absval <= mip.mipdata_->feastol) { - if (val > 0) { - if (mip.mipdata_->domain.col_lower_[col] == -kHighsInf) - abort = true; - else - tmpRhs -= val * mip.mipdata_->domain.col_lower_[col]; - } else { - if (mip.mipdata_->domain.col_upper_[col] == kHighsInf) - abort = true; - else - tmpRhs -= val * mip.mipdata_->domain.col_upper_[col]; - } - return true; + if (absval <= mip.mipdata_->feastol) { + if (val > 0) { + if (mip.mipdata_->domain.col_lower_[col] == -kHighsInf) + abort = true; + else + tmpRhs -= val * mip.mipdata_->domain.col_lower_[col]; + } else { + if (mip.mipdata_->domain.col_upper_[col] == kHighsInf) + abort = true; + else + tmpRhs -= val * mip.mipdata_->domain.col_upper_[col]; } - return false; - }; - - vectorsum.cleanup(IsZero); - if (abort) { - vectorsum.clear(); - return false; + return true; } - rhs = double(tmpRhs); + return false; + }; + + vectorsum.cleanup(IsZero); + if (abort) { + vectorsum.clear(); + return false; } + rhs = static_cast(tmpRhs); inds = vectorsum.getNonzeros(); - numNz = inds.size(); + numNz = static_cast(inds.size()); vals.resize(numNz); - - if (integral) - for (HighsInt i = 0; i != numNz; ++i) - vals[i] = std::round(vectorsum.getValue(inds[i])); - else - for (HighsInt i = 0; i != numNz; ++i) vals[i] = vectorsum.getValue(inds[i]); + for (HighsInt i = 0; i != numNz; ++i) vals[i] = vectorsum.getValue(inds[i]); vectorsum.clear(); + double viol = 0; + double sqrnorm = 0; + const std::vector& lpSolution = lprelaxation.getSolution().col_value; + for (HighsInt i = 0; i != numNz; ++i) { + if (lpSolution[i] >= + mip.mipdata_->domain.col_lower_[i] + mip.mipdata_->feastol) + continue; + viol += vals[i] * lpSolution[i]; + sqrnorm += vals[i] * vals[i]; + } + efficacy = viol / sqrt(sqrnorm); + return true; } diff --git a/highs/mip/HighsTransformedLp.h b/highs/mip/HighsTransformedLp.h index 403bcc7bfc..811eeaa079 100644 --- a/highs/mip/HighsTransformedLp.h +++ b/highs/mip/HighsTransformedLp.h @@ -61,13 +61,12 @@ class HighsTransformedLp { bool untransform(std::vector& vals, std::vector& inds, double& rhs, bool integral = false); - bool transformSNFRelaxation(std::vector vals, - std::vector inds, double rhs, + bool transformSNFRelaxation(std::vector& inds, + std::vector& vals, double& rhs, HighsCutGeneration::SNFRelaxation& snfr); - bool untransformSNFRelaxation(std::vector& vals, - std::vector& inds, double& rhs, - bool integral); + bool cleanup(std::vector& inds, std::vector& vals, + double& rhs, double& efficacy); }; #endif From 76b30a401fc16d68662a4083d39fedb2b705fa8a Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Jul 2025 17:59:33 +0200 Subject: [PATCH 06/64] Unify comment notation. Add tryGenFlowCut --- highs/mip/HighsCutGeneration.cpp | 124 ++++++++++++++++--------------- highs/mip/HighsCutGeneration.h | 35 +++++---- highs/mip/HighsTransformedLp.cpp | 14 +--- 3 files changed, 85 insertions(+), 88 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 1532f9bc03..74c81299e0 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -514,32 +514,29 @@ bool HighsCutGeneration::separateLiftedFlowCover() { double mp = kHighsInf; HighsCDouble ml = 0; HighsCDouble d1 = 0; - HighsCDouble d2 = 0; }; LiftingData ld; ld.m.resize(snfr.numNnzs); HighsCDouble sumN2mC2LE = 0.0; HighsCDouble sumC1LE = 0.0; - HighsCDouble sumN2mC2GT = 0.0; HighsCDouble sumC2 = 0.0; for (HighsInt i = 0; i != snfr.numNnzs; ++i) { - // col is in N2 \ C2 + // col is in N- \ C- if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == -1) { assert(snfr.vubCoef[i] >= 0); if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { - sumN2mC2GT += snfr.vubCoef[i]; ld.m[ld.r] = snfr.vubCoef[i]; ++ld.r; } else { sumN2mC2LE += snfr.vubCoef[i]; } } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == -1) { - // col is in C2 + // col is in C- assert(snfr.vubCoef[i] > 0); sumC2 += snfr.vubCoef[i]; } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == 1) { - // col is in C1 + // col is in C+ assert(snfr.vubCoef[i] > 0); if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { ld.m[ld.r] = snfr.vubCoef[i]; @@ -549,7 +546,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { sumC1LE += snfr.vubCoef[i]; } } else { - // col is in N1 \ C1 + // col is in N+ \ C+ assert(snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == 1); } } @@ -559,9 +556,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { ld.M.resize(ld.r + 1); ld.ml = std::min(snfr.lambda, static_cast(sumC1LE + sumN2mC2LE)); ld.d1 = sumC2 + snfr.rhs; - ld.d2 = ld.d1 + sumN2mC2GT + sumN2mC2LE; - // Sort into non-increasing order (TODO: Remove comment after checking) pdqsort_branchless( ld.m.begin(), ld.m.end(), [&](const HighsCDouble a, const HighsCDouble b) { return a > b; }); @@ -639,11 +634,13 @@ bool HighsCutGeneration::separateLiftedFlowCover() { ld.M[i]); }; - // Lift the flow cover cut + // Calculate the lifted simple generalized flow cover cut + // L- = {i \in N- \ C- : m_i > lambda} + // L-- = N- \ (L- union C-) HighsCDouble tmpRhs = ld.d1; rowlen = 0; for (HighsInt i = 0; i != snfr.numNnzs; ++i) { - // col is in N2 \ C2 + // col is in N- \ C- if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == -1) { if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { if (snfr.origBinCols[i] != -1) { @@ -669,7 +666,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { tmpRhs += snfr.aggrConstant[i]; } } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == -1) { - // col is in C2 + // col is in C- if (snfr.origBinCols[i] != -1) { double liftedbincoef = evaluateLiftingFunction(snfr.vubCoef[i]); if (liftedbincoef != 0) { @@ -680,7 +677,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { } } } else if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == 1) { - // col is in N1 \ C1 + // col is in N+ \ C+ std::pair alphabeta = getAlphaBeta(snfr.vubCoef[i]); assert(alphabeta.first == 0 || alphabeta.first == 1); @@ -704,7 +701,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { tmpRhs -= snfr.aggrConstant[i]; } } else { - // col is in C1 + // col is in C+ assert(snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == 1); HighsCDouble bincoef = snfr.aggrBinCoef[i]; HighsCDouble constant = snfr.aggrConstant[i]; @@ -730,8 +727,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { #ifndef NDEBUG // TODO: Remove this if algorithm changes to allow multiple bin col usages for (HighsInt i = 0; i < rowlen; i++) { - for (HighsInt j = i + 1; j < rowlen; j++) - assert(inds[i] != inds[j]); + for (HighsInt j = i + 1; j < rowlen; j++) assert(inds[i] != inds[j]); } #endif @@ -1349,6 +1345,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } #endif + // Generate a lifted simple generalized flow cover cut bool flowCoverSuccess = false; std::vector flowCoverVals; std::vector flowCoverInds; @@ -1357,41 +1354,10 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, if (!onlyInitialCMIRScale) { flowCoverVals = vals_; flowCoverInds = inds_; - rowlen = static_cast(flowCoverInds.size()); - this->inds = flowCoverInds.data(); - this->vals = flowCoverVals.data(); - this->rhs = rhs_; - flowCoverSuccess = - preprocessSNFRelaxation(); - if (!flowCoverSuccess) goto cmir; - initSNFRelaxation(); - assert(rowlen <= static_cast(flowCoverVals.size())); - flowCoverVals.resize(rowlen); - flowCoverInds.resize(rowlen); - flowCoverRhs = static_cast(rhs); - flowCoverSuccess = transLp.transformSNFRelaxation( - flowCoverInds, flowCoverVals, flowCoverRhs, snfr); - if (!flowCoverSuccess) goto cmir; - // Array resized as each continuous col may create a non-zero bin coef - flowCoverVals.resize(2 * snfr.numNnzs); - flowCoverInds.resize(2 * snfr.numNnzs); - this->inds = flowCoverInds.data(); - this->vals = flowCoverVals.data(); - this->rhs = flowCoverRhs; - flowCoverSuccess = computeFlowCover(); - if (!flowCoverSuccess) goto cmir; - flowCoverSuccess = - separateLiftedFlowCover(); - if (!flowCoverSuccess) goto cmir; - flowCoverInds.resize(rowlen); - flowCoverVals.resize(rowlen); - flowCoverRhs = static_cast(rhs); - flowCoverSuccess = transLp.cleanup(flowCoverInds, flowCoverVals, - flowCoverRhs, flowCoverEfficacy); - if (flowCoverEfficacy < 10 * feastol) flowCoverSuccess = false; + flowCoverSuccess = tryGenerateFlowCoverCut( + transLp, flowCoverInds, flowCoverVals, flowCoverRhs, flowCoverEfficacy); } -cmir: bool cmirSuccess = false; bool intsPositive = true; bool hasUnboundedInts = false; @@ -1399,7 +1365,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, bool hasContinuous = false; if (!transLp.transform(vals_, upper, solval, inds_, rhs_, intsPositive)) { cmirSuccess = false; - goto untransform; + goto postprocess; } rowlen = inds_.size(); @@ -1410,7 +1376,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, if (!preprocessBaseInequality(hasUnboundedInts, hasGeneralInts, hasContinuous)) { cmirSuccess = false; - goto untransform; + goto postprocess; } // it can happen that there is an unbounded integer variable during the @@ -1435,7 +1401,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, hasContinuous, std::max(flowCoverEfficacy, 10 * feastol), onlyInitialCMIRScale)) { cmirSuccess = false; - goto untransform; + goto postprocess; } // remove the complementation if exists @@ -1449,8 +1415,9 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, vals[i] = vals[rowlen]; } } + cmirSuccess = true; -untransform: +postprocess: if (!cmirSuccess && !flowCoverSuccess) return false; // transform the cut back into the original space, i.e. remove the bound @@ -1705,8 +1672,8 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { } bool HighsCutGeneration::computeFlowCover() { - // Compute the flow cover, i.e., get sets C1 subset N1 and C2 subset N2 - // with sum_{j in C1} u_j - sum_{j in C2} u_j = b + lambda, lambda > 0 + // Compute the flow cover, i.e., get sets C+ subset N+ and C- subset N- + // with sum_{j in C+} u_j - sum_{j in C-} u_j = b + lambda, lambda > 0 if (static_cast(snfr.flowCoverStatus.size()) < snfr.numNnzs) { snfr.flowCoverStatus.resize(snfr.numNnzs); } @@ -1719,7 +1686,7 @@ bool HighsCutGeneration::computeFlowCover() { for (HighsInt i = 0; i < snfr.numNnzs; ++i) { assert(snfr.coef[i] == 1 || snfr.coef[i] == -1); assert(snfr.binSolval[i] >= -feastol && snfr.binSolval[i] <= 1 + feastol); - // if u_i = 0 put i into N1 \ C1 or N2 \ C2, i.e., not the cover + // if u_i = 0 put i into N+ \ C+ or N- \ C-, i.e., not the cover if (abs(snfr.vubCoef[i]) < feastol) { snfr.flowCoverStatus[i] = -1; nNonFlowCover++; @@ -1733,21 +1700,21 @@ bool HighsCutGeneration::computeFlowCover() { n1itemsWeight += snfr.vubCoef[i]; } } else if (snfr.coef[i] == 1 && snfr.binSolval[i] < 0.5) { - // i is in N1 and x_i = 0 -> don't put in cover + // i is in N+ and x_i = 0 -> don't put in cover snfr.flowCoverStatus[i] = -1; nNonFlowCover++; } else if (snfr.coef[i] == 1 && snfr.binSolval[i] > 0.5) { - // i is in N1 and x_i = 1 -> put in cover + // i is in N+ and x_i = 1 -> put in cover snfr.flowCoverStatus[i] = 1; nFlowCover++; flowCoverWeight += snfr.vubCoef[i]; } else if (snfr.coef[i] == -1 && snfr.binSolval[i] > 0.5) { - // i is in N2 and x_i = 1 -> put in cover + // i is in N- and x_i = 1 -> put in cover snfr.flowCoverStatus[i] = 1; nFlowCover++; flowCoverWeight -= snfr.vubCoef[i]; } else { - // i is in N2 and x_i = 0 -> don't put in cover + // i is in N- and x_i = 0 -> don't put in cover assert(snfr.coef[i] == -1 && snfr.binSolval[i] < 0.5); snfr.flowCoverStatus[i] = -1; nNonFlowCover++; @@ -1757,9 +1724,11 @@ bool HighsCutGeneration::computeFlowCover() { double capacity = static_cast(-snfr.rhs + flowCoverWeight + n1itemsWeight); + // There is no flow cover if capacity is less than zero after fixing if (capacity < feastol) return false; - // Solve a knapsack greedily to assign items to C1, C2, N1\C1, N2\C2 + + // Solve a knapsack greedily to assign items to C+, C-, N+\C+, N-\C- double knapsackWeight = 0; std::vector weights(nitems); std::vector profits(nitems); @@ -1810,6 +1779,39 @@ bool HighsCutGeneration::computeFlowCover() { return true; } +bool HighsCutGeneration::tryGenerateFlowCoverCut(HighsTransformedLp& transLp, + std::vector& inds_, + std::vector& vals_, + double& rhs_, + double& efficacy) { + rowlen = static_cast(inds_.size()); + this->inds = inds_.data(); + this->vals = vals_.data(); + this->rhs = rhs_; + if (!preprocessSNFRelaxation()) return false; + initSNFRelaxation(); + vals_.resize(rowlen); + inds_.resize(rowlen); + rhs_ = static_cast(this->rhs); + if (!transLp.transformSNFRelaxation(inds_, vals_, rhs_, snfr)) return false; + + // Array resized as each continuous col may create a non-zero bin coef + vals_.resize(2 * snfr.numNnzs); + inds_.resize(2 * snfr.numNnzs); + this->inds = inds_.data(); + this->vals = vals_.data(); + this->rhs = rhs_; + + if (!computeFlowCover()) return false; + if (!separateLiftedFlowCover()) return false; + inds_.resize(rowlen); + vals_.resize(rowlen); + rhs_ = static_cast(rhs); + if (!transLp.cleanup(inds_, vals_, rhs_, efficacy)) return false; + if (efficacy < 10 * feastol) return false; + return true; +} + bool HighsCutGeneration::finalizeAndAddCut(std::vector& inds_, std::vector& vals_, double& rhs_) { diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index a178095c48..4a18743d91 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -72,6 +72,11 @@ class HighsCutGeneration { bool preprocessSNFRelaxation(); + bool tryGenerateFlowCoverCut(HighsTransformedLp& transLp, + std::vector& inds_, + std::vector& vals_, double& rhs_, + double& efficacy); + double scale(double val); bool postprocessCut(); @@ -113,21 +118,21 @@ class HighsCutGeneration { /// Single Node Flow Relaxation for flow cover cuts struct SNFRelaxation { - std::vector binColUsed; // has col been used in a vub - std::vector origBinColCoef; // original bin col coef - - HighsInt numNnzs; // number of nonzeros - std::vector coef; // coefficients of cols in SNFR - std::vector vubCoef; // coefficients in vub of cols in SNFR - std::vector binSolval; // vub bin col sol in SNFR - std::vector contSolval; // real sol in SNFR - std::vector origBinCols; // orig bin col used in SNFR - std::vector origContCols; // orig cont cols used in SNFR - std::vector aggrBinCoef; // aggr coef of orignal bin-col in SNFR - std::vector aggrContCoef; // aggr coef of original cont-col in SNFR - std::vector aggrConstant; // aggr original constant in SNFR - - std::vector flowCoverStatus; // (+1) in fcover (-1) not in fcover + std::vector binColUsed; // has col been used in a vub + std::vector origBinColCoef; // original bin col coef + + HighsInt numNnzs; // number of nonzeros + std::vector coef; // coefficients of cols in SNFR + std::vector vubCoef; // coefficients in vub of cols in SNFR + std::vector binSolval; // vub bin col sol in SNFR + std::vector contSolval; // real sol in SNFR + std::vector origBinCols; // orig bin col used in SNFR + std::vector origContCols; // orig cont cols used in SNFR + std::vector aggrBinCoef; // aggr coef of orignal bin-col in SNFR + std::vector aggrContCoef; // aggr coef of original cont-col in SNFR + std::vector aggrConstant; // aggr original constant in SNFR + + std::vector flowCoverStatus; // (+1) in fcover (-1) not in fcover double rhs; double lambda; }; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 8b41fc8942..e051d08dba 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -565,7 +565,7 @@ bool HighsTransformedLp::untransform(std::vector& vals, // Create a single node flow relaxation (SNFR) from an aggregated // mixed-integer row and find a valid flow cover. // Turn \sum c_i x_i + \sum a_i y_i <= a_0 (x_i binary, y_i real non-neg) -// into \sum_{j \in N1} y_j - \sum_{j \in N2} y_j <= b, where y_j <= u_j x_j +// into \sum_{j \in N+} y_j - \sum_{j \in N-} y_j <= b, where y_j <= u_j x_j bool HighsTransformedLp::transformSNFRelaxation( std::vector& inds, std::vector& vals, double& rhs, HighsCutGeneration::SNFRelaxation& snfr) { @@ -725,17 +725,6 @@ bool HighsTransformedLp::transformSNFRelaxation( infeasible, false); } - // store the old bound type so that we can restore it if the continuous - // column is relaxed out anyways. This allows to correctly transform and - // then untransform multiple base rows which is useful to compute cuts based - // on several transformed base rows. It could otherwise lead to bugs if a - // column is first transformed with a simple bound and not relaxed but for - // another base row is transformed and relaxed with a variable bound. Should - // the non-relaxed column now be untransformed we would wrongly use the - // variable bound even though this is not the correct way to untransform the - // column. - // BoundType oldBoundType = boundTypes[col]; - // Transform entry into the SNFR if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 1) { if (snfr.binColUsed[col] == false) { @@ -883,6 +872,7 @@ bool HighsTransformedLp::transformSNFRelaxation( return true; } +// Remove slack, small coefficients, and calculate the efficacy bool HighsTransformedLp::cleanup(std::vector& inds, std::vector& vals, double& rhs, From b49d5b50b6fa1e54625086756327b6ef68db413f Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 29 Jul 2025 18:05:05 +0200 Subject: [PATCH 07/64] Correct efficacy calculation --- highs/mip/HighsTransformedLp.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index e051d08dba..698ce58b3a 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -942,13 +942,17 @@ bool HighsTransformedLp::cleanup(std::vector& inds, double sqrnorm = 0; const std::vector& lpSolution = lprelaxation.getSolution().col_value; for (HighsInt i = 0; i != numNz; ++i) { - if (lpSolution[i] >= + if (lpSolution[i] <= mip.mipdata_->domain.col_lower_[i] + mip.mipdata_->feastol) continue; viol += vals[i] * lpSolution[i]; sqrnorm += vals[i] * vals[i]; } - efficacy = viol / sqrt(sqrnorm); + if (sqrnorm == 0) { + efficacy = 0; + } else { + efficacy = viol / sqrt(sqrnorm); + } return true; } From 41c8be508505ba06dec2a040d5bf9ec3b84875ae Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 31 Jul 2025 10:46:04 +0200 Subject: [PATCH 08/64] Update to correct efficacy calc --- highs/mip/HighsCutGeneration.cpp | 2 +- highs/mip/HighsTransformedLp.cpp | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 74c81299e0..918360e0e4 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1345,7 +1345,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } #endif - // Generate a lifted simple generalized flow cover cut + // Try to generate a lifted simple generalized flow cover cut bool flowCoverSuccess = false; std::vector flowCoverVals; std::vector flowCoverInds; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 698ce58b3a..cee883e644 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -942,9 +942,12 @@ bool HighsTransformedLp::cleanup(std::vector& inds, double sqrnorm = 0; const std::vector& lpSolution = lprelaxation.getSolution().col_value; for (HighsInt i = 0; i != numNz; ++i) { - if (lpSolution[i] <= + if (vals[i] >= 0 && lpSolution[i] <= mip.mipdata_->domain.col_lower_[i] + mip.mipdata_->feastol) continue; + if (vals[i] < 0 && lpSolution[i] >= + mip.mipdata_->domain.col_upper_[i] - mip.mipdata_->feastol) + continue; viol += vals[i] * lpSolution[i]; sqrnorm += vals[i] * vals[i]; } From 74a4c2351fcc621a6ebf2c19cccc2a42549034d2 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 5 Aug 2025 11:23:38 +0200 Subject: [PATCH 09/64] Fix bug where local domain change not repr --- highs/mip/HighsTransformedLp.cpp | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index cee883e644..1859d1614d 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -589,6 +589,15 @@ bool HighsTransformedLp::transformSNFRelaxation( : lprelaxation.slackUpper(col - slackOffset)); }; + auto colIsBinary = [&](const HighsInt col, const double lb, const double ub) { + // Check local domain too. Global domain change may not yet be reflected + if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 1 && + lprelaxation.colLower(col) >= 0 && lprelaxation.colUpper(col) <= 1) { + return true; + } + return false; + }; + auto getLpSolution = [&](HighsInt col) { HighsInt numCols = lprelaxation.numCols(); if (col < numCols) { @@ -664,7 +673,7 @@ bool HighsTransformedLp::transformSNFRelaxation( HighsInt col = inds[i]; double lb = getLb(col); double ub = getUb(col); - if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 1) { + if (colIsBinary(col, lb, ub)) { numBinCols++; snfr.origBinColCoef[col] = vals[i]; std::swap(inds[i], inds[numNz - numBinCols]); @@ -684,7 +693,7 @@ bool HighsTransformedLp::transformSNFRelaxation( if (ub - lb < mip.options_mip_->small_matrix_value) { rhs -= std::min(lb, ub) * vals[i]; tmpSnfrRhs -= std::min(lb, ub) * vals[i]; - if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 1) { + if (colIsBinary(col, lb, ub)) { snfr.origBinColCoef[col] = 0; } remove(i); @@ -726,7 +735,7 @@ bool HighsTransformedLp::transformSNFRelaxation( } // Transform entry into the SNFR - if (lprelaxation.isColIntegral(col) && lb == 0 && ub == 1) { + if (colIsBinary(col, lb, ub)) { if (snfr.binColUsed[col] == false) { if (vals[i] >= 0) { addSNFRentry(col, -1, getLpSolution(col), @@ -874,8 +883,7 @@ bool HighsTransformedLp::transformSNFRelaxation( // Remove slack, small coefficients, and calculate the efficacy bool HighsTransformedLp::cleanup(std::vector& inds, - std::vector& vals, - double& rhs, + std::vector& vals, double& rhs, double& efficacy) { HighsCDouble tmpRhs = rhs; const HighsMipSolver& mip = lprelaxation.getMipSolver(); @@ -942,11 +950,11 @@ bool HighsTransformedLp::cleanup(std::vector& inds, double sqrnorm = 0; const std::vector& lpSolution = lprelaxation.getSolution().col_value; for (HighsInt i = 0; i != numNz; ++i) { - if (vals[i] >= 0 && lpSolution[i] <= - mip.mipdata_->domain.col_lower_[i] + mip.mipdata_->feastol) + if (vals[i] >= 0 && lpSolution[i] <= mip.mipdata_->domain.col_lower_[i] + + mip.mipdata_->feastol) continue; - if (vals[i] < 0 && lpSolution[i] >= - mip.mipdata_->domain.col_upper_[i] - mip.mipdata_->feastol) + if (vals[i] < 0 && lpSolution[i] >= mip.mipdata_->domain.col_upper_[i] - + mip.mipdata_->feastol) continue; viol += vals[i] * lpSolution[i]; sqrnorm += vals[i] * vals[i]; From 76a3407290e66c3628b36c5f8e6b5f92d15b2f74 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 6 Aug 2025 17:21:17 +0200 Subject: [PATCH 10/64] Add more fixes --- highs/mip/HighsCutGeneration.cpp | 1 + highs/mip/HighsTransformedLp.cpp | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 918360e0e4..acb535f07b 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -575,6 +575,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { break; } } + ld.t++; auto getAlphaBeta = [&](double vubcoef) { HighsInt alpha; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 1859d1614d..2927614568 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -798,7 +798,7 @@ bool HighsTransformedLp::transformSNFRelaxation( switch (boundTypes[col]) { case BoundType::kSimpleLb: substsolval = static_cast( - vals[i] * (HighsCDouble(lpSolution.col_value[col]) - ub)); + vals[i] * (HighsCDouble(getLpSolution(col)) - ub)); vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * ub); if (vals[i] >= 0) { @@ -812,7 +812,7 @@ bool HighsTransformedLp::transformSNFRelaxation( break; case BoundType::kSimpleUb: substsolval = static_cast( - vals[i] * (HighsCDouble(lpSolution.col_value[col]) - lb)); + vals[i] * (HighsCDouble(getLpSolution(col)) - lb)); vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * lb); if (vals[i] >= 0) { @@ -827,7 +827,7 @@ bool HighsTransformedLp::transformSNFRelaxation( case BoundType::kVariableLb: vbcol = bestVlb[col].first; substsolval = static_cast( - vals[i] * (HighsCDouble(lpSolution.col_value[col]) - + vals[i] * (HighsCDouble(getLpSolution(col)) - bestVlb[col].second.constant) + (HighsCDouble(lpSolution.col_value[vbcol]) * snfr.origBinColCoef[vbcol])); @@ -850,7 +850,7 @@ bool HighsTransformedLp::transformSNFRelaxation( case BoundType::kVariableUb: vbcol = bestVub[col].first; substsolval = static_cast( - vals[i] * (HighsCDouble(lpSolution.col_value[col]) - + vals[i] * (HighsCDouble(getLpSolution(col)) - bestVub[col].second.constant) + (HighsCDouble(lpSolution.col_value[vbcol]) * snfr.origBinColCoef[vbcol])); @@ -950,13 +950,15 @@ bool HighsTransformedLp::cleanup(std::vector& inds, double sqrnorm = 0; const std::vector& lpSolution = lprelaxation.getSolution().col_value; for (HighsInt i = 0; i != numNz; ++i) { - if (vals[i] >= 0 && lpSolution[i] <= mip.mipdata_->domain.col_lower_[i] + - mip.mipdata_->feastol) + HighsInt col = inds[i]; + if (vals[i] >= 0 && + lpSolution[col] <= + mip.mipdata_->domain.col_lower_[col] + mip.mipdata_->feastol) continue; - if (vals[i] < 0 && lpSolution[i] >= mip.mipdata_->domain.col_upper_[i] - - mip.mipdata_->feastol) + if (vals[i] < 0 && lpSolution[col] >= mip.mipdata_->domain.col_upper_[col] - + mip.mipdata_->feastol) continue; - viol += vals[i] * lpSolution[i]; + viol += vals[i] * lpSolution[col]; sqrnorm += vals[i] * vals[i]; } if (sqrnorm == 0) { From 7327b68dac219592764ea1efba536bdc0516f65d Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 7 Aug 2025 18:02:44 +0200 Subject: [PATCH 11/64] Change when vbcoef is used --- highs/mip/HighsCutGeneration.cpp | 2 +- highs/mip/HighsTransformedLp.cpp | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index acb535f07b..b92764b0aa 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -632,7 +632,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { } assert(i == ld.r && ld.M[i] <= vubcoef + snfr.lambda + 1e-8); return static_cast(ld.r * HighsCDouble(snfr.lambda) + vubcoef - - ld.M[i]); + ld.M[ld.r]); }; // Calculate the lifted simple generalized flow cover cut diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 2927614568..45bab25233 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -753,8 +753,7 @@ bool HighsTransformedLp::transformSNFRelaxation( snfr.origBinColCoef[bestVlb[col].first], lb, ub, false)) { boundTypes[col] = BoundType::kSimpleLb; - } else if (vals[i] > 0 || - simpleLbDist[col] > lbDist[col] + mip.mipdata_->feastol) { + } else if (simpleLbDist[col] > lbDist[col] + mip.mipdata_->feastol) { boundTypes[col] = BoundType::kVariableLb; snfr.binColUsed[bestVlb[col].first] = true; } else @@ -764,8 +763,7 @@ bool HighsTransformedLp::transformSNFRelaxation( snfr.origBinColCoef[bestVub[col].first], lb, ub, true)) { boundTypes[col] = BoundType::kSimpleUb; - } else if (vals[i] < 0 || - simpleUbDist[col] > ubDist[col] + mip.mipdata_->feastol) { + } else if (simpleUbDist[col] > ubDist[col] + mip.mipdata_->feastol) { boundTypes[col] = BoundType::kVariableUb; snfr.binColUsed[bestVub[col].first] = true; } else { From d737abd1a81963fd80cd21fdd35fac72c902119c Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 7 Aug 2025 18:33:23 +0200 Subject: [PATCH 12/64] Add missing vlb / vub check --- highs/mip/HighsTransformedLp.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 45bab25233..6460d40ba0 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -629,6 +629,8 @@ bool HighsTransformedLp::transformSNFRelaxation( if (bincol == -1) return false; if (snfr.binColUsed[bincol]) return false; if (abs(vb.coef) >= 1e+6) return false; + if (isVub && lb < vb.constant) return false; + if (!isVub && ub > vb.constant) return false; const double sign = coef >= 0 ? 1 : -1; if (isVub) { double val = sign * ((coef * vb.coef) + origbincoef); From f8e19820d51ad58097856f7d2ec2d830b8c8bff6 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 8 Aug 2025 12:24:39 +0200 Subject: [PATCH 13/64] Add genflowcover param --- highs/mip/HighsCutGeneration.cpp | 5 +++-- highs/mip/HighsCutGeneration.h | 3 ++- highs/mip/HighsModkSeparator.cpp | 4 ++-- highs/mip/HighsPathSeparator.cpp | 4 ++-- highs/mip/HighsTableauSeparator.cpp | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index b92764b0aa..29117aba85 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1302,7 +1302,8 @@ static void checkNumerics(const double* vals, HighsInt len, double rhs) { bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, std::vector& inds_, std::vector& vals_, double& rhs_, - bool onlyInitialCMIRScale) { + bool onlyInitialCMIRScale, + bool genFlowCover) { #if 0 if (vals_.size() > 1) { std::vector indsCheck_ = inds_; @@ -1352,7 +1353,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, std::vector flowCoverInds; double flowCoverRhs = rhs_; double flowCoverEfficacy = 0; - if (!onlyInitialCMIRScale) { + if (genFlowCover) { flowCoverVals = vals_; flowCoverInds = inds_; flowCoverSuccess = tryGenerateFlowCoverCut( diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index 4a18743d91..362e635980 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -104,7 +104,8 @@ class HighsCutGeneration { /// separates the LP solution for the given single row relaxation bool generateCut(HighsTransformedLp& transLp, std::vector& inds, std::vector& vals, double& rhs, - bool onlyInitialCMIRScale = false); + bool onlyInitialCMIRScale = false, + bool genFlowCover = false); /// generate a conflict from the given proof constraint which cuts of the /// given local domain diff --git a/highs/mip/HighsModkSeparator.cpp b/highs/mip/HighsModkSeparator.cpp index 0f638f2552..43c7bb6b11 100644 --- a/highs/mip/HighsModkSeparator.cpp +++ b/highs/mip/HighsModkSeparator.cpp @@ -226,7 +226,7 @@ void HighsModkSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, lpAggregator.getCurrentAggregation(inds, vals, false); rhs = 0.0; - cutGen.generateCut(transLp, inds, vals, rhs, true); + cutGen.generateCut(transLp, inds, vals, rhs, true, false); if (k != 2) { lpAggregator.clear(); @@ -240,7 +240,7 @@ void HighsModkSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, lpAggregator.getCurrentAggregation(inds, vals, true); rhs = 0.0; - cutGen.generateCut(transLp, inds, vals, rhs, true); + cutGen.generateCut(transLp, inds, vals, rhs, true, false); lpAggregator.clear(); }; diff --git a/highs/mip/HighsPathSeparator.cpp b/highs/mip/HighsPathSeparator.cpp index f3b6df3062..cb738b5ff4 100644 --- a/highs/mip/HighsPathSeparator.cpp +++ b/highs/mip/HighsPathSeparator.cpp @@ -350,7 +350,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate cut double rhs = 0; - success = cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs); + success = cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, true); lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); if (!aggregatedPath.empty() || bestOutArcCol != -1 || @@ -359,7 +359,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate reverse cut rhs = 0; - success |= cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs); + success |= cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, true); if (success || (bestOutArcCol == -1 && bestInArcCol == -1)) break; diff --git a/highs/mip/HighsTableauSeparator.cpp b/highs/mip/HighsTableauSeparator.cpp index d67d1f25c1..c237871df0 100644 --- a/highs/mip/HighsTableauSeparator.cpp +++ b/highs/mip/HighsTableauSeparator.cpp @@ -229,12 +229,12 @@ void HighsTableauSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, baseRowInds.size()); double rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, true); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, true); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.clear(); From 45df9ca75196d66e25f1aee3356e44fb7d2ca4da Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 8 Aug 2025 15:36:29 +0200 Subject: [PATCH 14/64] Allow vbcol to be used mult. times --- highs/mip/HighsCutGeneration.cpp | 18 --------- highs/mip/HighsCutGeneration.h | 3 -- highs/mip/HighsTransformedLp.cpp | 65 +++++++++++++++----------------- 3 files changed, 30 insertions(+), 56 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 29117aba85..b7eda17963 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -725,13 +725,6 @@ bool HighsCutGeneration::separateLiftedFlowCover() { } } -#ifndef NDEBUG - // TODO: Remove this if algorithm changes to allow multiple bin col usages - for (HighsInt i = 0; i < rowlen; i++) { - for (HighsInt j = i + 1; j < rowlen; j++) assert(inds[i] != inds[j]); - } -#endif - rhs = static_cast(tmpRhs); return true; } @@ -1568,17 +1561,6 @@ bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, } void HighsCutGeneration::initSNFRelaxation() { - HighsInt numTransformedCol = lpRelaxation.numCols() + lpRelaxation.numRows(); - if (static_cast(snfr.binColUsed.size()) < numTransformedCol) { - snfr.binColUsed.resize(numTransformedCol); - snfr.origBinColCoef.resize(numTransformedCol); - } - for (HighsInt i = 0; i != static_cast(snfr.binColUsed.size()); - ++i) { - snfr.binColUsed[i] = false; - snfr.origBinColCoef[i] = 0; - } - if (static_cast(snfr.coef.size()) < rowlen) { snfr.origBinCols.resize(rowlen); snfr.origContCols.resize(rowlen); diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index 362e635980..827b86aa73 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -119,9 +119,6 @@ class HighsCutGeneration { /// Single Node Flow Relaxation for flow cover cuts struct SNFRelaxation { - std::vector binColUsed; // has col been used in a vub - std::vector origBinColCoef; // original bin col coef - HighsInt numNnzs; // number of nonzeros std::vector coef; // coefficients of cols in SNFR std::vector vubCoef; // coefficients in vub of cols in SNFR diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 6460d40ba0..3a74c92720 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -627,7 +627,6 @@ bool HighsTransformedLp::transformSNFRelaxation( double coef, double origbincoef, double lb, double ub, bool isVub) { if (bincol == -1) return false; - if (snfr.binColUsed[bincol]) return false; if (abs(vb.coef) >= 1e+6) return false; if (isVub && lb < vb.constant) return false; if (!isVub && ub > vb.constant) return false; @@ -653,10 +652,6 @@ bool HighsTransformedLp::transformSNFRelaxation( assert(binsolval >= -lprelaxation.getMipSolver().mipdata_->feastol && binsolval <= 1 + lprelaxation.getMipSolver().mipdata_->feastol); assert(vubcoef >= -1e-10); - for (HighsInt j = 0; j < snfr.numNnzs; j++) { - assert(snfr.origBinCols[j] == -1 || snfr.origBinCols[j] != origbincol); - assert(snfr.origContCols[j] == -1 || snfr.origContCols[j] != origcontcol); - } snfr.origBinCols[snfr.numNnzs] = origbincol; snfr.origContCols[snfr.numNnzs] = origcontcol; snfr.binSolval[snfr.numNnzs] = binsolval; @@ -677,7 +672,7 @@ bool HighsTransformedLp::transformSNFRelaxation( double ub = getUb(col); if (colIsBinary(col, lb, ub)) { numBinCols++; - snfr.origBinColCoef[col] = vals[i]; + vectorsum.add(col, vals[i]); std::swap(inds[i], inds[numNz - numBinCols]); std::swap(vals[i], vals[numNz - numBinCols]); continue; @@ -695,14 +690,18 @@ bool HighsTransformedLp::transformSNFRelaxation( if (ub - lb < mip.options_mip_->small_matrix_value) { rhs -= std::min(lb, ub) * vals[i]; tmpSnfrRhs -= std::min(lb, ub) * vals[i]; - if (colIsBinary(col, lb, ub)) { - snfr.origBinColCoef[col] = 0; - } remove(i); continue; } + // We are using vectorsum to keep track of the binary coefficients + if (colIsBinary(col, lb, ub) && vectorsum.getValue(col) == 0) { + ++i; + continue; + } + if (lb == -kHighsInf || ub == kHighsInf) { + vectorsum.clear(); return false; } @@ -738,53 +737,46 @@ bool HighsTransformedLp::transformSNFRelaxation( // Transform entry into the SNFR if (colIsBinary(col, lb, ub)) { - if (snfr.binColUsed[col] == false) { - if (vals[i] >= 0) { - addSNFRentry(col, -1, getLpSolution(col), - getLpSolution(col) * vals[i], 1, vals[i], 0, vals[i], 0); - } else { - addSNFRentry(col, -1, getLpSolution(col), - -getLpSolution(col) * vals[i], -1, -vals[i], 0, -vals[i], - 0); - } - snfr.binColUsed[col] = true; + if (vals[i] >= 0) { + addSNFRentry(col, -1, getLpSolution(col), + getLpSolution(col) * vals[i], 1, vals[i], 0, vals[i], 0); + } else { + addSNFRentry(col, -1, getLpSolution(col), + -getLpSolution(col) * vals[i], -1, -vals[i], 0, -vals[i], + 0); } } else { if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { if (!checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - snfr.origBinColCoef[bestVlb[col].first], lb, ub, + vectorsum.getValue(bestVlb[col].first), lb, ub, false)) { boundTypes[col] = BoundType::kSimpleLb; } else if (simpleLbDist[col] > lbDist[col] + mip.mipdata_->feastol) { boundTypes[col] = BoundType::kVariableLb; - snfr.binColUsed[bestVlb[col].first] = true; } else boundTypes[col] = BoundType::kSimpleLb; } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol) { if (!checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - snfr.origBinColCoef[bestVub[col].first], lb, ub, + vectorsum.getValue(bestVub[col].first), lb, ub, true)) { boundTypes[col] = BoundType::kSimpleUb; } else if (simpleUbDist[col] > ubDist[col] + mip.mipdata_->feastol) { boundTypes[col] = BoundType::kVariableUb; - snfr.binColUsed[bestVub[col].first] = true; } else { boundTypes[col] = BoundType::kSimpleUb; } } else if (vals[i] > 0) { if (checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - snfr.origBinColCoef[bestVlb[col].first], lb, ub, + vectorsum.getValue(bestVlb[col].first), lb, ub, false)) { - snfr.binColUsed[bestVlb[col].first] = true; boundTypes[col] = BoundType::kVariableLb; } else { boundTypes[col] = BoundType::kSimpleLb; } } else { if (checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - snfr.origBinColCoef[bestVub[col].first], lb, ub, + vectorsum.getValue(bestVub[col].first), lb, ub, true)) { - snfr.binColUsed[bestVub[col].first] = true; boundTypes[col] = BoundType::kVariableUb; } else { boundTypes[col] = BoundType::kSimpleUb; @@ -830,21 +822,22 @@ bool HighsTransformedLp::transformSNFRelaxation( vals[i] * (HighsCDouble(getLpSolution(col)) - bestVlb[col].second.constant) + (HighsCDouble(lpSolution.col_value[vbcol]) * - snfr.origBinColCoef[vbcol])); + vectorsum.getValue(vbcol))); vbcoef = static_cast(HighsCDouble(vals[i]) * bestVlb[col].second.coef + - snfr.origBinColCoef[vbcol]); + vectorsum.getValue(vbcol)); aggrconstant = static_cast(HighsCDouble(vals[i]) * bestVlb[col].second.constant); if (vals[i] >= 0) { addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], -substsolval, - -1, -vbcoef, aggrconstant, -snfr.origBinColCoef[vbcol], + -1, -vbcoef, aggrconstant, -vectorsum.getValue(vbcol), -vals[i]); } else { addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], substsolval, - 1, vbcoef, -aggrconstant, snfr.origBinColCoef[vbcol], + 1, vbcoef, -aggrconstant, vectorsum.getValue(vbcol), vals[i]); } + vectorsum.values[vbcol] = 0; tmpSnfrRhs -= aggrconstant; break; case BoundType::kVariableUb: @@ -853,21 +846,22 @@ bool HighsTransformedLp::transformSNFRelaxation( vals[i] * (HighsCDouble(getLpSolution(col)) - bestVub[col].second.constant) + (HighsCDouble(lpSolution.col_value[vbcol]) * - snfr.origBinColCoef[vbcol])); + vectorsum.getValue(vbcol))); vbcoef = static_cast(HighsCDouble(vals[i]) * bestVub[col].second.coef + - snfr.origBinColCoef[vbcol]); + vectorsum.getValue(vbcol)); aggrconstant = static_cast(HighsCDouble(vals[i]) * bestVub[col].second.constant); if (vals[i] >= 0) { addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], substsolval, - 1, vbcoef, -aggrconstant, snfr.origBinColCoef[vbcol], + 1, vbcoef, -aggrconstant, vectorsum.getValue(vbcol), vals[i]); } else { addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], -substsolval, - -1, -vbcoef, aggrconstant, -snfr.origBinColCoef[vbcol], + -1, -vbcoef, aggrconstant, -vectorsum.getValue(vbcol), -vals[i]); } + vectorsum.values[vbcol] = 0; tmpSnfrRhs -= aggrconstant; break; } @@ -876,6 +870,7 @@ bool HighsTransformedLp::transformSNFRelaxation( i++; } + vectorsum.clear(); snfr.rhs = static_cast(tmpSnfrRhs); if (numNz == 0 && rhs >= -mip.mipdata_->feastol) return false; return true; From 74aff960cc43e6b152fa84bdfc51ae35d16111a5 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 8 Aug 2025 17:20:59 +0200 Subject: [PATCH 15/64] Change when minefficacy used --- highs/mip/HighsCutGeneration.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index b7eda17963..3db4beb431 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -806,7 +806,7 @@ bool HighsCutGeneration::cmirCutGenerationHeuristic(double minEfficacy, deltas.erase(std::remove(deltas.begin(), deltas.end(), 0.0), deltas.end()); double bestdelta = -1; - double bestefficacy = minEfficacy; + double bestefficacy = feastol; for (double delta : deltas) { double scale = 1.0 / delta; @@ -916,6 +916,8 @@ bool HighsCutGeneration::cmirCutGenerationHeuristic(double minEfficacy, } } + if (bestefficacy < minEfficacy) return false; + HighsCDouble scale = 1.0 / HighsCDouble(bestdelta); HighsCDouble scalrhs = rhs * scale; double downrhs = floor(double(scalrhs)); @@ -1950,7 +1952,7 @@ bool HighsCutGeneration::tryGenerateCut(std::vector& inds_, rhs = tmpRhs; } else { // accept cut and increase minimum efficiency requirement for cmir cut - minMirEfficacy += efficacy; + minMirEfficacy += feastol; std::swap(tmpRhs, rhs); } } From dd658b54b284d5a1eb5ec48b15325d78f0221b98 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 11 Aug 2025 10:41:50 +0200 Subject: [PATCH 16/64] Correct efficacy calc --- highs/mip/HighsTransformedLp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 3a74c92720..14d8d91aac 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -946,6 +946,7 @@ bool HighsTransformedLp::cleanup(std::vector& inds, const std::vector& lpSolution = lprelaxation.getSolution().col_value; for (HighsInt i = 0; i != numNz; ++i) { HighsInt col = inds[i]; + viol += vals[i] * lpSolution[col]; if (vals[i] >= 0 && lpSolution[col] <= mip.mipdata_->domain.col_lower_[col] + mip.mipdata_->feastol) @@ -953,7 +954,6 @@ bool HighsTransformedLp::cleanup(std::vector& inds, if (vals[i] < 0 && lpSolution[col] >= mip.mipdata_->domain.col_upper_[col] - mip.mipdata_->feastol) continue; - viol += vals[i] * lpSolution[col]; sqrnorm += vals[i] * vals[i]; } if (sqrnorm == 0) { From c782d7593fb1b29d1e107a34043f852ae021d59e Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 11 Aug 2025 10:47:16 +0200 Subject: [PATCH 17/64] Correct efficacy again --- highs/mip/HighsTransformedLp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 14d8d91aac..32cf6bd465 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -941,7 +941,7 @@ bool HighsTransformedLp::cleanup(std::vector& inds, for (HighsInt i = 0; i != numNz; ++i) vals[i] = vectorsum.getValue(inds[i]); vectorsum.clear(); - double viol = 0; + double viol = -rhs; double sqrnorm = 0; const std::vector& lpSolution = lprelaxation.getSolution().col_value; for (HighsInt i = 0; i != numNz; ++i) { From 618811441086f4e4e4e69c01b300d1597e2f5a86 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 11 Aug 2025 12:41:25 +0200 Subject: [PATCH 18/64] Add additional documentation --- highs/mip/HighsCutGeneration.cpp | 1 + highs/mip/HighsTransformedLp.cpp | 50 ++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 3db4beb431..0a5e9aa63d 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1588,6 +1588,7 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { // numerical troubles, use bound constraints to cancel them and // reject base inequalities where that is not possible due to unbounded // variables + // 4. Don't consider any inequality with too many non-zeros HighsInt numZeros = 0; double maxact = -feastol; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 32cf6bd465..626f3f9aad 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -565,7 +565,7 @@ bool HighsTransformedLp::untransform(std::vector& vals, // Create a single node flow relaxation (SNFR) from an aggregated // mixed-integer row and find a valid flow cover. // Turn \sum c_i x_i + \sum a_i y_i <= a_0 (x_i binary, y_i real non-neg) -// into \sum_{j \in N+} y_j - \sum_{j \in N-} y_j <= b, where y_j <= u_j x_j +// into \sum_{j \in N+} y'_j - \sum_{j \in N-} y'_j <= b, where y'_j <= u_j x_j bool HighsTransformedLp::transformSNFRelaxation( std::vector& inds, std::vector& vals, double& rhs, HighsCutGeneration::SNFRelaxation& snfr) { @@ -626,6 +626,26 @@ bool HighsTransformedLp::transformSNFRelaxation( auto checkValidityVB = [&](HighsInt bincol, HighsImplications::VarBound vb, double coef, double origbincoef, double lb, double ub, bool isVub) { + // a_j coefficient of y_j in row. c_j coefficient of x_j in row. + // u_j, l_j, closest simple bounds imposed on y_j. + // y_j <= l'_j x_j + d_j || y_j >= u'_j x_j + d_j + // variable bound can only be used if following properties respected: + // |l'_j| <= 1e6 && |u'_j| <= 1e6 + // if a_j > 0 + // !isVub: (1) u_j <= d_j + // (2) a_j (u_j - d_j) + c_j <= 0 + // (3) a_j l'_j + c_j <= 0 + // isVub: (1) l_j >= d_j + // (2) a_j (l_j - d_j) + c_j >= 0 + // (3) a_j u'_j + c_j >= 0 + // if a_j < 0 + // !isVub: (1) u_j <= d_j + // (2) a_j (u_j - d_j) + c_j >= 0 + // (3) a_j l'_j + c_j >= 0 + // isVub: (1) l_j >= d_j + // (2) a_j (l_j - d_j) + c_j <= 0 + // (3) a_j u'_j + c_j <= 0 + // Note: c_j may change during transform if two columns use same bin col if (bincol == -1) return false; if (abs(vb.coef) >= 1e+6) return false; if (isVub && lb < vb.constant) return false; @@ -664,7 +684,8 @@ bool HighsTransformedLp::transformSNFRelaxation( snfr.numNnzs++; }; - // Place the non-binary variables to the front (all general ints relaxed) + // Place the non-binary columns to the front (all general ints relaxed) + // Use vectorsum to track original row coefficients of binary columns. HighsInt i = 0; while (i < numNz - numBinCols) { HighsInt col = inds[i]; @@ -737,6 +758,7 @@ bool HighsTransformedLp::transformSNFRelaxation( // Transform entry into the SNFR if (colIsBinary(col, lb, ub)) { + // Binary columns can be added directly to the SNFR if (vals[i] >= 0) { addSNFRentry(col, -1, getLpSolution(col), getLpSolution(col) * vals[i], 1, vals[i], 0, vals[i], 0); @@ -746,6 +768,7 @@ bool HighsTransformedLp::transformSNFRelaxation( 0); } } else { + // Decide whether to use {simple, variable} {lower, upper} bound if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { if (!checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], vectorsum.getValue(bestVlb[col].first), lb, ub, @@ -789,6 +812,10 @@ bool HighsTransformedLp::transformSNFRelaxation( HighsInt vbcol; switch (boundTypes[col]) { case BoundType::kSimpleLb: + // Case (1) -> a_j > 0, y'_j -> N- Case (2) -> a_j < 0, y'_j ->N+ + // (1) y'_j = -a_j(y_j - u_j), 0 <= y'_j <= a_j(u_j - l_j)x_j, x_j = 1 + // (2) y'_j = a_j(y_j - u_j), 0 <= y'_j <= -a_j(u_j - l_j)x_j, x_j = 1 + // rhs -= a_j * u_j substsolval = static_cast( vals[i] * (HighsCDouble(getLpSolution(col)) - ub)); vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); @@ -803,6 +830,11 @@ bool HighsTransformedLp::transformSNFRelaxation( tmpSnfrRhs -= aggrconstant; break; case BoundType::kSimpleUb: + // Case (1) -> a_j > 0, y'_j -> N+ Case (2) -> a_j < 0, y'_j ->N- + // (1) y'_j = a_j(y_j - l_j), 0 <= y'_j <= a_j(u_j - l_j)x_j, x_j = 1 + // (2) y'_j = -a_j(y_j - l_j), 0 <= y'_j <= -a_j(u_j - l_j)x_j, + // x_j = 1 + // rhs -= a_j * l_j substsolval = static_cast( vals[i] * (HighsCDouble(getLpSolution(col)) - lb)); vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); @@ -817,6 +849,13 @@ bool HighsTransformedLp::transformSNFRelaxation( tmpSnfrRhs -= aggrconstant; break; case BoundType::kVariableLb: + // vlb: l'_j x_j + d_j <= y_j. c_j is the coefficient of x_j in row + // Case (1) -> a_j > 0, y'_j -> N- Case (2) -> a_j < 0, y'_j ->N+ + // (1) y'_j = -(a_j(y_j - d_j) + c_j * x_j), + // 0 <= y'_j <= -(a_j l'_j + c_j)x_j + // (2) y'_j = a_j(y_j - d_j) + c_j * x_j, + // 0 <= y'_j <= (a_j l'_j + c_j)x_j + // rhs -= a_j * d_j vbcol = bestVlb[col].first; substsolval = static_cast( vals[i] * (HighsCDouble(getLpSolution(col)) - @@ -841,6 +880,13 @@ bool HighsTransformedLp::transformSNFRelaxation( tmpSnfrRhs -= aggrconstant; break; case BoundType::kVariableUb: + // vub: y_j >= u'_j x_j + d_j. c_j is the coefficient of x_j in row + // Case (1) -> a_j > 0, y'_j -> N+ Case (2) -> a_j < 0, y'_j ->N- + // (1) y'_j = a_j(y_j - d_j) + c_j * x_j), + // 0 <= y'_j <= (a_j u'_j + c_j)x_j + // (2) y'_j = -(a_j(y_j - d_j) + c_j * x_j), + // 0 <= y'_j <= -(a_j u'_j + c_j)x_j + // rhs -= a_j * d_j vbcol = bestVub[col].first; substsolval = static_cast( vals[i] * (HighsCDouble(getLpSolution(col)) - From db6c8cea0d671b70e34d010fbea194553a9200bf Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 13 Aug 2025 11:05:08 +0200 Subject: [PATCH 19/64] Turn off fc for tableau. Add cover > cmir > fc --- highs/mip/HighsCutGeneration.cpp | 2 +- highs/mip/HighsTableauSeparator.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 0a5e9aa63d..33621f3159 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1395,7 +1395,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, // try to generate a cut if (!tryGenerateCut(inds_, vals_, hasUnboundedInts, hasGeneralInts, - hasContinuous, std::max(flowCoverEfficacy, 10 * feastol), + hasContinuous, std::max(flowCoverEfficacy - feastol, 10 * feastol), onlyInitialCMIRScale)) { cmirSuccess = false; goto postprocess; diff --git a/highs/mip/HighsTableauSeparator.cpp b/highs/mip/HighsTableauSeparator.cpp index c237871df0..0ebf25504e 100644 --- a/highs/mip/HighsTableauSeparator.cpp +++ b/highs/mip/HighsTableauSeparator.cpp @@ -229,12 +229,12 @@ void HighsTableauSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, baseRowInds.size()); double rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, true); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, false); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, true); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, false); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.clear(); From 8a85830219059d7162441e66333a7a87d05cf0f4 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 13 Aug 2025 15:29:44 +0200 Subject: [PATCH 20/64] Update comments. Remove unneeded import --- highs/mip/HighsCutGeneration.h | 29 ++++++++++++++--------------- highs/mip/HighsTransformedLp.h | 1 - 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index 827b86aa73..ca28c9195a 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -20,7 +20,6 @@ #include "util/HighsCDouble.h" #include "util/HighsInt.h" #include "util/HighsRandom.h" -#include "util/HighsSparseVectorSum.h" class HighsLpRelaxation; class HighsTransformedLp; @@ -119,20 +118,20 @@ class HighsCutGeneration { /// Single Node Flow Relaxation for flow cover cuts struct SNFRelaxation { - HighsInt numNnzs; // number of nonzeros - std::vector coef; // coefficients of cols in SNFR - std::vector vubCoef; // coefficients in vub of cols in SNFR - std::vector binSolval; // vub bin col sol in SNFR - std::vector contSolval; // real sol in SNFR - std::vector origBinCols; // orig bin col used in SNFR - std::vector origContCols; // orig cont cols used in SNFR - std::vector aggrBinCoef; // aggr coef of orignal bin-col in SNFR - std::vector aggrContCoef; // aggr coef of original cont-col in SNFR - std::vector aggrConstant; // aggr original constant in SNFR - - std::vector flowCoverStatus; // (+1) in fcover (-1) not in fcover - double rhs; - double lambda; + HighsInt numNnzs; // |N-| + |N+| + std::vector coef; // (+-1) coefficient of col in SNFR + std::vector vubCoef; // u_j in y'_j <= u_j x_j in SNFR + std::vector binSolval; // lp[x_j], y'_j <= u_j x_j in SNFR + std::vector contSolval; // lp[y'_j] in y'_j <= u_j x_j in SNFR + std::vector origBinCols; // orig x_i, y'_j <= u_j x_j in SNFR + std::vector origContCols; // orig y_i used to make y'_j in SNFR + std::vector aggrBinCoef; // c_i row coef of x_i in orig aggrrow + std::vector aggrContCoef; // a_i row coef of y_i in orig aggrrow + std::vector aggrConstant; // constant shift used in SNFR transform + + std::vector flowCoverStatus; // (+1) in f-cover (-1) notin f-cover + double rhs; // in \sum_{j \in N+} y'_j - \sum_{j \in N-} y'_j <= b + double lambda; // in sum_{j in C+} u_j - sum_{j in C-} u_j = b + lambda }; private: diff --git a/highs/mip/HighsTransformedLp.h b/highs/mip/HighsTransformedLp.h index 811eeaa079..944ee9c947 100644 --- a/highs/mip/HighsTransformedLp.h +++ b/highs/mip/HighsTransformedLp.h @@ -23,7 +23,6 @@ #include "util/HighsInt.h" #include "util/HighsSparseVectorSum.h" -class HighsCutGeneration; class HighsLpRelaxation; /// Helper class to compute single-row relaxations from the current LP From 3856a5d0ff9236f737b93b8984dbb33984652f2d Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 13 Aug 2025 16:00:09 +0200 Subject: [PATCH 21/64] Revert cmir change. Fix min eff calc --- highs/mip/HighsCutGeneration.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 33621f3159..ef03f40f64 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -806,7 +806,7 @@ bool HighsCutGeneration::cmirCutGenerationHeuristic(double minEfficacy, deltas.erase(std::remove(deltas.begin(), deltas.end(), 0.0), deltas.end()); double bestdelta = -1; - double bestefficacy = feastol; + double bestefficacy = minEfficacy; for (double delta : deltas) { double scale = 1.0 / delta; @@ -916,8 +916,6 @@ bool HighsCutGeneration::cmirCutGenerationHeuristic(double minEfficacy, } } - if (bestefficacy < minEfficacy) return false; - HighsCDouble scale = 1.0 / HighsCDouble(bestdelta); HighsCDouble scalrhs = rhs * scale; double downrhs = floor(double(scalrhs)); @@ -1953,7 +1951,7 @@ bool HighsCutGeneration::tryGenerateCut(std::vector& inds_, rhs = tmpRhs; } else { // accept cut and increase minimum efficiency requirement for cmir cut - minMirEfficacy += feastol; + minMirEfficacy = std::max(minEfficacy, efficacy) + feastol; std::swap(tmpRhs, rhs); } } From 4d45a7226c49518889a1d59c57f9517afe440256 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 13 Aug 2025 17:33:38 +0200 Subject: [PATCH 22/64] Correct mineff for conflicts --- highs/mip/HighsCutGeneration.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index ef03f40f64..6fcee04cd1 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1951,7 +1951,11 @@ bool HighsCutGeneration::tryGenerateCut(std::vector& inds_, rhs = tmpRhs; } else { // accept cut and increase minimum efficiency requirement for cmir cut - minMirEfficacy = std::max(minEfficacy, efficacy) + feastol; + if (allowRejectCut) { + minMirEfficacy = std::max(minEfficacy, efficacy + feastol); + } else { + minMirEfficacy = efficacy + feastol; + } std::swap(tmpRhs, rhs); } } From 97c5757512b87f75b85cd23eb6cd488c740f6f64 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 13 Aug 2025 20:24:28 +0200 Subject: [PATCH 23/64] Make formatter happy --- highs/mip/HighsCutGeneration.cpp | 3 ++- highs/mip/HighsCutGeneration.h | 29 +++++++++++++++-------------- highs/mip/HighsPathSeparator.cpp | 6 ++++-- highs/mip/HighsTransformedLp.cpp | 29 ++++++++++++++--------------- 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 6fcee04cd1..f1ffce8ce6 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1393,7 +1393,8 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, // try to generate a cut if (!tryGenerateCut(inds_, vals_, hasUnboundedInts, hasGeneralInts, - hasContinuous, std::max(flowCoverEfficacy - feastol, 10 * feastol), + hasContinuous, + std::max(flowCoverEfficacy - feastol, 10 * feastol), onlyInitialCMIRScale)) { cmirSuccess = false; goto postprocess; diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index ca28c9195a..7cb744dfb1 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -118,20 +118,21 @@ class HighsCutGeneration { /// Single Node Flow Relaxation for flow cover cuts struct SNFRelaxation { - HighsInt numNnzs; // |N-| + |N+| - std::vector coef; // (+-1) coefficient of col in SNFR - std::vector vubCoef; // u_j in y'_j <= u_j x_j in SNFR - std::vector binSolval; // lp[x_j], y'_j <= u_j x_j in SNFR - std::vector contSolval; // lp[y'_j] in y'_j <= u_j x_j in SNFR - std::vector origBinCols; // orig x_i, y'_j <= u_j x_j in SNFR - std::vector origContCols; // orig y_i used to make y'_j in SNFR - std::vector aggrBinCoef; // c_i row coef of x_i in orig aggrrow - std::vector aggrContCoef; // a_i row coef of y_i in orig aggrrow - std::vector aggrConstant; // constant shift used in SNFR transform - - std::vector flowCoverStatus; // (+1) in f-cover (-1) notin f-cover - double rhs; // in \sum_{j \in N+} y'_j - \sum_{j \in N-} y'_j <= b - double lambda; // in sum_{j in C+} u_j - sum_{j in C-} u_j = b + lambda + HighsInt numNnzs; // |N-| + |N+| + std::vector coef; // (+-1) coefficient of col in SNFR + std::vector vubCoef; // u_j in y'_j <= u_j x_j in SNFR + std::vector binSolval; // lp[x_j], y'_j <= u_j x_j in SNFR + std::vector contSolval; // lp[y'_j] in y'_j <= u_j x_j in SNFR + std::vector origBinCols; // orig x_i, y'_j <= u_j x_j in SNFR + std::vector origContCols; // orig y_i used to make y'_j in SNFR + std::vector aggrBinCoef; // c_i row coef of x_i in orig aggrrow + std::vector aggrContCoef; // a_i row coef of y_i in orig aggrrow + std::vector aggrConstant; // constant shift used in SNFR transform + + std::vector + flowCoverStatus; // (+1) in f-cover (-1) notin f-cover + double rhs; // in \sum_{j \in N+} y'_j - \sum_{j \in N-} y'_j <= b + double lambda; // in sum_{j in C+} u_j - sum_{j in C-} u_j = b + lambda }; private: diff --git a/highs/mip/HighsPathSeparator.cpp b/highs/mip/HighsPathSeparator.cpp index cb738b5ff4..83a3828cec 100644 --- a/highs/mip/HighsPathSeparator.cpp +++ b/highs/mip/HighsPathSeparator.cpp @@ -350,7 +350,8 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate cut double rhs = 0; - success = cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, true); + success = cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, + false, true); lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); if (!aggregatedPath.empty() || bestOutArcCol != -1 || @@ -359,7 +360,8 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate reverse cut rhs = 0; - success |= cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, true); + success |= cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, + false, true); if (success || (bestOutArcCol == -1 && bestInArcCol == -1)) break; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 626f3f9aad..6e8bbc32b7 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -760,12 +760,11 @@ bool HighsTransformedLp::transformSNFRelaxation( if (colIsBinary(col, lb, ub)) { // Binary columns can be added directly to the SNFR if (vals[i] >= 0) { - addSNFRentry(col, -1, getLpSolution(col), - getLpSolution(col) * vals[i], 1, vals[i], 0, vals[i], 0); + addSNFRentry(col, -1, getLpSolution(col), getLpSolution(col) * vals[i], + 1, vals[i], 0, vals[i], 0); } else { - addSNFRentry(col, -1, getLpSolution(col), - -getLpSolution(col) * vals[i], -1, -vals[i], 0, -vals[i], - 0); + addSNFRentry(col, -1, getLpSolution(col), -getLpSolution(col) * vals[i], + -1, -vals[i], 0, -vals[i], 0); } } else { // Decide whether to use {simple, variable} {lower, upper} bound @@ -857,11 +856,11 @@ bool HighsTransformedLp::transformSNFRelaxation( // 0 <= y'_j <= (a_j l'_j + c_j)x_j // rhs -= a_j * d_j vbcol = bestVlb[col].first; - substsolval = static_cast( - vals[i] * (HighsCDouble(getLpSolution(col)) - - bestVlb[col].second.constant) + - (HighsCDouble(lpSolution.col_value[vbcol]) * - vectorsum.getValue(vbcol))); + substsolval = + static_cast(vals[i] * (HighsCDouble(getLpSolution(col)) - + bestVlb[col].second.constant) + + (HighsCDouble(lpSolution.col_value[vbcol]) * + vectorsum.getValue(vbcol))); vbcoef = static_cast(HighsCDouble(vals[i]) * bestVlb[col].second.coef + vectorsum.getValue(vbcol)); @@ -888,11 +887,11 @@ bool HighsTransformedLp::transformSNFRelaxation( // 0 <= y'_j <= -(a_j u'_j + c_j)x_j // rhs -= a_j * d_j vbcol = bestVub[col].first; - substsolval = static_cast( - vals[i] * (HighsCDouble(getLpSolution(col)) - - bestVub[col].second.constant) + - (HighsCDouble(lpSolution.col_value[vbcol]) * - vectorsum.getValue(vbcol))); + substsolval = + static_cast(vals[i] * (HighsCDouble(getLpSolution(col)) - + bestVub[col].second.constant) + + (HighsCDouble(lpSolution.col_value[vbcol]) * + vectorsum.getValue(vbcol))); vbcoef = static_cast(HighsCDouble(vals[i]) * bestVub[col].second.coef + vectorsum.getValue(vbcol)); From 61d5de29170e8351bbcb6e4e3b121dda7e9a7315 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 13 Aug 2025 20:58:42 +0200 Subject: [PATCH 24/64] Fix valgrind --- highs/mip/HighsTransformedLp.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 6e8bbc32b7..0196cdcb19 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -770,8 +770,10 @@ bool HighsTransformedLp::transformSNFRelaxation( // Decide whether to use {simple, variable} {lower, upper} bound if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { if (!checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - vectorsum.getValue(bestVlb[col].first), lb, ub, - false)) { + bestVlb[col].first == -1 + ? 0 + : vectorsum.getValue(bestVlb[col].first), + lb, ub, false)) { boundTypes[col] = BoundType::kSimpleLb; } else if (simpleLbDist[col] > lbDist[col] + mip.mipdata_->feastol) { boundTypes[col] = BoundType::kVariableLb; @@ -779,8 +781,10 @@ bool HighsTransformedLp::transformSNFRelaxation( boundTypes[col] = BoundType::kSimpleLb; } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol) { if (!checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - vectorsum.getValue(bestVub[col].first), lb, ub, - true)) { + bestVub[col].first == -1 + ? 0 + : vectorsum.getValue(bestVub[col].first), + lb, ub, true)) { boundTypes[col] = BoundType::kSimpleUb; } else if (simpleUbDist[col] > ubDist[col] + mip.mipdata_->feastol) { boundTypes[col] = BoundType::kVariableUb; @@ -789,16 +793,20 @@ bool HighsTransformedLp::transformSNFRelaxation( } } else if (vals[i] > 0) { if (checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - vectorsum.getValue(bestVlb[col].first), lb, ub, - false)) { + bestVlb[col].first == -1 + ? 0 + : vectorsum.getValue(bestVlb[col].first), + lb, ub, false)) { boundTypes[col] = BoundType::kVariableLb; } else { boundTypes[col] = BoundType::kSimpleLb; } } else { if (checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - vectorsum.getValue(bestVub[col].first), lb, ub, - true)) { + bestVub[col].first == -1 + ? 0 + : vectorsum.getValue(bestVub[col].first), + lb, ub, true)) { boundTypes[col] = BoundType::kVariableUb; } else { boundTypes[col] = BoundType::kSimpleUb; From abb145bf8fe1f915fa309b5c3e658124991b6a7d Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 13 Aug 2025 23:32:10 +0200 Subject: [PATCH 25/64] Assume integral support / coeffs are false for flow cover --- highs/mip/HighsCutGeneration.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index f1ffce8ce6..8d62fa230b 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1427,6 +1427,8 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, rhs_ = flowCoverRhs; std::swap(vals_, flowCoverVals); std::swap(inds_, flowCoverInds); + integralSupport = false; + integralCoefficients = false; } rowlen = inds_.size(); From 00d6c59da7d6b7543551754fbbd3ed1826e19c32 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 15 Aug 2025 11:37:00 +0200 Subject: [PATCH 26/64] Return min-eff-diff to 10 * feastol --- highs/mip/HighsCutGeneration.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 8d62fa230b..34dc4123d3 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1392,10 +1392,10 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } // try to generate a cut - if (!tryGenerateCut(inds_, vals_, hasUnboundedInts, hasGeneralInts, - hasContinuous, - std::max(flowCoverEfficacy - feastol, 10 * feastol), - onlyInitialCMIRScale)) { + if (!tryGenerateCut( + inds_, vals_, hasUnboundedInts, hasGeneralInts, hasContinuous, + std::max(flowCoverEfficacy - (10 * feastol), 10 * feastol), + onlyInitialCMIRScale)) { cmirSuccess = false; goto postprocess; } @@ -1957,7 +1957,7 @@ bool HighsCutGeneration::tryGenerateCut(std::vector& inds_, if (allowRejectCut) { minMirEfficacy = std::max(minEfficacy, efficacy + feastol); } else { - minMirEfficacy = efficacy + feastol; + minMirEfficacy = efficacy + (10 * feastol); } std::swap(tmpRhs, rhs); } From 84573f86973485fe90ea242262909776775eeaf9 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 20 Aug 2025 15:23:43 +0200 Subject: [PATCH 27/64] Priortise vbs more. Fix some comments --- highs/mip/HighsTransformedLp.cpp | 64 +++++++++++++------------------- 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 0196cdcb19..a814b354c5 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -628,7 +628,7 @@ bool HighsTransformedLp::transformSNFRelaxation( double ub, bool isVub) { // a_j coefficient of y_j in row. c_j coefficient of x_j in row. // u_j, l_j, closest simple bounds imposed on y_j. - // y_j <= l'_j x_j + d_j || y_j >= u'_j x_j + d_j + // y_j <= u'_j x_j + d_j || y_j >= l'_j x_j + d_j // variable bound can only be used if following properties respected: // |l'_j| <= 1e6 && |u'_j| <= 1e6 // if a_j > 0 @@ -768,56 +768,44 @@ bool HighsTransformedLp::transformSNFRelaxation( } } else { // Decide whether to use {simple, variable} {lower, upper} bound - if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { - if (!checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], + bool vlbValid = checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], bestVlb[col].first == -1 ? 0 : vectorsum.getValue(bestVlb[col].first), - lb, ub, false)) { - boundTypes[col] = BoundType::kSimpleLb; - } else if (simpleLbDist[col] > lbDist[col] + mip.mipdata_->feastol) { - boundTypes[col] = BoundType::kVariableLb; - } else - boundTypes[col] = BoundType::kSimpleLb; - } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol) { - if (!checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], + lb, ub, false); + bool vubValid = checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], bestVub[col].first == -1 ? 0 : vectorsum.getValue(bestVub[col].first), - lb, ub, true)) { - boundTypes[col] = BoundType::kSimpleUb; - } else if (simpleUbDist[col] > ubDist[col] + mip.mipdata_->feastol) { - boundTypes[col] = BoundType::kVariableUb; - } else { - boundTypes[col] = BoundType::kSimpleUb; - } + lb, ub, true); + BoundType boundType = BoundType::kSimpleLb; + if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol && vlbValid) { + boundType = BoundType::kVariableLb; + } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol && vubValid) { + boundType = BoundType::kVariableUb; + } else if (vals[i] > 0 && vlbValid) { + boundType = BoundType::kVariableLb; + } else if (vals[i] < 0 && vubValid) { + boundType = BoundType::kVariableUb; + } else if (vlbValid) { + boundType = BoundType::kVariableLb; + } else if (vubValid) { + boundType = BoundType::kVariableUb; + } else if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { + boundType = BoundType::kSimpleLb; + } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol) { + boundType = BoundType::kSimpleUb; } else if (vals[i] > 0) { - if (checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - bestVlb[col].first == -1 - ? 0 - : vectorsum.getValue(bestVlb[col].first), - lb, ub, false)) { - boundTypes[col] = BoundType::kVariableLb; - } else { - boundTypes[col] = BoundType::kSimpleLb; - } + boundType = BoundType::kSimpleLb; } else { - if (checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - bestVub[col].first == -1 - ? 0 - : vectorsum.getValue(bestVub[col].first), - lb, ub, true)) { - boundTypes[col] = BoundType::kVariableUb; - } else { - boundTypes[col] = BoundType::kSimpleUb; - } + boundType = BoundType::kSimpleUb; } double vbcoef; double substsolval; double aggrconstant; HighsInt vbcol; - switch (boundTypes[col]) { + switch (boundType) { case BoundType::kSimpleLb: // Case (1) -> a_j > 0, y'_j -> N- Case (2) -> a_j < 0, y'_j ->N+ // (1) y'_j = -a_j(y_j - u_j), 0 <= y'_j <= a_j(u_j - l_j)x_j, x_j = 1 @@ -887,7 +875,7 @@ bool HighsTransformedLp::transformSNFRelaxation( tmpSnfrRhs -= aggrconstant; break; case BoundType::kVariableUb: - // vub: y_j >= u'_j x_j + d_j. c_j is the coefficient of x_j in row + // vub: y_j <= u'_j x_j + d_j. c_j is the coefficient of x_j in row // Case (1) -> a_j > 0, y'_j -> N+ Case (2) -> a_j < 0, y'_j ->N- // (1) y'_j = a_j(y_j - d_j) + c_j * x_j), // 0 <= y'_j <= (a_j u'_j + c_j)x_j From ee6b8cd3d1ff14bf66de844df958a414a156f330 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 22 Aug 2025 09:59:18 +0200 Subject: [PATCH 28/64] Allow vlb / vub to be complemented --- highs/mip/HighsCutGeneration.cpp | 21 +++ highs/mip/HighsCutGeneration.h | 3 +- highs/mip/HighsTransformedLp.cpp | 230 +++++++++++++++++++++---------- 3 files changed, 180 insertions(+), 74 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 34dc4123d3..c93709360f 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -648,6 +648,10 @@ bool HighsCutGeneration::separateLiftedFlowCover() { // col is in L- vals[rowlen] = -snfr.lambda; inds[rowlen] = snfr.origBinCols[i]; + if (snfr.complementation[i]) { + tmpRhs -= vals[rowlen]; + vals[rowlen] = -vals[rowlen]; + } rowlen++; } else { tmpRhs += snfr.lambda; @@ -662,6 +666,10 @@ bool HighsCutGeneration::separateLiftedFlowCover() { if (snfr.origBinCols[i] != -1 && snfr.aggrBinCoef[i] != 0) { vals[rowlen] = -snfr.aggrBinCoef[i]; inds[rowlen] = snfr.origBinCols[i]; + if (snfr.complementation[i]) { + tmpRhs -= vals[rowlen]; + vals[rowlen] = -vals[rowlen]; + } rowlen++; } tmpRhs += snfr.aggrConstant[i]; @@ -673,6 +681,10 @@ bool HighsCutGeneration::separateLiftedFlowCover() { if (liftedbincoef != 0) { vals[rowlen] = -liftedbincoef; inds[rowlen] = snfr.origBinCols[i]; + if (snfr.complementation[i]) { + tmpRhs -= vals[rowlen]; + vals[rowlen] = -vals[rowlen]; + } rowlen++; tmpRhs -= liftedbincoef; } @@ -694,6 +706,10 @@ bool HighsCutGeneration::separateLiftedFlowCover() { if (binvarcoef != 0) { vals[rowlen] = static_cast(binvarcoef); inds[rowlen] = snfr.origBinCols[i]; + if (snfr.complementation[i]) { + tmpRhs -= vals[rowlen]; + vals[rowlen] = -vals[rowlen]; + } rowlen++; } } else { @@ -714,6 +730,10 @@ bool HighsCutGeneration::separateLiftedFlowCover() { if (snfr.origBinCols[i] != -1 && bincoef != 0) { vals[rowlen] = static_cast(bincoef); inds[rowlen] = snfr.origBinCols[i]; + if (snfr.complementation[i]) { + tmpRhs -= vals[rowlen]; + vals[rowlen] = -vals[rowlen]; + } rowlen++; } if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { @@ -1574,6 +1594,7 @@ void HighsCutGeneration::initSNFRelaxation() { snfr.aggrConstant.resize(rowlen); snfr.aggrBinCoef.resize(rowlen); snfr.aggrContCoef.resize(rowlen); + snfr.complementation.resize(rowlen); } snfr.rhs = 0; snfr.lambda = 0; diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index 7cb744dfb1..582e2ffd24 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -127,7 +127,8 @@ class HighsCutGeneration { std::vector origContCols; // orig y_i used to make y'_j in SNFR std::vector aggrBinCoef; // c_i row coef of x_i in orig aggrrow std::vector aggrContCoef; // a_i row coef of y_i in orig aggrrow - std::vector aggrConstant; // constant shift used in SNFR transform + std::vector aggrConstant; // constant shift used in SNFR transform + std::vector complementation; // was the original bincol complemented std::vector flowCoverStatus; // (+1) in f-cover (-1) notin f-cover diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index a814b354c5..a9c77b6ef3 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -625,7 +625,8 @@ bool HighsTransformedLp::transformSNFRelaxation( auto checkValidityVB = [&](HighsInt bincol, HighsImplications::VarBound vb, double coef, double origbincoef, double lb, - double ub, bool isVub) { + double ub, bool isVub, bool& complement, + bool& inclbincoef) { // a_j coefficient of y_j in row. c_j coefficient of x_j in row. // u_j, l_j, closest simple bounds imposed on y_j. // y_j <= u'_j x_j + d_j || y_j >= l'_j x_j + d_j @@ -648,41 +649,99 @@ bool HighsTransformedLp::transformSNFRelaxation( // Note: c_j may change during transform if two columns use same bin col if (bincol == -1) return false; if (abs(vb.coef) >= 1e+6) return false; - if (isVub && lb < vb.constant) return false; - if (!isVub && ub > vb.constant) return false; + complement = false; + inclbincoef = true; + if (isVub && lb < vb.constant) { + if (lb >= vb.constant + vb.coef) { + complement = true; + } else { + return false; + } + } + if (!isVub && ub > vb.constant) { + if (ub <= vb.constant + vb.coef) { + complement = true; + } else { + return false; + } + } const double sign = coef >= 0 ? 1 : -1; if (isVub) { - double val = sign * ((coef * vb.coef) + origbincoef); - if (val < 0 || val > kHighsInf) return false; - val = sign * ((coef * (lb - vb.constant)) + origbincoef); - if (val < 0) return false; + double val = complement ? sign * (coef * -vb.coef + origbincoef) + : sign * (coef * vb.coef + origbincoef); + if (val > kHighsInf) return false; + if (val < 0) { + val = complement ? sign * (coef * -vb.coef) : sign * (coef * vb.coef); + if (val < 0) return false; + inclbincoef = false; + } + if (inclbincoef) { + val = complement + ? sign * (coef * (lb - (vb.constant + vb.coef)) + origbincoef) + : sign * (coef * (lb - vb.constant) + origbincoef); + if (val < 0) { + val = complement ? sign * (coef * (lb - (vb.constant + vb.coef))) + : sign * (coef * (lb - vb.constant)); + if (val < 0) return false; + inclbincoef = false; + val = complement ? sign * (coef * -vb.coef) : sign * (coef * vb.coef); + if (val < 0) return false; + } + } else { + val = complement ? sign * (coef * (lb - (vb.constant + vb.coef))) + : sign * (coef * (lb - vb.constant)); + if (val < 0) return false; + } } else { - double val = sign * ((coef * vb.coef) + origbincoef); - if (val > 0 || -val > kHighsInf) return false; - val = sign * ((coef * (ub - vb.constant)) + origbincoef); - if (val > 0) return false; + double val = complement ? sign * ((coef * -vb.coef) + origbincoef) + : sign * ((coef * vb.coef) + origbincoef); + if (-val > kHighsInf) return false; + if (val > 0) { + val = complement ? sign * (coef * -vb.coef) : sign * (coef * vb.coef); + if (val > 0) return false; + inclbincoef = false; + } + if (inclbincoef) { + val = + complement + ? sign * ((coef * (ub - (vb.constant + vb.coef))) + origbincoef) + : sign * ((coef * (ub - vb.constant)) + origbincoef); + if (val > 0) { + val = complement ? sign * (coef * (ub - (vb.constant + vb.coef))) + : sign * (coef * (ub - vb.constant)); + if (val > 0) return false; + inclbincoef = false; + val = complement ? sign * (coef * -vb.coef) : sign * (coef * vb.coef); + if (val > 0) return false; + } + } else { + val = complement ? sign * (coef * (ub - (vb.constant + vb.coef))) + : sign * (coef * (ub - vb.constant)); + if (val > 0) return false; + } } return true; }; - auto addSNFRentry = [&](HighsInt origbincol, HighsInt origcontcol, - double binsolval, double contsolval, HighsInt coef, - double vubcoef, double aggrconstant, - double aggrbincoef, double aggrcontcoef) { - assert(binsolval >= -lprelaxation.getMipSolver().mipdata_->feastol && - binsolval <= 1 + lprelaxation.getMipSolver().mipdata_->feastol); - assert(vubcoef >= -1e-10); - snfr.origBinCols[snfr.numNnzs] = origbincol; - snfr.origContCols[snfr.numNnzs] = origcontcol; - snfr.binSolval[snfr.numNnzs] = binsolval; - snfr.contSolval[snfr.numNnzs] = contsolval; - snfr.coef[snfr.numNnzs] = coef; - snfr.vubCoef[snfr.numNnzs] = std::max(vubcoef, 0.0); - snfr.aggrConstant[snfr.numNnzs] = aggrconstant; - snfr.aggrBinCoef[snfr.numNnzs] = aggrbincoef; - snfr.aggrContCoef[snfr.numNnzs] = aggrcontcoef; - snfr.numNnzs++; - }; + auto addSNFRentry = + [&](HighsInt origbincol, HighsInt origcontcol, double binsolval, + double contsolval, HighsInt coef, double vubcoef, double aggrconstant, + double aggrbincoef, double aggrcontcoef, bool complement) { + assert(binsolval >= -lprelaxation.getMipSolver().mipdata_->feastol && + binsolval <= 1 + lprelaxation.getMipSolver().mipdata_->feastol); + assert(vubcoef >= -1e-10); + snfr.origBinCols[snfr.numNnzs] = origbincol; + snfr.origContCols[snfr.numNnzs] = origcontcol; + snfr.binSolval[snfr.numNnzs] = binsolval; + snfr.contSolval[snfr.numNnzs] = contsolval; + snfr.coef[snfr.numNnzs] = coef; + snfr.vubCoef[snfr.numNnzs] = std::max(vubcoef, 0.0); + snfr.aggrConstant[snfr.numNnzs] = aggrconstant; + snfr.aggrBinCoef[snfr.numNnzs] = aggrbincoef; + snfr.aggrContCoef[snfr.numNnzs] = aggrcontcoef; + snfr.complementation[snfr.numNnzs] = complement; + snfr.numNnzs++; + }; // Place the non-binary columns to the front (all general ints relaxed) // Use vectorsum to track original row coefficients of binary columns. @@ -761,36 +820,39 @@ bool HighsTransformedLp::transformSNFRelaxation( // Binary columns can be added directly to the SNFR if (vals[i] >= 0) { addSNFRentry(col, -1, getLpSolution(col), getLpSolution(col) * vals[i], - 1, vals[i], 0, vals[i], 0); + 1, vals[i], 0, vectorsum.getValue(col), 0, false); } else { addSNFRentry(col, -1, getLpSolution(col), -getLpSolution(col) * vals[i], - -1, -vals[i], 0, -vals[i], 0); + -1, -vals[i], 0, -vectorsum.getValue(col), 0, false); } } else { // Decide whether to use {simple, variable} {lower, upper} bound - bool vlbValid = checkValidityVB(bestVlb[col].first, bestVlb[col].second, vals[i], - bestVlb[col].first == -1 - ? 0 - : vectorsum.getValue(bestVlb[col].first), - lb, ub, false); - bool vubValid = checkValidityVB(bestVub[col].first, bestVub[col].second, vals[i], - bestVub[col].first == -1 - ? 0 - : vectorsum.getValue(bestVub[col].first), - lb, ub, true); - BoundType boundType = BoundType::kSimpleLb; + bool complementvlb = false; + bool inclbincolvlb = true; + bool vlbValid = checkValidityVB( + bestVlb[col].first, bestVlb[col].second, vals[i], + bestVlb[col].first == -1 ? 0 : vectorsum.getValue(bestVlb[col].first), + lb, ub, false, complementvlb, inclbincolvlb); + bool complementvub = false; + bool inclbincolvub = true; + bool vubValid = checkValidityVB( + bestVub[col].first, bestVub[col].second, vals[i], + bestVub[col].first == -1 ? 0 : vectorsum.getValue(bestVub[col].first), + lb, ub, true, complementvub, inclbincolvub); + auto boundType = BoundType::kSimpleLb; if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol && vlbValid) { boundType = BoundType::kVariableLb; - } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol && vubValid) { + } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol && + vubValid) { boundType = BoundType::kVariableUb; } else if (vals[i] > 0 && vlbValid) { boundType = BoundType::kVariableLb; } else if (vals[i] < 0 && vubValid) { boundType = BoundType::kVariableUb; } else if (vlbValid) { - boundType = BoundType::kVariableLb; + boundType = BoundType::kVariableLb; } else if (vubValid) { - boundType = BoundType::kVariableUb; + boundType = BoundType::kVariableUb; } else if (lbDist[col] < ubDist[col] - mip.mipdata_->feastol) { boundType = BoundType::kSimpleLb; } else if (ubDist[col] < lbDist[col] - mip.mipdata_->feastol) { @@ -817,10 +879,10 @@ bool HighsTransformedLp::transformSNFRelaxation( aggrconstant = static_cast(HighsCDouble(vals[i]) * ub); if (vals[i] >= 0) { addSNFRentry(-1, col, 1.0, -substsolval, -1, vbcoef, aggrconstant, - 0, -vals[i]); + 0, -vals[i], false); } else { addSNFRentry(-1, col, 1, substsolval, 1, -vbcoef, -aggrconstant, 0, - vals[i]); + vals[i], false); } tmpSnfrRhs -= aggrconstant; break; @@ -836,10 +898,10 @@ bool HighsTransformedLp::transformSNFRelaxation( aggrconstant = static_cast(HighsCDouble(vals[i]) * lb); if (vals[i] >= 0) { addSNFRentry(-1, col, 1, substsolval, 1, vbcoef, -aggrconstant, 0, - vals[i]); + vals[i], false); } else { addSNFRentry(-1, col, 1, -substsolval, -1, -vbcoef, aggrconstant, 0, - -vals[i]); + -vals[i], false); } tmpSnfrRhs -= aggrconstant; break; @@ -857,21 +919,32 @@ bool HighsTransformedLp::transformSNFRelaxation( bestVlb[col].second.constant) + (HighsCDouble(lpSolution.col_value[vbcol]) * vectorsum.getValue(vbcol))); - vbcoef = static_cast(HighsCDouble(vals[i]) * - bestVlb[col].second.coef + - vectorsum.getValue(vbcol)); - aggrconstant = static_cast(HighsCDouble(vals[i]) * - bestVlb[col].second.constant); + vbcoef = static_cast( + HighsCDouble(vals[i]) * (complementvlb + ? -bestVlb[col].second.coef + : bestVlb[col].second.coef) + + (inclbincolvlb ? vectorsum.getValue(vbcol) : 0)); + aggrconstant = static_cast( + HighsCDouble(vals[i]) * + (complementvlb + ? bestVlb[col].second.constant + bestVlb[col].second.coef + : bestVlb[col].second.constant)); if (vals[i] >= 0) { - addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], -substsolval, - -1, -vbcoef, aggrconstant, -vectorsum.getValue(vbcol), - -vals[i]); + addSNFRentry(vbcol, col, + complementvlb ? 1 - lpSolution.col_value[vbcol] + : lpSolution.col_value[vbcol], + -substsolval, -1, -vbcoef, aggrconstant, + inclbincolvlb ? -vectorsum.getValue(vbcol) : 0, + -vals[i], complementvlb); } else { - addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], substsolval, - 1, vbcoef, -aggrconstant, vectorsum.getValue(vbcol), - vals[i]); + addSNFRentry(vbcol, col, + complementvlb ? 1 - lpSolution.col_value[vbcol] + : lpSolution.col_value[vbcol], + substsolval, 1, vbcoef, -aggrconstant, + inclbincolvlb ? vectorsum.getValue(vbcol) : 0, vals[i], + complementvlb); } - vectorsum.values[vbcol] = 0; + if (inclbincolvlb) vectorsum.values[vbcol] = 0; tmpSnfrRhs -= aggrconstant; break; case BoundType::kVariableUb: @@ -888,21 +961,32 @@ bool HighsTransformedLp::transformSNFRelaxation( bestVub[col].second.constant) + (HighsCDouble(lpSolution.col_value[vbcol]) * vectorsum.getValue(vbcol))); - vbcoef = static_cast(HighsCDouble(vals[i]) * - bestVub[col].second.coef + - vectorsum.getValue(vbcol)); - aggrconstant = static_cast(HighsCDouble(vals[i]) * - bestVub[col].second.constant); + vbcoef = static_cast( + HighsCDouble(vals[i]) * (complementvub + ? -bestVub[col].second.coef + : bestVub[col].second.coef) + + (inclbincolvub ? vectorsum.getValue(vbcol) : 0)); + aggrconstant = static_cast( + HighsCDouble(vals[i]) * + (complementvub + ? bestVub[col].second.constant + bestVub[col].second.coef + : bestVub[col].second.constant)); if (vals[i] >= 0) { - addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], substsolval, - 1, vbcoef, -aggrconstant, vectorsum.getValue(vbcol), - vals[i]); + addSNFRentry(vbcol, col, + complementvub ? 1 - lpSolution.col_value[vbcol] + : lpSolution.col_value[vbcol], + substsolval, 1, vbcoef, -aggrconstant, + inclbincolvub ? vectorsum.getValue(vbcol) : 0, vals[i], + complementvub); } else { - addSNFRentry(vbcol, col, lpSolution.col_value[vbcol], -substsolval, - -1, -vbcoef, aggrconstant, -vectorsum.getValue(vbcol), - -vals[i]); + addSNFRentry(vbcol, col, + complementvub ? 1 - lpSolution.col_value[vbcol] + : lpSolution.col_value[vbcol], + -substsolval, -1, -vbcoef, aggrconstant, + inclbincolvub ? -vectorsum.getValue(vbcol) : 0, + -vals[i], complementvub); } - vectorsum.values[vbcol] = 0; + if (inclbincolvub) vectorsum.values[vbcol] = 0; tmpSnfrRhs -= aggrconstant; break; } From e64f692af54ed2de6ab0d436cc0a03beef41e621 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 22 Aug 2025 17:53:17 +0200 Subject: [PATCH 29/64] Fix some cases on incorrect compl. --- highs/mip/HighsCutGeneration.cpp | 6 +++--- highs/mip/HighsTransformedLp.cpp | 17 +++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index c93709360f..c55c4f3b36 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -682,7 +682,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { vals[rowlen] = -liftedbincoef; inds[rowlen] = snfr.origBinCols[i]; if (snfr.complementation[i]) { - tmpRhs -= vals[rowlen]; + tmpRhs -= -liftedbincoef; vals[rowlen] = -vals[rowlen]; } rowlen++; @@ -707,7 +707,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { vals[rowlen] = static_cast(binvarcoef); inds[rowlen] = snfr.origBinCols[i]; if (snfr.complementation[i]) { - tmpRhs -= vals[rowlen]; + tmpRhs -= binvarcoef; vals[rowlen] = -vals[rowlen]; } rowlen++; @@ -731,7 +731,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { vals[rowlen] = static_cast(bincoef); inds[rowlen] = snfr.origBinCols[i]; if (snfr.complementation[i]) { - tmpRhs -= vals[rowlen]; + tmpRhs -= bincoef; vals[rowlen] = -vals[rowlen]; } rowlen++; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index a9c77b6ef3..6529e767ef 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -666,6 +666,7 @@ bool HighsTransformedLp::transformSNFRelaxation( } } const double sign = coef >= 0 ? 1 : -1; + if (complement) origbincoef = -origbincoef; if (isVub) { double val = complement ? sign * (coef * -vb.coef + origbincoef) : sign * (coef * vb.coef + origbincoef); @@ -923,25 +924,25 @@ bool HighsTransformedLp::transformSNFRelaxation( HighsCDouble(vals[i]) * (complementvlb ? -bestVlb[col].second.coef : bestVlb[col].second.coef) + - (inclbincolvlb ? vectorsum.getValue(vbcol) : 0)); + (inclbincolvlb ? (complementvlb ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol)) : 0)); aggrconstant = static_cast( HighsCDouble(vals[i]) * (complementvlb ? bestVlb[col].second.constant + bestVlb[col].second.coef - : bestVlb[col].second.constant)); + : bestVlb[col].second.constant)) + (complementvlb && inclbincolvlb ? vectorsum.getValue(vbcol) : 0); if (vals[i] >= 0) { addSNFRentry(vbcol, col, complementvlb ? 1 - lpSolution.col_value[vbcol] : lpSolution.col_value[vbcol], -substsolval, -1, -vbcoef, aggrconstant, - inclbincolvlb ? -vectorsum.getValue(vbcol) : 0, + inclbincolvlb ? (complementvlb ? vectorsum.getValue(vbcol) : -vectorsum.getValue(vbcol)) : 0, -vals[i], complementvlb); } else { addSNFRentry(vbcol, col, complementvlb ? 1 - lpSolution.col_value[vbcol] : lpSolution.col_value[vbcol], substsolval, 1, vbcoef, -aggrconstant, - inclbincolvlb ? vectorsum.getValue(vbcol) : 0, vals[i], + inclbincolvlb ? (complementvlb ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol)) : 0, vals[i], complementvlb); } if (inclbincolvlb) vectorsum.values[vbcol] = 0; @@ -965,25 +966,25 @@ bool HighsTransformedLp::transformSNFRelaxation( HighsCDouble(vals[i]) * (complementvub ? -bestVub[col].second.coef : bestVub[col].second.coef) + - (inclbincolvub ? vectorsum.getValue(vbcol) : 0)); + (inclbincolvub ? (complementvub ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol)) : 0)); aggrconstant = static_cast( HighsCDouble(vals[i]) * (complementvub ? bestVub[col].second.constant + bestVub[col].second.coef - : bestVub[col].second.constant)); + : bestVub[col].second.constant)) + (complementvub && inclbincolvub ? vectorsum.getValue(vbcol) : 0); if (vals[i] >= 0) { addSNFRentry(vbcol, col, complementvub ? 1 - lpSolution.col_value[vbcol] : lpSolution.col_value[vbcol], substsolval, 1, vbcoef, -aggrconstant, - inclbincolvub ? vectorsum.getValue(vbcol) : 0, vals[i], + inclbincolvub ? (complementvub ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol)) : 0, vals[i], complementvub); } else { addSNFRentry(vbcol, col, complementvub ? 1 - lpSolution.col_value[vbcol] : lpSolution.col_value[vbcol], -substsolval, -1, -vbcoef, aggrconstant, - inclbincolvub ? -vectorsum.getValue(vbcol) : 0, + inclbincolvub ? (complementvub ? vectorsum.getValue(vbcol) : -vectorsum.getValue(vbcol)) : 0, -vals[i], complementvub); } if (inclbincolvub) vectorsum.values[vbcol] = 0; From a5dc18aa3f88f7f5c3a0759ddaf96dbec51a1475 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 22 Aug 2025 20:36:38 +0200 Subject: [PATCH 30/64] Fix error with complementation --- highs/mip/HighsCutGeneration.cpp | 2 +- highs/mip/HighsTransformedLp.cpp | 133 +++++++++++++++---------------- 2 files changed, 64 insertions(+), 71 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index c55c4f3b36..b0c272058e 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -682,7 +682,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { vals[rowlen] = -liftedbincoef; inds[rowlen] = snfr.origBinCols[i]; if (snfr.complementation[i]) { - tmpRhs -= -liftedbincoef; + tmpRhs -= vals[rowlen]; vals[rowlen] = -vals[rowlen]; } rowlen++; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 6529e767ef..86f6b37b49 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -665,62 +665,57 @@ bool HighsTransformedLp::transformSNFRelaxation( return false; } } + const double sign = coef >= 0 ? 1 : -1; - if (complement) origbincoef = -origbincoef; + double complorigbincoef = complement ? -origbincoef : origbincoef; + double vbconstant = complement ? vb.constant + vb.coef : vb.constant; + double vbcoef = complement ? -vb.coef : vb.coef; + double val; + if (isVub) { - double val = complement ? sign * (coef * -vb.coef + origbincoef) - : sign * (coef * vb.coef + origbincoef); + val = sign * (coef * vbcoef + (sign >= 0 ? std::min(0.0, complorigbincoef) : std::max(0.0, complorigbincoef))); if (val > kHighsInf) return false; if (val < 0) { - val = complement ? sign * (coef * -vb.coef) : sign * (coef * vb.coef); + val = sign * (coef * vbcoef); if (val < 0) return false; inclbincoef = false; } if (inclbincoef) { - val = complement - ? sign * (coef * (lb - (vb.constant + vb.coef)) + origbincoef) - : sign * (coef * (lb - vb.constant) + origbincoef); + val = sign * (coef * (lb - vbconstant) + (sign >= 0 ? std::min(0.0, origbincoef) : std::max(0.0, origbincoef))); if (val < 0) { - val = complement ? sign * (coef * (lb - (vb.constant + vb.coef))) - : sign * (coef * (lb - vb.constant)); + val = sign * (coef * (lb - vbconstant)); if (val < 0) return false; inclbincoef = false; - val = complement ? sign * (coef * -vb.coef) : sign * (coef * vb.coef); + val = sign * (coef * vbcoef); if (val < 0) return false; } } else { - val = complement ? sign * (coef * (lb - (vb.constant + vb.coef))) - : sign * (coef * (lb - vb.constant)); + val = sign * (coef * (lb - vbconstant)); if (val < 0) return false; } } else { - double val = complement ? sign * ((coef * -vb.coef) + origbincoef) - : sign * ((coef * vb.coef) + origbincoef); + val = sign * (coef * vbcoef + (sign >= 0 ? std::max(0.0, complorigbincoef) : std::min(0.0, complorigbincoef))); if (-val > kHighsInf) return false; if (val > 0) { - val = complement ? sign * (coef * -vb.coef) : sign * (coef * vb.coef); + val = sign * (coef * vbcoef); if (val > 0) return false; inclbincoef = false; } if (inclbincoef) { - val = - complement - ? sign * ((coef * (ub - (vb.constant + vb.coef))) + origbincoef) - : sign * ((coef * (ub - vb.constant)) + origbincoef); + val = sign * (coef * (ub - vbconstant) + (sign >= 0 ? std::max(0.0, origbincoef) : std::min(0.0, origbincoef))); if (val > 0) { - val = complement ? sign * (coef * (ub - (vb.constant + vb.coef))) - : sign * (coef * (ub - vb.constant)); + val = sign * (coef * (ub - vbconstant)); if (val > 0) return false; inclbincoef = false; - val = complement ? sign * (coef * -vb.coef) : sign * (coef * vb.coef); + val = sign * (coef * vbcoef); if (val > 0) return false; } } else { - val = complement ? sign * (coef * (ub - (vb.constant + vb.coef))) - : sign * (coef * (ub - vb.constant)); + val = sign * (coef * (ub - vbconstant)); if (val > 0) return false; } } + return true; }; @@ -864,9 +859,13 @@ bool HighsTransformedLp::transformSNFRelaxation( boundType = BoundType::kSimpleUb; } + double vbconstant; double vbcoef; double substsolval; + double aggrvbcoef; double aggrconstant; + double binsolval; + double bincoef; HighsInt vbcol; switch (boundType) { case BoundType::kSimpleLb: @@ -876,13 +875,13 @@ bool HighsTransformedLp::transformSNFRelaxation( // rhs -= a_j * u_j substsolval = static_cast( vals[i] * (HighsCDouble(getLpSolution(col)) - ub)); - vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); + aggrvbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * ub); if (vals[i] >= 0) { - addSNFRentry(-1, col, 1.0, -substsolval, -1, vbcoef, aggrconstant, + addSNFRentry(-1, col, 1.0, -substsolval, -1, aggrvbcoef, aggrconstant, 0, -vals[i], false); } else { - addSNFRentry(-1, col, 1, substsolval, 1, -vbcoef, -aggrconstant, 0, + addSNFRentry(-1, col, 1, substsolval, 1, -aggrvbcoef, -aggrconstant, 0, vals[i], false); } tmpSnfrRhs -= aggrconstant; @@ -895,13 +894,13 @@ bool HighsTransformedLp::transformSNFRelaxation( // rhs -= a_j * l_j substsolval = static_cast( vals[i] * (HighsCDouble(getLpSolution(col)) - lb)); - vbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); + aggrvbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * lb); if (vals[i] >= 0) { - addSNFRentry(-1, col, 1, substsolval, 1, vbcoef, -aggrconstant, 0, + addSNFRentry(-1, col, 1, substsolval, 1, aggrvbcoef, -aggrconstant, 0, vals[i], false); } else { - addSNFRentry(-1, col, 1, -substsolval, -1, -vbcoef, aggrconstant, 0, + addSNFRentry(-1, col, 1, -substsolval, -1, -aggrvbcoef, aggrconstant, 0, -vals[i], false); } tmpSnfrRhs -= aggrconstant; @@ -915,34 +914,31 @@ bool HighsTransformedLp::transformSNFRelaxation( // 0 <= y'_j <= (a_j l'_j + c_j)x_j // rhs -= a_j * d_j vbcol = bestVlb[col].first; + vbconstant = bestVlb[col].second.constant + (complementvlb ? bestVlb[col].second.coef : 0); + vbcoef = complementvlb ? -bestVlb[col].second.coef : bestVlb[col].second.coef; + bincoef = inclbincolvlb ? complementvlb ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol) : 0; + binsolval = lpSolution.col_value[vbcol]; + if (complementvlb) binsolval = 1 - binsolval; substsolval = static_cast(vals[i] * (HighsCDouble(getLpSolution(col)) - - bestVlb[col].second.constant) + - (HighsCDouble(lpSolution.col_value[vbcol]) * - vectorsum.getValue(vbcol))); - vbcoef = static_cast( - HighsCDouble(vals[i]) * (complementvlb - ? -bestVlb[col].second.coef - : bestVlb[col].second.coef) + - (inclbincolvlb ? (complementvlb ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol)) : 0)); + vbconstant) + + (HighsCDouble(binsolval) * + bincoef)); + aggrvbcoef = static_cast( + HighsCDouble(vals[i]) * vbcoef + bincoef); aggrconstant = static_cast( - HighsCDouble(vals[i]) * - (complementvlb - ? bestVlb[col].second.constant + bestVlb[col].second.coef - : bestVlb[col].second.constant)) + (complementvlb && inclbincolvlb ? vectorsum.getValue(vbcol) : 0); + HighsCDouble(vals[i]) * vbconstant + (complementvlb && inclbincolvlb ? vectorsum.getValue(vbcol) : 0)); if (vals[i] >= 0) { addSNFRentry(vbcol, col, - complementvlb ? 1 - lpSolution.col_value[vbcol] - : lpSolution.col_value[vbcol], - -substsolval, -1, -vbcoef, aggrconstant, - inclbincolvlb ? (complementvlb ? vectorsum.getValue(vbcol) : -vectorsum.getValue(vbcol)) : 0, + binsolval, + -substsolval, -1, -aggrvbcoef, aggrconstant, + -bincoef, -vals[i], complementvlb); } else { addSNFRentry(vbcol, col, - complementvlb ? 1 - lpSolution.col_value[vbcol] - : lpSolution.col_value[vbcol], - substsolval, 1, vbcoef, -aggrconstant, - inclbincolvlb ? (complementvlb ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol)) : 0, vals[i], + binsolval, + substsolval, 1, aggrvbcoef, -aggrconstant, + bincoef, vals[i], complementvlb); } if (inclbincolvlb) vectorsum.values[vbcol] = 0; @@ -957,34 +953,31 @@ bool HighsTransformedLp::transformSNFRelaxation( // 0 <= y'_j <= -(a_j u'_j + c_j)x_j // rhs -= a_j * d_j vbcol = bestVub[col].first; + vbconstant = bestVub[col].second.constant + (complementvub ? bestVub[col].second.coef : 0); + vbcoef = complementvub ? -bestVub[col].second.coef : bestVub[col].second.coef; + bincoef = inclbincolvub ? complementvub ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol) : 0; + binsolval = lpSolution.col_value[vbcol]; + if (complementvub) binsolval = 1 - binsolval; substsolval = static_cast(vals[i] * (HighsCDouble(getLpSolution(col)) - - bestVub[col].second.constant) + - (HighsCDouble(lpSolution.col_value[vbcol]) * - vectorsum.getValue(vbcol))); - vbcoef = static_cast( - HighsCDouble(vals[i]) * (complementvub - ? -bestVub[col].second.coef - : bestVub[col].second.coef) + - (inclbincolvub ? (complementvub ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol)) : 0)); + vbconstant) + + (HighsCDouble(binsolval) * + bincoef)); + aggrvbcoef = static_cast( + HighsCDouble(vals[i]) * vbcoef + bincoef); aggrconstant = static_cast( - HighsCDouble(vals[i]) * - (complementvub - ? bestVub[col].second.constant + bestVub[col].second.coef - : bestVub[col].second.constant)) + (complementvub && inclbincolvub ? vectorsum.getValue(vbcol) : 0); + HighsCDouble(vals[i]) * vbconstant + (complementvub && inclbincolvub ? vectorsum.getValue(vbcol) : 0)); if (vals[i] >= 0) { addSNFRentry(vbcol, col, - complementvub ? 1 - lpSolution.col_value[vbcol] - : lpSolution.col_value[vbcol], - substsolval, 1, vbcoef, -aggrconstant, - inclbincolvub ? (complementvub ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol)) : 0, vals[i], + binsolval, + substsolval, 1, aggrvbcoef, -aggrconstant, + bincoef, vals[i], complementvub); } else { addSNFRentry(vbcol, col, - complementvub ? 1 - lpSolution.col_value[vbcol] - : lpSolution.col_value[vbcol], - -substsolval, -1, -vbcoef, aggrconstant, - inclbincolvub ? (complementvub ? vectorsum.getValue(vbcol) : -vectorsum.getValue(vbcol)) : 0, + binsolval, + -substsolval, -1, -aggrvbcoef, aggrconstant, + -bincoef, -vals[i], complementvub); } if (inclbincolvub) vectorsum.values[vbcol] = 0; From 388861256cb802fa2c17691471925150658edb75 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Sat, 23 Aug 2025 00:53:32 +0200 Subject: [PATCH 31/64] Fix vb validity check --- highs/mip/HighsTransformedLp.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 86f6b37b49..0573943f4c 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -673,7 +673,7 @@ bool HighsTransformedLp::transformSNFRelaxation( double val; if (isVub) { - val = sign * (coef * vbcoef + (sign >= 0 ? std::min(0.0, complorigbincoef) : std::max(0.0, complorigbincoef))); + val = sign * (coef * vbcoef + complorigbincoef); if (val > kHighsInf) return false; if (val < 0) { val = sign * (coef * vbcoef); @@ -694,7 +694,7 @@ bool HighsTransformedLp::transformSNFRelaxation( if (val < 0) return false; } } else { - val = sign * (coef * vbcoef + (sign >= 0 ? std::max(0.0, complorigbincoef) : std::min(0.0, complorigbincoef))); + val = sign * (coef * vbcoef + complorigbincoef); if (-val > kHighsInf) return false; if (val > 0) { val = sign * (coef * vbcoef); @@ -981,6 +981,7 @@ bool HighsTransformedLp::transformSNFRelaxation( -vals[i], complementvub); } if (inclbincolvub) vectorsum.values[vbcol] = 0; + // TODO: CHECK IF THE RHS CHANGE SHOULD MATCH THE AGGRCONSTANT tmpSnfrRhs -= aggrconstant; break; } From 6f4ee6bb58ad66ca67e248b7271332442a759db5 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Sat, 23 Aug 2025 09:31:49 +0200 Subject: [PATCH 32/64] Fix error in aggrconstant --- highs/mip/HighsTransformedLp.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 0573943f4c..21892ce926 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -927,7 +927,7 @@ bool HighsTransformedLp::transformSNFRelaxation( aggrvbcoef = static_cast( HighsCDouble(vals[i]) * vbcoef + bincoef); aggrconstant = static_cast( - HighsCDouble(vals[i]) * vbconstant + (complementvlb && inclbincolvlb ? vectorsum.getValue(vbcol) : 0)); + HighsCDouble(vals[i]) * vbconstant); if (vals[i] >= 0) { addSNFRentry(vbcol, col, binsolval, @@ -941,8 +941,8 @@ bool HighsTransformedLp::transformSNFRelaxation( bincoef, vals[i], complementvlb); } + tmpSnfrRhs -= aggrconstant + (complementvlb && inclbincolvlb ? vectorsum.getValue(vbcol) : 0); if (inclbincolvlb) vectorsum.values[vbcol] = 0; - tmpSnfrRhs -= aggrconstant; break; case BoundType::kVariableUb: // vub: y_j <= u'_j x_j + d_j. c_j is the coefficient of x_j in row @@ -966,7 +966,7 @@ bool HighsTransformedLp::transformSNFRelaxation( aggrvbcoef = static_cast( HighsCDouble(vals[i]) * vbcoef + bincoef); aggrconstant = static_cast( - HighsCDouble(vals[i]) * vbconstant + (complementvub && inclbincolvub ? vectorsum.getValue(vbcol) : 0)); + HighsCDouble(vals[i]) * vbconstant); if (vals[i] >= 0) { addSNFRentry(vbcol, col, binsolval, @@ -980,9 +980,8 @@ bool HighsTransformedLp::transformSNFRelaxation( -bincoef, -vals[i], complementvub); } + tmpSnfrRhs -= aggrconstant + (complementvub && inclbincolvub ? vectorsum.getValue(vbcol) : 0); if (inclbincolvub) vectorsum.values[vbcol] = 0; - // TODO: CHECK IF THE RHS CHANGE SHOULD MATCH THE AGGRCONSTANT - tmpSnfrRhs -= aggrconstant; break; } } From ec6d5973fd92c32e99a2cfaaae542c231119f37d Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 25 Aug 2025 14:42:10 +0200 Subject: [PATCH 33/64] Change back way validity of cb was checked --- highs/mip/HighsTransformedLp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 21892ce926..afa579549a 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -681,7 +681,7 @@ bool HighsTransformedLp::transformSNFRelaxation( inclbincoef = false; } if (inclbincoef) { - val = sign * (coef * (lb - vbconstant) + (sign >= 0 ? std::min(0.0, origbincoef) : std::max(0.0, origbincoef))); + val = sign * (coef * (lb - vbconstant) + complorigbincoef); if (val < 0) { val = sign * (coef * (lb - vbconstant)); if (val < 0) return false; @@ -702,7 +702,7 @@ bool HighsTransformedLp::transformSNFRelaxation( inclbincoef = false; } if (inclbincoef) { - val = sign * (coef * (ub - vbconstant) + (sign >= 0 ? std::max(0.0, origbincoef) : std::min(0.0, origbincoef))); + val = sign * (coef * (ub - vbconstant) + complorigbincoef); if (val > 0) { val = sign * (coef * (ub - vbconstant)); if (val > 0) return false; From e2069d781a3f6d27b2ae1651c7cfd7d7291151c6 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 25 Aug 2025 17:00:39 +0200 Subject: [PATCH 34/64] Remove unused contsolval --- highs/mip/HighsCutGeneration.cpp | 1 - highs/mip/HighsCutGeneration.h | 11 ++- highs/mip/HighsTransformedLp.cpp | 148 ++++++++++++++----------------- 3 files changed, 71 insertions(+), 89 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index b0c272058e..62c60b30df 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1588,7 +1588,6 @@ void HighsCutGeneration::initSNFRelaxation() { snfr.origBinCols.resize(rowlen); snfr.origContCols.resize(rowlen); snfr.binSolval.resize(rowlen); - snfr.contSolval.resize(rowlen); snfr.coef.resize(rowlen); snfr.vubCoef.resize(rowlen); snfr.aggrConstant.resize(rowlen); diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index 582e2ffd24..6263044be9 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -122,16 +122,15 @@ class HighsCutGeneration { std::vector coef; // (+-1) coefficient of col in SNFR std::vector vubCoef; // u_j in y'_j <= u_j x_j in SNFR std::vector binSolval; // lp[x_j], y'_j <= u_j x_j in SNFR - std::vector contSolval; // lp[y'_j] in y'_j <= u_j x_j in SNFR std::vector origBinCols; // orig x_i, y'_j <= u_j x_j in SNFR std::vector origContCols; // orig y_i used to make y'_j in SNFR - std::vector aggrBinCoef; // c_i row coef of x_i in orig aggrrow - std::vector aggrContCoef; // a_i row coef of y_i in orig aggrrow - std::vector aggrConstant; // constant shift used in SNFR transform - std::vector complementation; // was the original bincol complemented + std::vector aggrBinCoef; // coef of x_i in y'_j aggregation + std::vector aggrContCoef; // coef of y_i in y'_j aggrregation + std::vector aggrConstant; // constant shift in y'_j aggregation + std::vector complementation; // was the original bincol complemented std::vector - flowCoverStatus; // (+1) in f-cover (-1) notin f-cover + flowCoverStatus; // (+1) in flow cover (-1) notin flow cover double rhs; // in \sum_{j \in N+} y'_j - \sum_{j \in N-} y'_j <= b double lambda; // in sum_{j in C+} u_j - sum_{j in C-} u_j = b + lambda }; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index afa579549a..b43d04a159 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -719,25 +719,24 @@ bool HighsTransformedLp::transformSNFRelaxation( return true; }; - auto addSNFRentry = - [&](HighsInt origbincol, HighsInt origcontcol, double binsolval, - double contsolval, HighsInt coef, double vubcoef, double aggrconstant, - double aggrbincoef, double aggrcontcoef, bool complement) { - assert(binsolval >= -lprelaxation.getMipSolver().mipdata_->feastol && - binsolval <= 1 + lprelaxation.getMipSolver().mipdata_->feastol); - assert(vubcoef >= -1e-10); - snfr.origBinCols[snfr.numNnzs] = origbincol; - snfr.origContCols[snfr.numNnzs] = origcontcol; - snfr.binSolval[snfr.numNnzs] = binsolval; - snfr.contSolval[snfr.numNnzs] = contsolval; - snfr.coef[snfr.numNnzs] = coef; - snfr.vubCoef[snfr.numNnzs] = std::max(vubcoef, 0.0); - snfr.aggrConstant[snfr.numNnzs] = aggrconstant; - snfr.aggrBinCoef[snfr.numNnzs] = aggrbincoef; - snfr.aggrContCoef[snfr.numNnzs] = aggrcontcoef; - snfr.complementation[snfr.numNnzs] = complement; - snfr.numNnzs++; - }; + auto addSNFRentry = [&](HighsInt origbincol, HighsInt origcontcol, + double binsolval, HighsInt coef, double vubcoef, + double aggrconstant, double aggrbincoef, + double aggrcontcoef, bool complement) { + assert(binsolval >= -lprelaxation.getMipSolver().mipdata_->feastol && + binsolval <= 1 + lprelaxation.getMipSolver().mipdata_->feastol); + assert(vubcoef >= -1e-10); + snfr.origBinCols[snfr.numNnzs] = origbincol; + snfr.origContCols[snfr.numNnzs] = origcontcol; + snfr.binSolval[snfr.numNnzs] = binsolval; + snfr.coef[snfr.numNnzs] = coef; + snfr.vubCoef[snfr.numNnzs] = std::max(vubcoef, 0.0); + snfr.aggrConstant[snfr.numNnzs] = aggrconstant; + snfr.aggrBinCoef[snfr.numNnzs] = aggrbincoef; + snfr.aggrContCoef[snfr.numNnzs] = aggrcontcoef; + snfr.complementation[snfr.numNnzs] = complement; + snfr.numNnzs++; + }; // Place the non-binary columns to the front (all general ints relaxed) // Use vectorsum to track original row coefficients of binary columns. @@ -815,11 +814,11 @@ bool HighsTransformedLp::transformSNFRelaxation( if (colIsBinary(col, lb, ub)) { // Binary columns can be added directly to the SNFR if (vals[i] >= 0) { - addSNFRentry(col, -1, getLpSolution(col), getLpSolution(col) * vals[i], - 1, vals[i], 0, vectorsum.getValue(col), 0, false); + addSNFRentry(col, -1, getLpSolution(col), 1, vals[i], 0, + vectorsum.getValue(col), 0, false); } else { - addSNFRentry(col, -1, getLpSolution(col), -getLpSolution(col) * vals[i], - -1, -vals[i], 0, -vectorsum.getValue(col), 0, false); + addSNFRentry(col, -1, getLpSolution(col), -1, -vals[i], 0, + -vectorsum.getValue(col), 0, false); } } else { // Decide whether to use {simple, variable} {lower, upper} bound @@ -861,7 +860,6 @@ bool HighsTransformedLp::transformSNFRelaxation( double vbconstant; double vbcoef; - double substsolval; double aggrvbcoef; double aggrconstant; double binsolval; @@ -873,16 +871,14 @@ bool HighsTransformedLp::transformSNFRelaxation( // (1) y'_j = -a_j(y_j - u_j), 0 <= y'_j <= a_j(u_j - l_j)x_j, x_j = 1 // (2) y'_j = a_j(y_j - u_j), 0 <= y'_j <= -a_j(u_j - l_j)x_j, x_j = 1 // rhs -= a_j * u_j - substsolval = static_cast( - vals[i] * (HighsCDouble(getLpSolution(col)) - ub)); aggrvbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * ub); if (vals[i] >= 0) { - addSNFRentry(-1, col, 1.0, -substsolval, -1, aggrvbcoef, aggrconstant, - 0, -vals[i], false); + addSNFRentry(-1, col, 1.0, -1, aggrvbcoef, aggrconstant, 0, + -vals[i], false); } else { - addSNFRentry(-1, col, 1, substsolval, 1, -aggrvbcoef, -aggrconstant, 0, - vals[i], false); + addSNFRentry(-1, col, 1, 1, -aggrvbcoef, -aggrconstant, 0, vals[i], + false); } tmpSnfrRhs -= aggrconstant; break; @@ -892,16 +888,14 @@ bool HighsTransformedLp::transformSNFRelaxation( // (2) y'_j = -a_j(y_j - l_j), 0 <= y'_j <= -a_j(u_j - l_j)x_j, // x_j = 1 // rhs -= a_j * l_j - substsolval = static_cast( - vals[i] * (HighsCDouble(getLpSolution(col)) - lb)); aggrvbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * lb); if (vals[i] >= 0) { - addSNFRentry(-1, col, 1, substsolval, 1, aggrvbcoef, -aggrconstant, 0, - vals[i], false); + addSNFRentry(-1, col, 1, 1, aggrvbcoef, -aggrconstant, 0, vals[i], + false); } else { - addSNFRentry(-1, col, 1, -substsolval, -1, -aggrvbcoef, aggrconstant, 0, - -vals[i], false); + addSNFRentry(-1, col, 1, -1, -aggrvbcoef, aggrconstant, 0, -vals[i], + false); } tmpSnfrRhs -= aggrconstant; break; @@ -914,34 +908,29 @@ bool HighsTransformedLp::transformSNFRelaxation( // 0 <= y'_j <= (a_j l'_j + c_j)x_j // rhs -= a_j * d_j vbcol = bestVlb[col].first; - vbconstant = bestVlb[col].second.constant + (complementvlb ? bestVlb[col].second.coef : 0); - vbcoef = complementvlb ? -bestVlb[col].second.coef : bestVlb[col].second.coef; - bincoef = inclbincolvlb ? complementvlb ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol) : 0; + vbconstant = bestVlb[col].second.constant + + (complementvlb ? bestVlb[col].second.coef : 0); + vbcoef = complementvlb ? -bestVlb[col].second.coef + : bestVlb[col].second.coef; + bincoef = inclbincolvlb ? complementvlb ? -vectorsum.getValue(vbcol) + : vectorsum.getValue(vbcol) + : 0; binsolval = lpSolution.col_value[vbcol]; if (complementvlb) binsolval = 1 - binsolval; - substsolval = - static_cast(vals[i] * (HighsCDouble(getLpSolution(col)) - - vbconstant) + - (HighsCDouble(binsolval) * - bincoef)); - aggrvbcoef = static_cast( - HighsCDouble(vals[i]) * vbcoef + bincoef); - aggrconstant = static_cast( - HighsCDouble(vals[i]) * vbconstant); + aggrvbcoef = + static_cast(HighsCDouble(vals[i]) * vbcoef + bincoef); + aggrconstant = + static_cast(HighsCDouble(vals[i]) * vbconstant); if (vals[i] >= 0) { - addSNFRentry(vbcol, col, - binsolval, - -substsolval, -1, -aggrvbcoef, aggrconstant, - -bincoef, - -vals[i], complementvlb); + addSNFRentry(vbcol, col, binsolval, -1, -aggrvbcoef, aggrconstant, + -bincoef, -vals[i], complementvlb); } else { - addSNFRentry(vbcol, col, - binsolval, - substsolval, 1, aggrvbcoef, -aggrconstant, - bincoef, vals[i], - complementvlb); + addSNFRentry(vbcol, col, binsolval, 1, aggrvbcoef, -aggrconstant, + bincoef, vals[i], complementvlb); } - tmpSnfrRhs -= aggrconstant + (complementvlb && inclbincolvlb ? vectorsum.getValue(vbcol) : 0); + tmpSnfrRhs -= + aggrconstant + + (complementvlb && inclbincolvlb ? vectorsum.getValue(vbcol) : 0); if (inclbincolvlb) vectorsum.values[vbcol] = 0; break; case BoundType::kVariableUb: @@ -953,34 +942,29 @@ bool HighsTransformedLp::transformSNFRelaxation( // 0 <= y'_j <= -(a_j u'_j + c_j)x_j // rhs -= a_j * d_j vbcol = bestVub[col].first; - vbconstant = bestVub[col].second.constant + (complementvub ? bestVub[col].second.coef : 0); - vbcoef = complementvub ? -bestVub[col].second.coef : bestVub[col].second.coef; - bincoef = inclbincolvub ? complementvub ? -vectorsum.getValue(vbcol) : vectorsum.getValue(vbcol) : 0; + vbconstant = bestVub[col].second.constant + + (complementvub ? bestVub[col].second.coef : 0); + vbcoef = complementvub ? -bestVub[col].second.coef + : bestVub[col].second.coef; + bincoef = inclbincolvub ? complementvub ? -vectorsum.getValue(vbcol) + : vectorsum.getValue(vbcol) + : 0; binsolval = lpSolution.col_value[vbcol]; if (complementvub) binsolval = 1 - binsolval; - substsolval = - static_cast(vals[i] * (HighsCDouble(getLpSolution(col)) - - vbconstant) + - (HighsCDouble(binsolval) * - bincoef)); - aggrvbcoef = static_cast( - HighsCDouble(vals[i]) * vbcoef + bincoef); - aggrconstant = static_cast( - HighsCDouble(vals[i]) * vbconstant); + aggrvbcoef = + static_cast(HighsCDouble(vals[i]) * vbcoef + bincoef); + aggrconstant = + static_cast(HighsCDouble(vals[i]) * vbconstant); if (vals[i] >= 0) { - addSNFRentry(vbcol, col, - binsolval, - substsolval, 1, aggrvbcoef, -aggrconstant, - bincoef, vals[i], - complementvub); + addSNFRentry(vbcol, col, binsolval, 1, aggrvbcoef, -aggrconstant, + bincoef, vals[i], complementvub); } else { - addSNFRentry(vbcol, col, - binsolval, - -substsolval, -1, -aggrvbcoef, aggrconstant, - -bincoef, - -vals[i], complementvub); + addSNFRentry(vbcol, col, binsolval, -1, -aggrvbcoef, aggrconstant, + -bincoef, -vals[i], complementvub); } - tmpSnfrRhs -= aggrconstant + (complementvub && inclbincolvub ? vectorsum.getValue(vbcol) : 0); + tmpSnfrRhs -= + aggrconstant + + (complementvub && inclbincolvub ? vectorsum.getValue(vbcol) : 0); if (inclbincolvub) vectorsum.values[vbcol] = 0; break; } From 1b97c0c4a1adcd238bf87da60fc88e7cdddd3135 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 26 Aug 2025 12:34:16 +0200 Subject: [PATCH 35/64] Minor performance improvements. More comments --- highs/mip/HighsCutGeneration.cpp | 48 ++++++++++++++++++++++---------- highs/mip/HighsTransformedLp.cpp | 2 ++ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 62c60b30df..a9b5dfce4a 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -558,7 +558,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { ld.d1 = sumC2 + snfr.rhs; pdqsort_branchless( - ld.m.begin(), ld.m.end(), + ld.m.begin(), ld.m.begin() + ld.r, [&](const HighsCDouble a, const HighsCDouble b) { return a > b; }); for (HighsInt i = 0; i != ld.r + 1; ++i) { @@ -1366,7 +1366,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, std::vector flowCoverInds; double flowCoverRhs = rhs_; double flowCoverEfficacy = 0; - if (genFlowCover) { + if (genFlowCover && !lpRelaxation.getMipSolver().submip) { flowCoverVals = vals_; flowCoverInds = inds_; flowCoverSuccess = tryGenerateFlowCoverCut( @@ -1610,8 +1610,10 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { // reject base inequalities where that is not possible due to unbounded // variables // 4. Don't consider any inequality with too many non-zeros + // 5. Don't consider any inequality with too few continuous cols HighsInt numZeros = 0; + HighsInt numCont = 0; double maxact = -feastol; double maxAbsVal = 0; HighsInt slackOffset = lpRelaxation.getMipSolver().numCol(); @@ -1628,40 +1630,47 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { }; for (HighsInt i = 0; i < rowlen; ++i) { + HighsInt col = inds[i]; + double lb = getLb(col); + double ub = getUb(col); maxAbsVal = std::max(std::abs(vals[i]), maxAbsVal); - if (getLb(inds[i]) == -kHighsInf || getUb(inds[i]) == kHighsInf) { + if (lb == -kHighsInf || ub == kHighsInf) { return false; } + if (!lpRelaxation.isColIntegral(col)) numCont++; } + if (numCont <= 1) return false; scale(maxAbsVal); for (HighsInt i = 0; i != rowlen; ++i) { HighsInt col = inds[i]; + double lb = getLb(col); + double ub = getUb(col); // relax variables with small contributions if possible - if (std::abs(vals[i]) * (getUb(col) - getLb(col)) <= 10 * feastol) { + if (std::abs(vals[i]) * (ub - lb) <= 10 * feastol) { if (vals[i] < 0) { - if (getUb(col) == kHighsInf) return false; - rhs -= vals[i] * getUb(col); + if (ub == kHighsInf) return false; + rhs -= vals[i] * ub; ++numZeros; vals[i] = 0.0; } else if (vals[i] > 0) { - if (getLb(col) == -kHighsInf) return false; - rhs -= vals[i] * getLb(col); + if (lb == -kHighsInf) return false; + rhs -= vals[i] * lb; ++numZeros; vals[i] = 0.0; } } if (vals[i] > 0) { - maxact += vals[i] * getUb(col); + maxact += vals[i] * ub; } else if (vals[i] < 0) { - maxact += vals[i] * getLb(col); + maxact += vals[i] * lb; } } - HighsInt maxLen = 100 + 0.15 * (lpRelaxation.numCols()); + HighsInt maxLen = 0.15 * (lpRelaxation.numCols()); if (rowlen - numZeros > maxLen) return false; if (numZeros != 0) { @@ -1737,22 +1746,27 @@ bool HighsCutGeneration::computeFlowCover() { if (capacity < feastol) return false; // Solve a knapsack greedily to assign items to C+, C-, N+\C+, N-\C- + // z_j is a binary variable deciding whether the item goes into the knapsack + // max sum_{j in N+} ( x_j - 1 ) z_j + sum_{j in N-} x_j + // sum_{j in N+} u'_j z_j + sum_{j in N-} u'_j z_j < -b + sum_{j in N+} u'_j + // z_j in {0,1} for all j in N+ & N- + double knapsackWeight = 0; std::vector weights(nitems); - std::vector profits(nitems); + std::vector profitweightratios(nitems); std::vector perm(nitems); std::iota(perm.begin(), perm.end(), 0); for (HighsInt i = 0; i < nitems; ++i) { weights[i] = snfr.vubCoef[items[i]]; if (snfr.coef[items[i]] == 1) { - profits[i] = 1 - snfr.binSolval[items[i]]; + profitweightratios[i] = (1 - snfr.binSolval[items[i]]) / weights[i]; } else { - profits[i] = snfr.binSolval[items[i]]; + profitweightratios[i] = snfr.binSolval[items[i]] / weights[i]; } } pdqsort_branchless(perm.begin(), perm.end(), [&](const HighsInt a, const HighsInt b) { - return profits[a] / weights[a] > profits[b] / weights[b]; + return profitweightratios[a] > profitweightratios[b]; }); // Greedily add items to knapsack for (HighsInt i = 0; i < nitems; ++i) { @@ -1761,19 +1775,23 @@ bool HighsCutGeneration::computeFlowCover() { if (knapsackWeight + weights[j] < capacity) { knapsackWeight += weights[j]; if (snfr.coef[k] == 1) { + // j in N+ with z_j = 1 => j in N+ \ C+ snfr.flowCoverStatus[k] = -1; nNonFlowCover++; } else { + // j in N- with z_j = 1 => j in C- snfr.flowCoverStatus[k] = 1; nFlowCover++; flowCoverWeight -= snfr.vubCoef[k]; } } else { if (snfr.coef[k] == 1) { + // j in N+ with z_j = 0 => j in C+ snfr.flowCoverStatus[k] = 1; nFlowCover++; flowCoverWeight += snfr.vubCoef[k]; } else { + // j in N- with z_j = 0 => j in N- \ C- snfr.flowCoverStatus[k] = -1; nNonFlowCover++; } diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index b43d04a159..90c99117fd 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -647,6 +647,8 @@ bool HighsTransformedLp::transformSNFRelaxation( // (2) a_j (l_j - d_j) + c_j <= 0 // (3) a_j u'_j + c_j <= 0 // Note: c_j may change during transform if two columns use same bin col + // complement: Should x_j be replaced by x'_j = (1 - x_j) + // inclbincoef: Should c_j be used and included in the aggregation if (bincol == -1) return false; if (abs(vb.coef) >= 1e+6) return false; complement = false; From ee761ddebddb5f2855faaa460a0b37959d61f52b Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 26 Aug 2025 15:10:45 +0200 Subject: [PATCH 36/64] More minor code improvements --- highs/mip/HighsCutGeneration.cpp | 14 +++++++------- highs/mip/HighsTransformedLp.cpp | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index a9b5dfce4a..92623fa83e 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -507,12 +507,12 @@ bool HighsCutGeneration::separateLiftedMixedIntegerCover() { bool HighsCutGeneration::separateLiftedFlowCover() { // Compute the lifting data (ld) first struct LiftingData { - std::vector m; + std::vector m; std::vector M; HighsInt r = 0; HighsInt t = 0; double mp = kHighsInf; - HighsCDouble ml = 0; + double ml = 0; HighsCDouble d1 = 0; }; LiftingData ld; @@ -559,7 +559,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { pdqsort_branchless( ld.m.begin(), ld.m.begin() + ld.r, - [&](const HighsCDouble a, const HighsCDouble b) { return a > b; }); + [&](const double a, const double b) { return a > b; }); for (HighsInt i = 0; i != ld.r + 1; ++i) { if (i == 0) { @@ -626,7 +626,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { snfr.lambda + vubcoef <= ld.M[i] + ld.ml + feastol + std::max(0.0, static_cast( - ld.m[i] - (ld.mp - snfr.lambda) - ld.ml))); + ld.m[i] - (HighsCDouble(ld.mp) - snfr.lambda) - ld.ml))); return static_cast(i * HighsCDouble(snfr.lambda) + vubcoef - ld.M[i]); } @@ -1613,7 +1613,7 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { // 5. Don't consider any inequality with too few continuous cols HighsInt numZeros = 0; - HighsInt numCont = 0; + HighsInt numContCols = 0; double maxact = -feastol; double maxAbsVal = 0; HighsInt slackOffset = lpRelaxation.getMipSolver().numCol(); @@ -1637,10 +1637,10 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { if (lb == -kHighsInf || ub == kHighsInf) { return false; } - if (!lpRelaxation.isColIntegral(col)) numCont++; + if (col < slackOffset && !lpRelaxation.isColIntegral(col)) numContCols++; } - if (numCont <= 1) return false; + if (numContCols == 0) return false; scale(maxAbsVal); for (HighsInt i = 0; i != rowlen; ++i) { diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 90c99117fd..1e02818f3b 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -772,7 +772,7 @@ bool HighsTransformedLp::transformSNFRelaxation( } // We are using vectorsum to keep track of the binary coefficients - if (colIsBinary(col, lb, ub) && vectorsum.getValue(col) == 0) { + if (i >= numNz - numBinCols && vectorsum.getValue(col) == 0) { ++i; continue; } @@ -813,7 +813,7 @@ bool HighsTransformedLp::transformSNFRelaxation( } // Transform entry into the SNFR - if (colIsBinary(col, lb, ub)) { + if (i >= numNz - numBinCols) { // Binary columns can be added directly to the SNFR if (vals[i] >= 0) { addSNFRentry(col, -1, getLpSolution(col), 1, vals[i], 0, From 3102326852c885548d8d7089b0aa045eff4d6601 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 26 Aug 2025 17:34:39 +0200 Subject: [PATCH 37/64] Only run when vb has been used --- highs/mip/HighsTransformedLp.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 1e02818f3b..8ef717e873 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -757,6 +757,7 @@ bool HighsTransformedLp::transformSNFRelaxation( ++i; } + HighsInt numVbUsed = 0; i = 0; while (i < numNz) { HighsInt col = inds[i]; @@ -909,6 +910,7 @@ bool HighsTransformedLp::transformSNFRelaxation( // (2) y'_j = a_j(y_j - d_j) + c_j * x_j, // 0 <= y'_j <= (a_j l'_j + c_j)x_j // rhs -= a_j * d_j + numVbUsed++; vbcol = bestVlb[col].first; vbconstant = bestVlb[col].second.constant + (complementvlb ? bestVlb[col].second.coef : 0); @@ -943,6 +945,7 @@ bool HighsTransformedLp::transformSNFRelaxation( // (2) y'_j = -(a_j(y_j - d_j) + c_j * x_j), // 0 <= y'_j <= -(a_j u'_j + c_j)x_j // rhs -= a_j * d_j + numVbUsed++; vbcol = bestVub[col].first; vbconstant = bestVub[col].second.constant + (complementvub ? bestVub[col].second.coef : 0); @@ -977,7 +980,7 @@ bool HighsTransformedLp::transformSNFRelaxation( vectorsum.clear(); snfr.rhs = static_cast(tmpSnfrRhs); - if (numNz == 0 && rhs >= -mip.mipdata_->feastol) return false; + if (numNz == 0 || numVbUsed == 0) return false; return true; } From 44f3fae5df2a9ecf7a2c040564e2b03a6bd90c49 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 26 Aug 2025 20:04:33 +0200 Subject: [PATCH 38/64] Remove unusedvb flag. Try fcc for tableau --- highs/mip/HighsTableauSeparator.cpp | 4 ++-- highs/mip/HighsTransformedLp.cpp | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/highs/mip/HighsTableauSeparator.cpp b/highs/mip/HighsTableauSeparator.cpp index 0ebf25504e..3c2c9b168d 100644 --- a/highs/mip/HighsTableauSeparator.cpp +++ b/highs/mip/HighsTableauSeparator.cpp @@ -229,12 +229,12 @@ void HighsTableauSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, baseRowInds.size()); double rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, false); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, cutpool.getNumCuts() - numCuts >= 20); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, false); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, cutpool.getNumCuts() - numCuts >= 20); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.clear(); diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 8ef717e873..da10901f0d 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -757,7 +757,6 @@ bool HighsTransformedLp::transformSNFRelaxation( ++i; } - HighsInt numVbUsed = 0; i = 0; while (i < numNz) { HighsInt col = inds[i]; @@ -910,7 +909,6 @@ bool HighsTransformedLp::transformSNFRelaxation( // (2) y'_j = a_j(y_j - d_j) + c_j * x_j, // 0 <= y'_j <= (a_j l'_j + c_j)x_j // rhs -= a_j * d_j - numVbUsed++; vbcol = bestVlb[col].first; vbconstant = bestVlb[col].second.constant + (complementvlb ? bestVlb[col].second.coef : 0); @@ -945,7 +943,6 @@ bool HighsTransformedLp::transformSNFRelaxation( // (2) y'_j = -(a_j(y_j - d_j) + c_j * x_j), // 0 <= y'_j <= -(a_j u'_j + c_j)x_j // rhs -= a_j * d_j - numVbUsed++; vbcol = bestVub[col].first; vbconstant = bestVub[col].second.constant + (complementvub ? bestVub[col].second.coef : 0); @@ -980,7 +977,7 @@ bool HighsTransformedLp::transformSNFRelaxation( vectorsum.clear(); snfr.rhs = static_cast(tmpSnfrRhs); - if (numNz == 0 || numVbUsed == 0) return false; + if (numNz == 0) return false; return true; } From 69f9016f4d56233d7eb654440b9cec1312b9732b Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 27 Aug 2025 13:38:03 +0200 Subject: [PATCH 39/64] Skip scaling if already scaled. Add comments --- highs/mip/HighsCutGeneration.cpp | 2 +- highs/mip/HighsTableauSeparator.cpp | 4 ++-- highs/mip/HighsTransformedLp.cpp | 10 ++++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 92623fa83e..06ccb1105d 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1641,7 +1641,7 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { } if (numContCols == 0) return false; - scale(maxAbsVal); + if (maxAbsVal > 1 + feastol || maxAbsVal < 0.5 - feastol) scale(maxAbsVal); for (HighsInt i = 0; i != rowlen; ++i) { HighsInt col = inds[i]; diff --git a/highs/mip/HighsTableauSeparator.cpp b/highs/mip/HighsTableauSeparator.cpp index 3c2c9b168d..0ebf25504e 100644 --- a/highs/mip/HighsTableauSeparator.cpp +++ b/highs/mip/HighsTableauSeparator.cpp @@ -229,12 +229,12 @@ void HighsTableauSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, baseRowInds.size()); double rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, cutpool.getNumCuts() - numCuts >= 20); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, false); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, cutpool.getNumCuts() - numCuts >= 20); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, false); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.clear(); diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index da10901f0d..964bf21d0d 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -902,7 +902,8 @@ bool HighsTransformedLp::transformSNFRelaxation( tmpSnfrRhs -= aggrconstant; break; case BoundType::kVariableLb: - // vlb: l'_j x_j + d_j <= y_j. c_j is the coefficient of x_j in row + // vlb: l'_j x_j + d_j <= y_j. c_j coef of x_j in row + // if compl: -l'_j x_j + (l'_j + d_j) <= y_j. -c_j coef of x_j in row // Case (1) -> a_j > 0, y'_j -> N- Case (2) -> a_j < 0, y'_j ->N+ // (1) y'_j = -(a_j(y_j - d_j) + c_j * x_j), // 0 <= y'_j <= -(a_j l'_j + c_j)x_j @@ -932,11 +933,12 @@ bool HighsTransformedLp::transformSNFRelaxation( } tmpSnfrRhs -= aggrconstant + - (complementvlb && inclbincolvlb ? vectorsum.getValue(vbcol) : 0); + (complementvlb && inclbincolvlb ? -bincoef : 0); if (inclbincolvlb) vectorsum.values[vbcol] = 0; break; case BoundType::kVariableUb: - // vub: y_j <= u'_j x_j + d_j. c_j is the coefficient of x_j in row + // vub: y_j <= u'_j x_j + d_j. c_j coef of x_j in row + // if compl: y_j <= -u'_j x_j + (u'_j + d_j). -c_j coef of x_j in row // Case (1) -> a_j > 0, y'_j -> N+ Case (2) -> a_j < 0, y'_j ->N- // (1) y'_j = a_j(y_j - d_j) + c_j * x_j), // 0 <= y'_j <= (a_j u'_j + c_j)x_j @@ -966,7 +968,7 @@ bool HighsTransformedLp::transformSNFRelaxation( } tmpSnfrRhs -= aggrconstant + - (complementvub && inclbincolvub ? vectorsum.getValue(vbcol) : 0); + (complementvub && inclbincolvub ? -bincoef : 0); if (inclbincolvub) vectorsum.values[vbcol] = 0; break; } From abfd94bede809a3a0e12618b4b820e21ff932a10 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 27 Aug 2025 14:28:15 +0200 Subject: [PATCH 40/64] Formatting --- highs/mip/HighsCutGeneration.cpp | 28 +++++++++++++--------------- highs/mip/HighsTransformedLp.cpp | 12 +++++------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 06ccb1105d..2b93c99eb0 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -557,9 +557,8 @@ bool HighsCutGeneration::separateLiftedFlowCover() { ld.ml = std::min(snfr.lambda, static_cast(sumC1LE + sumN2mC2LE)); ld.d1 = sumC2 + snfr.rhs; - pdqsort_branchless( - ld.m.begin(), ld.m.begin() + ld.r, - [&](const double a, const double b) { return a > b; }); + pdqsort_branchless(ld.m.begin(), ld.m.begin() + ld.r, + [&](const double a, const double b) { return a > b; }); for (HighsInt i = 0; i != ld.r + 1; ++i) { if (i == 0) { @@ -621,12 +620,13 @@ bool HighsCutGeneration::separateLiftedFlowCover() { if (tmp < vubcoef + snfr.lambda - 1e-8) { return static_cast(i * HighsCDouble(snfr.lambda)); } - assert( - ld.M[i] <= vubcoef + snfr.lambda + feastol && - snfr.lambda + vubcoef <= - ld.M[i] + ld.ml + feastol + - std::max(0.0, static_cast( - ld.m[i] - (HighsCDouble(ld.mp) - snfr.lambda) - ld.ml))); + assert(ld.M[i] <= vubcoef + snfr.lambda + feastol && + snfr.lambda + vubcoef <= + ld.M[i] + ld.ml + feastol + + std::max(0.0, static_cast( + ld.m[i] - + (HighsCDouble(ld.mp) - snfr.lambda) - + ld.ml))); return static_cast(i * HighsCDouble(snfr.lambda) + vubcoef - ld.M[i]); } @@ -1594,6 +1594,7 @@ void HighsCutGeneration::initSNFRelaxation() { snfr.aggrBinCoef.resize(rowlen); snfr.aggrContCoef.resize(rowlen); snfr.complementation.resize(rowlen); + snfr.flowCoverStatus.resize(rowlen); } snfr.rhs = 0; snfr.lambda = 0; @@ -1691,14 +1692,11 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { bool HighsCutGeneration::computeFlowCover() { // Compute the flow cover, i.e., get sets C+ subset N+ and C- subset N- // with sum_{j in C+} u_j - sum_{j in C-} u_j = b + lambda, lambda > 0 - if (static_cast(snfr.flowCoverStatus.size()) < snfr.numNnzs) { - snfr.flowCoverStatus.resize(snfr.numNnzs); - } std::vector items(snfr.numNnzs, -1); HighsInt nNonFlowCover = 0; HighsInt nFlowCover = 0; HighsInt nitems = 0; - double n1itemsWeight = 0; + HighsCDouble n1itemsWeight = 0; HighsCDouble flowCoverWeight = 0; for (HighsInt i = 0; i < snfr.numNnzs; ++i) { assert(snfr.coef[i] == 1 || snfr.coef[i] == -1); @@ -1748,8 +1746,8 @@ bool HighsCutGeneration::computeFlowCover() { // Solve a knapsack greedily to assign items to C+, C-, N+\C+, N-\C- // z_j is a binary variable deciding whether the item goes into the knapsack // max sum_{j in N+} ( x_j - 1 ) z_j + sum_{j in N-} x_j - // sum_{j in N+} u'_j z_j + sum_{j in N-} u'_j z_j < -b + sum_{j in N+} u'_j - // z_j in {0,1} for all j in N+ & N- + // sum_{j in N+} u'_j z_j + sum_{j in N-} u'_j z_j < -b + sum_{j in N+} u'_j + // z_j in {0,1} for all j in N+ & N- double knapsackWeight = 0; std::vector weights(nitems); diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 964bf21d0d..23a1237633 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -869,9 +869,9 @@ bool HighsTransformedLp::transformSNFRelaxation( HighsInt vbcol; switch (boundType) { case BoundType::kSimpleLb: - // Case (1) -> a_j > 0, y'_j -> N- Case (2) -> a_j < 0, y'_j ->N+ - // (1) y'_j = -a_j(y_j - u_j), 0 <= y'_j <= a_j(u_j - l_j)x_j, x_j = 1 - // (2) y'_j = a_j(y_j - u_j), 0 <= y'_j <= -a_j(u_j - l_j)x_j, x_j = 1 + // Case (1) -> a_j > 0, y'_j -> N- Case (2) -> a_j < 0, y'_j -> N+ + // (1) y'_j = -a_j(y_j - u_j), 0 <= y'_j <= a_j(u_j - l_j)x_j, x_j=1 + // (2) y'_j = a_j(y_j - u_j), 0 <= y'_j <= -a_j(u_j - l_j)x_j, x_j=1 // rhs -= a_j * u_j aggrvbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * ub); @@ -932,8 +932,7 @@ bool HighsTransformedLp::transformSNFRelaxation( bincoef, vals[i], complementvlb); } tmpSnfrRhs -= - aggrconstant + - (complementvlb && inclbincolvlb ? -bincoef : 0); + aggrconstant + (complementvlb && inclbincolvlb ? -bincoef : 0); if (inclbincolvlb) vectorsum.values[vbcol] = 0; break; case BoundType::kVariableUb: @@ -967,8 +966,7 @@ bool HighsTransformedLp::transformSNFRelaxation( -bincoef, -vals[i], complementvub); } tmpSnfrRhs -= - aggrconstant + - (complementvub && inclbincolvub ? -bincoef : 0); + aggrconstant + (complementvub && inclbincolvub ? -bincoef : 0); if (inclbincolvub) vectorsum.values[vbcol] = 0; break; } From 99a0d65854cea61e152bd5173321da3bc8456927 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 27 Aug 2025 16:37:01 +0200 Subject: [PATCH 41/64] Reenable scaling at all times --- highs/mip/HighsCutGeneration.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 2b93c99eb0..514d0aac66 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1642,7 +1642,7 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { } if (numContCols == 0) return false; - if (maxAbsVal > 1 + feastol || maxAbsVal < 0.5 - feastol) scale(maxAbsVal); + scale(maxAbsVal); for (HighsInt i = 0; i != rowlen; ++i) { HighsInt col = inds[i]; From b840f88a27c4d262fa06fdc245bbf77d1c34394d Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 28 Aug 2025 15:46:25 +0200 Subject: [PATCH 42/64] Fix tests. Check if already scaled. Add epsilon to knapsack" --- check/TestCallbacks.cpp | 2 +- check/TestMipSolver.cpp | 2 +- highs/mip/HighsCutGeneration.cpp | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/check/TestCallbacks.cpp b/check/TestCallbacks.cpp index 0d4364c3a5..f91924b624 100644 --- a/check/TestCallbacks.cpp +++ b/check/TestCallbacks.cpp @@ -417,7 +417,7 @@ TEST_CASE("highs-callback-mip-interrupt", "[highs_callback]") { HighsStatus status = highs.run(); REQUIRE(status == HighsStatus::kWarning); REQUIRE(highs.getModelStatus() == HighsModelStatus::kInterrupt); - REQUIRE(highs.getInfo().objective_function_value > egout_optimal_objective); + REQUIRE(highs.getInfo().objective_function_value >= egout_optimal_objective); highs.resetGlobalScheduler(true); } diff --git a/check/TestMipSolver.cpp b/check/TestMipSolver.cpp index 1fbb622abf..53609b6b80 100644 --- a/check/TestMipSolver.cpp +++ b/check/TestMipSolver.cpp @@ -587,7 +587,7 @@ TEST_CASE("MIP-get-saved-solutions", "[highs_test_mip_solver]") { TEST_CASE("MIP-objective-target", "[highs_test_mip_solver]") { const double egout_optimal_objective = 568.1007; - const double egout_objective_target = 610; + const double egout_objective_target = 680; std::string filename = std::string(HIGHS_DIR) + "/check/instances/egout.mps"; Highs highs; highs.setOptionValue("output_flag", dev_run); diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 514d0aac66..203de885f5 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1366,7 +1366,8 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, std::vector flowCoverInds; double flowCoverRhs = rhs_; double flowCoverEfficacy = 0; - if (genFlowCover && !lpRelaxation.getMipSolver().submip) { + if (genFlowCover && !lpRelaxation.getMipSolver().submip && + !lpRelaxation.getMipSolver().mipdata_->continuous_cols.empty()) { flowCoverVals = vals_; flowCoverInds = inds_; flowCoverSuccess = tryGenerateFlowCoverCut( @@ -1642,7 +1643,7 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { } if (numContCols == 0) return false; - scale(maxAbsVal); + if (maxAbsVal > 1 + feastol || maxAbsVal < 0.5 - feastol) scale(maxAbsVal); for (HighsInt i = 0; i != rowlen; ++i) { HighsInt col = inds[i]; @@ -1737,8 +1738,8 @@ bool HighsCutGeneration::computeFlowCover() { } assert(nNonFlowCover + nFlowCover + nitems == snfr.numNnzs); - double capacity = - static_cast(-snfr.rhs + flowCoverWeight + n1itemsWeight); + double capacity = static_cast(-snfr.rhs + flowCoverWeight + + n1itemsWeight - feastol); // There is no flow cover if capacity is less than zero after fixing if (capacity < feastol) return false; From 4412a3d07157a5fa5d52d4428d4cc28248e75724 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 28 Aug 2025 18:17:38 +0200 Subject: [PATCH 43/64] Skip lifting if viol does not increase --- highs/mip/HighsCutGeneration.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 203de885f5..15c58154a2 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -576,6 +576,16 @@ bool HighsCutGeneration::separateLiftedFlowCover() { } ld.t++; + const HighsSolution& lpSolution = lpRelaxation.getLpSolver().getSolution(); + auto getLpSolution = [&](const HighsInt col) { + HighsInt numCols = lpRelaxation.numCols(); + if (col < numCols) { + return lpSolution.col_value[col]; + } else { + return lpSolution.row_value[col - numCols]; + } + }; + auto getAlphaBeta = [&](double vubcoef) { HighsInt alpha; HighsCDouble beta{}; @@ -696,6 +706,14 @@ bool HighsCutGeneration::separateLiftedFlowCover() { assert(alphabeta.first == 0 || alphabeta.first == 1); if (alphabeta.first == 1) { assert(alphabeta.second > 0); + // Do not lift the column if doing so decreases the violation + double viol = snfr.aggrConstant[i]; + if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { + viol += snfr.aggrContCoef[i] * getLpSolution(snfr.origContCols[i]); + } + viol += (snfr.aggrBinCoef[i] - static_cast(alphabeta.second)) * + (snfr.binSolval[i]); + if (viol <= 0) continue; if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { vals[rowlen] = snfr.aggrContCoef[i]; inds[rowlen] = snfr.origContCols[i]; From 27c6c2a053c97d84f8b751f488cd9d4e9c9b9aca Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 28 Aug 2025 20:21:34 +0200 Subject: [PATCH 44/64] undo relaxed lifting. Set binsolval to be fractional for simple case --- highs/mip/HighsCutGeneration.cpp | 18 ------------------ highs/mip/HighsTransformedLp.cpp | 8 ++++---- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 15c58154a2..203de885f5 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -576,16 +576,6 @@ bool HighsCutGeneration::separateLiftedFlowCover() { } ld.t++; - const HighsSolution& lpSolution = lpRelaxation.getLpSolver().getSolution(); - auto getLpSolution = [&](const HighsInt col) { - HighsInt numCols = lpRelaxation.numCols(); - if (col < numCols) { - return lpSolution.col_value[col]; - } else { - return lpSolution.row_value[col - numCols]; - } - }; - auto getAlphaBeta = [&](double vubcoef) { HighsInt alpha; HighsCDouble beta{}; @@ -706,14 +696,6 @@ bool HighsCutGeneration::separateLiftedFlowCover() { assert(alphabeta.first == 0 || alphabeta.first == 1); if (alphabeta.first == 1) { assert(alphabeta.second > 0); - // Do not lift the column if doing so decreases the violation - double viol = snfr.aggrConstant[i]; - if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { - viol += snfr.aggrContCoef[i] * getLpSolution(snfr.origContCols[i]); - } - viol += (snfr.aggrBinCoef[i] - static_cast(alphabeta.second)) * - (snfr.binSolval[i]); - if (viol <= 0) continue; if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { vals[rowlen] = snfr.aggrContCoef[i]; inds[rowlen] = snfr.origContCols[i]; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 23a1237633..3912f064dc 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -876,10 +876,10 @@ bool HighsTransformedLp::transformSNFRelaxation( aggrvbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * ub); if (vals[i] >= 0) { - addSNFRentry(-1, col, 1.0, -1, aggrvbcoef, aggrconstant, 0, + addSNFRentry(-1, col, (ub - getLpSolution(col)) / (ub - lb), -1, aggrvbcoef, aggrconstant, 0, -vals[i], false); } else { - addSNFRentry(-1, col, 1, 1, -aggrvbcoef, -aggrconstant, 0, vals[i], + addSNFRentry(-1, col, (ub - getLpSolution(col)) / (ub - lb), 1, -aggrvbcoef, -aggrconstant, 0, vals[i], false); } tmpSnfrRhs -= aggrconstant; @@ -893,10 +893,10 @@ bool HighsTransformedLp::transformSNFRelaxation( aggrvbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * lb); if (vals[i] >= 0) { - addSNFRentry(-1, col, 1, 1, aggrvbcoef, -aggrconstant, 0, vals[i], + addSNFRentry(-1, col, (getLpSolution(col) - lb) / (ub - lb), 1, aggrvbcoef, -aggrconstant, 0, vals[i], false); } else { - addSNFRentry(-1, col, 1, -1, -aggrvbcoef, aggrconstant, 0, -vals[i], + addSNFRentry(-1, col, (getLpSolution(col) - lb) / (ub - lb), -1, -aggrvbcoef, aggrconstant, 0, -vals[i], false); } tmpSnfrRhs -= aggrconstant; From 8b8a3f75ed1414f0015b3e96cd9ceb2115c1f7af Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 29 Aug 2025 18:36:59 +0200 Subject: [PATCH 45/64] Only generate cuts when pathlen=1 --- highs/mip/HighsPathSeparator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsPathSeparator.cpp b/highs/mip/HighsPathSeparator.cpp index 83a3828cec..4c2e4b59bb 100644 --- a/highs/mip/HighsPathSeparator.cpp +++ b/highs/mip/HighsPathSeparator.cpp @@ -351,7 +351,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate cut double rhs = 0; success = cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, - false, true); + false, currPathLen == 1); lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); if (!aggregatedPath.empty() || bestOutArcCol != -1 || @@ -361,7 +361,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate reverse cut rhs = 0; success |= cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, - false, true); + false, currPathLen == 1); if (success || (bestOutArcCol == -1 && bestInArcCol == -1)) break; From 803860d50ac32019bff0223fbde67e00f5ff68fd Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Sat, 30 Aug 2025 18:21:10 +0200 Subject: [PATCH 46/64] No longer restrict cut size given lower limits --- highs/mip/HighsCutGeneration.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 203de885f5..436e23950c 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1672,7 +1672,7 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { } } - HighsInt maxLen = 0.15 * (lpRelaxation.numCols()); + HighsInt maxLen = 100 + 0.15 * (lpRelaxation.numCols()); if (rowlen - numZeros > maxLen) return false; if (numZeros != 0) { From 028306813455ee87ac92c6d1af48b5d415736f12 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 1 Sep 2025 10:30:54 +0200 Subject: [PATCH 47/64] Handle cases where global bounds tightened --- highs/mip/HighsTransformedLp.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 3912f064dc..5bb1e52eec 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -875,12 +875,14 @@ bool HighsTransformedLp::transformSNFRelaxation( // rhs -= a_j * u_j aggrvbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * ub); + binsolval = std::min( + 1.0, std::max(0.0, (ub - getLpSolution(col)) / (ub - lb))); if (vals[i] >= 0) { - addSNFRentry(-1, col, (ub - getLpSolution(col)) / (ub - lb), -1, aggrvbcoef, aggrconstant, 0, + addSNFRentry(-1, col, binsolval, -1, aggrvbcoef, aggrconstant, 0, -vals[i], false); } else { - addSNFRentry(-1, col, (ub - getLpSolution(col)) / (ub - lb), 1, -aggrvbcoef, -aggrconstant, 0, vals[i], - false); + addSNFRentry(-1, col, binsolval, 1, -aggrvbcoef, -aggrconstant, 0, + vals[i], false); } tmpSnfrRhs -= aggrconstant; break; @@ -892,12 +894,14 @@ bool HighsTransformedLp::transformSNFRelaxation( // rhs -= a_j * l_j aggrvbcoef = static_cast(vals[i] * (HighsCDouble(ub) - lb)); aggrconstant = static_cast(HighsCDouble(vals[i]) * lb); + binsolval = std::min( + 1.0, std::max(0.0, (getLpSolution(col) - lb) / (ub - lb))); if (vals[i] >= 0) { - addSNFRentry(-1, col, (getLpSolution(col) - lb) / (ub - lb), 1, aggrvbcoef, -aggrconstant, 0, vals[i], - false); + addSNFRentry(-1, col, binsolval, 1, aggrvbcoef, -aggrconstant, 0, + vals[i], false); } else { - addSNFRentry(-1, col, (getLpSolution(col) - lb) / (ub - lb), -1, -aggrvbcoef, aggrconstant, 0, -vals[i], - false); + addSNFRentry(-1, col, binsolval, -1, -aggrvbcoef, aggrconstant, 0, + -vals[i], false); } tmpSnfrRhs -= aggrconstant; break; From 391cdfa835449356454dc4f3e70a2af00e4fd67b Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 14 Oct 2025 15:34:11 +0200 Subject: [PATCH 48/64] Make new epsilon. Create lambda function --- highs/mip/HighsCutGeneration.cpp | 96 +++++++++++++------------------- 1 file changed, 39 insertions(+), 57 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 436e23950c..509c19c0f5 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -505,6 +505,7 @@ bool HighsCutGeneration::separateLiftedMixedIntegerCover() { // Lifted flow cover inequalities for mixed 0-1 integer programs. // Mathematical Programming, 85(3), 439-467. bool HighsCutGeneration::separateLiftedFlowCover() { + const double vubEpsilon = 1e-8; // Compute the lifting data (ld) first struct LiftingData { std::vector m; @@ -525,7 +526,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { // col is in N- \ C- if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == -1) { assert(snfr.vubCoef[i] >= 0); - if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { + if (snfr.vubCoef[i] > snfr.lambda + vubEpsilon) { ld.m[ld.r] = snfr.vubCoef[i]; ++ld.r; } else { @@ -538,7 +539,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { } else if (snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == 1) { // col is in C+ assert(snfr.vubCoef[i] > 0); - if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { + if (snfr.vubCoef[i] > snfr.lambda + vubEpsilon) { ld.m[ld.r] = snfr.vubCoef[i]; ++ld.r; ld.mp = std::min(ld.mp, snfr.vubCoef[i]); @@ -582,11 +583,11 @@ bool HighsCutGeneration::separateLiftedFlowCover() { double vubcoefpluslambda = vubcoef + snfr.lambda; HighsInt i = 0; - while (i < ld.r && vubcoefpluslambda >= ld.M[i + 1] + 1e-8) { + while (i < ld.r && vubcoefpluslambda >= ld.M[i + 1] + vubEpsilon) { ++i; } - if (vubcoef <= ld.M[i] - 1e-8) { + if (vubcoef <= ld.M[i] - vubEpsilon) { assert(ld.M[i] < vubcoefpluslambda); alpha = 1; beta = -i * HighsCDouble(snfr.lambda) + ld.M[i]; @@ -599,25 +600,24 @@ bool HighsCutGeneration::separateLiftedFlowCover() { auto evaluateLiftingFunction = [&](double vubcoef) { HighsInt i = 0; - while (i < ld.r && vubcoef + snfr.lambda >= ld.M[i + 1] + 1e-8) { + while (i < ld.r && vubcoef + snfr.lambda >= ld.M[i + 1] + vubEpsilon) { ++i; } if (i < ld.t) { HighsCDouble liftedcoef = i * HighsCDouble(snfr.lambda); - if (ld.M[i] < vubcoef + 1e-8) { + if (ld.M[i] < vubcoef + vubEpsilon) { return static_cast(liftedcoef); } - assert(i > 0 && ld.M[i] < vubcoef + snfr.lambda - 1e-8 && + assert(i > 0 && ld.M[i] < vubcoef + snfr.lambda - vubEpsilon && vubcoef <= ld.M[i]); liftedcoef += vubcoef; liftedcoef -= ld.M[i]; return static_cast(liftedcoef); - } - if (i < ld.r) { + } else if (i < ld.r) { HighsCDouble tmp = HighsCDouble(ld.m[i]) - ld.mp - ld.ml + snfr.lambda; if (tmp < 0) tmp = 0; tmp += ld.M[i] + ld.ml; - if (tmp < vubcoef + snfr.lambda - 1e-8) { + if (tmp < vubcoef + snfr.lambda - vubEpsilon) { return static_cast(i * HighsCDouble(snfr.lambda)); } assert(ld.M[i] <= vubcoef + snfr.lambda + feastol && @@ -630,7 +630,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { return static_cast(i * HighsCDouble(snfr.lambda) + vubcoef - ld.M[i]); } - assert(i == ld.r && ld.M[i] <= vubcoef + snfr.lambda + 1e-8); + assert(i == ld.r && ld.M[i] <= vubcoef + snfr.lambda + vubEpsilon); return static_cast(ld.r * HighsCDouble(snfr.lambda) + vubcoef - ld.M[ld.r]); }; @@ -640,37 +640,37 @@ bool HighsCutGeneration::separateLiftedFlowCover() { // L-- = N- \ (L- union C-) HighsCDouble tmpRhs = ld.d1; rowlen = 0; + + auto addCutNonZero = [&](const double& coef, const HighsInt& index, + const bool complement) -> void { + vals[rowlen] = coef; + inds[rowlen] = index; + if (complement) { + tmpRhs -= vals[rowlen]; + vals[rowlen] = -vals[rowlen]; + } + rowlen++; + }; + for (HighsInt i = 0; i != snfr.numNnzs; ++i) { // col is in N- \ C- if (snfr.flowCoverStatus[i] == -1 && snfr.coef[i] == -1) { - if (snfr.vubCoef[i] > snfr.lambda + 1e-8) { + if (snfr.vubCoef[i] > snfr.lambda + vubEpsilon) { if (snfr.origBinCols[i] != -1) { // col is in L- - vals[rowlen] = -snfr.lambda; - inds[rowlen] = snfr.origBinCols[i]; - if (snfr.complementation[i]) { - tmpRhs -= vals[rowlen]; - vals[rowlen] = -vals[rowlen]; - } - rowlen++; + addCutNonZero(-snfr.lambda, snfr.origBinCols[i], + snfr.complementation[i]); } else { tmpRhs += snfr.lambda; } } else { // col is in L-- if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { - vals[rowlen] = -snfr.aggrContCoef[i]; - inds[rowlen] = snfr.origContCols[i]; - rowlen++; + addCutNonZero(-snfr.aggrContCoef[i], snfr.origContCols[i], false); } if (snfr.origBinCols[i] != -1 && snfr.aggrBinCoef[i] != 0) { - vals[rowlen] = -snfr.aggrBinCoef[i]; - inds[rowlen] = snfr.origBinCols[i]; - if (snfr.complementation[i]) { - tmpRhs -= vals[rowlen]; - vals[rowlen] = -vals[rowlen]; - } - rowlen++; + addCutNonZero(-snfr.aggrBinCoef[i], snfr.origBinCols[i], + snfr.complementation[i]); } tmpRhs += snfr.aggrConstant[i]; } @@ -679,13 +679,8 @@ bool HighsCutGeneration::separateLiftedFlowCover() { if (snfr.origBinCols[i] != -1) { double liftedbincoef = evaluateLiftingFunction(snfr.vubCoef[i]); if (liftedbincoef != 0) { - vals[rowlen] = -liftedbincoef; - inds[rowlen] = snfr.origBinCols[i]; - if (snfr.complementation[i]) { - tmpRhs -= vals[rowlen]; - vals[rowlen] = -vals[rowlen]; - } - rowlen++; + addCutNonZero(-liftedbincoef, snfr.origBinCols[i], + snfr.complementation[i]); tmpRhs -= liftedbincoef; } } @@ -697,20 +692,13 @@ bool HighsCutGeneration::separateLiftedFlowCover() { if (alphabeta.first == 1) { assert(alphabeta.second > 0); if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { - vals[rowlen] = snfr.aggrContCoef[i]; - inds[rowlen] = snfr.origContCols[i]; - rowlen++; + addCutNonZero(snfr.aggrContCoef[i], snfr.origContCols[i], false); } HighsCDouble binvarcoef = snfr.aggrBinCoef[i] - alphabeta.second; if (snfr.origBinCols[i] != -1) { if (binvarcoef != 0) { - vals[rowlen] = static_cast(binvarcoef); - inds[rowlen] = snfr.origBinCols[i]; - if (snfr.complementation[i]) { - tmpRhs -= binvarcoef; - vals[rowlen] = -vals[rowlen]; - } - rowlen++; + addCutNonZero(static_cast(binvarcoef), snfr.origBinCols[i], + snfr.complementation[i]); } } else { tmpRhs -= binvarcoef; @@ -722,24 +710,18 @@ bool HighsCutGeneration::separateLiftedFlowCover() { assert(snfr.flowCoverStatus[i] == 1 && snfr.coef[i] == 1); HighsCDouble bincoef = snfr.aggrBinCoef[i]; HighsCDouble constant = snfr.aggrConstant[i]; - if (snfr.origBinCols[i] != -1 && snfr.vubCoef[i] >= snfr.lambda + 1e-8) { + if (snfr.origBinCols[i] != -1 && + snfr.vubCoef[i] >= snfr.lambda + vubEpsilon) { // col is in C++ constant += HighsCDouble(snfr.vubCoef[i]) - snfr.lambda; bincoef -= HighsCDouble(snfr.vubCoef[i]) - snfr.lambda; } if (snfr.origBinCols[i] != -1 && bincoef != 0) { - vals[rowlen] = static_cast(bincoef); - inds[rowlen] = snfr.origBinCols[i]; - if (snfr.complementation[i]) { - tmpRhs -= bincoef; - vals[rowlen] = -vals[rowlen]; - } - rowlen++; + addCutNonZero(static_cast(bincoef), snfr.origBinCols[i], + snfr.complementation[i]); } if (snfr.origContCols[i] != -1 && snfr.aggrContCoef[i] != 0) { - vals[rowlen] = snfr.aggrContCoef[i]; - inds[rowlen] = snfr.origContCols[i]; - rowlen++; + addCutNonZero(snfr.aggrContCoef[i], snfr.origContCols[i], false); } tmpRhs -= constant; } From bf3c9bbdb9014e986945ded95951fe02c5e9a75f Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 14 Oct 2025 18:32:37 +0200 Subject: [PATCH 49/64] Maintain old solution path --- highs/mip/HighsCutGeneration.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 509c19c0f5..64e05741c2 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1973,10 +1973,10 @@ bool HighsCutGeneration::tryGenerateCut(std::vector& inds_, rhs = tmpRhs; } else { // accept cut and increase minimum efficiency requirement for cmir cut - if (allowRejectCut) { - minMirEfficacy = std::max(minEfficacy, efficacy + feastol); + if (minEfficacy > 10 * feastol) { + minMirEfficacy = std::max(minEfficacy, efficacy + 10 * feastol); } else { - minMirEfficacy = efficacy + (10 * feastol); + minMirEfficacy += efficacy; } std::swap(tmpRhs, rhs); } From 3e0df3c7eb1b66026457caf4e102c9dff5794c79 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 15 Oct 2025 11:44:29 +0200 Subject: [PATCH 50/64] Add option to enable flow cover cuts --- highs/lp_data/HighsOptions.h | 7 +++++++ highs/mip/HighsCutGeneration.cpp | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index 62b70252e6..78d1795a92 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -448,6 +448,7 @@ struct HighsOptionsStruct { bool mip_heuristic_run_root_reduced_cost; bool mip_heuristic_run_zi_round; bool mip_heuristic_run_shifting; + bool mip_cut_flow_cover; double mip_min_logging_interval; #ifdef HIGHS_DEBUGSOL @@ -600,6 +601,7 @@ struct HighsOptionsStruct { mip_heuristic_run_root_reduced_cost(false), mip_heuristic_run_zi_round(false), mip_heuristic_run_shifting(false), + mip_cut_flow_cover(false), mip_min_logging_interval(0.0), #ifdef HIGHS_DEBUGSOL mip_debug_solution_file(""), @@ -1177,6 +1179,11 @@ class HighsOptions : public HighsOptionsStruct { &mip_heuristic_run_shifting, false); records.push_back(record_bool); + record_bool = + new OptionRecordBool("mip_cut_flow_cover", "Enable flow cover cuts", + advanced, &mip_cut_flow_cover, true); + records.push_back(record_bool); + record_double = new OptionRecordDouble( "mip_rel_gap", "Tolerance on relative gap, |ub-lb|/|ub|, to determine whether " diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 64e05741c2..045c040269 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1349,7 +1349,8 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, double flowCoverRhs = rhs_; double flowCoverEfficacy = 0; if (genFlowCover && !lpRelaxation.getMipSolver().submip && - !lpRelaxation.getMipSolver().mipdata_->continuous_cols.empty()) { + !lpRelaxation.getMipSolver().mipdata_->continuous_cols.empty() && + lpRelaxation.getMipSolver().options_mip_->mip_cut_flow_cover) { flowCoverVals = vals_; flowCoverInds = inds_; flowCoverSuccess = tryGenerateFlowCoverCut( From 7ca70f077765faee540ed9022206d0e9fcee116e Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 20 Oct 2025 12:59:06 +0200 Subject: [PATCH 51/64] Add relative min eff. Stricter row size lims --- highs/mip/HighsCutGeneration.cpp | 18 ++++++++---------- highs/mip/HighsTransformedLp.cpp | 10 +++++----- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 045c040269..a22c5d261b 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1398,7 +1398,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, // try to generate a cut if (!tryGenerateCut( inds_, vals_, hasUnboundedInts, hasGeneralInts, hasContinuous, - std::max(flowCoverEfficacy - (10 * feastol), 10 * feastol), + std::max(0.9 * flowCoverEfficacy, 10 * feastol), onlyInitialCMIRScale)) { cmirSuccess = false; goto postprocess; @@ -1597,6 +1597,9 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { // 4. Don't consider any inequality with too many non-zeros // 5. Don't consider any inequality with too few continuous cols + HighsInt maxLen = 100 + 0.05 * (lpRelaxation.numCols()); + if (rowlen > maxLen) return false; + HighsInt numZeros = 0; HighsInt numContCols = 0; double maxact = -feastol; @@ -1655,9 +1658,6 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { } } - HighsInt maxLen = 100 + 0.15 * (lpRelaxation.numCols()); - if (rowlen - numZeros > maxLen) return false; - if (numZeros != 0) { // remove zeros in place for (HighsInt i = rowlen - 1; i >= 0; --i) { @@ -1689,10 +1689,8 @@ bool HighsCutGeneration::computeFlowCover() { if (abs(snfr.vubCoef[i]) < feastol) { snfr.flowCoverStatus[i] = -1; nNonFlowCover++; - continue; - } - // x_i is fractional -> becomes an item in knapsack (decides if in cover) - if (fractionality(snfr.binSolval[i]) > feastol) { + } else if (fractionality(snfr.binSolval[i]) > feastol) { + // x_i is fractional -> becomes an item in knapsack (decides if in cover) items[nitems] = i; nitems++; if (snfr.coef[i] == 1) { @@ -1736,8 +1734,6 @@ bool HighsCutGeneration::computeFlowCover() { double knapsackWeight = 0; std::vector weights(nitems); std::vector profitweightratios(nitems); - std::vector perm(nitems); - std::iota(perm.begin(), perm.end(), 0); for (HighsInt i = 0; i < nitems; ++i) { weights[i] = snfr.vubCoef[items[i]]; if (snfr.coef[items[i]] == 1) { @@ -1746,6 +1742,8 @@ bool HighsCutGeneration::computeFlowCover() { profitweightratios[i] = snfr.binSolval[items[i]] / weights[i]; } } + std::vector perm(nitems); + std::iota(perm.begin(), perm.end(), 0); pdqsort_branchless(perm.begin(), perm.end(), [&](const HighsInt a, const HighsInt b) { return profitweightratios[a] > profitweightratios[b]; diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 5bb1e52eec..2cd238dfb2 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -764,6 +764,11 @@ bool HighsTransformedLp::transformSNFRelaxation( double lb = getLb(col); double ub = getUb(col); + if (lb == -kHighsInf || ub == kHighsInf) { + vectorsum.clear(); + return false; + } + if (ub - lb < mip.options_mip_->small_matrix_value) { rhs -= std::min(lb, ub) * vals[i]; tmpSnfrRhs -= std::min(lb, ub) * vals[i]; @@ -777,11 +782,6 @@ bool HighsTransformedLp::transformSNFRelaxation( continue; } - if (lb == -kHighsInf || ub == kHighsInf) { - vectorsum.clear(); - return false; - } - // the code below uses the difference between the column upper and lower // bounds as the upper bound for the slack from the variable upper bound // constraint (upper[j] = ub - lb) and thus assumes that the variable upper From a5cbd8dcfb0f283b5b71a4ef94c2520cd89a6eb0 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 20 Oct 2025 14:55:05 +0200 Subject: [PATCH 52/64] Add missing assert. Re-enable cuts for all aggrs --- highs/mip/HighsCutGeneration.cpp | 1 + highs/mip/HighsPathSeparator.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index a22c5d261b..a895c246e8 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -575,6 +575,7 @@ bool HighsCutGeneration::separateLiftedFlowCover() { break; } } + assert(ld.m[ld.t] == ld.mp); ld.t++; auto getAlphaBeta = [&](double vubcoef) { diff --git a/highs/mip/HighsPathSeparator.cpp b/highs/mip/HighsPathSeparator.cpp index 4c2e4b59bb..83a3828cec 100644 --- a/highs/mip/HighsPathSeparator.cpp +++ b/highs/mip/HighsPathSeparator.cpp @@ -351,7 +351,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate cut double rhs = 0; success = cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, - false, currPathLen == 1); + false, true); lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); if (!aggregatedPath.empty() || bestOutArcCol != -1 || @@ -361,7 +361,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate reverse cut rhs = 0; success |= cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, - false, currPathLen == 1); + false, true); if (success || (bestOutArcCol == -1 && bestInArcCol == -1)) break; From ffdb2a46ae59ba1214413098a4e86098ebf38157 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 19 Nov 2025 13:51:35 +0100 Subject: [PATCH 53/64] Generate flow cover cut only if cmir was successful --- highs/mip/HighsCutGeneration.cpp | 103 ++++++++++++++++++------------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index a895c246e8..f16187f618 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1343,8 +1343,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } #endif - // Try to generate a lifted simple generalized flow cover cut - bool flowCoverSuccess = false; + // Copy data to later generate lifted simple generalized flow cover cut std::vector flowCoverVals; std::vector flowCoverInds; double flowCoverRhs = rhs_; @@ -1354,30 +1353,26 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, lpRelaxation.getMipSolver().options_mip_->mip_cut_flow_cover) { flowCoverVals = vals_; flowCoverInds = inds_; - flowCoverSuccess = tryGenerateFlowCoverCut( - transLp, flowCoverInds, flowCoverVals, flowCoverRhs, flowCoverEfficacy); + genFlowCover = true; + } else { + genFlowCover = false; } - bool cmirSuccess = false; bool intsPositive = true; - bool hasUnboundedInts = false; - bool hasGeneralInts = false; - bool hasContinuous = false; - if (!transLp.transform(vals_, upper, solval, inds_, rhs_, intsPositive)) { - cmirSuccess = false; - goto postprocess; - } + if (!transLp.transform(vals_, upper, solval, inds_, rhs_, intsPositive)) + return false; rowlen = inds_.size(); this->inds = inds_.data(); this->vals = vals_.data(); this->rhs = rhs_; complementation.clear(); + bool hasUnboundedInts = false; + bool hasGeneralInts = false; + bool hasContinuous = false; if (!preprocessBaseInequality(hasUnboundedInts, hasGeneralInts, - hasContinuous)) { - cmirSuccess = false; - goto postprocess; - } + hasContinuous)) + return false; // it can happen that there is an unbounded integer variable during the // transform call so that the integers are not transformed to positive values. @@ -1397,13 +1392,9 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } // try to generate a cut - if (!tryGenerateCut( - inds_, vals_, hasUnboundedInts, hasGeneralInts, hasContinuous, - std::max(0.9 * flowCoverEfficacy, 10 * feastol), - onlyInitialCMIRScale)) { - cmirSuccess = false; - goto postprocess; - } + if (!tryGenerateCut(inds_, vals_, hasUnboundedInts, hasGeneralInts, + hasContinuous, 10 * feastol, onlyInitialCMIRScale)) + return false; // remove the complementation if exists removeComplementation(); @@ -1416,24 +1407,53 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, vals[i] = vals[rowlen]; } } - cmirSuccess = true; - -postprocess: - if (!cmirSuccess && !flowCoverSuccess) return false; // transform the cut back into the original space, i.e. remove the bound // substitution and replace implicit slack variables - if (cmirSuccess) { - rhs_ = (double)rhs; - vals_.resize(rowlen); - inds_.resize(rowlen); - if (!transLp.untransform(vals_, inds_, rhs_)) return false; - } else { - rhs_ = flowCoverRhs; - std::swap(vals_, flowCoverVals); - std::swap(inds_, flowCoverInds); - integralSupport = false; - integralCoefficients = false; + rhs_ = (double)rhs; + vals_.resize(rowlen); + inds_.resize(rowlen); + if (!transLp.untransform(vals_, inds_, rhs_)) return false; + + const auto& sol = lpRelaxation.getSolution().col_value; + + // Try to generate a lifted simple generalized flow cover cut + if (genFlowCover) { + bool flowCoverSuccess = tryGenerateFlowCoverCut( + transLp, flowCoverInds, flowCoverVals, flowCoverRhs, flowCoverEfficacy); + if (flowCoverSuccess) { + HighsInt rowlen_ = inds_.size(); + double viol = -rhs_; + double sqrnorm = 0; + double efficacy = 0; + for (HighsInt i = 0; i != rowlen_; ++i) { + HighsInt col = inds_[i]; + viol += vals_[i] * sol[col]; + if (vals_[i] >= 0 && + sol[col] <= + lpRelaxation.getMipSolver().mipdata_->domain.col_lower_[col] + + lpRelaxation.getMipSolver().mipdata_->feastol) + continue; + if (vals_[i] < 0 && + sol[col] >= + lpRelaxation.getMipSolver().mipdata_->domain.col_upper_[col] - + lpRelaxation.getMipSolver().mipdata_->feastol) + continue; + sqrnorm += vals_[i] * vals_[i]; + } + if (sqrnorm == 0) { + efficacy = 0; + } else { + efficacy = viol / sqrt(sqrnorm); + } + if (flowCoverSuccess && flowCoverEfficacy > 1.2 * efficacy) { + rhs_ = flowCoverRhs; + std::swap(vals_, flowCoverVals); + std::swap(inds_, flowCoverInds); + integralSupport = false; + integralCoefficients = false; + } + } } rowlen = inds_.size(); @@ -1455,7 +1475,6 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, // finally determine the violation of the cut in the original space HighsCDouble violation = -rhs_; - const auto& sol = lpRelaxation.getSolution().col_value; for (HighsInt i = 0; i != rowlen; ++i) violation += sol[inds[i]] * vals_[i]; if (violation <= 10 * feastol) return false; @@ -1973,11 +1992,7 @@ bool HighsCutGeneration::tryGenerateCut(std::vector& inds_, rhs = tmpRhs; } else { // accept cut and increase minimum efficiency requirement for cmir cut - if (minEfficacy > 10 * feastol) { - minMirEfficacy = std::max(minEfficacy, efficacy + 10 * feastol); - } else { - minMirEfficacy += efficacy; - } + minMirEfficacy += efficacy; std::swap(tmpRhs, rhs); } } From c9ee426b86330dae5e3b75561ac5458b5a8cf7ec Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 19 Nov 2025 13:54:33 +0100 Subject: [PATCH 54/64] Remove two redundant checks / assignments --- highs/mip/HighsCutGeneration.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index f16187f618..8fd61ad20a 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1353,7 +1353,6 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, lpRelaxation.getMipSolver().options_mip_->mip_cut_flow_cover) { flowCoverVals = vals_; flowCoverInds = inds_; - genFlowCover = true; } else { genFlowCover = false; } @@ -1446,7 +1445,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } else { efficacy = viol / sqrt(sqrnorm); } - if (flowCoverSuccess && flowCoverEfficacy > 1.2 * efficacy) { + if (flowCoverEfficacy > 1.2 * efficacy) { rhs_ = flowCoverRhs; std::swap(vals_, flowCoverVals); std::swap(inds_, flowCoverInds); From 7e3fbc5509a8bee49146a70f60e877fa0e253978 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 25 Nov 2025 16:00:33 +0100 Subject: [PATCH 55/64] Change eff requirement. Simply logic --- highs/mip/HighsCutGeneration.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 8fd61ad20a..1c80673817 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1440,12 +1440,8 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, continue; sqrnorm += vals_[i] * vals_[i]; } - if (sqrnorm == 0) { - efficacy = 0; - } else { - efficacy = viol / sqrt(sqrnorm); - } - if (flowCoverEfficacy > 1.2 * efficacy) { + if (sqrnorm > 0) efficacy = viol / sqrt(sqrnorm); + if (flowCoverEfficacy > efficacy + feastol) { rhs_ = flowCoverRhs; std::swap(vals_, flowCoverVals); std::swap(inds_, flowCoverInds); From af6d8a135c80b14512fca2f0be306b2ef8a0f216 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 30 Jan 2026 12:59:26 +0100 Subject: [PATCH 56/64] Minimise double operations --- highs/mip/HighsTransformedLp.cpp | 55 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 2cd238dfb2..44432e8f93 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -672,49 +672,50 @@ bool HighsTransformedLp::transformSNFRelaxation( double complorigbincoef = complement ? -origbincoef : origbincoef; double vbconstant = complement ? vb.constant + vb.coef : vb.constant; double vbcoef = complement ? -vb.coef : vb.coef; - double val; + double val1; + double val2; if (isVub) { - val = sign * (coef * vbcoef + complorigbincoef); - if (val > kHighsInf) return false; - if (val < 0) { - val = sign * (coef * vbcoef); - if (val < 0) return false; + val1 = sign * (coef * vbcoef + complorigbincoef); + if (val1 > kHighsInf) return false; + if (val1 < 0) { + val1 -= sign * complorigbincoef; + if (val1 < 0) return false; inclbincoef = false; } if (inclbincoef) { - val = sign * (coef * (lb - vbconstant) + complorigbincoef); - if (val < 0) { - val = sign * (coef * (lb - vbconstant)); - if (val < 0) return false; + val2 = sign * (coef * (lb - vbconstant) + complorigbincoef); + if (val2 < 0) { + val2 -= sign * complorigbincoef; + if (val2 < 0) return false; inclbincoef = false; - val = sign * (coef * vbcoef); - if (val < 0) return false; + val1 -= sign * complorigbincoef; + if (val1 < 0) return false; } } else { - val = sign * (coef * (lb - vbconstant)); - if (val < 0) return false; + val2 = sign * (coef * (lb - vbconstant)); + if (val2 < 0) return false; } } else { - val = sign * (coef * vbcoef + complorigbincoef); - if (-val > kHighsInf) return false; - if (val > 0) { - val = sign * (coef * vbcoef); - if (val > 0) return false; + val1 = sign * (coef * vbcoef + complorigbincoef); + if (-val1 > kHighsInf) return false; + if (val1 > 0) { + val1 -= sign * complorigbincoef; + if (val1 > 0) return false; inclbincoef = false; } if (inclbincoef) { - val = sign * (coef * (ub - vbconstant) + complorigbincoef); - if (val > 0) { - val = sign * (coef * (ub - vbconstant)); - if (val > 0) return false; + val2 = sign * (coef * (ub - vbconstant) + complorigbincoef); + if (val2 > 0) { + val2 -= sign * complorigbincoef; + if (val2 > 0) return false; inclbincoef = false; - val = sign * (coef * vbcoef); - if (val > 0) return false; + val1 -= sign * complorigbincoef; + if (val1 > 0) return false; } } else { - val = sign * (coef * (ub - vbconstant)); - if (val > 0) return false; + val2 = sign * (coef * (ub - vbconstant)); + if (val2 > 0) return false; } } From c3179d05c27f569abb682b5345cac09ad000332c Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 30 Jan 2026 12:59:57 +0100 Subject: [PATCH 57/64] Enable flow cover for tableau sepa --- highs/mip/HighsPathSeparator.cpp | 7 +++++-- highs/mip/HighsTableauSeparator.cpp | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/highs/mip/HighsPathSeparator.cpp b/highs/mip/HighsPathSeparator.cpp index 83a3828cec..6ffc3c3a49 100644 --- a/highs/mip/HighsPathSeparator.cpp +++ b/highs/mip/HighsPathSeparator.cpp @@ -164,6 +164,9 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, colOutArcs[col].second = outArcRows.size(); } + const bool genFlowCover = + mip.mipdata_->num_nodes - mip.mipdata_->num_nodes_before_run == 0 && + !mip.submip; HighsCutGeneration cutGen(lpRelaxation, cutpool); std::vector baseRowInds; std::vector baseRowVals; @@ -351,7 +354,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate cut double rhs = 0; success = cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, - false, true); + false, genFlowCover); lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); if (!aggregatedPath.empty() || bestOutArcCol != -1 || @@ -361,7 +364,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate reverse cut rhs = 0; success |= cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, - false, true); + false, genFlowCover); if (success || (bestOutArcCol == -1 && bestInArcCol == -1)) break; diff --git a/highs/mip/HighsTableauSeparator.cpp b/highs/mip/HighsTableauSeparator.cpp index 0ebf25504e..bda80912ad 100644 --- a/highs/mip/HighsTableauSeparator.cpp +++ b/highs/mip/HighsTableauSeparator.cpp @@ -188,6 +188,9 @@ void HighsTableauSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, HighsInt numCuts = cutpool.getNumCuts(); const double bestScoreFac[] = {0.0025, 0.01}; + const bool genFlowCover = + mip.mipdata_->num_nodes - mip.mipdata_->num_nodes_before_run == 0 && + !mip.submip; for (const auto& fracvar : fractionalBasisvars) { if (cutpool.getNumCuts() - numCuts >= 1000) break; @@ -229,12 +232,12 @@ void HighsTableauSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, baseRowInds.size()); double rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, false); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, genFlowCover); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, false); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, genFlowCover); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.clear(); From 8fcffa3dc67603ad863e558ec9f4823eaec47879 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 30 Jan 2026 13:00:31 +0100 Subject: [PATCH 58/64] Rework when flow cover is generated --- highs/mip/HighsCutGeneration.cpp | 121 ++++++++++++++++++------------- 1 file changed, 70 insertions(+), 51 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 1c80673817..6430e49a92 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1351,8 +1351,20 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, if (genFlowCover && !lpRelaxation.getMipSolver().submip && !lpRelaxation.getMipSolver().mipdata_->continuous_cols.empty() && lpRelaxation.getMipSolver().options_mip_->mip_cut_flow_cover) { - flowCoverVals = vals_; - flowCoverInds = inds_; + bool hasContinuousBeforePreprocess = false; + for (size_t i = 0; i != inds_.size(); ++i) { + if (lpRelaxation.isColIntegral(inds_[i]) && + std::abs(vals_[i]) > 10 * feastol) { + hasContinuousBeforePreprocess = true; + break; + } + } + if (!hasContinuousBeforePreprocess) { + genFlowCover = false; + } else { + flowCoverVals = vals_; + flowCoverInds = inds_; + } } else { genFlowCover = false; } @@ -1391,65 +1403,72 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, } // try to generate a cut - if (!tryGenerateCut(inds_, vals_, hasUnboundedInts, hasGeneralInts, - hasContinuous, 10 * feastol, onlyInitialCMIRScale)) - return false; + bool cmirSuccess = + tryGenerateCut(inds_, vals_, hasUnboundedInts, hasGeneralInts, + hasContinuous, 10 * feastol, onlyInitialCMIRScale); - // remove the complementation if exists - removeComplementation(); + if (cmirSuccess) { + // remove the complementation if exists + removeComplementation(); - // remove zeros in place - for (HighsInt i = rowlen - 1; i >= 0; --i) { - if (vals[i] == 0.0) { - --rowlen; - inds[i] = inds[rowlen]; - vals[i] = vals[rowlen]; + // remove zeros in place + for (HighsInt i = rowlen - 1; i >= 0; --i) { + if (vals[i] == 0.0) { + --rowlen; + inds[i] = inds[rowlen]; + vals[i] = vals[rowlen]; + } } - } - // transform the cut back into the original space, i.e. remove the bound - // substitution and replace implicit slack variables - rhs_ = (double)rhs; - vals_.resize(rowlen); - inds_.resize(rowlen); - if (!transLp.untransform(vals_, inds_, rhs_)) return false; + // transform the cut back into the original space, i.e. remove the bound + // substitution and replace implicit slack variables + rhs_ = (double)rhs; + vals_.resize(rowlen); + inds_.resize(rowlen); + cmirSuccess = transLp.untransform(vals_, inds_, rhs_); + } const auto& sol = lpRelaxation.getSolution().col_value; // Try to generate a lifted simple generalized flow cover cut - if (genFlowCover) { - bool flowCoverSuccess = tryGenerateFlowCoverCut( - transLp, flowCoverInds, flowCoverVals, flowCoverRhs, flowCoverEfficacy); - if (flowCoverSuccess) { - HighsInt rowlen_ = inds_.size(); - double viol = -rhs_; - double sqrnorm = 0; - double efficacy = 0; - for (HighsInt i = 0; i != rowlen_; ++i) { - HighsInt col = inds_[i]; - viol += vals_[i] * sol[col]; - if (vals_[i] >= 0 && - sol[col] <= - lpRelaxation.getMipSolver().mipdata_->domain.col_lower_[col] + - lpRelaxation.getMipSolver().mipdata_->feastol) - continue; - if (vals_[i] < 0 && - sol[col] >= - lpRelaxation.getMipSolver().mipdata_->domain.col_upper_[col] - - lpRelaxation.getMipSolver().mipdata_->feastol) - continue; - sqrnorm += vals_[i] * vals_[i]; - } - if (sqrnorm > 0) efficacy = viol / sqrt(sqrnorm); - if (flowCoverEfficacy > efficacy + feastol) { - rhs_ = flowCoverRhs; - std::swap(vals_, flowCoverVals); - std::swap(inds_, flowCoverInds); - integralSupport = false; - integralCoefficients = false; - } + bool flowCoverSuccess = + genFlowCover + ? tryGenerateFlowCoverCut(transLp, flowCoverInds, flowCoverVals, + flowCoverRhs, flowCoverEfficacy) + : false; + if (flowCoverSuccess && cmirSuccess) { + HighsInt rowlen_ = inds_.size(); + double viol = -rhs_; + double sqrnorm = 0; + double efficacy = 0; + for (HighsInt i = 0; i != rowlen_; ++i) { + HighsInt col = inds_[i]; + viol += vals_[i] * sol[col]; + if (vals_[i] >= 0 && + sol[col] <= + lpRelaxation.getMipSolver().mipdata_->domain.col_lower_[col] + + lpRelaxation.getMipSolver().mipdata_->feastol) + continue; + if (vals_[i] < 0 && + sol[col] >= + lpRelaxation.getMipSolver().mipdata_->domain.col_upper_[col] - + lpRelaxation.getMipSolver().mipdata_->feastol) + continue; + sqrnorm += vals_[i] * vals_[i]; + } + if (sqrnorm > 0) efficacy = viol / sqrt(sqrnorm); + if (flowCoverEfficacy < efficacy + 10 * feastol) { + flowCoverSuccess = false; } } + if (!flowCoverSuccess && !cmirSuccess) return false; + if (flowCoverSuccess) { + rhs_ = flowCoverRhs; + std::swap(vals_, flowCoverVals); + std::swap(inds_, flowCoverInds); + integralSupport = false; + integralCoefficients = false; + } rowlen = inds_.size(); inds = inds_.data(); From 0cd33684f09e43ee2992a3a392125c66e97627a7 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 4 Mar 2026 10:29:33 +0100 Subject: [PATCH 59/64] Simplify when flow cover used. Fix bug in col type logic --- highs/mip/HighsCutGeneration.cpp | 8 ++++---- highs/mip/HighsPathSeparator.cpp | 7 ++----- highs/mip/HighsTableauSeparator.cpp | 9 ++++----- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 6430e49a92..64ad082872 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1351,15 +1351,15 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, if (genFlowCover && !lpRelaxation.getMipSolver().submip && !lpRelaxation.getMipSolver().mipdata_->continuous_cols.empty() && lpRelaxation.getMipSolver().options_mip_->mip_cut_flow_cover) { - bool hasContinuousBeforePreprocess = false; + bool hasNonBinaryBeforePreprocess = false; for (size_t i = 0; i != inds_.size(); ++i) { - if (lpRelaxation.isColIntegral(inds_[i]) && + if (!lpRelaxation.getMipSolver().mipdata_->domain.isBinary(inds_[i]) && std::abs(vals_[i]) > 10 * feastol) { - hasContinuousBeforePreprocess = true; + hasNonBinaryBeforePreprocess = true; break; } } - if (!hasContinuousBeforePreprocess) { + if (!hasNonBinaryBeforePreprocess) { genFlowCover = false; } else { flowCoverVals = vals_; diff --git a/highs/mip/HighsPathSeparator.cpp b/highs/mip/HighsPathSeparator.cpp index 7f8f7d204d..d4aa2e0c87 100644 --- a/highs/mip/HighsPathSeparator.cpp +++ b/highs/mip/HighsPathSeparator.cpp @@ -164,9 +164,6 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, colOutArcs[col].second = outArcRows.size(); } - const bool genFlowCover = - mip.mipdata_->num_nodes - mip.mipdata_->num_nodes_before_run == 0 && - !mip.submip; HighsCutGeneration cutGen(lpRelaxation, cutpool); std::vector baseRowInds; std::vector baseRowVals; @@ -354,7 +351,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate cut double rhs = 0; success = cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, - false, genFlowCover); + false, !mip.submip); lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); if (!aggregatedPath.empty() || bestOutArcCol != -1 || @@ -364,7 +361,7 @@ void HighsPathSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, // generate reverse cut rhs = 0; success |= cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, - false, genFlowCover); + false, !mip.submip); if (success || (bestOutArcCol == -1 && bestInArcCol == -1)) break; diff --git a/highs/mip/HighsTableauSeparator.cpp b/highs/mip/HighsTableauSeparator.cpp index 577cd94e45..3abea0f3df 100644 --- a/highs/mip/HighsTableauSeparator.cpp +++ b/highs/mip/HighsTableauSeparator.cpp @@ -188,9 +188,6 @@ void HighsTableauSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, HighsInt numCuts = cutpool.getNumCuts(); const double bestScoreFac[] = {0.0025, 0.01}; - const bool genFlowCover = - mip.mipdata_->num_nodes - mip.mipdata_->num_nodes_before_run == 0 && - !mip.submip; for (const auto& fracvar : fractionalBasisvars) { if (cutpool.getNumCuts() - numCuts >= 1000) break; @@ -232,12 +229,14 @@ void HighsTableauSeparator::separateLpSolution(HighsLpRelaxation& lpRelaxation, baseRowInds.size()); double rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, genFlowCover); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, + !mip.submip); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.getCurrentAggregation(baseRowInds, baseRowVals, true); rhs = 0; - cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, genFlowCover); + cutGen.generateCut(transLp, baseRowInds, baseRowVals, rhs, false, + !mip.submip); if (mip.mipdata_->domain.infeasible()) break; lpAggregator.clear(); From c443e922d13e141666e2d96a761a2747b3edf876 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 4 Mar 2026 17:39:33 +0100 Subject: [PATCH 60/64] Fix remove function --- highs/mip/HighsTransformedLp.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 44432e8f93..15ba3505c6 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -609,7 +609,7 @@ bool HighsTransformedLp::transformSNFRelaxation( auto remove = [&](HighsInt position) { numNz--; - if (position < numNz - numBinCols - 1) { + if (position < numNz - numBinCols) { std::swap(vals[position], vals[numNz - numBinCols]); std::swap(vals[numNz - numBinCols], vals[numNz]); std::swap(inds[position], inds[numNz - numBinCols]); @@ -620,7 +620,6 @@ bool HighsTransformedLp::transformSNFRelaxation( } inds[numNz] = 0; vals[numNz] = 0; - numNz--; }; auto checkValidityVB = [&](HighsInt bincol, HighsImplications::VarBound vb, From 309f5d40afe8d39e9c6d7e840b05c50751d4425a Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 4 Mar 2026 18:17:12 +0100 Subject: [PATCH 61/64] Add minor code optimisations --- highs/mip/HighsCutGeneration.cpp | 58 +++++++++++++++++++++----------- highs/mip/HighsCutGeneration.h | 11 ++++++ highs/mip/HighsTransformedLp.cpp | 4 +-- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 64ad082872..e5995e5540 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -19,7 +19,9 @@ HighsCutGeneration::HighsCutGeneration(const HighsLpRelaxation& lpRelaxation, randgen(lpRelaxation.getMipSolver().options_mip_->random_seed + lpRelaxation.getNumLpIterations() + cutpool.getNumCuts()), feastol(lpRelaxation.getMipSolver().mipdata_->feastol), - epsilon(lpRelaxation.getMipSolver().mipdata_->epsilon) {} + epsilon(lpRelaxation.getMipSolver().mipdata_->epsilon) { + initFlowCover(); +} bool HighsCutGeneration::determineCover(bool lpSol) { if (rhs <= 10 * feastol) return false; @@ -1350,7 +1352,8 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, double flowCoverEfficacy = 0; if (genFlowCover && !lpRelaxation.getMipSolver().submip && !lpRelaxation.getMipSolver().mipdata_->continuous_cols.empty() && - lpRelaxation.getMipSolver().options_mip_->mip_cut_flow_cover) { + lpRelaxation.getMipSolver().options_mip_->mip_cut_flow_cover && + static_cast(inds_.size()) <= getMaxFlowCoverLen()) { bool hasNonBinaryBeforePreprocess = false; for (size_t i = 0; i != inds_.size(); ++i) { if (!lpRelaxation.getMipSolver().mipdata_->domain.isBinary(inds_[i]) && @@ -1601,6 +1604,10 @@ bool HighsCutGeneration::generateConflict(HighsDomain& localdomain, return cutindex != -1; } +HighsInt HighsCutGeneration::getMaxFlowCoverLen() const { + return 100 + static_cast(0.05 * lpRelaxation.numCols()); +} + void HighsCutGeneration::initSNFRelaxation() { if (static_cast(snfr.coef.size()) < rowlen) { snfr.origBinCols.resize(rowlen); @@ -1619,6 +1626,16 @@ void HighsCutGeneration::initSNFRelaxation() { snfr.numNnzs = 0; } +void HighsCutGeneration::initFlowCover() { + if (!lpRelaxation.getMipSolver().submip) { + const HighsInt maxLen = 2 * getMaxFlowCoverLen(); + fc.flowCoverItems.reserve(maxLen); + fc.flowCoverWeights.reserve(maxLen); + fc.flowCoverProfitWeightRatios.reserve(maxLen); + fc.flowCoverPerm.reserve(maxLen); + } +} + bool HighsCutGeneration::preprocessSNFRelaxation() { // preprocess the inequality before generating a single node flow relaxation. // 1. Determine the maximal activity to check for trivial redundancy @@ -1631,7 +1648,7 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { // 4. Don't consider any inequality with too many non-zeros // 5. Don't consider any inequality with too few continuous cols - HighsInt maxLen = 100 + 0.05 * (lpRelaxation.numCols()); + const HighsInt maxLen = getMaxFlowCoverLen(); if (rowlen > maxLen) return false; HighsInt numZeros = 0; @@ -1710,7 +1727,7 @@ bool HighsCutGeneration::preprocessSNFRelaxation() { bool HighsCutGeneration::computeFlowCover() { // Compute the flow cover, i.e., get sets C+ subset N+ and C- subset N- // with sum_{j in C+} u_j - sum_{j in C-} u_j = b + lambda, lambda > 0 - std::vector items(snfr.numNnzs, -1); + fc.flowCoverItems.resize(snfr.numNnzs); HighsInt nNonFlowCover = 0; HighsInt nFlowCover = 0; HighsInt nitems = 0; @@ -1725,7 +1742,7 @@ bool HighsCutGeneration::computeFlowCover() { nNonFlowCover++; } else if (fractionality(snfr.binSolval[i]) > feastol) { // x_i is fractional -> becomes an item in knapsack (decides if in cover) - items[nitems] = i; + fc.flowCoverItems[nitems] = i; nitems++; if (snfr.coef[i] == 1) { n1itemsWeight += snfr.vubCoef[i]; @@ -1766,28 +1783,31 @@ bool HighsCutGeneration::computeFlowCover() { // z_j in {0,1} for all j in N+ & N- double knapsackWeight = 0; - std::vector weights(nitems); - std::vector profitweightratios(nitems); + fc.flowCoverWeights.resize(nitems); + fc.flowCoverProfitWeightRatios.resize(nitems); for (HighsInt i = 0; i < nitems; ++i) { - weights[i] = snfr.vubCoef[items[i]]; - if (snfr.coef[items[i]] == 1) { - profitweightratios[i] = (1 - snfr.binSolval[items[i]]) / weights[i]; + fc.flowCoverWeights[i] = snfr.vubCoef[fc.flowCoverItems[i]]; + if (snfr.coef[fc.flowCoverItems[i]] == 1) { + fc.flowCoverProfitWeightRatios[i] = + (1 - snfr.binSolval[fc.flowCoverItems[i]]) / fc.flowCoverWeights[i]; } else { - profitweightratios[i] = snfr.binSolval[items[i]] / weights[i]; + fc.flowCoverProfitWeightRatios[i] = + snfr.binSolval[fc.flowCoverItems[i]] / fc.flowCoverWeights[i]; } } - std::vector perm(nitems); - std::iota(perm.begin(), perm.end(), 0); - pdqsort_branchless(perm.begin(), perm.end(), + fc.flowCoverPerm.resize(nitems); + std::iota(fc.flowCoverPerm.begin(), fc.flowCoverPerm.end(), 0); + pdqsort_branchless(fc.flowCoverPerm.begin(), fc.flowCoverPerm.end(), [&](const HighsInt a, const HighsInt b) { - return profitweightratios[a] > profitweightratios[b]; + return fc.flowCoverProfitWeightRatios[a] > + fc.flowCoverProfitWeightRatios[b]; }); // Greedily add items to knapsack for (HighsInt i = 0; i < nitems; ++i) { - const HighsInt j = perm[i]; - const HighsInt k = items[j]; - if (knapsackWeight + weights[j] < capacity) { - knapsackWeight += weights[j]; + const HighsInt j = fc.flowCoverPerm[i]; + const HighsInt k = fc.flowCoverItems[j]; + if (knapsackWeight + fc.flowCoverWeights[j] < capacity) { + knapsackWeight += fc.flowCoverWeights[j]; if (snfr.coef[k] == 1) { // j in N+ with z_j = 1 => j in N+ \ C+ snfr.flowCoverStatus[k] = -1; diff --git a/highs/mip/HighsCutGeneration.h b/highs/mip/HighsCutGeneration.h index 6263044be9..491c477c7e 100644 --- a/highs/mip/HighsCutGeneration.h +++ b/highs/mip/HighsCutGeneration.h @@ -69,6 +69,8 @@ class HighsCutGeneration { bool separateLiftedFlowCover(); + HighsInt getMaxFlowCoverLen() const; + bool preprocessSNFRelaxation(); bool tryGenerateFlowCoverCut(HighsTransformedLp& transLp, @@ -135,9 +137,18 @@ class HighsCutGeneration { double lambda; // in sum_{j in C+} u_j - sum_{j in C-} u_j = b + lambda }; + struct flowCover { + std::vector flowCoverItems; + std::vector flowCoverWeights; + std::vector flowCoverProfitWeightRatios; + std::vector flowCoverPerm; + }; + private: SNFRelaxation snfr; void initSNFRelaxation(); + flowCover fc; + void initFlowCover(); public: SNFRelaxation& getSNFRelaxation() { return snfr; } diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 15ba3505c6..8fa1177bd4 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -788,7 +788,7 @@ bool HighsTransformedLp::transformSNFRelaxation( // bound constraints are tight. this assumption may not be satisfied when // new bound changes were derived during cut generation and, therefore, we // tighten the best variable upper bound. - if (bestVub[col].first != -1 && + if (i < numNz - numBinCols && bestVub[col].first != -1 && bestVub[col].second.maxValue() > ub + mip.mipdata_->feastol) { bool redundant = false; bool infeasible = false; @@ -803,7 +803,7 @@ bool HighsTransformedLp::transformSNFRelaxation( // bound constraints are tight. this assumption may not be satisfied when // new bound changes were derived during cut generation and, therefore, we // tighten the best variable lower bound. - if (bestVlb[col].first != -1 && + if (i < numNz - numBinCols && bestVlb[col].first != -1 && bestVlb[col].second.minValue() < lb - mip.mipdata_->feastol) { bool redundant = false; bool infeasible = false; From 6ef5ffdad06c957a40211d0560149df626e9fbbe Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 6 Mar 2026 11:00:20 +0100 Subject: [PATCH 62/64] Fix two minor correctness issues --- highs/mip/HighsCutGeneration.cpp | 5 +++-- highs/mip/HighsTransformedLp.cpp | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index e5995e5540..87c138db81 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1356,7 +1356,8 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, static_cast(inds_.size()) <= getMaxFlowCoverLen()) { bool hasNonBinaryBeforePreprocess = false; for (size_t i = 0; i != inds_.size(); ++i) { - if (!lpRelaxation.getMipSolver().mipdata_->domain.isBinary(inds_[i]) && + if (inds_[i] < lpRelaxation.getMipSolver().numCol() && + !lpRelaxation.getMipSolver().mipdata_->domain.isBinary(inds_[i]) && std::abs(vals_[i]) > 10 * feastol) { hasNonBinaryBeforePreprocess = true; break; @@ -1737,7 +1738,7 @@ bool HighsCutGeneration::computeFlowCover() { assert(snfr.coef[i] == 1 || snfr.coef[i] == -1); assert(snfr.binSolval[i] >= -feastol && snfr.binSolval[i] <= 1 + feastol); // if u_i = 0 put i into N+ \ C+ or N- \ C-, i.e., not the cover - if (abs(snfr.vubCoef[i]) < feastol) { + if (std::abs(snfr.vubCoef[i]) < feastol) { snfr.flowCoverStatus[i] = -1; nNonFlowCover++; } else if (fractionality(snfr.binSolval[i]) > feastol) { diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 8fa1177bd4..53b6be00da 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -615,6 +615,7 @@ bool HighsTransformedLp::transformSNFRelaxation( std::swap(inds[position], inds[numNz - numBinCols]); std::swap(inds[numNz - numBinCols], inds[numNz]); } else { + numBinCols--; inds[position] = inds[numNz]; vals[position] = vals[numNz]; } From b83b8902ad6af0bdfd45859aa5e4f14290776831 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 6 Mar 2026 11:21:19 +0100 Subject: [PATCH 63/64] Tighten conditions when flow cover is called --- highs/mip/HighsCutGeneration.cpp | 46 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/highs/mip/HighsCutGeneration.cpp b/highs/mip/HighsCutGeneration.cpp index 87c138db81..49e0d3b959 100644 --- a/highs/mip/HighsCutGeneration.cpp +++ b/highs/mip/HighsCutGeneration.cpp @@ -1346,24 +1346,26 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, #endif // Copy data to later generate lifted simple generalized flow cover cut + const HighsMipSolver& mip = lpRelaxation.getMipSolver(); std::vector flowCoverVals; std::vector flowCoverInds; double flowCoverRhs = rhs_; double flowCoverEfficacy = 0; - if (genFlowCover && !lpRelaxation.getMipSolver().submip && - !lpRelaxation.getMipSolver().mipdata_->continuous_cols.empty() && - lpRelaxation.getMipSolver().options_mip_->mip_cut_flow_cover && + if (genFlowCover && !mip.submip && !mip.mipdata_->continuous_cols.empty() && + mip.options_mip_->mip_cut_flow_cover && static_cast(inds_.size()) <= getMaxFlowCoverLen()) { - bool hasNonBinaryBeforePreprocess = false; + bool hasContinuousColBeforePreProcess = false; for (size_t i = 0; i != inds_.size(); ++i) { - if (inds_[i] < lpRelaxation.getMipSolver().numCol() && - !lpRelaxation.getMipSolver().mipdata_->domain.isBinary(inds_[i]) && + if (inds_[i] < mip.numCol() && mip.isColContinuous(inds_[i]) && + mip.mipdata_->domain.col_upper_[inds_[i]] - + mip.mipdata_->domain.col_lower_[inds_[i]] > + feastol && std::abs(vals_[i]) > 10 * feastol) { - hasNonBinaryBeforePreprocess = true; + hasContinuousColBeforePreProcess = true; break; } } - if (!hasNonBinaryBeforePreprocess) { + if (!hasContinuousColBeforePreProcess) { genFlowCover = false; } else { flowCoverVals = vals_; @@ -1448,15 +1450,11 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, for (HighsInt i = 0; i != rowlen_; ++i) { HighsInt col = inds_[i]; viol += vals_[i] * sol[col]; - if (vals_[i] >= 0 && - sol[col] <= - lpRelaxation.getMipSolver().mipdata_->domain.col_lower_[col] + - lpRelaxation.getMipSolver().mipdata_->feastol) + if (vals_[i] >= 0 && sol[col] <= mip.mipdata_->domain.col_lower_[col] + + mip.mipdata_->feastol) continue; - if (vals_[i] < 0 && - sol[col] >= - lpRelaxation.getMipSolver().mipdata_->domain.col_upper_[col] - - lpRelaxation.getMipSolver().mipdata_->feastol) + if (vals_[i] < 0 && sol[col] >= mip.mipdata_->domain.col_upper_[col] - + mip.mipdata_->feastol) continue; sqrnorm += vals_[i] * vals_[i]; } @@ -1479,8 +1477,7 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, vals = vals_.data(); rhs = rhs_; - lpRelaxation.getMipSolver().mipdata_->debugSolution.checkCut(inds, vals, - rowlen, rhs_); + mip.mipdata_->debugSolution.checkCut(inds, vals, rowlen, rhs_); // apply cut postprocessing including scaling and removal of small // coefficients if (!postprocessCut()) return false; @@ -1488,8 +1485,8 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, vals_.resize(rowlen); inds_.resize(rowlen); - lpRelaxation.getMipSolver().mipdata_->debugSolution.checkCut( - inds_.data(), vals_.data(), rowlen, rhs_); + mip.mipdata_->debugSolution.checkCut(inds_.data(), vals_.data(), rowlen, + rhs_); // finally determine the violation of the cut in the original space HighsCDouble violation = -rhs_; @@ -1497,14 +1494,13 @@ bool HighsCutGeneration::generateCut(HighsTransformedLp& transLp, if (violation <= 10 * feastol) return false; - lpRelaxation.getMipSolver().mipdata_->domain.tightenCoefficients( - inds, vals, rowlen, rhs_); + mip.mipdata_->domain.tightenCoefficients(inds, vals, rowlen, rhs_); // if the cut is violated by a small factor above the feasibility // tolerance, add it to the cutpool - HighsInt cutindex = cutpool.addCut(lpRelaxation.getMipSolver(), inds_.data(), - vals_.data(), inds_.size(), rhs_, - integralSupport && integralCoefficients); + HighsInt cutindex = + cutpool.addCut(mip, inds_.data(), vals_.data(), inds_.size(), rhs_, + integralSupport && integralCoefficients); // only return true if cut was accepted by the cutpool, i.e. not a duplicate // of a cut already in the pool From 9d78b82e6f3a07dcd4ae514b89f88f7b39f59151 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 6 Mar 2026 11:33:30 +0100 Subject: [PATCH 64/64] Dont generate fcc if no vb used --- highs/mip/HighsTransformedLp.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/highs/mip/HighsTransformedLp.cpp b/highs/mip/HighsTransformedLp.cpp index 53b6be00da..f212ac928c 100644 --- a/highs/mip/HighsTransformedLp.cpp +++ b/highs/mip/HighsTransformedLp.cpp @@ -743,6 +743,7 @@ bool HighsTransformedLp::transformSNFRelaxation( // Place the non-binary columns to the front (all general ints relaxed) // Use vectorsum to track original row coefficients of binary columns. + HighsInt numVbsUsed = 0; HighsInt i = 0; while (i < numNz - numBinCols) { HighsInt col = inds[i]; @@ -755,6 +756,10 @@ bool HighsTransformedLp::transformSNFRelaxation( std::swap(vals[i], vals[numNz - numBinCols]); continue; } + if (lb == -kHighsInf || ub == kHighsInf) { + vectorsum.clear(); + return false; + } ++i; } @@ -765,11 +770,6 @@ bool HighsTransformedLp::transformSNFRelaxation( double lb = getLb(col); double ub = getUb(col); - if (lb == -kHighsInf || ub == kHighsInf) { - vectorsum.clear(); - return false; - } - if (ub - lb < mip.options_mip_->small_matrix_value) { rhs -= std::min(lb, ub) * vals[i]; tmpSnfrRhs -= std::min(lb, ub) * vals[i]; @@ -939,6 +939,7 @@ bool HighsTransformedLp::transformSNFRelaxation( tmpSnfrRhs -= aggrconstant + (complementvlb && inclbincolvlb ? -bincoef : 0); if (inclbincolvlb) vectorsum.values[vbcol] = 0; + numVbsUsed++; break; case BoundType::kVariableUb: // vub: y_j <= u'_j x_j + d_j. c_j coef of x_j in row @@ -973,6 +974,7 @@ bool HighsTransformedLp::transformSNFRelaxation( tmpSnfrRhs -= aggrconstant + (complementvub && inclbincolvub ? -bincoef : 0); if (inclbincolvub) vectorsum.values[vbcol] = 0; + numVbsUsed++; break; } } @@ -982,7 +984,7 @@ bool HighsTransformedLp::transformSNFRelaxation( vectorsum.clear(); snfr.rhs = static_cast(tmpSnfrRhs); - if (numNz == 0) return false; + if (numNz == 0 || numVbsUsed == 0) return false; return true; }