Skip to content

Commit f8b218d

Browse files
authored
Merge pull request #2531 from fwesselm/dualBoundTightening
Dual fixing
2 parents 26c42ae + b80e8c7 commit f8b218d

File tree

3 files changed

+340
-7
lines changed

3 files changed

+340
-7
lines changed

check/TestPresolve.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,3 +838,27 @@ TEST_CASE("presolve-egout-ac", "[highs_test_presolve]") {
838838

839839
h.resetGlobalScheduler(true);
840840
}
841+
842+
TEST_CASE("dual-bound-tightening", "[highs_test_presolve]") {
843+
std::string model_file =
844+
std::string(HIGHS_DIR) + "/check/instances/gesa2.mps";
845+
846+
Highs highs;
847+
highs.setOptionValue("output_flag", dev_run);
848+
highs.readModel(model_file);
849+
850+
// complement variables to get code coverage
851+
HighsLp lp = highs.getLp();
852+
std::transform(lp.a_matrix_.value_.begin(), lp.a_matrix_.value_.end(),
853+
lp.a_matrix_.value_.begin(), [](double v) { return -v; });
854+
std::transform(lp.col_cost_.begin(), lp.col_cost_.end(), lp.col_cost_.begin(),
855+
[](double v) { return -v; });
856+
std::transform(lp.col_upper_.begin(), lp.col_upper_.end(),
857+
lp.col_upper_.begin(), [](double v) { return -v; });
858+
std::transform(lp.col_lower_.begin(), lp.col_lower_.end(),
859+
lp.col_lower_.begin(), [](double v) { return -v; });
860+
std::swap(lp.col_lower_, lp.col_upper_);
861+
862+
highs.passModel(lp);
863+
REQUIRE(highs.presolve() == HighsStatus::kOk);
864+
}

highs/presolve/HPresolve.cpp

Lines changed: 312 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ bool HPresolve::isRanged(HighsInt row) const {
243243
model->row_upper_[row] != kHighsInf);
244244
}
245245

246+
bool HPresolve::isRedundant(HighsInt row) const {
247+
return (impliedRowBounds.getSumLower(row) >=
248+
model->row_lower_[row] - primal_feastol &&
249+
impliedRowBounds.getSumUpper(row) <=
250+
model->row_upper_[row] + primal_feastol);
251+
}
252+
246253
bool HPresolve::isImpliedEquationAtLower(HighsInt row) const {
247254
// if the implied lower bound on a row dual is strictly positive then the row
248255
// is an implied equation (using its lower bound) due to complementary
@@ -3225,12 +3232,19 @@ HPresolve::Result HPresolve::singletonCol(HighsPostsolveStack& postsolve_stack,
32253232
HPRESOLVE_CHECKED_CALL(detectDominatedCol(postsolve_stack, col, false));
32263233
if (colDeleted[col]) return Result::kOk;
32273234

3235+
// check if variable is implied integer
32283236
if (mipsolver != nullptr)
32293237
HPRESOLVE_CHECKED_CALL(
32303238
static_cast<Result>(convertImpliedInteger(col, row)));
32313239

3240+
// dual fixing
3241+
HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col));
3242+
if (colDeleted[col]) return Result::kOk;
3243+
3244+
// update column implied bounds
32323245
updateColImpliedBounds(row, col, colCoef);
32333246

3247+
// update row dual implied bounds
32343248
if (model->integrality_[col] != HighsVarType::kInteger)
32353249
updateRowDualImpliedBounds(row, col, colCoef);
32363250

@@ -3300,10 +3314,7 @@ HPresolve::Result HPresolve::rowPresolve(HighsPostsolveStack& postsolve_stack,
33003314
};
33013315

33023316
auto checkRowRedundant = [&](HighsInt row) {
3303-
if (impliedRowBounds.getSumLower(row) >=
3304-
model->row_lower_[row] - primal_feastol &&
3305-
impliedRowBounds.getSumUpper(row) <=
3306-
model->row_upper_[row] + primal_feastol) {
3317+
if (isRedundant(row)) {
33073318
// row is redundant
33083319
int presolveRule =
33093320
rowsize[row] != 0 ? kPresolveRuleRedundantRow : kPresolveRuleEmptyRow;
@@ -4365,6 +4376,7 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack,
43654376
isUpperImplied(col),
43664377
impliedDualRowBounds.getNumInfSumLowerOrig(col));
43674378

4379+
// check if variable is implied integer
43684380
HPRESOLVE_CHECKED_CALL(static_cast<Result>(convertImpliedInteger(col)));
43694381

43704382
// shift integral variables to have a lower bound of zero
@@ -4383,12 +4395,15 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack,
43834395
transformColumn(postsolve_stack, col, -1.0, model->col_upper_[col]);
43844396
}
43854397
}
4386-
4387-
if (model->integrality_[col] == HighsVarType::kInteger) return Result::kOk;
43884398
}
43894399

