diff --git a/check/CMakeLists.txt b/check/CMakeLists.txt index 366a4c5924..f258f4c514 100644 --- a/check/CMakeLists.txt +++ b/check/CMakeLists.txt @@ -57,6 +57,7 @@ if (NOT FAST_BUILD OR ALL_TESTS) TestHighsSparseMatrix.cpp TestHSet.cpp TestICrash.cpp + TestIis.cpp TestIpm.cpp TestIpx.cpp TestLogging.cpp diff --git a/check/TestIis.cpp b/check/TestIis.cpp new file mode 100644 index 0000000000..d68dd8388e --- /dev/null +++ b/check/TestIis.cpp @@ -0,0 +1,438 @@ +#include +#include + +#include "HCheckConfig.h" +#include "Highs.h" +#include "catch.hpp" +//#include "io/FilereaderLp.h" + +const bool dev_run = true; +const double inf = kHighsInf; + +void testIis(const std::string& model, const HighsIis& iis); + +void testMps(std::string& model, const HighsInt iis_strategy, + const HighsModelStatus require_model_status = + HighsModelStatus::kInfeasible); + +TEST_CASE("lp-incompatible-bounds", "[iis]") { + // LP has row0 and col2 with inconsistent bounds. + // + // When prioritising rows, row0 and its constituent columns (1, 2) should be + // found + // + // When prioritising columns, col2 and its constituent rows (0, 1) should be + // found + HighsLp lp; + lp.num_col_ = 3; + lp.num_row_ = 2; + lp.col_cost_ = {0, 0, 0}; + lp.col_lower_ = {0, 0, 0}; + lp.col_upper_ = {1, 1, -1}; + lp.row_lower_ = {1, 0}; + lp.row_upper_ = {0, 1}; + lp.a_matrix_.format_ = MatrixFormat::kRowwise; + lp.a_matrix_.start_ = {0, 2, 4}; + lp.a_matrix_.index_ = {1, 2, 0, 2}; + lp.a_matrix_.value_ = {1, 1, 1, 1}; + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.passModel(lp); + REQUIRE(highs.run() == HighsStatus::kOk); + REQUIRE(highs.getModelStatus() == HighsModelStatus::kInfeasible); + highs.setOptionValue("iis_strategy", kIisStrategyFromLpRowPriority); + HighsIis iis; + REQUIRE(highs.getIis(iis) == HighsStatus::kOk); + REQUIRE(iis.col_index_.size() == 0); + REQUIRE(iis.row_index_.size() == 1); + REQUIRE(iis.row_index_[0] == 0); + REQUIRE(iis.row_bound_[0] == kIisBoundStatusBoxed); + highs.setOptionValue("iis_strategy", kIisStrategyFromLpColPriority); + REQUIRE(highs.getIis(iis) == HighsStatus::kOk); + REQUIRE(iis.col_index_.size() == 1); + REQUIRE(iis.row_index_.size() == 0); + REQUIRE(iis.col_index_[0] == 2); + REQUIRE(iis.col_bound_[0] == kIisBoundStatusBoxed); +} + +TEST_CASE("lp-empty-infeasible-row", "[iis]") { + // Second row is empty, with bounds of [1, 2] + const HighsInt empty_row = 1; + HighsLp lp; + lp.num_col_ = 2; + lp.num_row_ = 3; + lp.col_cost_ = {0, 0}; + lp.col_lower_ = {0, 0}; + lp.col_upper_ = {inf, inf}; + lp.row_lower_ = {-inf, 1, -inf}; + lp.row_upper_ = {8, 2, 9}; + lp.a_matrix_.format_ = MatrixFormat::kRowwise; + lp.a_matrix_.start_ = {0, 2, 2, 4}; + lp.a_matrix_.index_ = {0, 1, 0, 1}; + lp.a_matrix_.value_ = {2, 1, 1, 3}; + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.passModel(lp); + REQUIRE(highs.run() == HighsStatus::kOk); + REQUIRE(highs.getModelStatus() == HighsModelStatus::kInfeasible); + HighsIis iis; + REQUIRE(highs.getIis(iis) == HighsStatus::kOk); + REQUIRE(iis.col_index_.size() == 0); + REQUIRE(iis.row_index_.size() == 1); + REQUIRE(iis.row_index_[0] == empty_row); + REQUIRE(iis.row_bound_[0] == kIisBoundStatusLower); + REQUIRE(highs.changeRowBounds(empty_row, -2, -1) == HighsStatus::kOk); + REQUIRE(highs.run() == HighsStatus::kOk); + REQUIRE(highs.getModelStatus() == HighsModelStatus::kInfeasible); + REQUIRE(highs.getIis(iis) == HighsStatus::kOk); + REQUIRE(iis.col_index_.size() == 0); + REQUIRE(iis.row_index_.size() == 1); + REQUIRE(iis.row_index_[0] == empty_row); + REQUIRE(iis.row_bound_[0] == kIisBoundStatusUpper); +} + +TEST_CASE("lp-get-iis", "[iis]") { + HighsLp lp; + lp.num_col_ = 2; + lp.num_row_ = 3; + lp.col_cost_ = {0, 0}; + lp.col_lower_ = {0, 0}; + lp.col_upper_ = {inf, inf}; + lp.row_lower_ = {-inf, -inf, -inf}; + lp.row_upper_ = {8, 9, -2}; + lp.a_matrix_.format_ = MatrixFormat::kRowwise; + lp.a_matrix_.start_ = {0, 2, 4, 6}; + lp.a_matrix_.index_ = {0, 1, 0, 1, 0, 1}; + lp.a_matrix_.value_ = {2, 1, 1, 3, 1, 1}; + // lp.col_name_ = {"Col0", "Col1"}; + // lp.row_name_ = {"Row0", "Row1", "Row2"}; + Highs highs; + highs.setOptionValue("output_flag", dev_run); + highs.passModel(lp); + REQUIRE(highs.run() == HighsStatus::kOk); + REQUIRE(highs.getModelStatus() == HighsModelStatus::kInfeasible); + highs.setOptionValue("iis_strategy", kIisStrategyFromLpRowPriority); + HighsIis iis; + REQUIRE(highs.getIis(iis) == HighsStatus::kOk); + REQUIRE(iis.col_index_.size() == 2); + REQUIRE(iis.row_index_.size() == 1); + REQUIRE(iis.col_index_[0] == 0); + REQUIRE(iis.col_index_[1] == 1); + REQUIRE(iis.row_index_[0] == 2); +} + +TEST_CASE("lp-get-iis-woodinfe", "[iis]") { + std::string model = "woodinfe"; + testMps(model, kIisStrategyFromLpRowPriority); + // testMps(model, kIisStrategyFromRayRowPriority); +} + +TEST_CASE("lp-get-iis-galenet", "[iis]") { + // Dual ray corresponds to constraints + // + // r0: 0 <= c0 + c1 - c3 - c4 <=0 + // + // r1: 20 <= c2 + c3 + // + // r2: 30 <= c4 + // + // Where + // + // 0 <= c0 <= 10 + // + // 0 <= c1 <= 10 + // + // 0 <= c2 <= 2 + // + // 0 <= c3 <= 20 + // + // 0 <= c4 <= 30 + // + // This is infeasible since c4 >= 30 and c4 <= 30 fices c4 = 30, + // then c0 + c1 >= c3 + c4 >= 30 cannot be satisfied due to the + // upper bounds of 10 on these variables + // + // r1 can be removed and infeasibility is retained, but not r0 or r2 + // + // The upper bound on r0 can be removed + // + // The lower bounds on c0 and c1 can be removed, but not their upper + // bounds + // + // c2 can be removed, as it is empty once r1 is removed + // + // c3 can be removed, as the value of c4 is sufficient to make r0 + // infeasible + // + // The bounds on c4 can be removed, since it's the lower bound from + // r2 that makes r0 infeasible + // + // Hence only empty columns can be removed + std::string model = "galenet"; + testMps(model, kIisStrategyFromLpRowPriority); + // testMps(model, kIisStrategyFromRayRowPriority); +} + +TEST_CASE("lp-get-iis-avgas", "[iis]") { + std::string model = "avgas"; + // For the whole LP calculation the elasticity filter only + // identified feasibility, so the model status is not set + testMps(model, kIisStrategyFromLpRowPriority, HighsModelStatus::kNotset); + // For the ray calculation the model is solved, so its status is + // known + // testMps(model, kIisStrategyFromRayRowPriority, + // HighsModelStatus::kOptimal); +} + +TEST_CASE("lp-feasibility-relaxation", "[iis]") { + // Using infeasible LP from AMPL documentation + HighsLp lp; + lp.num_col_ = 2; + lp.num_row_ = 3; + lp.col_cost_ = {1, -2}; + lp.col_lower_ = {5, -inf}; + lp.col_upper_ = {inf, inf}; + lp.col_names_ = {"X", "Y"}; + lp.row_lower_ = {2, -inf, -inf}; + lp.row_upper_ = {inf, 1, 20}; + lp.row_names_ = {"R0", "R1", "R2"}; + lp.a_matrix_.start_ = {0, 3, 6}; + lp.a_matrix_.index_ = {0, 1, 2, 0, 1, 2}; + lp.a_matrix_.value_ = {-1, -3, 20, 21, 2, 1}; + lp.integrality_ = {HighsVarType::kInteger, HighsVarType::kInteger}; + Highs h; + h.setOptionValue("output_flag", dev_run); + const HighsSolution& solution = h.getSolution(); + h.passModel(lp); + // h.run(); + + const bool all_tests = false; + const bool test0 = false || all_tests; + const bool test1 = false || all_tests; + const bool test2 = true || all_tests; + const bool test3 = false || all_tests; + if (test0) { + // Vanilla feasibility relaxation + if (dev_run) + printf( + "==============================\n" + "Vanilla feasibility relaxation\n" + "==============================\n"); + REQUIRE(h.feasibilityRelaxation(1, 1, 1) == HighsStatus::kOk); + // Should get solution (1, 1) + h.writeSolution("", 1); + REQUIRE(solution.col_value[0] == 1); + REQUIRE(solution.col_value[1] == 1); + } + + if (test1) { + // Now we want to force all variable lower bounds to be respected + if (dev_run) + printf( + "========================\n" + "Respect all lower bounds\n" + "========================\n"); + h.feasibilityRelaxation(-1, 1, 1); + // Should get solution (5, 1) + h.writeSolution("", 1); + REQUIRE(solution.col_value[0] == 5); + REQUIRE(solution.col_value[1] == 1); + } + + if (test2) { + if (dev_run) + printf( + "===============================\n" + "Local penalties RHS {1, -1, 10}\n" + "===============================\n"); + // Now test local penalties + // + // constraint 0: normal weight + // + // constraint 1: cannot be violated + // + // constraint 2: rather not violate + // + std::vector local_rhs_penalty = {1, -1, 10}; + h.feasibilityRelaxation(1, 1, 0, nullptr, nullptr, + local_rhs_penalty.data()); + // Should get slacks (-3, 4, 0) + h.writeSolution("", 1); + REQUIRE(solution.row_value[0] == lp.row_lower_[0] - 3); + REQUIRE(solution.row_value[1] == lp.row_upper_[1] - 4); + REQUIRE(solution.row_value[2] == lp.row_upper_[2]); + } + + if (test3) { + if (dev_run) + printf( + "==============================\n" + "Local penalties RHS {10, 1, 1}\n" + "==============================\n"); + // + // constraint 0: rather not violate + // + // constraint 1: normal weight + // + // constraint 2: normal weight + // + std::vector local_rhs_penalty = {10, 1, 1}; + // Should get row activities (18, 2, -1) + REQUIRE(solution.row_value[0] == 18); + REQUIRE(solution.row_value[1] == 2); + REQUIRE(solution.row_value[2] == -1); + } +} + +void testIis(const std::string& model, const HighsIis& iis) { + HighsModelStatus model_status; + std::string model_file = + std::string(HIGHS_DIR) + "/check/instances/" + model + ".mps"; + HighsInt num_iis_col = iis.col_index_.size(); + HighsInt num_iis_row = iis.row_index_.size(); + + Highs highs; + highs.setOptionValue("output_flag", false); + REQUIRE(highs.readModel(model_file) == HighsStatus::kOk); + const HighsLp& incumbent_lp = highs.getLp(); + HighsLp lp = highs.getLp(); + // Zero the objective + lp.col_cost_.assign(lp.num_col_, 0); + REQUIRE(highs.changeColsCost(0, lp.num_col_ - 1, lp.col_cost_.data()) == + HighsStatus::kOk); + + // Save the bounds + std::vector iis_col_lower; + std::vector iis_col_upper; + std::vector iis_row_lower; + std::vector iis_row_upper; + for (HighsInt iisCol = 0; iisCol < num_iis_col; iisCol++) { + HighsInt iCol = iis.col_index_[iisCol]; + iis_col_lower.push_back(lp.col_lower_[iCol]); + iis_col_upper.push_back(lp.col_upper_[iCol]); + } + for (HighsInt iisRow = 0; iisRow < num_iis_row; iisRow++) { + HighsInt iRow = iis.row_index_[iisRow]; + iis_row_lower.push_back(lp.row_lower_[iRow]); + iis_row_upper.push_back(lp.row_upper_[iRow]); + } + + // Free all the columns and rows + lp.col_lower_.assign(lp.num_col_, -inf); + lp.col_upper_.assign(lp.num_col_, inf); + lp.row_lower_.assign(lp.num_row_, -inf); + lp.row_upper_.assign(lp.num_row_, inf); + // Restore the bounds for the IIS columns and rows + for (HighsInt iisCol = 0; iisCol < num_iis_col; iisCol++) { + HighsInt iCol = iis.col_index_[iisCol]; + lp.col_lower_[iCol] = iis_col_lower[iisCol]; + lp.col_upper_[iCol] = iis_col_upper[iisCol]; + } + for (HighsInt iisRow = 0; iisRow < num_iis_row; iisRow++) { + HighsInt iRow = iis.row_index_[iisRow]; + lp.row_lower_[iRow] = iis_row_lower[iisRow]; + lp.row_upper_[iRow] = iis_row_upper[iisRow]; + } + + REQUIRE(highs.passModel(lp) == HighsStatus::kOk); + REQUIRE(highs.run() == HighsStatus::kOk); + REQUIRE(highs.getModelStatus() == HighsModelStatus::kInfeasible); + + for (HighsInt iisCol = 0; iisCol < num_iis_col; iisCol++) { + HighsInt iCol = iis.col_index_[iisCol]; + HighsInt iis_bound = iis.col_bound_[iisCol]; + const double lower = lp.col_lower_[iCol]; + const double upper = lp.col_upper_[iCol]; + double to_lower = lower; + double to_upper = upper; + REQUIRE(iis_bound != kIisBoundStatusDropped); + REQUIRE(iis_bound != kIisBoundStatusNull); + REQUIRE(iis_bound != kIisBoundStatusBoxed); + if (iis_bound == kIisBoundStatusLower) { + to_lower = -inf; + } else if (iis_bound == kIisBoundStatusUpper) { + to_upper = inf; + } else if (iis_bound == kIisBoundStatusFree) { + if (dev_run) + printf("IIS Col %2d (LP col %6d) status %s\n", int(iisCol), int(iCol), + iis.iisBoundStatusToString(iis_bound).c_str()); + continue; + } + REQUIRE(highs.changeColBounds(iCol, to_lower, to_upper) == + HighsStatus::kOk); + REQUIRE(highs.run() == HighsStatus::kOk); + model_status = highs.getModelStatus(); + if (dev_run) + printf( + "IIS Col %2d (LP col %6d) status %s: removal yields model status " + "%s\n", + int(iisCol), int(iCol), iis.iisBoundStatusToString(iis_bound).c_str(), + highs.modelStatusToString(model_status).c_str()); + REQUIRE(model_status == HighsModelStatus::kOptimal); + REQUIRE(highs.changeColBounds(iCol, lower, upper) == HighsStatus::kOk); + } + for (HighsInt iisRow = 0; iisRow < num_iis_row; iisRow++) { + HighsInt iRow = iis.row_index_[iisRow]; + HighsInt iis_bound = iis.row_bound_[iisRow]; + const double lower = lp.row_lower_[iRow]; + const double upper = lp.row_upper_[iRow]; + double to_lower = lower; + double to_upper = upper; + REQUIRE(iis_bound != kIisBoundStatusDropped); + REQUIRE(iis_bound != kIisBoundStatusNull); + REQUIRE(iis_bound != kIisBoundStatusFree); + REQUIRE(iis_bound != kIisBoundStatusBoxed); + if (iis_bound == kIisBoundStatusLower) { + to_lower = -inf; + } else if (iis_bound == kIisBoundStatusUpper) { + to_upper = inf; + } + REQUIRE(highs.changeRowBounds(iRow, to_lower, to_upper) == + HighsStatus::kOk); + REQUIRE(highs.run() == HighsStatus::kOk); + model_status = highs.getModelStatus(); + if (dev_run) + printf( + "IIS Row %2d (LP row %6d) status %s: removal yields model status " + "%s\n", + int(iisRow), int(iRow), iis.iisBoundStatusToString(iis_bound).c_str(), + highs.modelStatusToString(model_status).c_str()); + // if (model_status != HighsModelStatus::kOptimal) + // highs.writeSolution("", kSolutionStylePretty); + REQUIRE(model_status == HighsModelStatus::kOptimal); + REQUIRE(highs.changeRowBounds(iRow, lower, upper) == HighsStatus::kOk); + } +} + +void testMps(std::string& model, const HighsInt iis_strategy, + const HighsModelStatus require_model_status) { + std::string model_file = + std::string(HIGHS_DIR) + "/check/instances/" + model + ".mps"; + Highs highs; + highs.setOptionValue("output_flag", dev_run); + + REQUIRE(highs.readModel(model_file) == HighsStatus::kOk); + // if (iis_strategy == kIisStrategyFromRayRowPriority || + // iis_strategy == kIisStrategyFromRayColPriority) { + // // For a ray strategy, solve the LP first + // REQUIRE(highs.run() == HighsStatus::kOk); + // } + highs.setOptionValue("iis_strategy", iis_strategy); + HighsIis iis; + REQUIRE(highs.getIis(iis) == HighsStatus::kOk); + HighsInt num_iis_col = iis.col_index_.size(); + HighsInt num_iis_row = iis.row_index_.size(); + HighsModelStatus model_status = highs.getModelStatus(); + REQUIRE(model_status == require_model_status); + if (model_status == HighsModelStatus::kInfeasible) { + REQUIRE(num_iis_col > 0); + REQUIRE(num_iis_row > 0); + if (dev_run) + printf("Model %s has IIS with %d columns and %d rows\n", model.c_str(), + int(num_iis_col), int(num_iis_row)); + testIis(model, iis); + } else { + REQUIRE(num_iis_col == 0); + REQUIRE(num_iis_row == 0); + } +} diff --git a/cmake/sources-python.cmake b/cmake/sources-python.cmake index baff378e61..36c402ae19 100644 --- a/cmake/sources-python.cmake +++ b/cmake/sources-python.cmake @@ -184,6 +184,7 @@ set(highs_sources_python src/lp_data/Highs.cpp src/lp_data/HighsCallback.cpp src/lp_data/HighsDebug.cpp + src/lp_data/HighsIis.cpp src/lp_data/HighsInfo.cpp src/lp_data/HighsInfoDebug.cpp src/lp_data/HighsInterface.cpp @@ -298,6 +299,7 @@ set(highs_headers_python src/lp_data/HighsCallback.h src/lp_data/HighsCallbackStruct.h src/lp_data/HighsDebug.h + src/lp_data/HighsIis.h src/lp_data/HighsInfo.h src/lp_data/HighsInfoDebug.h src/lp_data/HighsLp.h diff --git a/cmake/sources.cmake b/cmake/sources.cmake index 6c7e4647df..29dce19f0d 100644 --- a/cmake/sources.cmake +++ b/cmake/sources.cmake @@ -184,6 +184,7 @@ set(highs_sources lp_data/Highs.cpp lp_data/HighsCallback.cpp lp_data/HighsDebug.cpp + lp_data/HighsIis.cpp lp_data/HighsInfo.cpp lp_data/HighsInfoDebug.cpp lp_data/HighsDeprecated.cpp @@ -302,6 +303,7 @@ set(highs_headers lp_data/HighsCallback.h lp_data/HighsCallbackStruct.h lp_data/HighsDebug.h + lp_data/HighsIis.h lp_data/HighsInfo.h lp_data/HighsInfoDebug.h lp_data/HighsLp.h diff --git a/src/Highs.h b/src/Highs.h index 6a87f6e5e6..c9c42f3a8c 100644 --- a/src/Highs.h +++ b/src/Highs.h @@ -17,6 +17,7 @@ #include #include "lp_data/HighsCallback.h" +#include "lp_data/HighsIis.h" #include "lp_data/HighsLpUtils.h" #include "lp_data/HighsRanging.h" #include "lp_data/HighsSolutionDebug.h" @@ -499,6 +500,16 @@ class Highs { */ HighsStatus getRanging(HighsRanging& ranging); + /** + * @brief Solve the feasibility relaxation problem + */ + HighsStatus feasibilityRelaxation(const double global_lower_penalty, + const double global_upper_penalty, + const double global_rhs_penalty, + const double* local_lower_penalty = nullptr, + const double* local_upper_penalty = nullptr, + const double* local_rhs_penalty = nullptr); + /** * @brief Get the ill-conditioning information for the current basis */ @@ -507,6 +518,12 @@ class Highs { const HighsInt method = 0, const double ill_conditioning_bound = 1e-4); + /** + * @brief Get (any) irreducible infeasible subsystem (IIS) + * information for the incumbent model + */ + HighsStatus getIis(HighsIis& iis); + /** * @brief Get the current model objective value */ @@ -1216,6 +1233,14 @@ class Highs { HighsStatus getBasisInverseRowSparse(const HighsInt row, HVector& row_ep_buffer); + /** + * @Brief Get the primal simplex phase 1 dual values. Advanced + * method: for HiGHS IIS calculation + */ + const std::vector& getPrimalPhase1Dual() const { + return ekk_instance_.primal_phase1_dual_; + } + // Start of deprecated methods std::string compilationDate() const { return "deprecated"; } @@ -1332,6 +1357,7 @@ class Highs { HighsOptions options_; HighsInfo info_; HighsRanging ranging_; + HighsIis iis_; std::vector saved_objective_and_solution_; @@ -1421,6 +1447,9 @@ class Highs { // Invalidates ekk_instance_ void invalidateEkk(); + // Invalidates iis_ + void invalidateIis(); + HighsStatus returnFromWriteSolution(FILE* file, const HighsStatus return_status); HighsStatus returnFromRun(const HighsStatus return_status, @@ -1503,6 +1532,27 @@ class Highs { double* primal_ray_value); HighsStatus getRangingInterface(); + HighsStatus getIisInterface(); + + HighsStatus elasticityFilterReturn( + const HighsStatus return_status, const bool feasible_model, + const HighsInt original_num_col, const HighsInt original_num_row, + const std::vector& original_col_cost, + const std::vector& original_col_lower, + const std::vector original_col_upper, + const std::vector original_integrality); + HighsStatus elasticityFilter(const double global_lower_penalty, + const double global_upper_penalty, + const double global_rhs_penalty, + const double* local_lower_penalty, + const double* local_upper_penalty, + const double* local_rhs_penalty, + const bool get_infeasible_row, + std::vector& infeasible_row_subset); + HighsStatus extractIis(HighsInt& num_iis_col, HighsInt& num_iis_row, + HighsInt* iis_col_index, HighsInt* iis_row_index, + HighsInt* iis_col_bound, HighsInt* iis_row_bound); + bool aFormatOk(const HighsInt num_nz, const HighsInt format); bool qFormatOk(const HighsInt num_nz, const HighsInt format); void clearZeroHessian(); diff --git a/src/lp_data/HConst.h b/src/lp_data/HConst.h index d7980f4cc8..e94745d715 100644 --- a/src/lp_data/HConst.h +++ b/src/lp_data/HConst.h @@ -265,6 +265,15 @@ enum PresolveRuleType : int { kPresolveRuleCount, }; +enum IisStrategy { + kIisStrategyMin = 0, + kIisStrategyFromLpRowPriority = kIisStrategyMin, // 0 + kIisStrategyFromLpColPriority, // 1 + // kIisStrategyFromRayRowPriority, // 2 + // kIisStrategyFromRayColPriority, // 3 + kIisStrategyMax = kIisStrategyFromLpColPriority +}; + // Default and max allowed power-of-two matrix scale factor const HighsInt kDefaultAllowedMatrixPow2Scale = 20; const HighsInt kMaxAllowedMatrixPow2Scale = 30; diff --git a/src/lp_data/Highs.cpp b/src/lp_data/Highs.cpp index 82f068171a..76a4dbc2ac 100644 --- a/src/lp_data/Highs.cpp +++ b/src/lp_data/Highs.cpp @@ -1666,6 +1666,19 @@ HighsStatus Highs::getRanging(HighsRanging& ranging) { return return_status; } +HighsStatus Highs::feasibilityRelaxation(const double global_lower_penalty, + const double global_upper_penalty, + const double global_rhs_penalty, + const double* local_lower_penalty, + const double* local_upper_penalty, + const double* local_rhs_penalty) { + std::vector infeasible_row_subset; + return elasticityFilter(global_lower_penalty, global_upper_penalty, + global_rhs_penalty, local_lower_penalty, + local_upper_penalty, local_rhs_penalty, false, + infeasible_row_subset); +} + HighsStatus Highs::getIllConditioning(HighsIllConditioning& ill_conditioning, const bool constraint, const HighsInt method, @@ -1679,6 +1692,33 @@ HighsStatus Highs::getIllConditioning(HighsIllConditioning& ill_conditioning, ill_conditioning_bound); } +HighsStatus Highs::getIis(HighsIis& iis) { + if (this->model_status_ == HighsModelStatus::kOptimal || + this->model_status_ == HighsModelStatus::kUnbounded) { + // Strange to call getIis for a model that's known to be feasible + highsLogUser( + options_.log_options, HighsLogType::kInfo, + "Calling Highs::getIis for a model that is known to be feasible\n"); + iis.invalidate(); + // No IIS exists, so validate the empty HighsIis instance + iis.valid_ = true; + return HighsStatus::kOk; + } + HighsStatus return_status = HighsStatus::kOk; + if (this->model_status_ != HighsModelStatus::kNotset && + this->model_status_ != HighsModelStatus::kInfeasible) { + return_status = HighsStatus::kWarning; + highsLogUser(options_.log_options, HighsLogType::kWarning, + "Calling Highs::getIis for a model with status %s\n", + this->modelStatusToString(this->model_status_).c_str()); + } + return_status = + interpretCallStatus(options_.log_options, this->getIisInterface(), + return_status, "getIisInterface"); + iis = this->iis_; + return return_status; +} + bool Highs::hasInvert() const { return ekk_instance_.status_.has_invert; } const HighsInt* Highs::getBasicVariablesArray() const { @@ -2375,6 +2415,7 @@ static HighsStatus analyseSetCreateError(HighsLogOptions log_options, const HighsInt create_error, const bool ordered, const HighsInt num_set_entries, + const HighsInt* set, const HighsInt dimension) { if (create_error == kIndexCollectionCreateIllegalSetSize) { highsLogUser(log_options, HighsLogType::kError, @@ -2395,10 +2436,13 @@ static HighsStatus analyseSetCreateError(HighsLogOptions log_options, "Set supplied to Highs::%s not ordered\n", method.c_str()); } } else if (create_error < 0) { + HighsInt illegal_set_index = -1 - create_error; + HighsInt illegal_set_entry = set[illegal_set_index]; highsLogUser( log_options, HighsLogType::kError, - "Set supplied to Highs::%s has entry %d out of range [0, %d)\n", - method.c_str(), int(-1 - create_error), int(dimension)); + "Set supplied to Highs::%s has entry %d of %d out of range [0, %d)\n", + method.c_str(), int(illegal_set_index), int(illegal_set_entry), + int(dimension)); } assert(create_error != kIndexCollectionCreateIllegalSetDimension); return HighsStatus::kError; @@ -2421,7 +2465,7 @@ HighsStatus Highs::changeColsIntegrality(const HighsInt num_set_entries, if (create_error) return analyseSetCreateError(options_.log_options, "changeColsIntegrality", create_error, true, num_set_entries, - model_.lp_.num_col_); + local_set.data(), model_.lp_.num_col_); HighsStatus call_status = changeIntegralityInterface(index_collection, local_integrality.data()); HighsStatus return_status = HighsStatus::kOk; @@ -2491,7 +2535,7 @@ HighsStatus Highs::changeColsCost(const HighsInt num_set_entries, if (create_error) return analyseSetCreateError(options_.log_options, "changeColsCost", create_error, true, num_set_entries, - model_.lp_.num_col_); + local_set.data(), model_.lp_.num_col_); HighsStatus call_status = changeCostsInterface(index_collection, local_cost.data()); HighsStatus return_status = HighsStatus::kOk; @@ -2570,7 +2614,7 @@ HighsStatus Highs::changeColsBounds(const HighsInt num_set_entries, if (create_error) return analyseSetCreateError(options_.log_options, "changeColsBounds", create_error, true, num_set_entries, - model_.lp_.num_col_); + local_set.data(), model_.lp_.num_col_); HighsStatus call_status = changeColBoundsInterface( index_collection, local_lower.data(), local_upper.data()); HighsStatus return_status = HighsStatus::kOk; @@ -2651,7 +2695,7 @@ HighsStatus Highs::changeRowsBounds(const HighsInt num_set_entries, if (create_error) return analyseSetCreateError(options_.log_options, "changeRowsBounds", create_error, true, num_set_entries, - model_.lp_.num_row_); + local_set.data(), model_.lp_.num_row_); HighsStatus call_status = changeRowBoundsInterface( index_collection, local_lower.data(), local_upper.data()); HighsStatus return_status = HighsStatus::kOk; @@ -2756,7 +2800,8 @@ HighsStatus Highs::getCols(const HighsInt num_set_entries, const HighsInt* set, create(index_collection, num_set_entries, set, model_.lp_.num_col_); if (create_error) return analyseSetCreateError(options_.log_options, "getCols", create_error, - false, num_set_entries, model_.lp_.num_col_); + false, num_set_entries, set, + model_.lp_.num_col_); getColsInterface(index_collection, num_col, costs, lower, upper, num_nz, start, index, value); return returnFromHighs(HighsStatus::kOk); @@ -2875,7 +2920,8 @@ HighsStatus Highs::getRows(const HighsInt num_set_entries, const HighsInt* set, create(index_collection, num_set_entries, set, model_.lp_.num_row_); if (create_error) return analyseSetCreateError(options_.log_options, "getRows", create_error, - false, num_set_entries, model_.lp_.num_row_); + false, num_set_entries, set, + model_.lp_.num_row_); getRowsInterface(index_collection, num_row, lower, upper, num_nz, start, index, value); return returnFromHighs(HighsStatus::kOk); @@ -2983,7 +3029,7 @@ HighsStatus Highs::deleteCols(const HighsInt num_set_entries, create(index_collection, num_set_entries, set, model_.lp_.num_col_); if (create_error) return analyseSetCreateError(options_.log_options, "deleteCols", - create_error, false, num_set_entries, + create_error, false, num_set_entries, set, model_.lp_.num_col_); deleteColsInterface(index_collection); return returnFromHighs(HighsStatus::kOk); @@ -3027,7 +3073,7 @@ HighsStatus Highs::deleteRows(const HighsInt num_set_entries, create(index_collection, num_set_entries, set, model_.lp_.num_row_); if (create_error) return analyseSetCreateError(options_.log_options, "deleteRows", - create_error, false, num_set_entries, + create_error, false, num_set_entries, set, model_.lp_.num_row_); deleteRowsInterface(index_collection); return returnFromHighs(HighsStatus::kOk); @@ -3361,12 +3407,15 @@ void Highs::invalidateUserSolverData() { invalidateRanging(); invalidateInfo(); invalidateEkk(); + invalidateIis(); } void Highs::invalidateModelStatusSolutionAndInfo() { invalidateModelStatus(); invalidateSolution(); + invalidateRanging(); invalidateInfo(); + invalidateIis(); } void Highs::invalidateModelStatus() { @@ -3396,6 +3445,8 @@ void Highs::invalidateRanging() { ranging_.invalidate(); } void Highs::invalidateEkk() { ekk_instance_.invalidate(); } +void Highs::invalidateIis() { iis_.invalidate(); } + HighsStatus Highs::completeSolutionFromDiscreteAssignment() { // Determine whether the current solution of a MIP is feasible and, // if not, try to assign values to continuous variables and discrete @@ -4327,7 +4378,9 @@ HighsStatus Highs::returnFromHighs(HighsStatus highs_return_status) { const bool dimensions_ok = lpDimensionsOk("returnFromHighs", model_.lp_, options_.log_options); if (!dimensions_ok) { - printf("LP Dimension error in returnFromHighs()\n"); + highsLogDev(options_.log_options, HighsLogType::kError, + "LP Dimension error in returnFromHighs()\n"); + return_status = HighsStatus::kError; } assert(dimensions_ok); if (ekk_instance_.status_.has_nla) { diff --git a/src/lp_data/HighsIis.cpp b/src/lp_data/HighsIis.cpp new file mode 100644 index 0000000000..9f244415ca --- /dev/null +++ b/src/lp_data/HighsIis.cpp @@ -0,0 +1,540 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* */ +/* This file is part of the HiGHS linear optimization suite */ +/* */ +/* Written and engineered 2008-2024 by Julian Hall, Ivet Galabova, */ +/* Leona Gottwald and Michael Feldmeier */ +/* */ +/* Available as open-source under the MIT License */ +/* */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/**@file lp_data/HighsIis.cpp + * @brief Class-independent utilities for HiGHS + */ + +#include "Highs.h" + +void HighsIis::invalidate() { + this->valid_ = false; + this->strategy_ = kIisStrategyMin; + this->col_index_.clear(); + this->row_index_.clear(); + this->col_bound_.clear(); + this->row_bound_.clear(); + this->info_.clear(); +} + +std::string HighsIis::iisBoundStatusToString(HighsInt bound_status) const { + if (bound_status == kIisBoundStatusDropped) return "Dropped"; + if (bound_status == kIisBoundStatusNull) return " Null"; + if (bound_status == kIisBoundStatusFree) return " Free"; + if (bound_status == kIisBoundStatusLower) return " Lower"; + if (bound_status == kIisBoundStatusUpper) return " Upper"; + if (bound_status == kIisBoundStatusBoxed) return " Boxed"; + return "*****"; +} + +void HighsIis::report(const std::string message, const HighsLp& lp) const { + HighsInt num_iis_col = this->col_index_.size(); + HighsInt num_iis_row = this->row_index_.size(); + if (num_iis_col > 10 || num_iis_row > 10) return; + printf("\nIIS %s\n===\n", message.c_str()); + printf("Column: "); + for (HighsInt iCol = 0; iCol < num_iis_col; iCol++) printf("%9d ", iCol); + printf("\nStatus: "); + for (HighsInt iCol = 0; iCol < num_iis_col; iCol++) + printf("%9s ", iisBoundStatusToString(this->col_bound_[iCol]).c_str()); + printf("\nLower: "); + for (HighsInt iCol = 0; iCol < num_iis_col; iCol++) + printf("%9.2g ", lp.col_lower_[iCol]); + printf("\nUpper: "); + for (HighsInt iCol = 0; iCol < num_iis_col; iCol++) + printf("%9.2g ", lp.col_upper_[iCol]); + printf("\n"); + printf("Row: Status Lower Upper\n"); + for (HighsInt iRow = 0; iRow < num_iis_row; iRow++) + printf("%2d %9s %9.2g %9.2g\n", int(iRow), + iisBoundStatusToString(this->row_bound_[iRow]).c_str(), + lp.row_lower_[iRow], lp.row_upper_[iRow]); + printf("\n"); +} + +void HighsIis::addCol(const HighsInt col, const HighsInt status) { + this->col_index_.push_back(col); + this->col_bound_.push_back(status); +} + +void HighsIis::addRow(const HighsInt row, const HighsInt status) { + this->row_index_.push_back(row); + this->row_bound_.push_back(status); +} + +void HighsIis::removeCol(const HighsInt col) { + HighsInt num_col = this->col_index_.size(); + assert(col < num_col); + this->col_index_[col] = this->col_index_[num_col - 1]; + this->col_index_.resize(num_col - 1); +} + +void HighsIis::removeRow(const HighsInt row) { + HighsInt num_row = this->row_index_.size(); + assert(row < num_row); + this->row_index_[row] = this->row_index_[num_row - 1]; + this->row_index_.resize(num_row - 1); +} + +bool HighsIis::trivial(const HighsLp& lp, const HighsOptions& options) { + this->invalidate(); + const bool col_priority = + // options.iis_strategy == kIisStrategyFromRayColPriority || + options.iis_strategy == kIisStrategyFromLpColPriority; + for (HighsInt k = 0; k < 2; k++) { + if ((col_priority && k == 0) || (!col_priority && k == 1)) { + // Loop over columns first + for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) { + if (lp.col_lower_[iCol] - lp.col_upper_[iCol] > + 2 * options.primal_feasibility_tolerance) { + this->addCol(iCol, kIisBoundStatusBoxed); + break; + } + } + if (this->col_index_.size() > 0) break; + } else { + // Loop over rows first + for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) { + if (lp.row_lower_[iRow] - lp.row_upper_[iRow] > + 2 * options.primal_feasibility_tolerance) { + this->addRow(iRow, kIisBoundStatusBoxed); + break; + } + } + if (this->row_index_.size() > 0) break; + } + } + HighsInt num_iis_col = this->col_index_.size(); + HighsInt num_iis_row = this->row_index_.size(); + // If one is found then we're done + if (num_iis_col + num_iis_row > 0) { + // Should have found exactly 1 + assert((num_iis_col == 1 || num_iis_row == 1) && + num_iis_col + num_iis_row < 2); + this->valid_ = true; + this->strategy_ = options.iis_strategy; + return true; + } + // Now look for empty rows that cannot have zero activity + std::vector count; + count.assign(lp.num_row_, 0); + for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) { + for (HighsInt iEl = lp.a_matrix_.start_[iCol]; + iEl < lp.a_matrix_.start_[iCol + 1]; iEl++) + count[lp.a_matrix_.index_[iEl]]++; + } + assert(this->row_index_.size() == 0); + for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) { + if (count[iRow] > 0) continue; + if (lp.row_lower_[iRow] > options.primal_feasibility_tolerance) { + this->addRow(iRow, kIisBoundStatusLower); + } else if (lp.row_upper_[iRow] < -options.primal_feasibility_tolerance) { + this->addRow(iRow, kIisBoundStatusUpper); + } + if (this->row_index_.size() > 0) { + this->valid_ = true; + this->strategy_ = options.iis_strategy; + return true; + } + } + return false; +} + +HighsStatus HighsIis::getData(const HighsLp& lp, const HighsOptions& options, + const HighsBasis& basis, + const std::vector& infeasible_row) { + // Check for trivial IIS should have been done earlier + assert(!this->trivial(lp, options)); + // The number of infeasible rows must be positive + assert(infeasible_row.size() > 0); + // Identify the LP corresponding to the set of infeasible rows + std::vector from_row = infeasible_row; + std::vector from_col; + std::vector to_row; + to_row.assign(lp.num_row_, -1); + assert(lp.a_matrix_.isColwise()); + // Determine how to detect whether a row is in infeasible_row and + // (then) gather information about it + for (HighsInt iX = 0; iX < HighsInt(infeasible_row.size()); iX++) + to_row[infeasible_row[iX]] = iX; + // Identify the columns (from_col) with nonzeros in the infeasible + // rows + for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) { + bool use_col = false; + for (HighsInt iEl = lp.a_matrix_.start_[iCol]; + iEl < lp.a_matrix_.start_[iCol + 1]; iEl++) + use_col = use_col || to_row[lp.a_matrix_.index_[iEl]] >= 0; + if (use_col) from_col.push_back(iCol); + } + HighsInt to_num_col = from_col.size(); + HighsInt to_num_row = from_row.size(); + HighsLp to_lp; + to_lp.num_col_ = to_num_col; + to_lp.num_row_ = to_num_row; + to_lp.a_matrix_.num_col_ = to_lp.num_col_; + to_lp.a_matrix_.num_row_ = to_lp.num_row_; + const bool has_col_names = lp.col_names_.size() > 0; + for (HighsInt iCol = 0; iCol < to_num_col; iCol++) { + to_lp.col_cost_.push_back(0); + to_lp.col_lower_.push_back(lp.col_lower_[from_col[iCol]]); + to_lp.col_upper_.push_back(lp.col_upper_[from_col[iCol]]); + if (has_col_names) + to_lp.col_names_.push_back(lp.col_names_[from_col[iCol]]); + for (HighsInt iEl = lp.a_matrix_.start_[from_col[iCol]]; + iEl < lp.a_matrix_.start_[from_col[iCol] + 1]; iEl++) { + HighsInt iRow = lp.a_matrix_.index_[iEl]; + if (to_row[iRow] >= 0) { + to_lp.a_matrix_.index_.push_back(to_row[iRow]); + to_lp.a_matrix_.value_.push_back(lp.a_matrix_.value_[iEl]); + } + } + to_lp.a_matrix_.start_.push_back(to_lp.a_matrix_.index_.size()); + } + const bool has_row_names = lp.row_names_.size() > 0; + for (HighsInt iRow = 0; iRow < to_num_row; iRow++) { + to_lp.row_lower_.push_back(lp.row_lower_[from_row[iRow]]); + to_lp.row_upper_.push_back(lp.row_upper_[from_row[iRow]]); + if (has_row_names) + to_lp.row_names_.push_back(lp.row_names_[from_row[iRow]]); + } + if (this->compute(to_lp, options) != HighsStatus::kOk) + return HighsStatus::kError; + // Indirect the values into the original LP + for (HighsInt iCol = 0; iCol < HighsInt(this->col_index_.size()); iCol++) + this->col_index_[iCol] = from_col[this->col_index_[iCol]]; + for (HighsInt iRow = 0; iRow < HighsInt(this->row_index_.size()); iRow++) + this->row_index_[iRow] = from_row[this->row_index_[iRow]]; + if (kIisDevReport) this->report("On exit", lp); + return HighsStatus::kOk; +} + +HighsStatus HighsIis::compute(const HighsLp& lp, const HighsOptions& options, + const HighsBasis* basis) { + const HighsLogOptions& log_options = options.log_options; + const bool row_priority = + // options.iis_strategy == kIisStrategyFromRayRowPriority || + options.iis_strategy == kIisStrategyFromLpRowPriority; + // Initially all columns and rows are candidates for the IIS + for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) this->addCol(iCol); + for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) this->addRow(iRow); + Highs highs; + const HighsInfo& info = highs.getInfo(); + highs.setOptionValue("output_flag", kIisDevReport); + highs.setOptionValue("presolve", kHighsOffString); + const HighsLp& incumbent_lp = highs.getLp(); + const HighsBasis& incumbent_basis = highs.getBasis(); + const HighsSolution& solution = highs.getSolution(); + HighsStatus run_status = highs.passModel(lp); + assert(run_status == HighsStatus::kOk); + if (basis) highs.setBasis(*basis); + + // Zero the objective + std::vector cost; + cost.assign(lp.num_col_, 0); + run_status = highs.changeColsCost(0, lp.num_col_ - 1, cost.data()); + assert(run_status == HighsStatus::kOk); + // Solve the LP + if (basis) highs.setBasis(*basis); + const bool use_sensitivity_filter = false; + std::vector primal_phase1_dual; + bool row_deletion = false; + HighsInt iX = -1; + bool drop_lower = false; + + // Lambda for gathering data when solving an LP + auto solveLp = [&]() -> HighsStatus { + HighsIisInfo iis_info; + iis_info.simplex_time = -highs.getRunTime(); + iis_info.simplex_iterations = -info.simplex_iteration_count; + run_status = highs.run(); + assert(run_status == HighsStatus::kOk); + if (run_status != HighsStatus::kOk) return run_status; + HighsModelStatus model_status = highs.getModelStatus(); + if (use_sensitivity_filter && + model_status == HighsModelStatus::kInfeasible) { + printf("\nHighsIis::compute %s deletion for %d and %s bound\n", + row_deletion ? "Row" : "Col", int(iX), + drop_lower ? "Lower" : "Upper"); + bool output_flag; + highs.getOptionValue("output_flag", output_flag); + highs.setOptionValue("output_flag", true); + HighsInt simplex_strategy; + highs.getOptionValue("simplex_strategy", simplex_strategy); + highs.setOptionValue("simplex_strategy", kSimplexStrategyPrimal); + // Solve the LP + run_status = highs.run(); + if (run_status != HighsStatus::kOk) return run_status; + highs.writeSolution("", kSolutionStylePretty); + const HighsInt* basic_index = highs.getBasicVariablesArray(); + std::vector rhs; + rhs.assign(lp.num_row_, 0); + // Get duals for nonbasic rows, and initialise duals so that basic duals + // are zero + assert(101 == 202); + + for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) { + HighsInt iVar = basic_index[iRow]; + const double lower = iVar < lp.num_col_ + ? lp.col_lower_[iVar] + : lp.row_lower_[iVar - lp.num_col_]; + const double upper = iVar < lp.num_col_ + ? lp.col_upper_[iVar] + : lp.row_upper_[iVar - lp.num_col_]; + const double value = iVar < lp.num_col_ + ? solution.col_value[iVar] + : solution.row_value[iVar - lp.num_col_]; + if (value < lower - options.primal_feasibility_tolerance) { + rhs[iRow] = -1; + } else if (value > upper + options.primal_feasibility_tolerance) { + rhs[iRow] = 1; + } + } + HVector pi; + pi.setup(lp.num_row_); + highs.getBasisTransposeSolve(rhs.data(), &pi.array[0], NULL, NULL); + pi.count = lp.num_row_; + std::vector reduced_costs_value; + std::vector reduced_costs_index; + lp.a_matrix_.productTransposeQuad(reduced_costs_value, + reduced_costs_index, pi); + + primal_phase1_dual = highs.getPrimalPhase1Dual(); + HighsInt num_zero_dual = 0; + for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) { + const HighsBasisStatus status = incumbent_basis.col_status[iCol]; + const double dual = primal_phase1_dual[iCol]; + const double lower = lp.col_lower_[iCol]; + const double upper = lp.col_upper_[iCol]; + const double value = solution.col_value[iCol]; + if (status != HighsBasisStatus::kBasic && + std::fabs(dual) < options.dual_feasibility_tolerance) { + num_zero_dual++; + // Small dual for nonbasic variable + printf( + "HighsIis::compute Column %d [%g, %g, %g] with status %s has " + "dual %g\n", + int(iCol), lower, value, upper, + highs.basisStatusToString(status).c_str(), dual); + // assert(123 == 456); + } + } + for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) { + const HighsBasisStatus status = incumbent_basis.row_status[iRow]; + const double dual = primal_phase1_dual[lp.num_col_ + iRow]; + const double lower = lp.row_lower_[iRow]; + const double upper = lp.row_upper_[iRow]; + const double value = solution.row_value[iRow]; + if (status != HighsBasisStatus::kBasic && + std::fabs(dual) < options.dual_feasibility_tolerance) { + num_zero_dual++; + // Small dual for nonbasic variable + printf( + "HighsIis::compute Row %d [%g, %g, %g] with status %s has " + "dual %g\n", + int(iRow), lower, value, upper, + highs.basisStatusToString(status).c_str(), dual); + // assert(123 == 456); + } + } + highs.setOptionValue("output_flag", output_flag); + highs.setOptionValue("simplex_strategy", simplex_strategy); + assert(!num_zero_dual); + } + iis_info.simplex_time += highs.getRunTime(); + iis_info.simplex_iterations += info.simplex_iteration_count; + this->info_.push_back(iis_info); + return run_status; + }; + + run_status = solveLp(); + if (run_status != HighsStatus::kOk) return run_status; + + assert(highs.getModelStatus() == HighsModelStatus::kInfeasible); + + // Pass twice: rows before columns, or columns before rows, according to + // row_priority + for (HighsInt k = 0; k < 2; k++) { + row_deletion = (row_priority && k == 0) || (!row_priority && k == 1); + std::string type = row_deletion ? "Row" : "Col"; + // Perform deletion pass + HighsInt num_index = row_deletion ? lp.num_row_ : lp.num_col_; + for (iX = 0; iX < num_index; iX++) { + const HighsInt ix_status = + row_deletion ? this->row_bound_[iX] : this->col_bound_[iX]; + if (ix_status == kIisBoundStatusDropped || + ix_status == kIisBoundStatusFree) + continue; + double lower = row_deletion ? lp.row_lower_[iX] : lp.col_lower_[iX]; + double upper = row_deletion ? lp.row_upper_[iX] : lp.col_upper_[iX]; + // Record whether the upper bound has been dropped due to the lower bound + // being kept + if (lower > -kHighsInf) { + // Drop the lower bound temporarily + bool drop_lower = true; + run_status = row_deletion + ? highs.changeRowBounds(iX, -kHighsInf, upper) + : highs.changeColBounds(iX, -kHighsInf, upper); + assert(run_status == HighsStatus::kOk); + // Solve the LP + run_status = solveLp(); + if (run_status != HighsStatus::kOk) return run_status; + HighsModelStatus model_status = highs.getModelStatus(); + if (model_status == HighsModelStatus::kOptimal) { + // Now feasible, so restore the lower bound + run_status = row_deletion ? highs.changeRowBounds(iX, lower, upper) + : highs.changeColBounds(iX, lower, upper); + assert(run_status == HighsStatus::kOk); + // If the lower bound must be kept, then any finite upper bound + // must be dropped + const bool apply_reciprocal_rule = true; + if (apply_reciprocal_rule) { + if (upper < kHighsInf) { + // Drop the upper bound permanently + upper = kHighsInf; + run_status = row_deletion + ? highs.changeRowBounds(iX, lower, upper) + : highs.changeColBounds(iX, lower, upper); + assert(run_status == HighsStatus::kOk); + } + assert(upper >= kHighsInf); + // Since upper = kHighsInf, allow the loop to run so that + // bound status is set as if upper were set to kHighsInf + // by relaxing it and finding that the LP was still + // infeasible + } + } else { + // Bound can be dropped permanently + assert(model_status == HighsModelStatus::kInfeasible); + lower = -kHighsInf; + } + } + if (upper < kHighsInf) { + // Drop the upper bound temporarily + run_status = row_deletion ? highs.changeRowBounds(iX, lower, kHighsInf) + : highs.changeColBounds(iX, lower, kHighsInf); + assert(run_status == HighsStatus::kOk); + // Solve the LP + run_status = solveLp(); + if (run_status != HighsStatus::kOk) return run_status; + HighsModelStatus model_status = highs.getModelStatus(); + if (model_status == HighsModelStatus::kOptimal) { + // Now feasible, so restore the upper bound + run_status = row_deletion ? highs.changeRowBounds(iX, lower, upper) + : highs.changeColBounds(iX, lower, upper); + assert(run_status == HighsStatus::kOk); + } else { + // Bound can be dropped permanently + assert(model_status == HighsModelStatus::kInfeasible); + upper = kHighsInf; + } + } + const bool debug_bound_change = true; + if (debug_bound_change) { + // Check bounds have been changed correctly + double check_lower; + double check_upper; + double check_cost; + HighsInt check_num_ix; + HighsInt check_num_nz; + run_status = + row_deletion + ? highs.getRows(iX, iX, check_num_ix, &check_lower, + &check_upper, check_num_nz, nullptr, nullptr, + nullptr) + : highs.getCols(iX, iX, check_num_ix, &check_cost, &check_lower, + &check_upper, check_num_nz, nullptr, nullptr, + nullptr); + assert(run_status == HighsStatus::kOk); + assert(check_lower == lower); + assert(check_upper == upper); + } + HighsInt iss_bound_status = kIisBoundStatusNull; + if (lower <= -kHighsInf) { + if (upper >= kHighsInf) { + if (row_deletion) { + // Free rows can be dropped + iss_bound_status = kIisBoundStatusDropped; + } else { + // Free columns can only be dropped if they are empty + iss_bound_status = kIisBoundStatusFree; + } + } else { + iss_bound_status = kIisBoundStatusUpper; + } + } else { + if (upper >= kHighsInf) { + iss_bound_status = kIisBoundStatusLower; + } else { + // FX or BX: shouldn't happen + iss_bound_status = kIisBoundStatusBoxed; + } + } + assert(iss_bound_status != kIisBoundStatusNull); + assert(iss_bound_status != kIisBoundStatusBoxed); + if (row_deletion) { + this->row_bound_[iX] = iss_bound_status; + } else { + this->col_bound_[iX] = iss_bound_status; + } + highsLogUser(log_options, HighsLogType::kInfo, "%s %d has status %s\n", + type.c_str(), int(iX), + iisBoundStatusToString(iss_bound_status).c_str()); + } + if (k == 1) continue; + // End of first pass: look to simplify second pass + if (kIisDevReport) this->report("End of deletion", incumbent_lp); + if (row_deletion) { + // Mark empty columns as dropped + for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) { + bool empty_col = true; + for (HighsInt iEl = lp.a_matrix_.start_[iCol]; + iEl < lp.a_matrix_.start_[iCol + 1]; iEl++) { + if (this->row_bound_[lp.a_matrix_.index_[iEl]] != + kIisBoundStatusDropped) { + empty_col = false; + break; + } + } + if (empty_col) { + highsLogUser(log_options, HighsLogType::kInfo, + "Col %d has status Dropped: Empty\n", int(iCol)); + this->col_bound_[iCol] = kIisBoundStatusDropped; + run_status = highs.changeColBounds(iCol, -kHighsInf, kHighsInf); + assert(run_status == HighsStatus::kOk); + } + } + } + if (kIisDevReport) this->report("End of pass 1", incumbent_lp); + } + if (kIisDevReport) this->report("End of pass 2", incumbent_lp); + HighsInt iss_num_col = 0; + for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) { + if (this->col_bound_[iCol] != kIisBoundStatusDropped) { + this->col_index_[iss_num_col] = this->col_index_[iCol]; + this->col_bound_[iss_num_col] = this->col_bound_[iCol]; + iss_num_col++; + } + } + HighsInt iss_num_row = 0; + for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) { + if (this->row_bound_[iRow] != kIisBoundStatusDropped) { + this->row_index_[iss_num_row] = this->row_index_[iRow]; + this->row_bound_[iss_num_row] = this->row_bound_[iRow]; + iss_num_row++; + } + } + this->col_index_.resize(iss_num_col); + this->col_bound_.resize(iss_num_col); + this->row_index_.resize(iss_num_row); + this->row_bound_.resize(iss_num_row); + this->valid_ = true; + this->strategy_ = options.iis_strategy; + return HighsStatus::kOk; +} diff --git a/src/lp_data/HighsIis.h b/src/lp_data/HighsIis.h new file mode 100644 index 0000000000..f49665de32 --- /dev/null +++ b/src/lp_data/HighsIis.h @@ -0,0 +1,65 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* */ +/* This file is part of the HiGHS linear optimization suite */ +/* */ +/* Written and engineered 2008-2024 by Julian Hall, Ivet Galabova, */ +/* Leona Gottwald and Michael Feldmeier */ +/* */ +/* Available as open-source under the MIT License */ +/* */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/**@file lp_data/HighsIis.h + * @brief Class-independent utilities for HiGHS + */ +#ifndef LP_DATA_HIGHSIIS_H_ +#define LP_DATA_HIGHSIIS_H_ + +#include "lp_data/HighsLp.h" + +const bool kIisDevReport = true; + +enum IisBoundStatus { + kIisBoundStatusDropped = -1, + kIisBoundStatusNull, // 0 + kIisBoundStatusFree, // 1 + kIisBoundStatusLower, // 2 + kIisBoundStatusUpper, // 3 + kIisBoundStatusBoxed // 4 +}; + +struct HighsIisInfo { + double simplex_time = 0; + HighsInt simplex_iterations = 0; +}; + +class HighsIis { + public: + HighsIis() {} + + void invalidate(); + std::string iisBoundStatusToString(HighsInt bound_status) const; + void report(const std::string message, const HighsLp& lp) const; + void addCol(const HighsInt col, const HighsInt status = kIisBoundStatusNull); + void addRow(const HighsInt row, const HighsInt status = kIisBoundStatusNull); + void removeCol(const HighsInt col); + void removeRow(const HighsInt row); + HighsStatus getData(const HighsLp& lp, const HighsOptions& options, + const HighsBasis& basis, + const std::vector& infeasible_row); + + HighsStatus compute(const HighsLp& lp, const HighsOptions& options, + const HighsBasis* basis = nullptr); + + bool trivial(const HighsLp& lp, const HighsOptions& options); + + // Data members + bool valid_ = false; + HighsInt strategy_ = kIisStrategyMin; + std::vector col_index_; + std::vector row_index_; + std::vector col_bound_; + std::vector row_bound_; + std::vector info_; +}; + +#endif // LP_DATA_HIGHSIIS_H_ diff --git a/src/lp_data/HighsInterface.cpp b/src/lp_data/HighsInterface.cpp index d5ffd342a0..4b3726b217 100644 --- a/src/lp_data/HighsInterface.cpp +++ b/src/lp_data/HighsInterface.cpp @@ -1549,6 +1549,644 @@ HighsStatus Highs::getRangingInterface() { return getRangingData(this->ranging_, solver_object); } +HighsStatus Highs::getIisInterface() { + if (this->iis_.valid_) return HighsStatus::kOk; + this->iis_.invalidate(); + HighsLp& lp = model_.lp_; + // Check for trivial IIS: empty infeasible row or inconsistent bounds + if (this->iis_.trivial(lp, options_)) return HighsStatus::kOk; + HighsInt num_row = lp.num_row_; + if (num_row == 0) { + // For an LP with no rows, the only scope for infeasibility is + // inconsistent columns bounds - which has already been assessed, + // so validate the empty HighsIis instance + this->iis_.valid_ = true; + return HighsStatus::kOk; + } + const bool ray_option = false; + // options_.iis_strategy == kIisStrategyFromRayRowPriority || + // options_.iis_strategy == kIisStrategyFromRayColPriority; + if (this->model_status_ == HighsModelStatus::kInfeasible && ray_option && + !ekk_instance_.status_.has_invert) { + // Model is known to be infeasible, and a dual ray option is + // chosen, but it has no INVERT, presumably because infeasibility + // detected in presolve, so solve without presolve + std::string presolve = options_.presolve; + options_.presolve = kHighsOffString; + + HighsIisInfo iis_info; + iis_info.simplex_time = -this->getRunTime(); + iis_info.simplex_iterations = -info_.simplex_iteration_count; + HighsStatus run_status = this->run(); + options_.presolve = presolve; + if (run_status != HighsStatus::kOk) return run_status; + iis_info.simplex_time += this->getRunTime(); + iis_info.simplex_iterations += -info_.simplex_iteration_count; + this->iis_.info_.push_back(iis_info); + + // Model should remain infeasible! + if (this->model_status_ != HighsModelStatus::kInfeasible) { + highsLogUser( + options_.log_options, HighsLogType::kError, + "Model status has switched from %s to %s when solving without " + "presolve\n", + this->modelStatusToString(HighsModelStatus::kInfeasible).c_str(), + this->modelStatusToString(this->model_status_).c_str()); + return HighsStatus::kError; + } + } + const bool has_dual_ray = ekk_instance_.status_.has_dual_ray; + if (ray_option && !has_dual_ray) + highsLogUser( + options_.log_options, HighsLogType::kWarning, + "No known dual ray from which to compute IIS: using whole model\n"); + std::vector infeasible_row_subset; + if (ray_option && has_dual_ray) { + // Compute the dual ray to identify an infeasible subset of rows + assert(ekk_instance_.status_.has_invert); + assert(!lp.is_moved_); + std::vector rhs; + HighsInt iRow = ekk_instance_.info_.dual_ray_row_; + rhs.assign(num_row, 0); + rhs[iRow] = 1; + std::vector dual_ray_value(num_row); + HighsInt* dual_ray_num_nz = 0; + basisSolveInterface(rhs, dual_ray_value.data(), dual_ray_num_nz, NULL, + true); + for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) + if (dual_ray_value[iRow]) infeasible_row_subset.push_back(iRow); + } else { + // Full LP option chosen or no dual ray to use + // + // Working on the whole model so clear all solver data + this->invalidateUserSolverData(); + // 1789 Remove this check! + HighsLp check_lp_before = this->model_.lp_; + // Apply the elasticity filter to the whole model in order to + // determine an infeasible subset of rows + HighsStatus return_status = + this->elasticityFilter(-1.0, -1.0, 1.0, nullptr, nullptr, nullptr, true, + infeasible_row_subset); + HighsLp check_lp_after = this->model_.lp_; + assert(check_lp_before.equalButForScalingAndNames(check_lp_after)); + if (return_status != HighsStatus::kOk) return return_status; + } + HighsStatus return_status = HighsStatus::kOk; + if (infeasible_row_subset.size() == 0) { + // No subset of infeasible rows, so model is feasible + this->iis_.valid_ = true; + } else { + return_status = + this->iis_.getData(lp, options_, basis_, infeasible_row_subset); + if (return_status == HighsStatus::kOk) { + // Existence of non-empty IIS => infeasibility + if (this->iis_.col_index_.size() > 0 || this->iis_.row_index_.size() > 0) + this->model_status_ = HighsModelStatus::kInfeasible; + } + // Analyse the LP solution data + const HighsInt num_lp_solved = this->iis_.info_.size(); + double min_time = kHighsInf; + double sum_time = 0; + double max_time = 0; + HighsInt min_iterations = kHighsIInf; + HighsInt sum_iterations = 0; + HighsInt max_iterations = 0; + for (HighsInt iX = 0; iX < num_lp_solved; iX++) { + double time = this->iis_.info_[iX].simplex_time; + HighsInt iterations = this->iis_.info_[iX].simplex_iterations; + min_time = std::min(time, min_time); + sum_time += time; + max_time = std::max(time, max_time); + min_iterations = std::min(iterations, min_iterations); + sum_iterations += iterations; + max_iterations = std::max(iterations, max_iterations); + } + highsLogUser(options_.log_options, HighsLogType::kInfo, + " (min / average / max) iteration count (%6d / %6.2g / % 6d)" + " and time (%6.2f / %6.2f / % 6.2f) \n", + int(this->iis_.col_index_.size()), + int(this->iis_.row_index_.size()), int(num_lp_solved), + int(min_iterations), + num_lp_solved > 0 ? (1.0 * sum_iterations) / num_lp_solved : 0, + int(max_iterations), min_time, + num_lp_solved > 0 ? sum_time / num_lp_solved : 0, max_time); + } + return return_status; +} + +HighsStatus Highs::elasticityFilterReturn( + const HighsStatus return_status, const bool feasible_model, + const HighsInt original_num_col, const HighsInt original_num_row, + const std::vector& original_col_cost, + const std::vector& original_col_lower, + const std::vector original_col_upper, + const std::vector original_integrality) { + const HighsLp& lp = this->model_.lp_; + // Delete any additional rows and columns, and restore the original + // column costs and bounds + HighsStatus run_status; + run_status = this->deleteRows(original_num_row, lp.num_row_ - 1); + assert(run_status == HighsStatus::kOk); + + run_status = this->deleteCols(original_num_col, lp.num_col_ - 1); + assert(run_status == HighsStatus::kOk); + + run_status = + this->changeColsCost(0, original_num_col - 1, original_col_cost.data()); + assert(run_status == HighsStatus::kOk); + + run_status = + this->changeColsBounds(0, original_num_col - 1, original_col_lower.data(), + original_col_upper.data()); + assert(run_status == HighsStatus::kOk); + + if (original_integrality.size()) { + this->changeColsIntegrality(0, original_num_col - 1, + original_integrality.data()); + assert(run_status == HighsStatus::kOk); + } + + assert(lp.num_col_ == original_num_col); + assert(lp.num_row_ == original_num_row); + + if (return_status == HighsStatus::kOk) { + // Solution is invalidated by deleting rows and columns, but + // primal values are correct. Have to recompute row acivities, + // though + this->model_.lp_.a_matrix_.productQuad(this->solution_.row_value, + this->solution_.col_value); + this->solution_.value_valid = true; + } + + // If the model is feasible, then the status of model is not known + if (feasible_model) this->model_status_ = HighsModelStatus::kNotset; + + return return_status; +} + +HighsStatus Highs::elasticityFilter( + const double global_lower_penalty, const double global_upper_penalty, + const double global_rhs_penalty, const double* local_lower_penalty, + const double* local_upper_penalty, const double* local_rhs_penalty, + const bool get_infeasible_row, + std::vector& infeasible_row_subset) { + this->writeModel("infeasible.mps"); + // Solve the feasibility relaxation problem for the given penalties, + // continuing to act as the elasticity filter get_infeasible_row is + // true, resulting in an infeasibility subset for further refinement + // as an IIS + // + // Construct the e-LP: + // + // Constraints L <= Ax <= U; l <= x <= u + // + // Transformed to + // + // L <= Ax + e_L - e_U <= U, + // + // l <= x + e_l - e_u <= u, + // + // where the elastic variables are not used if the corresponding + // bound is infinite or the local/global penalty is negative. + // + // x is free, and the objective is the linear function of the + // elastic variables given by the local/global penalties + // + // col_of_ecol lists the column indices corresponding to the entries in + // bound_of_col_of_ecol so that the results can be interpreted + // + // row_of_ecol lists the row indices corresponding to the entries in + // bound_of_row_of_ecol so that the results can be interpreted + std::vector col_of_ecol; + std::vector row_of_ecol; + std::vector bound_of_row_of_ecol; + std::vector bound_of_col_of_ecol; + std::vector erow_lower; + std::vector erow_upper; + std::vector erow_start; + std::vector erow_index; + std::vector erow_value; + // Accumulate names for ecols and erows, re-using ecol_name for the + // names of row ecols after defining the names of col ecols + std::vector ecol_name; + std::vector erow_name; + std::vector ecol_cost; + std::vector ecol_lower; + std::vector ecol_upper; + const HighsLp& lp = this->model_.lp_; + HighsInt evar_ix = lp.num_col_; + HighsStatus run_status; + const bool write_model = true; + HighsInt col_ecol_offset; + HighsInt row_ecol_offset; + // Take copies of the original model dimensions and column data + // vectors, as they will be modified in forming the e-LP + const HighsInt original_num_col = lp.num_col_; + const HighsInt original_num_row = lp.num_row_; + const std::vector original_col_cost = lp.col_cost_; + const std::vector original_col_lower = lp.col_lower_; + const std::vector original_col_upper = lp.col_upper_; + const std::vector original_integrality = lp.integrality_; + // Zero the column costs + std::vector zero_costs; + zero_costs.assign(original_num_col, 0); + run_status = this->changeColsCost(0, lp.num_col_ - 1, zero_costs.data()); + assert(run_status == HighsStatus::kOk); + + if (get_infeasible_row && lp.integrality_.size()) { + // Set any integrality to continuous + std::vector all_continuous; + all_continuous.assign(original_num_col, HighsVarType::kContinuous); + run_status = + this->changeColsIntegrality(0, lp.num_col_ - 1, all_continuous.data()); + assert(run_status == HighsStatus::kOk); + } + + // For the columns + const bool has_local_lower_penalty = local_lower_penalty; + const bool has_global_elastic_lower = global_lower_penalty >= 0; + const bool has_elastic_lower = + has_local_lower_penalty || has_global_elastic_lower; + const bool has_local_upper_penalty = local_upper_penalty; + const bool has_global_elastic_upper = global_upper_penalty >= 0; + const bool has_elastic_upper = + has_local_upper_penalty || has_global_elastic_upper; + const bool has_elastic_columns = has_elastic_lower || has_elastic_upper; + // For the rows + const bool has_local_rhs_penalty = local_rhs_penalty; + const bool has_global_elastic_rhs = global_rhs_penalty >= 0; + const bool has_elastic_rows = has_local_rhs_penalty || has_global_elastic_rhs; + assert(has_elastic_columns || has_elastic_rows); + + if (has_elastic_columns) { + // Accumulate bounds to be used for columns + std::vector col_lower; + std::vector col_upper; + // When defining names, need to know the column number + HighsInt previous_num_col = lp.num_col_; + const bool has_col_names = lp.col_names_.size() > 0; + erow_start.push_back(0); + for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) { + const double lower = lp.col_lower_[iCol]; + const double upper = lp.col_upper_[iCol]; + // Original bounds used unless e-variable introduced + col_lower.push_back(lower); + col_upper.push_back(upper); + // Free columns have no erow + if (lower <= -kHighsInf && upper >= kHighsInf) continue; + + // Get the penalty for violating the lower bounds on this column + const double lower_penalty = has_local_lower_penalty + ? local_lower_penalty[iCol] + : global_lower_penalty; + // Negative lower penalty and infininte upper bound implies that the + // bounds cannot be violated + if (lower_penalty < 0 && upper >= kHighsInf) continue; + + // Get the penalty for violating the upper bounds on this column + const double upper_penalty = has_local_upper_penalty + ? local_upper_penalty[iCol] + : global_upper_penalty; + // Infininte upper bound and negative lower penalty implies that the + // bounds cannot be violated + if (lower <= -kHighsInf && upper_penalty < 0) continue; + erow_lower.push_back(lower); + erow_upper.push_back(upper); + if (has_col_names) + erow_name.push_back("row_" + std::to_string(iCol) + "_" + + lp.col_names_[iCol] + "_erow"); + // Define the entry for x[iCol] + erow_index.push_back(iCol); + erow_value.push_back(1); + if (lower > -kHighsInf && lower_penalty >= 0) { + // New e_l variable + col_of_ecol.push_back(iCol); + if (has_col_names) + ecol_name.push_back("col_" + std::to_string(iCol) + "_" + + lp.col_names_[iCol] + "_lower"); + // Save the original lower bound on this column and free its + // lower bound + bound_of_col_of_ecol.push_back(lower); + col_lower[iCol] = -kHighsInf; + erow_index.push_back(evar_ix); + erow_value.push_back(1); + ecol_cost.push_back(lower_penalty); + evar_ix++; + } + if (upper < kHighsInf && upper_penalty >= 0) { + // New e_u variable + col_of_ecol.push_back(iCol); + if (has_col_names) + ecol_name.push_back("col_" + std::to_string(iCol) + "_" + + lp.col_names_[iCol] + "_upper"); + // Save the original upper bound on this column and free its + // upper bound + bound_of_col_of_ecol.push_back(upper); + col_upper[iCol] = kHighsInf; + erow_index.push_back(evar_ix); + erow_value.push_back(-1); + ecol_cost.push_back(upper_penalty); + evar_ix++; + } + erow_start.push_back(erow_index.size()); + HighsInt row_nz = + erow_start[erow_start.size() - 1] - erow_start[erow_start.size() - 2]; + printf("eRow for column %d has %d nonzeros\n", int(iCol), int(row_nz)); + assert(row_nz == 2 || row_nz == 3); + } + HighsInt num_new_col = col_of_ecol.size(); + HighsInt num_new_row = erow_start.size() - 1; + HighsInt num_new_nz = erow_start[num_new_row]; + if (kIisDevReport) + printf( + "Elasticity filter: For columns there are %d variables and %d " + "constraints\n", + int(num_new_col), int(num_new_row)); + // Apply the original column bound changes + assert(col_lower.size() == static_cast(lp.num_col_)); + assert(col_upper.size() == static_cast(lp.num_col_)); + run_status = this->changeColsBounds(0, lp.num_col_ - 1, col_lower.data(), + col_upper.data()); + assert(run_status == HighsStatus::kOk); + // Add the new columns + ecol_lower.assign(num_new_col, 0); + ecol_upper.assign(num_new_col, kHighsInf); + run_status = this->addCols(num_new_col, ecol_cost.data(), ecol_lower.data(), + ecol_upper.data(), 0, nullptr, nullptr, nullptr); + assert(run_status == HighsStatus::kOk); + // Add the new rows + assert(erow_start.size() == static_cast(num_new_row + 1)); + assert(erow_index.size() == static_cast(num_new_nz)); + assert(erow_value.size() == static_cast(num_new_nz)); + run_status = this->addRows(num_new_row, erow_lower.data(), + erow_upper.data(), num_new_nz, erow_start.data(), + erow_index.data(), erow_value.data()); + assert(run_status == HighsStatus::kOk); + if (has_col_names) { + for (HighsInt iCol = 0; iCol < num_new_col; iCol++) + this->passColName(previous_num_col + iCol, ecol_name[iCol]); + for (HighsInt iRow = 0; iRow < num_new_row; iRow++) + this->passRowName(original_num_row + iRow, erow_name[iRow]); + } + assert(ecol_cost.size() == static_cast(num_new_col)); + assert(ecol_lower.size() == static_cast(num_new_col)); + assert(ecol_upper.size() == static_cast(num_new_col)); + if (write_model) { + printf("\nAfter adding %d e-rows\n=============\n", int(num_new_col)); + bool output_flag; + run_status = this->getOptionValue("output_flag", output_flag); + this->setOptionValue("output_flag", true); + this->writeModel(""); + this->setOptionValue("output_flag", output_flag); + } + } + if (has_elastic_rows) { + // Add the columns corresponding to the e_L and e_U variables for + // the constraints + HighsInt previous_num_col = lp.num_col_; + row_ecol_offset = previous_num_col; + ecol_name.clear(); + ecol_cost.clear(); + std::vector ecol_start; + std::vector ecol_index; + std::vector ecol_value; + ecol_start.push_back(0); + const bool has_row_names = lp.row_names_.size() > 0; + for (HighsInt iRow = 0; iRow < original_num_row; iRow++) { + // Get the penalty for violating the bounds on this row + const double penalty = + has_local_rhs_penalty ? local_rhs_penalty[iRow] : global_rhs_penalty; + // Negative penalty implies that the bounds cannot be violated + if (penalty < 0) continue; + const double lower = lp.row_lower_[iRow]; + const double upper = lp.row_upper_[iRow]; + if (lower > -kHighsInf) { + // Create an e-var for the row lower bound + row_of_ecol.push_back(iRow); + if (has_row_names) + ecol_name.push_back("row_" + std::to_string(iRow) + "_" + + lp.row_names_[iRow] + "_lower"); + bound_of_row_of_ecol.push_back(lower); + // Define the sub-matrix column + ecol_index.push_back(iRow); + ecol_value.push_back(1); + ecol_start.push_back(ecol_index.size()); + ecol_cost.push_back(penalty); + evar_ix++; + } + if (upper < kHighsInf) { + // Create an e-var for the row upper bound + row_of_ecol.push_back(iRow); + if (has_row_names) + ecol_name.push_back("row_" + std::to_string(iRow) + "_" + + lp.row_names_[iRow] + "_upper"); + bound_of_row_of_ecol.push_back(upper); + // Define the sub-matrix column + ecol_index.push_back(iRow); + ecol_value.push_back(-1); + ecol_start.push_back(ecol_index.size()); + ecol_cost.push_back(penalty); + evar_ix++; + } + } + HighsInt num_new_col = ecol_start.size() - 1; + HighsInt num_new_nz = ecol_start[num_new_col]; + ecol_lower.assign(num_new_col, 0); + ecol_upper.assign(num_new_col, kHighsInf); + assert(ecol_cost.size() == static_cast(num_new_col)); + assert(ecol_lower.size() == static_cast(num_new_col)); + assert(ecol_upper.size() == static_cast(num_new_col)); + assert(ecol_start.size() == static_cast(num_new_col + 1)); + assert(ecol_index.size() == static_cast(num_new_nz)); + assert(ecol_value.size() == static_cast(num_new_nz)); + run_status = this->addCols(num_new_col, ecol_cost.data(), ecol_lower.data(), + ecol_upper.data(), num_new_nz, ecol_start.data(), + ecol_index.data(), ecol_value.data()); + assert(run_status == HighsStatus::kOk); + if (has_row_names) { + for (HighsInt iCol = 0; iCol < num_new_col; iCol++) + this->passColName(previous_num_col + iCol, ecol_name[iCol]); + } + + if (write_model) { + bool output_flag; + printf("\nAfter adding %d e-cols\n=============\n", int(num_new_col)); + run_status = this->getOptionValue("output_flag", output_flag); + this->setOptionValue("output_flag", true); + this->writeModel(""); + this->setOptionValue("output_flag", output_flag); + } + } + + if (write_model) this->writeModel("elastic.mps"); + + // Lambda for gathering data when solving an LP + auto solveLp = [&]() -> HighsStatus { + HighsIisInfo iis_info; + iis_info.simplex_time = -this->getRunTime(); + iis_info.simplex_iterations = -info_.simplex_iteration_count; + run_status = this->run(); + assert(run_status == HighsStatus::kOk); + if (run_status != HighsStatus::kOk) return run_status; + iis_info.simplex_time += this->getRunTime(); + iis_info.simplex_iterations += info_.simplex_iteration_count; + this->iis_.info_.push_back(iis_info); + return run_status; + }; + + run_status = solveLp(); + + if (run_status != HighsStatus::kOk) + return elasticityFilterReturn(run_status, false, original_num_col, + original_num_row, original_col_cost, + original_col_lower, original_col_upper, + original_integrality); + if (kIisDevReport) this->writeSolution("", kSolutionStylePretty); + // Model status should be optimal, unless model is unbounded + assert(this->model_status_ == HighsModelStatus::kOptimal || + this->model_status_ == HighsModelStatus::kUnbounded); + + if (!get_infeasible_row) + return elasticityFilterReturn(HighsStatus::kOk, false, original_num_col, + original_num_row, original_col_cost, + original_col_lower, original_col_upper, + original_integrality); + const HighsSolution& solution = this->getSolution(); + // Now fix e-variables that are positive and re-solve until e-LP is infeasible + HighsInt loop_k = 0; + bool feasible_model = false; + for (;;) { + if (kIisDevReport) + printf("\nElasticity filter pass %d\n==============\n", int(loop_k)); + HighsInt num_fixed = 0; + if (has_elastic_columns) { + for (HighsInt eCol = 0; eCol < col_of_ecol.size(); eCol++) { + HighsInt iCol = col_of_ecol[eCol]; + if (solution.col_value[col_ecol_offset + eCol] > + this->options_.primal_feasibility_tolerance) { + if (kIisDevReport) + printf( + "E-col %2d (column %2d) corresponds to column %2d with bound " + "%g " + "and has solution value %g\n", + int(eCol), int(col_ecol_offset + eCol), int(iCol), + bound_of_col_of_ecol[eCol], + solution.col_value[col_ecol_offset + eCol]); + this->changeColBounds(col_ecol_offset + eCol, 0, 0); + num_fixed++; + } + } + } + if (has_elastic_rows) { + for (HighsInt eCol = 0; eCol < row_of_ecol.size(); eCol++) { + HighsInt iRow = row_of_ecol[eCol]; + if (solution.col_value[row_ecol_offset + eCol] > + this->options_.primal_feasibility_tolerance) { + if (kIisDevReport) + printf( + "E-row %2d (column %2d) corresponds to row %2d with bound " + "%g " + "and has solution value %g\n", + int(eCol), int(row_ecol_offset + eCol), int(iRow), + bound_of_row_of_ecol[eCol], + solution.col_value[row_ecol_offset + eCol]); + this->changeColBounds(row_ecol_offset + eCol, 0, 0); + num_fixed++; + } + } + } + if (num_fixed == 0) { + // No elastic variables were positive, so problem is feasible + feasible_model = true; + break; + } + HighsStatus run_status = solveLp(); + if (run_status != HighsStatus::kOk) + return elasticityFilterReturn(run_status, feasible_model, + original_num_col, original_num_row, + original_col_cost, original_col_lower, + original_col_upper, original_integrality); + if (kIisDevReport) this->writeSolution("", kSolutionStylePretty); + HighsModelStatus model_status = this->getModelStatus(); + if (model_status == HighsModelStatus::kInfeasible) break; + loop_k++; + } + + infeasible_row_subset.clear(); + HighsInt num_enforced_col_ecol = 0; + HighsInt num_enforced_row_ecol = 0; + if (has_elastic_columns) { + for (HighsInt eCol = 0; eCol < col_of_ecol.size(); eCol++) { + HighsInt iCol = col_of_ecol[eCol]; + if (lp.col_upper_[col_ecol_offset + eCol] == 0) { + num_enforced_col_ecol++; + printf( + "Col e-col %2d (column %2d) corresponds to column %2d with bound " + "%g " + "and is enforced\n", + int(eCol), int(col_ecol_offset + eCol), int(iCol), + bound_of_col_of_ecol[eCol]); + } + } + } + if (has_elastic_rows) { + for (HighsInt eCol = 0; eCol < row_of_ecol.size(); eCol++) { + HighsInt iRow = row_of_ecol[eCol]; + if (lp.col_upper_[row_ecol_offset + eCol] == 0) { + num_enforced_row_ecol++; + infeasible_row_subset.push_back(iRow); + if (kIisDevReport) + printf( + "Row e-col %2d (column %2d) corresponds to row %2d with bound " + "%g and is enforced\n", + int(eCol), int(row_ecol_offset + eCol), int(iRow), + bound_of_row_of_ecol[eCol]); + } + } + } + if (feasible_model) + assert(num_enforced_col_ecol == 0 && num_enforced_row_ecol == 0); + + highsLogUser( + options_.log_options, HighsLogType::kInfo, + "Elasticity filter after %d passes enforces bounds on %d cols and %d " + "rows\n", + int(loop_k), int(num_enforced_col_ecol), int(num_enforced_row_ecol)); + + if (kIisDevReport) + printf( + "\nElasticity filter after %d passes enforces bounds on %d cols and %d " + "rows\n", + int(loop_k), int(num_enforced_col_ecol), int(num_enforced_row_ecol)); + + return elasticityFilterReturn(HighsStatus::kOk, feasible_model, + original_num_col, original_num_row, + original_col_cost, original_col_lower, + original_col_upper, original_integrality); +} + +HighsStatus Highs::extractIis(HighsInt& num_iis_col, HighsInt& num_iis_row, + HighsInt* iis_col_index, HighsInt* iis_row_index, + HighsInt* iis_col_bound, + HighsInt* iis_row_bound) { + assert(this->iis_.valid_); + num_iis_col = this->iis_.col_index_.size(); + num_iis_row = this->iis_.row_index_.size(); + if (iis_col_index || iis_col_bound) { + for (HighsInt iCol = 0; iCol < num_iis_col; iCol++) { + if (iis_col_index) iis_col_index[iCol] = this->iis_.col_index_[iCol]; + if (iis_col_bound) iis_col_bound[iCol] = this->iis_.col_bound_[iCol]; + } + } + if (iis_row_index || iis_row_bound) { + for (HighsInt iRow = 0; iRow < num_iis_row; iRow++) { + if (iis_row_index) iis_row_index[iRow] = this->iis_.row_index_[iRow]; + if (iis_row_bound) iis_row_bound[iRow] = this->iis_.row_bound_[iRow]; + } + } + return HighsStatus::kOk; +} + bool Highs::aFormatOk(const HighsInt num_nz, const HighsInt format) { if (!num_nz) return true; const bool ok_format = format == (HighsInt)MatrixFormat::kColwise || @@ -1795,17 +2433,21 @@ HighsStatus Highs::optionChangeAction() { dl_user_bound_scale_value = std::pow(2, dl_user_bound_scale); } // Now consider impact on primal feasibility of user bound scaling - // and/or primal_feasibility_tolerance change + // and/or primal_feasibility_tolerance change. + // double new_max_primal_infeasibility = info.max_primal_infeasibility * dl_user_bound_scale_value; if (new_max_primal_infeasibility > options.primal_feasibility_tolerance) { - // Not primal feasible - this->model_status_ = HighsModelStatus::kNotset; - if (info.primal_solution_status == kSolutionStatusFeasible) - highsLogUser(options_.log_options, HighsLogType::kInfo, - "Option change leads to loss of primal feasibility\n"); - info.primal_solution_status = kSolutionStatusInfeasible; - info.num_primal_infeasibilities = kHighsIllegalInfeasibilityCount; + // Not primal feasible: only act if the model is currently primal + // feasible or dl_user_bound_scale_value > 1 + if (info.num_primal_infeasibilities == 0 && dl_user_bound_scale_value > 1) { + this->model_status_ = HighsModelStatus::kNotset; + if (info.primal_solution_status == kSolutionStatusFeasible) + highsLogUser(options_.log_options, HighsLogType::kInfo, + "Option change leads to loss of primal feasibility\n"); + info.primal_solution_status = kSolutionStatusInfeasible; + info.num_primal_infeasibilities = kHighsIllegalInfeasibilityCount; + } } else if (!is_mip && info.primal_solution_status == kSolutionStatusInfeasible) { highsLogUser(options_.log_options, HighsLogType::kInfo, @@ -1827,7 +2469,10 @@ HighsStatus Highs::optionChangeAction() { } } if (dl_user_bound_scale) { - // Update info and solution with respect to non-trivial user bound scaling + // Update info and solution with respect to non-trivial user bound + // scaling + // + // max and sum of infeasibilities scales: num is handled later info.objective_function_value *= dl_user_bound_scale_value; info.max_primal_infeasibility *= dl_user_bound_scale_value; info.sum_primal_infeasibilities *= dl_user_bound_scale_value; @@ -1863,14 +2508,17 @@ HighsStatus Highs::optionChangeAction() { double new_max_dual_infeasibility = info.max_dual_infeasibility * dl_user_cost_scale_value; if (new_max_dual_infeasibility > options.dual_feasibility_tolerance) { - // Not dual feasible - this->model_status_ = HighsModelStatus::kNotset; - if (info.dual_solution_status == kSolutionStatusFeasible) { - highsLogUser(options_.log_options, HighsLogType::kInfo, - "Option change leads to loss of dual feasibility\n"); - info.dual_solution_status = kSolutionStatusInfeasible; + // Not dual feasible: only act if the model is currently dual + // feasible or dl_user_bound_scale_value > 1 + if (info.num_dual_infeasibilities == 0 && dl_user_cost_scale_value > 1) { + this->model_status_ = HighsModelStatus::kNotset; + if (info.dual_solution_status == kSolutionStatusFeasible) { + highsLogUser(options_.log_options, HighsLogType::kInfo, + "Option change leads to loss of dual feasibility\n"); + info.dual_solution_status = kSolutionStatusInfeasible; + } + info.num_dual_infeasibilities = kHighsIllegalInfeasibilityCount; } - info.num_dual_infeasibilities = kHighsIllegalInfeasibilityCount; } else if (info.dual_solution_status == kSolutionStatusInfeasible) { highsLogUser(options_.log_options, HighsLogType::kInfo, "Option change leads to gain of dual feasibility\n"); @@ -1885,6 +2533,8 @@ HighsStatus Highs::optionChangeAction() { } // Now update data and solution with respect to non-trivial user // cost scaling + // + // max and sum of infeasibilities scales: num is handled earlier info.objective_function_value *= dl_user_cost_scale_value; info.max_dual_infeasibility *= dl_user_cost_scale_value; info.sum_dual_infeasibilities *= dl_user_cost_scale_value; @@ -1903,6 +2553,8 @@ HighsStatus Highs::optionChangeAction() { } } if (!user_bound_scale_ok || !user_cost_scale_ok) return HighsStatus::kError; + if (this->iis_.valid_ && options_.iis_strategy != this->iis_.strategy_) + this->iis_.invalidate(); return HighsStatus::kOk; } diff --git a/src/lp_data/HighsLp.cpp b/src/lp_data/HighsLp.cpp index 23546b5660..842755a546 100644 --- a/src/lp_data/HighsLp.cpp +++ b/src/lp_data/HighsLp.cpp @@ -68,6 +68,34 @@ bool HighsLp::operator==(const HighsLp& lp) const { return equal; } +bool HighsLp::equalButForNames(const HighsLp& lp) const { + bool equal = equalButForScalingAndNames(lp); + equal = equalScaling(lp) && equal; + return equal; +} + +bool HighsLp::equalButForScalingAndNames(const HighsLp& lp) const { + bool equal_vectors = true; + equal_vectors = this->num_col_ == lp.num_col_ && equal_vectors; + equal_vectors = this->num_row_ == lp.num_row_ && equal_vectors; + equal_vectors = this->sense_ == lp.sense_ && equal_vectors; + equal_vectors = this->offset_ == lp.offset_ && equal_vectors; + equal_vectors = this->model_name_ == lp.model_name_ && equal_vectors; + equal_vectors = this->col_cost_ == lp.col_cost_ && equal_vectors; + equal_vectors = this->col_upper_ == lp.col_upper_ && equal_vectors; + equal_vectors = this->col_lower_ == lp.col_lower_ && equal_vectors; + equal_vectors = this->row_upper_ == lp.row_upper_ && equal_vectors; + equal_vectors = this->row_lower_ == lp.row_lower_ && equal_vectors; +#ifndef NDEBUG + if (!equal_vectors) printf("HighsLp::equalButForNames: Unequal vectors\n"); +#endif + const bool equal_matrix = this->a_matrix_ == lp.a_matrix_; +#ifndef NDEBUG + if (!equal_matrix) printf("HighsLp::equalButForNames: Unequal matrix\n"); +#endif + return equal_vectors && equal_matrix; +} + bool HighsLp::equalNames(const HighsLp& lp) const { bool equal = true; equal = this->objective_name_ == lp.objective_name_ && equal; @@ -76,21 +104,8 @@ bool HighsLp::equalNames(const HighsLp& lp) const { return equal; } -bool HighsLp::equalButForNames(const HighsLp& lp) const { +bool HighsLp::equalScaling(const HighsLp& lp) const { bool equal = true; - equal = this->num_col_ == lp.num_col_ && equal; - equal = this->num_row_ == lp.num_row_ && equal; - equal = this->sense_ == lp.sense_ && equal; - equal = this->offset_ == lp.offset_ && equal; - equal = this->model_name_ == lp.model_name_ && equal; - equal = this->col_cost_ == lp.col_cost_ && equal; - equal = this->col_upper_ == lp.col_upper_ && equal; - equal = this->col_lower_ == lp.col_lower_ && equal; - equal = this->row_upper_ == lp.row_upper_ && equal; - equal = this->row_lower_ == lp.row_lower_ && equal; - - equal = this->a_matrix_ == lp.a_matrix_; - equal = this->scale_.strategy == lp.scale_.strategy && equal; equal = this->scale_.has_scaling == lp.scale_.has_scaling && equal; equal = this->scale_.num_col == lp.scale_.num_col && equal; @@ -98,6 +113,9 @@ bool HighsLp::equalButForNames(const HighsLp& lp) const { equal = this->scale_.cost == lp.scale_.cost && equal; equal = this->scale_.col == lp.scale_.col && equal; equal = this->scale_.row == lp.scale_.row && equal; +#ifndef NDEBUG + if (!equal) printf("HighsLp::equalScaling: Unequal scaling\n"); +#endif return equal; } @@ -434,6 +452,7 @@ void HighsLp::deleteColsFromVectors( this->col_cost_.resize(new_num_col); this->col_lower_.resize(new_num_col); this->col_upper_.resize(new_num_col); + if (have_integrality) this->integrality_.resize(new_num_col); if (have_names) this->col_names_.resize(new_num_col); } diff --git a/src/lp_data/HighsLp.h b/src/lp_data/HighsLp.h index 1e8b766759..fc337211c7 100644 --- a/src/lp_data/HighsLp.h +++ b/src/lp_data/HighsLp.h @@ -61,7 +61,9 @@ class HighsLp { bool operator==(const HighsLp& lp) const; bool equalButForNames(const HighsLp& lp) const; + bool equalButForScalingAndNames(const HighsLp& lp) const; bool equalNames(const HighsLp& lp) const; + bool equalScaling(const HighsLp& lp) const; bool isMip() const; bool hasSemiVariables() const; bool hasInfiniteCost(const double infinite_cost) const; diff --git a/src/lp_data/HighsLpUtils.cpp b/src/lp_data/HighsLpUtils.cpp index 248c6aa842..ebe803bab0 100644 --- a/src/lp_data/HighsLpUtils.cpp +++ b/src/lp_data/HighsLpUtils.cpp @@ -1613,6 +1613,9 @@ void changeLpIntegrality(HighsLp& lp, if (mask && !col_mask[col]) continue; lp.integrality_[col] = new_integrality[usr_col]; } + // If integrality_ contains only HighsVarType::kContinuous then + // clear it + if (!lp.isMip()) lp.integrality_.clear(); } void changeLpCosts(HighsLp& lp, const HighsIndexCollection& index_collection, diff --git a/src/lp_data/HighsOptions.h b/src/lp_data/HighsOptions.h index 6b4a6ad840..8f934b30ce 100644 --- a/src/lp_data/HighsOptions.h +++ b/src/lp_data/HighsOptions.h @@ -345,6 +345,9 @@ struct HighsOptionsStruct { HighsInt qp_iteration_limit; HighsInt qp_nullspace_limit; + // Options for IIS calculation + HighsInt iis_strategy; + // Advanced options HighsInt log_dev_level; bool log_githash; @@ -1093,6 +1096,22 @@ class HighsOptions : public HighsOptionsStruct { &qp_nullspace_limit, 0, 4000, kHighsIInf); records.push_back(record_int); + record_int = new OptionRecordInt( + "iis_strategy", + "Strategy for IIS calculation: " + // "Use LP and p" + "Prioritise rows (default) / " + // "Use LP and p" + "Prioritise columns" + // "Use unbounded dual ray and prioritise low number of rows + // (default) / " "Use ray and prioritise low numbers of columns " + " (0/1" + // "/2/3)", + ")", + advanced, &iis_strategy, kIisStrategyMin, kIisStrategyFromLpRowPriority, + kIisStrategyMax); + records.push_back(record_int); + // Fix the number of user settable options num_user_settable_options_ = static_cast(records.size()); diff --git a/src/meson.build b/src/meson.build index 61dd94f9e7..56b83517ea 100644 --- a/src/meson.build +++ b/src/meson.build @@ -205,6 +205,7 @@ _srcs = [ 'lp_data/Highs.cpp', 'lp_data/HighsCallback.cpp', 'lp_data/HighsDebug.cpp', + 'lp_data/HighsIis.cpp', 'lp_data/HighsInfo.cpp', 'lp_data/HighsInfoDebug.cpp', 'lp_data/HighsDeprecated.cpp', diff --git a/src/simplex/HEkk.cpp b/src/simplex/HEkk.cpp index b94d0959f6..459bdd7a52 100644 --- a/src/simplex/HEkk.cpp +++ b/src/simplex/HEkk.cpp @@ -161,6 +161,8 @@ void HEkk::clearEkkData() { this->debug_max_relative_dual_steepest_edge_weight_error = 0; clearBadBasisChange(); + + this->primal_phase1_dual_.clear(); } void HEkk::clearEkkDataInfo() { diff --git a/src/simplex/HEkk.h b/src/simplex/HEkk.h index 6384e22c10..d259680794 100644 --- a/src/simplex/HEkk.h +++ b/src/simplex/HEkk.h @@ -244,6 +244,7 @@ class HEkk { double debug_max_relative_dual_steepest_edge_weight_error; std::vector bad_basis_change_; + std::vector primal_phase1_dual_; private: bool isUnconstrainedLp(); diff --git a/src/simplex/HEkkPrimal.cpp b/src/simplex/HEkkPrimal.cpp index 9a258ad90e..c440fb0f07 100644 --- a/src/simplex/HEkkPrimal.cpp +++ b/src/simplex/HEkkPrimal.cpp @@ -253,6 +253,11 @@ HighsStatus HEkkPrimal::solve(const bool pass_force_phase2) { // LP identified as not having an optimal solution assert(ekk_instance_.model_status_ == HighsModelStatus::kInfeasible || ekk_instance_.model_status_ == HighsModelStatus::kUnbounded); + // If infeasible, save the primal phase 1 dual values before + // they are overwritten with the duals for the original + // objective + if (ekk_instance_.model_status_ == HighsModelStatus::kInfeasible) + ekk_instance_.primal_phase1_dual_ = ekk_instance_.info_.workDual_; break; } if (solve_phase == kSolvePhaseOptimalCleanup) {