Skip to content

Commit e2ae806

Browse files
committed
Merge branch 'latest' of https://github.com/ERGO-Code/HiGHS into clique-table-through-presolve
2 parents 88c7fe0 + 6c4741b commit e2ae806

2 files changed

Lines changed: 105 additions & 18 deletions

File tree

‎check/TestPresolve.cpp‎

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,14 +1276,100 @@ TEST_CASE("test-non-stop-initial-sweep", "[highs_test_presolve]") {
12761276
h.resetGlobalScheduler(true);
12771277
}
12781278

1279+
TEST_CASE("test-duplicate-row", "[highs_test_presolve]") {
1280+
HighsLp lp;
1281+
lp.num_col_ = 3;
1282+
lp.num_row_ = 2;
1283+
lp.col_cost_ = {2, -1, 1};
1284+
lp.col_lower_ = {0, 0, 0};
1285+
lp.col_upper_ = {kHighsInf, 2, kHighsInf};
1286+
lp.a_matrix_.start_ = {0, 2, 4, 6};
1287+
lp.a_matrix_.index_ = {0, 1, 0, 1, 0, 1};
1288+
Highs h;
1289+
h.setOptionValue("output_flag", dev_run);
1290+
h.setOptionValue("presolve_rule_logging", true);
1291+
h.setOptionValue(kSolverString, kIpxString);
1292+
1293+
lp.row_lower_ = {-kHighsInf, -4};
1294+
lp.row_upper_ = {0, 0};
1295+
for (HighsInt bound_flip = 0; bound_flip < 2; bound_flip++) {
1296+
lp.a_matrix_.value_ = {1, -4, -1, 4, 3, -12};
1297+
for (HighsInt lhs_flip = 0; lhs_flip < 2; lhs_flip++) {
1298+
// Base model has parallel rows
1299+
//
1300+
// r0: x - y + 3z <= 0
1301+
// r1: -4 <= -4x + 4y - 12z <= 0
1302+
//
1303+
// After x is fixed at 0 (dominated column) these parallel rows
1304+
// are deduced to be the doubleton equation
1305+
//
1306+
// -y + 3z = 0
1307+
//
1308+
// with r1 being removed - presumably because the elimination
1309+
// multiplier (-0.25) is then less than 1 in magnitude.
1310+
//
1311+
// This allows z to be substituted and y is then fixed at 2,
1312+
// reducing the problem to empty
1313+
//
1314+
// In substitution, the row dual for the equation is 1/3
1315+
//
1316+
// * r0 is made basic because its lower bound was tightened, and
1317+
// the sign of the dual means that it can't be nonbasic so it
1318+
// can't be nonbasic
1319+
//
1320+
// * r1 is made nonbasic and the dual is now correctly scaled by
1321+
// _multiplying_ by (-0.25) - since the row values are larger,
1322+
// the dual must be reduced in magnitude - and should be viewed
1323+
// as being at its upper bound
1324+
//
1325+
// However, in DuplicateRow::undo, computeRowDualAndStatus was
1326+
// previously only passed "tightened", and no indication of
1327+
// whether that was at its lower or upper bound. Hence it made
1328+
// r1 nonbasic at its lower bound when it tightens the lower
1329+
// bound of r0, due to the negative sign of the scale factor.
1330+
//
1331+
// However, it's the upper bound on r1 that tightens the lower
1332+
// bound on r0. By passing the direction sign -1 (+1) if r0 is
1333+
// tightened at its lower (upper) bound, r1 is now set to be
1334+
// nonbasic at the correct bound.
1335+
//
1336+
// The spurious lower bound on r1 is necessary to expose the
1337+
// consequences of the error since the simplex solver only needs
1338+
// the basic/nonbasic status to set values of variables to
1339+
// bound, unless they are ranged, in which case the
1340+
// HighsBasisStatus being lower or upper is used.
1341+
//
1342+
// The multiple passes ensure code coverage in
1343+
// DuplicateRow::undo - all four cases are passed to
1344+
// computeRowDualAndStatus - and test the correctness of both
1345+
// primal-dual and basis postsolve.
1346+
//
1347+
h.setOptionValue("run_crossover", kHighsOnString);
1348+
for (HighsInt k = 0; k < 2; k++) {
1349+
REQUIRE(h.passModel(lp) == HighsStatus::kOk);
1350+
REQUIRE(h.run() == HighsStatus::kOk);
1351+
1352+
REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal);
1353+
h.setOptionValue("run_crossover", kHighsOffString);
1354+
}
1355+
lp.a_matrix_.value_ = {-1, 4, 1, -4, -3, 12};
1356+
}
1357+
lp.row_lower_ = {0, 0};
1358+
lp.row_upper_ = {kHighsInf, 4};
1359+
}
1360+
1361+
h.resetGlobalScheduler(true);
1362+
}
1363+
12791364
/*
12801365
TEST_CASE("test-fuzzing", "[highs_test_presolve]") {
12811366
Highs h;
12821367
// h.setOptionValue("output_flag", dev_run);
12831368
// if (dev_run) {
12841369
printf("\n====================\nWithout presolve\n====================\n");
12851370
1286-
const std::string model = "issue-010";
1371+
const std::string model = "issue-002";
1372+
const bool reduces_to_empty = true;
12871373
std::string model_file = std::string(HIGHS_DIR) + "/build/OscarFuzzing/" +
12881374
model + "/" + model + ".mps";
12891375
@@ -1303,24 +1389,23 @@ TEST_CASE("test-fuzzing", "[highs_test_presolve]") {
13031389
std::string options_file =
13041390
std::string(HIGHS_DIR) + "/build/OscarFuzzing/" + model + "/options.txt";
13051391
REQUIRE(h.readOptions(options_file) == HighsStatus::kOk);
1306-
1307-
// REQUIRE(h.setOptionValue("presolve_rule_off", 1 << kPresolveRuleColStuffing) == HighsStatus::kOk);
1308-
13091392
HighsOptions options = h.getOptions();
13101393
1311-
printf("\n====================\nPresolved LP\n====================\n");
1312-
h.presolve();
1394+
if (!reduces_to_empty) {
1395+
printf("\n====================\nPresolved LP\n====================\n");
1396+
h.presolve();
13131397
1314-
HighsLp lp = h.getPresolvedLp();
1398+
HighsLp lp = h.getPresolvedLp();
13151399
1316-
h.clear();
1317-
h.passModel(lp);
1400+
h.clear();
1401+
h.passModel(lp);
13181402
1319-
h.setOptionValue(kPresolveString, kHighsOffString);
1403+
h.setOptionValue(kPresolveString, kHighsOffString);
13201404
1321-
h.run();
1322-
h.writeSolution("", 1);
1323-
h.clear();
1405+
h.run();
1406+
h.writeSolution("", 1);
1407+
h.clear();
1408+
}
13241409
13251410
printf(
13261411
"\n====================\nPresolve no crossover\n====================\n");
@@ -1331,6 +1416,8 @@ TEST_CASE("test-fuzzing", "[highs_test_presolve]") {
13311416
h.setOptionValue("log_dev_level", 1);
13321417
h.writeOptions("", true);
13331418
1419+
// h.setOptionValue(kSolverString, kIpxString);
1420+
13341421
h.run();
13351422
h.writeSolution("", 1);
13361423

‎highs/presolve/HighsPostsolveStack.cpp‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -495,12 +495,12 @@ void HighsPostsolveStack::DuplicateRow::undo(const HighsOptions& options,
495495
: computeStatus(solution.row_dual[row], basis.row_status[row],
496496
options.dual_feasibility_tolerance);
497497

498-
auto computeRowDualAndStatus = [&](bool tightened) {
498+
auto computeRowDualAndStatus = [&](bool tightened, const HighsInt dir) {
499499
if (tightened) {
500500
solution.row_dual[duplicateRow] =
501-
solution.row_dual[row] / duplicateRowScale;
501+
solution.row_dual[row] * duplicateRowScale;
502502
if (basis.valid) {
503-
if (duplicateRowScale > 0)
503+
if (dir * duplicateRowScale > 0)
504504
basis.row_status[duplicateRow] = HighsBasisStatus::kUpper;
505505
else
506506
basis.row_status[duplicateRow] = HighsBasisStatus::kLower;
@@ -529,10 +529,10 @@ void HighsPostsolveStack::DuplicateRow::undo(const HighsOptions& options,
529529
// if row sits on its upper bound, and the row upper bound was
530530
// tightened using the parallel row we make the row basic and
531531
// transfer its dual value to the parallel row with the proper scale
532-
computeRowDualAndStatus(rowUpperTightened);
532+
computeRowDualAndStatus(rowUpperTightened, 1);
533533
break;
534534
case HighsBasisStatus::kLower:
535-
computeRowDualAndStatus(rowLowerTightened);
535+
computeRowDualAndStatus(rowLowerTightened, -1);
536536
break;
537537
default:
538538
assert(false);

0 commit comments

Comments
 (0)