diff --git a/check/TestMipSolver.cpp b/check/TestMipSolver.cpp index 9792ab771a0..0758b19b905 100644 --- a/check/TestMipSolver.cpp +++ b/check/TestMipSolver.cpp @@ -2541,6 +2541,24 @@ TEST_CASE("issue-3433", "[highs_test_mip_solver]") { highs.resetGlobalScheduler(true); } +TEST_CASE("issue-3451", "[highs_test_mip_solver]") { + // reduced version of the model in issue 3451. IPX stops with "no progress" + // before centring when computing the analytic centre, but the KKT check + // reports the solution as optimal. Fixing columns at bounds of this + // uncentred point used to make the feasible model infeasible. The same + // cause led to a wrong optimal objective in issue 3452 + std::string filename = + std::string(HIGHS_DIR) + "/check/instances/issue-3451.mps"; + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.setOptionValue("mip_rel_gap", 0); + highs.readModel(filename); + const HighsModelStatus require_model_status = HighsModelStatus::kOptimal; + const double optimal_objective = -135.865250363; + solve(highs, kHighsOnString, require_model_status, optimal_objective); + highs.resetGlobalScheduler(true); +} + TEST_CASE("conflict-pool-sync-empty-conflict", "[highs_test_mip_solver]") { // an empty conflict, which can be derived when a proof is violated by the // global bounds alone, is stored at the end of the conflict entries and diff --git a/check/instances/issue-3451.mps b/check/instances/issue-3451.mps new file mode 100644 index 00000000000..3e4d13caf3b --- /dev/null +++ b/check/instances/issue-3451.mps @@ -0,0 +1,369 @@ +NAME issue-3451 +ROWS + N obj + G r0 + L r1 + E r2 + E r3 + L r4 + E r5 + E r6 + G r7 + L r8 + E r9 + E r11 + G r12 + G r13 + G r14 + L r16 + L r17 + L r18 + E r19 + G r20 + E r23 +COLUMNS + MARK0000 'MARKER' 'INTORG' + x0 obj -1 + x0 r0 -8 + x0 r2 -9 + x0 r4 -2 + x0 r5 -5 + x0 r7 5 + x0 r9 9 + x0 r14 -4 + x0 r18 8 + x0 r20 -8 + x1 obj 10 + x1 r4 -6 + x1 r8 8 + x1 r12 7 + x2 obj -1 + x2 r1 1 + x2 r2 -5 + x2 r8 -4 + x2 r16 -8 + x2 r17 -7 + x2 r19 -8 + x2 r20 -4 + x4 obj 6 + x4 r0 -6 + x4 r4 4 + x4 r5 1 + x4 r8 5 + x4 r18 -4 + x5 obj 9 + x5 r2 4 + x5 r3 -3 + x5 r6 -9 + x5 r20 -8 + x6 obj -5 + x6 r2 -5 + x6 r7 -3 + x6 r8 -3 + x6 r11 3 + x6 r14 5 + x6 r17 1 + x6 r23 3 + x7 obj 2 + x7 r4 7 + x7 r5 -8 + x7 r6 3 + x7 r8 6 + x7 r12 -2 + x7 r14 2 + x7 r17 -9 + x7 r19 7 + MARK0001 'MARKER' 'INTEND' + x8 obj 6 + x8 r1 -4 + x8 r2 -9 + x8 r3 -3 + x8 r17 -5 + x9 obj -3 + x9 r2 4 + x9 r7 -5 + x9 r14 -4 + x9 r16 -1 + x9 r19 -9 + MARK0002 'MARKER' 'INTORG' + x10 obj -1.0000091470480299 + x10 r3 9 + x10 r6 3 + x10 r7 8 + x10 r8 2 + x10 r9 6 + x10 r11 -1 + x10 r12 1 + x10 r23 -1 + x11 obj 6 + x11 r1 3 + x11 r3 4 + x12 obj 10 + x12 r1 -3 + x12 r4 -7 + x12 r9 -2 + x12 r11 -2 + x12 r12 -1 + x12 r13 5 + x12 r16 -2 + x12 r23 -2 + MARK0003 'MARKER' 'INTEND' + x13 obj -3 + x13 r0 -9 + x13 r3 -1 + x13 r4 5 + x13 r7 2 + x13 r13 4 + x13 r16 1 + MARK0004 'MARKER' 'INTORG' + x14 obj -5 + x14 r0 -2 + x14 r1 5 + x14 r4 9 + x14 r5 6 + x14 r14 -2 + x14 r17 4 + x14 r18 -2 + x14 r19 -5 + x15 obj -2 + x15 r0 -1 + x15 r16 -9 + x15 r20 4 + x16 obj -4 + x16 r0 2 + x16 r2 -4 + x16 r4 8 + x16 r5 -7 + x16 r9 6 + x16 r17 3 + x17 obj 10 + x17 r0 3 + x17 r1 5 + x17 r17 -5 + x18 obj -4 + x18 r1 9 + x18 r2 2 + x18 r6 -7 + x18 r8 -9 + x18 r11 6 + x18 r14 -7 + x18 r17 8 + x18 r23 6 + x19 obj -9 + x19 r1 9 + x19 r3 -6 + x19 r7 8 + x19 r8 9 + x19 r14 9 + x19 r17 2 + x19 r20 3 + MARK0005 'MARKER' 'INTEND' + x20 obj 5 + x20 r4 -8 + x20 r6 2 + x20 r8 4 + x20 r13 8 + x21 obj 7 + x21 r2 9 + x21 r4 2 + x21 r14 5 + MARK0006 'MARKER' 'INTORG' + x22 obj 10 + x22 r2 -2 + x22 r7 8 + x22 r13 -5 + x22 r16 1 + x22 r18 -6 + MARK0007 'MARKER' 'INTEND' + x23 obj -7 + x23 r3 -5 + x23 r5 -2 + x23 r7 1 + x23 r14 6 + x23 r16 -1 + x23 r17 6 + x23 r19 5 + x23 r20 -8 + MARK0008 'MARKER' 'INTORG' + x24 obj -7 + x24 r1 -1 + x24 r6 -7 + x24 r7 1 + x24 r13 5 + x25 obj -8 + x25 r1 1 + x25 r4 -6 + x25 r6 2 + x25 r17 -5 + x26 obj -9 + x26 r1 7 + x26 r2 4 + x26 r7 7 + x26 r9 1 + x26 r11 3 + x26 r12 -5 + x26 r13 9 + x26 r16 2 + x26 r17 1 + x26 r23 3 + x27 obj -6 + x27 r6 6 + x27 r9 -6 + x27 r16 -2 + x27 r17 1 + MARK0009 'MARKER' 'INTEND' + x28 obj -1 + x28 r5 5 + x28 r12 6 + MARK0010 'MARKER' 'INTORG' + x29 obj 8 + x29 r5 -9 + x29 r12 8 + x29 r19 -3 + x30 r3 9 + x30 r7 -2 + x30 r12 -3 + x30 r13 7 + x30 r16 1 + x31 obj -8 + x31 r0 6 + x31 r2 -5 + x31 r7 2 + x31 r9 -5 + x31 r13 9 + x31 r19 8 + x31 r20 -5 + MARK0011 'MARKER' 'INTEND' + x32 obj -2 + x32 r1 6 + x32 r9 6 + x32 r11 -8 + x32 r14 -4 + x32 r23 -8 + x33 obj 5 + x33 r5 -4 + x33 r8 4 + x33 r12 -7 + x33 r14 -1 + MARK0012 'MARKER' 'INTORG' + x35 obj -5 + x35 r0 -9 + x35 r7 -5 + x35 r9 5 + x35 r13 6 + x35 r14 -7 + x35 r17 -6 + x35 r18 2 + MARK0013 'MARKER' 'INTEND' + x36 r6 9 + x36 r9 -8 + x36 r19 -6 + x36 r20 6 + MARK0014 'MARKER' 'INTORG' + x37 obj -8 + x37 r0 -1 + x37 r3 5 + x37 r11 8 + x37 r13 -5 + x37 r23 7.9999538430192381 + MARK0015 'MARKER' 'INTEND' + x40 obj 7 + x40 r3 1 + x40 r5 9 + x40 r11 -4 + x40 r13 5 + x40 r18 -5 + x40 r20 -3 + x40 r23 -4 + MARK0016 'MARKER' 'INTORG' + x42 obj 8 + x42 r1 -2 + x42 r4 -5 + x42 r5 6 + x42 r7 4 + x42 r9 -6 + x42 r16 4 + x42 r18 1 + x42 r20 4 + x43 obj -3 + x43 r0 -8 + x43 r1 2 + x43 r3 -4 + x43 r5 -1 + x43 r8 3 + x43 r11 -1 + x43 r18 2 + x43 r20 5 + x43 r23 -1 + x44 obj -7 + x44 r1 -9 + x44 r4 -5 + x44 r9 7 + x44 r13 1 + x44 r16 2 + x44 r19 7 + MARK0017 'MARKER' 'INTEND' +RHS + RHS_V obj 20.630962959642641 + RHS_V r0 16 + RHS_V r1 257 + RHS_V r2 -79.564016153248986 + RHS_V r3 -119.66503469642078 + RHS_V r4 136 + RHS_V r5 -47.334829482709921 + RHS_V r6 -50.398430863341822 + RHS_V r7 85.782306880204203 + RHS_V r8 228 + RHS_V r9 35.670521359842411 + RHS_V r11 -33.09179631635169 + RHS_V r12 -51 + RHS_V r13 192 + RHS_V r14 142 + RHS_V r16 -54.435386239591594 + RHS_V r17 -172.97788360025527 + RHS_V r18 -36 + RHS_V r19 29.202867840254626 + RHS_V r20 -101 + RHS_V r23 -33.09179631635169 +BOUNDS + UI BOUND x0 4 + BV BOUND x1 + BV BOUND x2 + UI BOUND x4 9 + UI BOUND x5 16 + UI BOUND x6 12 + UI BOUND x7 17 + UP BOUND x8 19 + UP BOUND x9 7 + UI BOUND x10 7 + UI BOUND x11 8 + UI BOUND x12 2 + UP BOUND x13 2 + UI BOUND x14 17 + UI BOUND x15 4 + UI BOUND x16 14 + UI BOUND x17 17 + BV BOUND x18 + UI BOUND x19 13 + UP BOUND x20 8 + UP BOUND x21 12 + UI BOUND x22 7 + UP BOUND x23 11 + UI BOUND x24 6 + UI BOUND x25 8 + UI BOUND x26 14 + UI BOUND x27 8 + UP BOUND x28 7 + UI BOUND x29 14 + UI BOUND x30 2 + UI BOUND x31 8 + UP BOUND x32 4 + UP BOUND x33 2 + UI BOUND x35 15 + UP BOUND x36 3 + UI BOUND x37 7 + UP BOUND x40 8 + UI BOUND x42 3 + UI BOUND x43 13 + UI BOUND x44 6 +ENDATA diff --git a/docs/src/structures/enums.md b/docs/src/structures/enums.md index f6962f629d2..f8d4530f998 100644 --- a/docs/src/structures/enums.md +++ b/docs/src/structures/enums.md @@ -63,6 +63,16 @@ This defines the nature of any basis information: * `kBasisValidityInvalid`: There is no basisn information * `kBasisValidityValid`: The basis information is valid +## CentringStatus + +This defines the outcome of any centring by the interior point +solver. The model status only indicates whether the LP is solved, so +it can be optimal even if the solution is not centred: + + * `kCentringStatusNone`: Centring was not performed, either because it was not requested or because the interior point solver did not reach optimality + * `kCentringStatusFailed`: Centring was performed, but the solution could not be centred + * `kCentringStatusSuccess`: The solution is centred + ## HighsModelStatus This defines the status of the model after a call to `run` diff --git a/docs/src/structures/structs/HighsInfo.md b/docs/src/structures/structs/HighsInfo.md index 2c7d42a698f..d1a71a15247 100644 --- a/docs/src/structures/structs/HighsInfo.md +++ b/docs/src/structures/structs/HighsInfo.md @@ -38,6 +38,10 @@ Scalar information about a solved model is communicated via an instance of the H - Comparison with [BasisValidity](@ref) gives the status of any basis information - Type: integer +## centring\_status +- Comparison with [CentringStatus](@ref) gives the outcome of any centring by the interior point solver when the `run_centring` option is true +- Type: integer + ## objective\_function\_value - The optimal value of the objective function - Type: double diff --git a/highs/ipm/IpxWrapper.cpp b/highs/ipm/IpxWrapper.cpp index 77b0f7da887..f9318173791 100644 --- a/highs/ipm/IpxWrapper.cpp +++ b/highs/ipm/IpxWrapper.cpp @@ -198,6 +198,13 @@ HighsStatus solveLpIpx(const HighsOptions& options, HighsTimer& timer, if (report_solve_data) reportSolveData(options.log_options, ipx_info); highs_info.ipm_iteration_count += (HighsInt)ipx_info.iter; highs_info.crossover_iteration_count += (HighsInt)ipx_info.updates_crossover; + // Record the outcome of any centring, since the model status only + // indicates whether the LP is solved. Centring is not tried if the + // IPM stops before reaching optimality + highs_info.centring_status = + !ipx_info.centring_tried ? kCentringStatusNone + : ipx_info.centring_success ? kCentringStatusSuccess + : kCentringStatusFailed; // If not solved... if (solve_status != IPX_STATUS_solved) { diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index b7e66ed24eb..e57a2a09794 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -155,6 +155,14 @@ enum BasisValidity { kBasisValidityMax = kBasisValidityValid }; +enum CentringStatus { + kCentringStatusNone = 0, + kCentringStatusFailed, + kCentringStatusSuccess, + kCentringStatusMin = kCentringStatusNone, + kCentringStatusMax = kCentringStatusSuccess +}; + const std::string kHighsBasisFileV1 = "v1"; // Deprecated const std::string kHighsBasisFileV2 = "v2"; diff --git a/highs/lp_data/HighsInfo.cpp b/highs/lp_data/HighsInfo.cpp index 16c0b3a7b19..318c18d9751 100644 --- a/highs/lp_data/HighsInfo.cpp +++ b/highs/lp_data/HighsInfo.cpp @@ -23,6 +23,7 @@ void HighsInfo::invalidate() { pdlp_iteration_count = -1; qp_iteration_count = -1; basis_validity = kBasisValidityInvalid; + centring_status = kCentringStatusNone; objective_function_value = 0; mip_dual_bound = 0; mip_gap = kHighsInf; @@ -87,6 +88,7 @@ bool HighsInfo::equal(const HighsInfo& info_) const { return false; if (info_.dual_solution_status != this->dual_solution_status) return false; if (info_.basis_validity != this->basis_validity) return false; + if (info_.centring_status != this->centring_status) return false; if (info_.objective_function_value != this->objective_function_value) return false; if (info_.mip_dual_bound != this->mip_dual_bound) return false; diff --git a/highs/lp_data/HighsInfo.h b/highs/lp_data/HighsInfo.h index 6a19e069b73..19bf7f23972 100644 --- a/highs/lp_data/HighsInfo.h +++ b/highs/lp_data/HighsInfo.h @@ -103,6 +103,7 @@ struct HighsInfoStruct { HighsInt primal_solution_status; HighsInt dual_solution_status; HighsInt basis_validity; + HighsInt centring_status; double objective_function_value; double mip_dual_bound; double mip_gap; @@ -234,6 +235,12 @@ class HighsInfo : public HighsInfoStruct { advanced, &basis_validity, kBasisValidityInvalid); records.push_back(record_int); + record_int = new InfoRecordInt( + "centring_status", + "IPM centring status: 0 => None; 1 => Failed; 2 => Success", advanced, + ¢ring_status, kCentringStatusNone); + records.push_back(record_int); + record_double = new InfoRecordDouble("objective_function_value", "Objective function value", advanced, &objective_function_value, 0); diff --git a/highs/mip/HighsMipSolverData.cpp b/highs/mip/HighsMipSolverData.cpp index 9c1eb8f5cc9..a23cd48060a 100644 --- a/highs/mip/HighsMipSolverData.cpp +++ b/highs/mip/HighsMipSolverData.cpp @@ -486,6 +486,12 @@ void HighsMipSolverData::startAnalyticCenterComputation( } if (HighsInt(sol.size()) != mipsolver.numCol()) return; analyticCenterStatus = ipm.getModelStatus(); + // The model status only indicates that the LP is solved, which is + // true for any feasible point since the costs are zero. Fixing + // variables in finishAnalyticCenterComputation requires the point to + // be centred, so only accept an optimal status if centring succeeded + if (ipm.getInfo().centring_status != kCentringStatusSuccess) + analyticCenterStatus = HighsModelStatus::kUnknown; analyticCenter = sol; }); } diff --git a/highspy/highs_bindings.cpp b/highspy/highs_bindings.cpp index 7bd55cc73fd..7e5988b92fb 100644 --- a/highspy/highs_bindings.cpp +++ b/highspy/highs_bindings.cpp @@ -1187,6 +1187,7 @@ PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) { &HighsInfo::primal_solution_status) .def_readwrite("dual_solution_status", &HighsInfo::dual_solution_status) .def_readwrite("basis_validity", &HighsInfo::basis_validity) + .def_readwrite("centring_status", &HighsInfo::centring_status) .def_readwrite("objective_function_value", &HighsInfo::objective_function_value) .def_readwrite("mip_dual_bound", &HighsInfo::mip_dual_bound) diff --git a/highspy/highspy/_core/__init__.pyi b/highspy/highspy/_core/__init__.pyi index 00b30a82e77..4d43982f1d2 100644 --- a/highspy/highspy/_core/__init__.pyi +++ b/highspy/highspy/_core/__init__.pyi @@ -198,6 +198,7 @@ class HighsIisInfo: class HighsInfo: basis_validity: int + centring_status: int crossover_iteration_count: int dual_solution_status: int ipm_iteration_count: int