4400+
// dual fixing
4401+
HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col));
4402+
if (colDeleted[col]) return Result::kOk;
4403+
43904404
// update dual implied bounds of all rows in given column
4391-
updateRowDualImpliedBounds(col);
4405+
if (model->integrality_[col] != HighsVarType::kInteger)
4406+
updateRowDualImpliedBounds(col);
43924407

43934408
return Result::kOk;
43944409
}
@@ -4523,6 +4538,296 @@ HPresolve::Result HPresolve::detectDominatedCol(
45234538
return Result::kOk;
45244539
}
45254540

4541+
HPresolve::Result HPresolve::dualFixing(HighsPostsolveStack& postsolve_stack,
4542+
HighsInt col) {
4543+
// fix variables or tighten bounds using dual arguments
4544+
// see section 4.4 "Dual fixing, substitution and bound strengthening",
4545+
// Achterberg et al., Presolve Reductions in Mixed Integer Programming,
4546+
// INFORMS Journal on Computing 32(2):473-506.
4547+
assert(!colDeleted[col]);
4548+
4549+
// return if variable is already fixed
4550+
if (model->col_lower_[col] == model->col_upper_[col]) return Result::kOk;
4551+
4552+
// struct for storing non-zeros while searching for substitutions
4553+
struct nonZeros {
4554+
HighsInt row;
4555+
double jval;
4556+
double kval;
4557+
};
4558+
std::vector<nonZeros> nzs;
4559+
nzs.reserve(colsize[col]);
4560+
4561+
// lambda for checking whether a row provides an implied lower bound
4562+
// (direction = 1) or implied upper bound (direction = -1)
4563+
auto hasImpliedBound = [&](HighsInt row, HighsInt direction, double val) {
4564+
return ((direction * val < 0 && model->row_upper_[row] != kHighsInf) ||
4565+
(direction * val > 0 && model->row_lower_[row] != -kHighsInf));
4566+
};
4567+
4568+
// lambda for computing locks
4569+
auto computeLocks = [&](HighsInt col, HighsInt& numDownLocks,
4570+
HighsInt& numUpLocks, HighsInt& downLockRow,
4571+
HighsInt& upLockRow) {
4572+
// initialise
4573+
numDownLocks = 0;
4574+
numUpLocks = 0;
4575+
downLockRow = -1;
4576+
upLockRow = -1;
4577+
4578+
// consider objective function
4579+
if (model->col_cost_[col] > 0)
4580+
numUpLocks++;
4581+
else if (model->col_cost_[col] < 0)
4582+
numDownLocks++;
4583+
4584+
// check coefficients
4585+
for (const auto& nz : getColumnVector(col)) {
4586+
// update number of locks
4587+
if (hasImpliedBound(nz.index(), HighsInt{1}, nz.value())) {
4588+
// implied lower bound -> downlock
4589+
numDownLocks++;
4590+
downLockRow = nz.index();
4591+
}
4592+
if (hasImpliedBound(nz.index(), HighsInt{-1}, nz.value())) {
4593+
// implied upper bound -> uplock
4594+
numUpLocks++;
4595+
upLockRow = nz.index();
4596+
}
4597+
4598+
// stop early if there are locks in both directions, since the variable
4599+
// cannot be fixed in this case.
4600+
if (numDownLocks > 1 && numUpLocks > 1) break;
4601+
}
4602+
};
4603+
4604+
// lambda for variable substitution
4605+
auto substituteCol = [&](HighsInt col, HighsInt row, HighsInt direction,
4606+
double colBound, double otherColBound) {
4607+
// check lhs and rhs for finiteness
4608+
bool lhsFinite = model->row_lower_[row] != -kHighsInf;
4609+
bool rhsFinite = model->row_upper_[row] != kHighsInf;
4610+
4611+
// use storeRow and getStoredRow since getRowVector's rowroot[row] would be
4612+
// overwritten by subsequent findNonZero calls, which would produce
4613+
// undefined behavior
4614+
storeRow(row);
4615+
for (const auto& rowNz : getStoredRow()) {
4616+
// skip column index that was passed to this lambda
4617+
if (rowNz.index() == col) continue;
4618+
4619+
// only consider non-fixed binary variables
4620+
if (model->integrality_[rowNz.index()] != HighsVarType::kInteger ||
4621+
model->col_lower_[rowNz.index()] != 0.0 ||
4622+
model->col_upper_[rowNz.index()] != 1.0)
4623+
continue;
4624+
4625+
// skip binary variable if setting it to its lower bound does not make the
4626+
// row redundant
4627+
if ((rhsFinite && impliedRowBounds.getResidualSumUpperOrig(
4628+
row, rowNz.index(), rowNz.value()) >
4629+
model->row_upper_[row] + primal_feastol) ||
4630+
(lhsFinite && impliedRowBounds.getResidualSumLowerOrig(
4631+
row, rowNz.index(), rowNz.value()) <
4632+
model->row_lower_[row] - primal_feastol))
4633+
continue;
4634+
4635+
// now compute the implied lower bound (direction = 1) or implied upper
4636+
// bound (direction = -1) provided that the binary variable is set to its
4637+
// upper bound. store triplets (row, nonzero, nonzero) in a vector to
4638+
// speed up search
4639+
nzs.clear();
4640+
if (colsize[col] < colsize[rowNz.index()]) {
4641+
for (const auto& colNz : getColumnVector(col)) {
4642+
// skip non-zeros that do not yield an implied bound
4643+
if (!hasImpliedBound(colNz.index(), direction, colNz.value()))
4644+
continue;
4645+
HighsInt nzPos = findNonzero(colNz.index(), rowNz.index());
4646+
if (nzPos == -1) continue;
4647+
nzs.push_back({colNz.index(), colNz.value(), Avalue[nzPos]});
4648+
}
4649+
} else {
4650+
for (const auto& colNz : getColumnVector(rowNz.index())) {
4651+
HighsInt nzPos = findNonzero(colNz.index(), col);
4652+
if (nzPos == -1) continue;
4653+
// skip non-zeros that do not yield an implied bound
4654+
if (!hasImpliedBound(colNz.index(), direction, Avalue[nzPos]))
4655+
continue;
4656+
nzs.push_back({colNz.index(), Avalue[nzPos], colNz.value()});
4657+
}
4658+
}
4659+
4660+
// find best bound
4661+
double bestBound = -kHighsInf;
4662+
for (const auto& triplet : nzs) {
4663+
// compute implied bound from row given that the binary variable is at
4664+
// its upper bound
4665+
double rhs = 0.0;
4666+
double residual = 0.0;
4667+
if (direction * triplet.jval < 0) {
4668+
rhs = model->row_upper_[triplet.row];
4669+
residual = impliedRowBounds.getResidualSumLowerOrig(triplet.row, col,
4670+
triplet.jval) +
4671+
std::max(triplet.kval, 0.0);
4672+
} else {
4673+
rhs = model->row_lower_[triplet.row];
4674+
residual = impliedRowBounds.getResidualSumUpperOrig(triplet.row, col,
4675+
triplet.jval) +
4676+
std::min(triplet.kval, 0.0);
4677+
}
4678+
// direction = 1: compute implied lower bound
4679+
// direction = -1: compute implied upper bound
4680+
double candidateBound = direction * (rhs - residual) / triplet.jval;
4681+
// remember best bound (note the sign switch for direction < 0 above)
4682+
bestBound = std::max(bestBound, candidateBound);
4683+
}
4684+
4685+
// round bound
4686+
if (model->integrality_[col] != HighsVarType::kContinuous)
4687+
bestBound = std::ceil(bestBound - primal_feastol);
4688+
4689+
// check if lower / upper bound is implied
4690+
if (bestBound >= direction * colBound - primal_feastol) {
4691+
// substitute variable
4692+
double offset = otherColBound;
4693+
double scale = colBound - otherColBound;
4694+
postsolve_stack.doubletonEquation(
4695+
-1, col, rowNz.index(), 1.0, -scale, offset, model->col_lower_[col],
4696+
model->col_upper_[col], 0.0, false, false,
4697+
HighsPostsolveStack::RowType::kEq, HighsEmptySlice());
4698+
markColDeleted(col);
4699+
substitute(col, rowNz.index(), offset, scale);
4700+
HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack));
4701+
break;
4702+
}
4703+
}
4704+
return Result::kOk;
4705+
};
4706+
4707+
// lambda for computing tighter bounds
4708+
auto hasTighterBound = [&](HighsInt col, HighsInt direction,
4709+
double currentBound, double& newBound) {
4710+
// return if objective coefficient has wrong sign
4711+
if (direction * model->col_cost_[col] < 0) return false;
4712+
4713+
// do not accept huge bounds
4714+
double hugeBound = primal_feastol / kHighsTiny;
4715+
4716+
// initialise
4717+
newBound = -kHighsInf;
4718+
currentBound *= direction;
4719+
4720+
for (const auto& nz : getColumnVector(col)) {
4721+
// get row index and coefficient
4722+
HighsInt row = nz.index();
4723+
double val = nz.value();
4724+
4725+
// skip rows that are already redundant
4726+
if (isRedundant(row)) continue;
4727+
4728+
// initialise
4729+
double rhs = 0.0;
4730+
double residual = 0.0;
4731+
4732+
if (direction * val < 0.0) {
4733+
// skip rows with infinite rhs
4734+
rhs = model->row_upper_[row];
4735+
if (rhs == kHighsInf) continue;
4736+
4737+
// compute residual
4738+
residual = impliedRowBounds.getResidualSumUpperOrig(row, col, val);
4739+
if (residual == kHighsInf) return false;
4740+
} else {
4741+
// skip rows with infinite lhs
4742+
rhs = model->row_lower_[row];
4743+
if (rhs == -kHighsInf) continue;
4744+
4745+
// compute residual
4746+
residual = impliedRowBounds.getResidualSumLowerOrig(row, col, val);
4747+
if (residual == -kHighsInf) return false;
4748+
}
4749+
4750+
// compute bound
4751+
double candidateBound =
4752+
direction *
4753+
static_cast<double>((static_cast<HighsCDouble>(rhs) -
4754+
static_cast<HighsCDouble>(residual)) /
4755+
val);
4756+
4757+
// round up to make sure that all rows are redundant
4758+
candidateBound = std::ceil(candidateBound - primal_feastol);
4759+
4760+
// take largest bound
4761+
newBound = std::max(newBound, candidateBound);
4762+
4763+
// stop looking if bound is too large
4764+
if (newBound >= currentBound - primal_feastol || newBound > hugeBound)
4765+
return false;
4766+
}
4767+
4768+
// return if no bound was found
4769+
if (newBound == -kHighsInf) return false;
4770+
4771+
// flip sign
4772+
newBound *= direction;
4773+
return true;
4774+
};
4775+
4776+
// compute locks
4777+
HighsInt numDownLocks = 0;
4778+
HighsInt numUpLocks = 0;
4779+
HighsInt downLockRow = -1;
4780+
HighsInt upLockRow = -1;
4781+
computeLocks(col, numDownLocks, numUpLocks, downLockRow, upLockRow);
4782+
4783+
// check if variable can be fixed
4784+
if (numDownLocks == 0 || numUpLocks == 0) {
4785+
// fix variable
4786+
if (numDownLocks == 0 ? fixColToLowerOrUnbounded(postsolve_stack, col)
4787+
: fixColToUpperOrUnbounded(postsolve_stack, col)) {
4788+
// handle unboundedness
4789+
presolve_status_ = HighsPresolveStatus::kUnboundedOrInfeasible;
4790+
return Result::kDualInfeasible;
4791+
}
4792+
} else {
4793+
if (mipsolver != nullptr && model->col_lower_[col] != -kHighsInf &&
4794+
model->col_upper_[col] != kHighsInf) {
4795+
// try substitution
4796+
if (numDownLocks == 1 && downLockRow != -1) {
4797+
HPRESOLVE_CHECKED_CALL(substituteCol(col, downLockRow, HighsInt{1},
4798+
model->col_upper_[col],
4799+
model->col_lower_[col]));
4800+
} else if (numUpLocks == 1 && upLockRow != -1) {
4801+
HPRESOLVE_CHECKED_CALL(substituteCol(col, upLockRow, HighsInt{-1},
4802+
model->col_lower_[col],
4803+
model->col_upper_[col]));
4804+
}
4805+
if (colDeleted[col]) return Result::kOk;
4806+
}
4807+
// try to strengthen bounds
4808+
double newBound = 0.0;
4809+
if (hasTighterBound(col, HighsInt{1}, model->col_upper_[col], newBound)) {
4810+
// do not make bounds inconsistent
4811+
newBound = std::max(newBound, model->col_lower_[col]);
4812+
// update upper bound
4813+
// only modify bounds on continuous variables if it leads to fixing
4814+
if (model->integrality_[col] != HighsVarType::kContinuous ||
4815+
newBound == model->col_lower_[col])
4816+
changeColUpper(col, newBound);
4817+
} else if (hasTighterBound(col, HighsInt{-1}, model->col_lower_[col],
4818+
newBound)) {
4819+
// do not make bounds inconsistent
4820+
newBound = std::min(newBound, model->col_upper_[col]);
4821+
// update lower bound
4822+
// only modify bounds on continuous variables if it leads to fixing
4823+
if (model->integrality_[col] != HighsVarType::kContinuous ||
4824+
newBound == model->col_upper_[col])
4825+
changeColLower(col, newBound);
4826+
}
4827+
}
4828+
return Result::kOk;
4829+
}
4830+
45264831
HPresolve::Result HPresolve::initialRowAndColPresolve(
45274832
HighsPostsolveStack& postsolve_stack) {
45284833
// do a full scan over the rows as the singleton arrays and the changed row

0 commit comments

Comments
 (0)