@@ -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/*
12801365TEST_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-009";
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,23 +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- // REQUIRE(h.setOptionValue("presolve_rule_off", 1 <<
1307- kPresolveRuleColStuffing) == HighsStatus::kOk); HighsOptions options =
1308- h.getOptions();
1392+ HighsOptions options = h.getOptions();
13091393
1310- printf("\n====================\nPresolved LP\n====================\n");
1311- h.presolve();
1394+ if (!reduces_to_empty) {
1395+ printf("\n====================\nPresolved LP\n====================\n");
1396+ h.presolve();
13121397
1313- HighsLp lp = h.getPresolvedLp();
1398+ HighsLp lp = h.getPresolvedLp();
13141399
1315- h.clear();
1316- h.passModel(lp);
1400+ h.clear();
1401+ h.passModel(lp);
13171402
1318- h.setOptionValue(kPresolveString, kHighsOffString);
1403+ h.setOptionValue(kPresolveString, kHighsOffString);
13191404
1320- h.run();
1321- h.writeSolution("", 1);
1322- h.clear();
1405+ h.run();
1406+ h.writeSolution("", 1);
1407+ h.clear();
1408+ }
13231409
13241410 printf(
13251411 "\n====================\nPresolve no crossover\n====================\n");
@@ -1330,6 +1416,8 @@ h.getOptions();
13301416 h.setOptionValue("log_dev_level", 1);
13311417 h.writeOptions("", true);
13321418
1419+ // h.setOptionValue(kSolverString, kIpxString);
1420+
13331421 h.run();
13341422 h.writeSolution("", 1);
13351423
0 commit comments