diff --git a/check/TestMipSolver.cpp b/check/TestMipSolver.cpp index d6a4d35d0d4..ae6da640fe1 100644 --- a/check/TestMipSolver.cpp +++ b/check/TestMipSolver.cpp @@ -1605,3 +1605,39 @@ TEST_CASE("issue-3118a", "[highs_test_mip_solver]") { highs.resetGlobalScheduler(true); } + +TEST_CASE("issue-3173", "[highs_test_mip_solver]") { + // HPresolve::checkColBounds() treats a column as fixed when its bounds are + // equal only within the feasibility tolerance. The condition bounds the + // resulting perturbation of the row activities, but not of the objective, so + // a column that is nearly fixed in the constraints but has a large cost can + // be pinned at the wrong end of its range, losing |cost| * (ub - lb). + // + // On this model the column has bounds differing by 3.4e-8 and a cost of + // -7.3e10, so pinning it at the lower bound costs 2496.5. Presolve reduces + // the model to empty and reports that value as optimal, with default options + // and no branch-and-bound nodes. + // + // The optimal objective was verified independently of the MIP solver by + // enumerating all 2^8 binary assignments and solving the resulting LP for + // each. + const std::string filename = + std::string(HIGHS_DIR) + "/check/instances/3173-1.mps"; + const double optimal_objective = -39829.5816585168; + Highs highs; + highs.setOptionValue("output_flag", dev_run); + REQUIRE(highs.readModel(filename) == HighsStatus::kOk); + REQUIRE(highs.run() == HighsStatus::kOk); + REQUIRE(highs.getModelStatus() == HighsModelStatus::kOptimal); + // The column is still pinned within the feasibility tolerance, so allow a + // small relative error - but nothing like the 6.3% the defect produces. + const double relative_error = + std::fabs(highs.getInfo().objective_function_value - optimal_objective) / + std::max(1.0, std::fabs(optimal_objective)); + if (dev_run) + printf("issue-3173: objective %g, require %g, relative error %g\n", + highs.getInfo().objective_function_value, optimal_objective, + relative_error); + REQUIRE(relative_error < 1e-6); + highs.resetGlobalScheduler(true); +} diff --git a/check/instances/3173-1.mps b/check/instances/3173-1.mps new file mode 100644 index 00000000000..01374c10d64 --- /dev/null +++ b/check/instances/3173-1.mps @@ -0,0 +1,288 @@ +NAME HIGHSBUG +ROWS + N COST + L r0 + L r1 + L r2 + L r3 + L r4 + L r5 + L r6 + L r7 + L r8 + L r9 + L r10 + L r11 + L r12 + L r13 + L r14 + L r15 + L r16 + L r17 + L r18 + L r19 + L r20 + L r21 + L r22 + L r23 + L r24 + L r25 + L r26 + L r27 + L r28 + L r29 + L r30 + L r31 + L r32 + L r33 + L r34 + L r35 + L r36 + L r37 + L r38 + L r39 +COLUMNS + MARKER 'MARKER' 'INTORG' + c0 COST -1000000 + c0 r0 -128 + c0 r7 128 + c0 r13 64 + c0 r15 10 + c0 r17 2.5 + c0 r19 -2.5 + c0 r22 10 + c0 r25 -12 + c0 r33 -3 + c0 r39 17 + c1 COST -1 + c1 r0 -20 + c1 r2 2 + c1 r3 -20 + c1 r5 -2 + c1 r11 128 + c1 r18 -2.5 + c1 r21 -1 + c1 r23 -16 + c1 r30 -40 + c1 r34 -1.5 + c1 r35 -1 + c1 r36 -2.5 + c2 COST -10000 + c2 r2 -32 + c2 r10 3 + c2 r22 -8 + c2 r29 1 + c2 r37 -10 + c3 COST -0.25 + c3 r4 -1.5 + c3 r9 -100 + c3 r12 -3 + c3 r13 50 + c3 r20 -0.5 + c3 r22 -10 + c3 r29 1 + c3 r32 -300 + c3 r34 -10 + c4 COST -0.5 + c4 r1 4 + c4 r8 -3 + c4 r9 -20 + c4 r11 -10 + c4 r16 50 + c4 r20 1 + c4 r21 -16 + c4 r34 50 + c4 r38 -100 + c5 COST -5 + c5 r3 100 + c5 r5 16 + c5 r6 128 + c5 r7 -100 + c5 r13 -10 + c5 r15 5 + c5 r18 5 + c5 r20 -64 + c5 r23 5 + c5 r28 1 + c5 r36 -62.5 + c6 COST -5 + c6 r0 -10 + c6 r1 -8 + c6 r3 -32 + c6 r21 64 + c6 r23 -20 + c6 r26 -4 + c6 r28 1 + c6 r31 -10 + c6 r35 -2.5 + c6 r37 20 + c6 r39 8 + c7 COST -5 + c7 r1 1.5 + c7 r4 19 + c7 r14 64 + c7 r24 -200 + c7 r27 -2 + c7 r29 1 + MARKER 'MARKER' 'INTEND' + c8 COST -10000 + c8 r0 64 + c8 r31 1 + c9 COST -2.5 + c9 r0 -20 + c9 r1 3 + c10 COST -2.5 + c10 r1 -100 + c10 r2 2 + c10 r33 0.5 + c10 r35 20 + c11 COST -0.5 + c11 r2 -16 + c11 r3 100 + c12 COST -1000000 + c12 r3 -32 + c12 r4 64 + c12 r34 -2 + c13 COST -0.5 + c13 r4 -5 + c13 r5 100 + c14 COST -100 + c14 r5 -16 + c14 r6 64 + c14 r24 1 + c14 r25 1 + c14 r26 1 + c14 r27 1 + c15 COST -10 + c15 r6 -10 + c15 r7 5 + c16 COST -100 + c16 r7 -0.5 + c16 r8 20 + c17 COST -1 + c17 r8 -4 + c17 r9 32 + c17 r32 1 + c17 r37 -8 + c18 COST -10000 + c18 r9 -3 + c18 r10 50 + c18 r37 128 + c19 COST -100 + c19 r10 -8 + c19 r11 16 + c20 COST -0.25 + c20 r11 -5 + c20 r12 50 + c20 r35 1 + c21 COST -100 + c21 r12 -128 + c21 r13 16 + c22 COST -2.5 + c22 r13 -64 + c22 r14 64 + c23 COST -10 + c23 r14 -100 + c23 r15 50 + c24 COST -1 + c24 r15 -3 + c24 r16 4 + c24 r34 20 + c24 r35 -128 + c25 COST -100 + c25 r16 -50 + c25 r17 1 + c25 r30 1 + c26 COST -10 + c26 r17 -16 + c26 r18 128 + c26 r36 32 + c27 COST -1 + c27 r18 -0.5 + c27 r19 8 + c28 COST -100 + c28 r19 -64 + c28 r20 20 + c29 COST -10000 + c29 r20 -10 + c29 r21 10 + c29 r38 100 + c30 COST -0.5 + c30 r21 -1 + c30 r22 100 + c31 COST -2.5 + c31 r22 -2.5 + c31 r23 1.5 + c32 COST -9.9999999999999995e-07 + c32 r23 -8 +RHS + RHS r0 12 + RHS r1 20 + RHS r2 5 + RHS r5 1 + RHS r7 12 + RHS r8 33 + RHS r9 7 + RHS r10 9 + RHS r11 20 + RHS r12 5 + RHS r13 5 + RHS r14 2 + RHS r15 7 + RHS r16 7 + RHS r17 3 + RHS r20 7 + RHS r22 2 + RHS r23 20 + RHS r24 40 + RHS r25 14 + RHS r26 9.0999999999999996 + RHS r27 5.915 + RHS r28 1 + RHS r29 1 + RHS r30 10 + RHS r31 2 + RHS r32 10 + RHS r33 2 + RHS r34 2 + RHS r35 2 + RHS r36 9 + RHS r37 20 + RHS r38 2 + RHS r39 5 +RANGES +BOUNDS + UP BND c0 1 + UP BND c1 1 + UP BND c2 1 + UP BND c3 1 + UP BND c4 1 + UP BND c5 1 + UP BND c6 1 + UP BND c7 1 + UP BND c8 20 + UP BND c9 1000 + UP BND c10 1000 + UP BND c11 20 + UP BND c12 50 + UP BND c13 20 + UP BND c14 200 + UP BND c15 100 + UP BND c16 100 + UP BND c17 200 + UP BND c18 100 + UP BND c19 500 + UP BND c20 100 + UP BND c21 1000 + UP BND c22 1000 + UP BND c23 50 + UP BND c24 100 + UP BND c25 50 + UP BND c26 500 + UP BND c27 500 + UP BND c28 50 + UP BND c29 500 + UP BND c30 200 + UP BND c31 200 + UP BND c32 20 +ENDATA diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 954e432d2f0..5aed724e285 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -2226,9 +2226,20 @@ HPresolve::Result HPresolve::checkColBounds(HighsInt col, bool* isFixed) { assert(!colDeleted[col]); double boundDiff = model->col_upper_[col] - model->col_lower_[col]; if (isFixed != nullptr) *isFixed = false; + // The bounds are only equal to within the feasibility tolerance, so fixing + // the column perturbs the row activities by at most + // getMaxAbsColVal(col) * boundDiff and the objective by at most + // |cost| * boundDiff. Both have to be negligible: a column can be nearly + // fixed in the constraints while still being worth a great deal in the + // objective, and fixing it at the wrong end of its range then loses that + // amount. Note that for an empty column the row activity term is zero + // whatever boundDiff is, so without the objective term such a column would + // always be treated as fixed here, pre-empting emptyCol() which fixes it at + // the bound that is best for the objective. if (boundDiff <= primal_feastol && (boundDiff <= options->small_matrix_value || - getMaxAbsColVal(col) * boundDiff <= primal_feastol)) { + (getMaxAbsColVal(col) * boundDiff <= primal_feastol && + std::fabs(model->col_cost_[col]) * boundDiff <= primal_feastol))) { // check for primal infeasibility if (boundDiff < -primal_feastol) return Result::kPrimalInfeasible; // check for unboundedness