Skip to content

Commit b173b26

Browse files
authored
Merge pull request ERGO-Code#2298 from ERGO-Code/fix-1423
Fix 1423: Add Feasibility Jump
2 parents 042c262 + 4e44e0f commit b173b26

17 files changed

+1164
-89
lines changed

BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ cc_library(
3737
]),
3838
hdrs = glob([
3939
"**/*.h",
40+
"highs/mip/feasibilityjump.hh",
4041
"highs/qpsolver/*.hpp",
4142
"highs/Highs.h",
4243
"extern/filereaderlp/*.hpp",

check/TestCheckSolution.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
113113

114114
highs.run();
115115
if (dev_run) printf("Num nodes = %d\n", int(info.mip_node_count));
116-
REQUIRE(info.mip_node_count < scratch_num_nodes);
116+
REQUIRE(info.mip_node_count != scratch_num_nodes);
117117
highs.clear();
118118
}
119119

@@ -132,7 +132,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
132132

133133
highs.run();
134134
if (dev_run) printf("Num nodes = %d\n", int(info.mip_node_count));
135-
REQUIRE(info.mip_node_count < scratch_num_nodes);
135+
REQUIRE(info.mip_node_count != scratch_num_nodes);
136136
highs.clear();
137137
}
138138

@@ -162,7 +162,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
162162

163163
highs.run();
164164
if (dev_run) printf("Num nodes = %d\n", int(info.mip_node_count));
165-
REQUIRE(info.mip_node_count < scratch_num_nodes);
165+
REQUIRE(info.mip_node_count != scratch_num_nodes);
166166
highs.clear();
167167
}
168168

@@ -185,7 +185,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
185185

186186
highs.run();
187187
if (dev_run) printf("Num nodes = %d\n", int(info.mip_node_count));
188-
REQUIRE(info.mip_node_count < scratch_num_nodes);
188+
REQUIRE(info.mip_node_count != scratch_num_nodes);
189189
highs.clear();
190190
}
191191

@@ -232,7 +232,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
232232
return_status = highs.setSolution(starting_solution);
233233
REQUIRE(return_status == HighsStatus::kOk);
234234
highs.run();
235-
REQUIRE(info.mip_node_count < scratch_num_nodes);
235+
REQUIRE(info.mip_node_count != scratch_num_nodes);
236236
highs.clear();
237237
}
238238

@@ -283,7 +283,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
283283
return_status = highs.setSolution(num_entries, index.data(), value.data());
284284
REQUIRE(return_status == HighsStatus::kOk);
285285
highs.run();
286-
REQUIRE(info.mip_node_count < scratch_num_nodes);
286+
REQUIRE(info.mip_node_count != scratch_num_nodes);
287287
highs.clear();
288288
}
289289
assert(other_tests);

check/TestLpSolvers.cpp

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,9 +495,6 @@ TEST_CASE("highs-files-mip", "[highs_lp_solver]") {
495495

496496
REQUIRE(run_status == HighsStatus::kOk);
497497

498-
// This also causes the meson build CI test to fail!
499-
REQUIRE(h.getInfo().mip_node_count < mip_node_count);
500-
501498
std::remove(write_model_file.c_str());
502499
std::remove(write_solution_file.c_str());
503500

check/TestMipSolver.cpp

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ TEST_CASE("MIP-unbounded", "[highs_test_mip_solver]") {
294294
lp.col_upper_ = {inf};
295295
lp.integrality_ = {HighsVarType::kInteger};
296296

297-
bool use_presolve = true;
297+
bool use_presolve = false;
298298
HighsModelStatus require_model_status;
299299
for (HighsInt k = 0; k < 2; k++) {
300300
if (use_presolve) {
@@ -304,7 +304,11 @@ TEST_CASE("MIP-unbounded", "[highs_test_mip_solver]") {
304304
require_model_status = HighsModelStatus::kUnboundedOrInfeasible;
305305
} else {
306306
// With use_presolve = false, MIP solver returns
307-
// HighsModelStatus::kUnbounded
307+
// HighsModelStatus::kUnbounded, because the all-zeros trivial
308+
// heuristic finds a feasible point
309+
//
310+
// Feasibility jump appears to find one before the all-zeros
311+
// trivial heuristic
308312
highs.setOptionValue("presolve", kHighsOffString);
309313
require_model_status = HighsModelStatus::kUnbounded;
310314
}
@@ -317,8 +321,8 @@ TEST_CASE("MIP-unbounded", "[highs_test_mip_solver]") {
317321
model_status = highs.getModelStatus();
318322
REQUIRE(model_status == require_model_status);
319323

320-
// Second time through loop is without presolve
321-
use_presolve = false;
324+
// Second time through loop is with presolve
325+
use_presolve = true;
322326
}
323327
// Two-variable problem that is also primal unbounded as an LP, but
324328
// primal infeasible as a MIP.
@@ -339,13 +343,12 @@ TEST_CASE("MIP-unbounded", "[highs_test_mip_solver]") {
339343
lp.a_matrix_.value_ = {1, 2};
340344
lp.a_matrix_.format_ = MatrixFormat::kRowwise;
341345

342-
use_presolve = true;
346+
use_presolve = false;
343347
for (HighsInt k = 0; k < 2; k++) {
344348
if (use_presolve) {
345349
// With use_presolve = true, LP solver returns
346350
// HighsModelStatus::kUnbounded because it solves the LP after
347351
// presolve has returned
348-
// HighsModelStatus::kUnboundedOrInfeasible
349352
highs.setOptionValue("presolve", kHighsOnString);
350353
require_model_status = HighsModelStatus::kUnbounded;
351354
} else {
@@ -364,38 +367,25 @@ TEST_CASE("MIP-unbounded", "[highs_test_mip_solver]") {
364367
model_status = highs.getModelStatus();
365368
REQUIRE(model_status == require_model_status);
366369

367-
// Second time through loop is without presolve
368-
use_presolve = false;
370+
// Second time through loop is with presolve
371+
use_presolve = true;
369372
}
370373

371374
// Now as a MIP - infeasible
372375
lp.integrality_ = {HighsVarType::kContinuous, HighsVarType::kInteger};
373-
use_presolve = true;
374-
for (HighsInt k = 0; k < 2; k++) {
375-
if (use_presolve) {
376-
// With use_presolve = true, MIP solver returns
377-
// HighsModelStatus::kUnboundedOrInfeasible from presolve
378-
highs.setOptionValue("presolve", kHighsOnString);
379-
require_model_status = HighsModelStatus::kUnboundedOrInfeasible;
380-
} else {
381-
// With use_presolve = false, MIP solver returns
382-
// HighsModelStatus::kUnboundedOrInfeasible
383-
highs.setOptionValue("presolve", kHighsOffString);
384-
require_model_status = HighsModelStatus::kUnboundedOrInfeasible;
385-
}
376+
// With(out) presolve, Highs::infeasibleBoundsOk() performs inward
377+
// integer rounding of [0.25, 0.75] to [1, 0] so identifes
378+
// infeasiblility. Hence MIP solver returns
379+
// HighsModelStatus::kInfeasible
386380

387-
return_status = highs.passModel(lp);
388-
REQUIRE(return_status == HighsStatus::kOk);
389-
390-
return_status = highs.run();
391-
REQUIRE(return_status == HighsStatus::kOk);
381+
return_status = highs.passModel(lp);
382+
REQUIRE(return_status == HighsStatus::kOk);
392383

393-
model_status = highs.getModelStatus();
394-
REQUIRE(model_status == require_model_status);
384+
return_status = highs.run();
385+
REQUIRE(return_status == HighsStatus::kOk);
395386

396-
// Second time through loop is without presolve
397-
use_presolve = false;
398-
}
387+
model_status = highs.getModelStatus();
388+
REQUIRE(model_status == HighsModelStatus::kInfeasible);
399389
}
400390

401391
TEST_CASE("MIP-od", "[highs_test_mip_solver]") {
@@ -568,7 +558,7 @@ TEST_CASE("MIP-get-saved-solutions", "[highs_test_mip_solver]") {
568558
const std::vector<HighsObjectiveSolution> saved_objective_and_solution =
569559
highs.getSavedMipSolutions();
570560
const HighsInt num_saved_solution = saved_objective_and_solution.size();
571-
REQUIRE(num_saved_solution == 3);
561+
REQUIRE(num_saved_solution > 0);
572562
const HighsInt last_saved_solution = num_saved_solution - 1;
573563
REQUIRE(saved_objective_and_solution[last_saved_solution].objective ==
574564
highs.getInfo().objective_function_value);
@@ -693,9 +683,22 @@ TEST_CASE("IP-infeasible-unbounded", "[highs_test_mip_solver]") {
693683
required_model_status = HighsModelStatus::kUnbounded;
694684
}
695685
} else {
696-
// Presolve on, and identifies primal infeasible or unbounded
697-
required_model_status = HighsModelStatus::kUnboundedOrInfeasible;
686+
// Presolve on
687+
if (l == 0) {
688+
// Inward integer rounding proves infeasiblilty
689+
required_model_status = HighsModelStatus::kInfeasible;
690+
} else {
691+
// Presolve identifies primal infeasible or unbounded
692+
required_model_status = HighsModelStatus::kUnboundedOrInfeasible;
693+
}
698694
}
695+
if (dev_run)
696+
printf(
697+
"For k = %d and l = %d, original bounds on col 1 are [%g, %g]: "
698+
"model status is \"%s\" and required status is \"%s\"\n",
699+
int(k), int(l), lp.col_lower_[1], lp.col_upper_[1],
700+
highs.modelStatusToString(highs.getModelStatus()).c_str(),
701+
highs.modelStatusToString(required_model_status).c_str());
699702
REQUIRE(highs.getModelStatus() == required_model_status);
700703
}
701704
highs.setOptionValue("presolve", kHighsOnString);
@@ -704,8 +707,8 @@ TEST_CASE("IP-infeasible-unbounded", "[highs_test_mip_solver]") {
704707

705708
TEST_CASE("IP-with-fract-bounds-no-presolve", "[highs_test_mip_solver]") {
706709
Highs highs;
707-
// No presolve
708710
highs.setOptionValue("output_flag", dev_run);
711+
// No presolve
709712
highs.setOptionValue("presolve", kHighsOffString);
710713

711714
// IP without constraints and fractional bounds on variables

cmake/sources-python.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ set(highs_sources_python
210210
highs/mip/HighsDebugSol.cpp
211211
highs/mip/HighsDomain.cpp
212212
highs/mip/HighsDynamicRowMatrix.cpp
213+
highs/mip/HighsFeasibilityJump.cpp
213214
highs/mip/HighsGFkSolve.cpp
214215
highs/mip/HighsImplications.cpp
215216
highs/mip/HighsLpAggregator.cpp
@@ -320,6 +321,7 @@ set(highs_headers_python
320321
highs/lp_data/HighsSolve.h
321322
highs/lp_data/HighsStatus.h
322323
highs/lp_data/HStruct.h
324+
highs/mip/feasibilityjump.hh
323325
highs/mip/HighsCliqueTable.h
324326
highs/mip/HighsConflictPool.h
325327
highs/mip/HighsCutGeneration.h

cmake/sources.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ set(highs_sources
212212
mip/HighsDebugSol.cpp
213213
mip/HighsDomain.cpp
214214
mip/HighsDynamicRowMatrix.cpp
215+
mip/HighsFeasibilityJump.cpp
215216
mip/HighsGFkSolve.cpp
216217
mip/HighsImplications.cpp
217218
mip/HighsLpAggregator.cpp
@@ -325,6 +326,7 @@ set(highs_headers
325326
lp_data/HighsSolve.h
326327
lp_data/HighsStatus.h
327328
lp_data/HStruct.h
329+
mip/feasibilityjump.hh
328330
mip/HighsCliqueTable.h
329331
mip/HighsConflictPool.h
330332
mip/HighsCutGeneration.h

highs/lp_data/HStruct.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,12 @@ struct HighsIllConditioning {
159159
};
160160

161161
struct HighsLinearObjective {
162-
double weight;
163-
double offset;
162+
double weight = 0;
163+
double offset = 0;
164164
std::vector<double> coefficients;
165-
double abs_tolerance;
166-
double rel_tolerance;
167-
HighsInt priority;
165+
double abs_tolerance = -1;
166+
double rel_tolerance = -1;
167+
HighsInt priority = 0;
168168
void clear();
169169
};
170170

highs/lp_data/HighsInterface.cpp

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3408,12 +3408,19 @@ bool Highs::infeasibleBoundsOk() {
34083408
HighsInt num_true_infeasible_bound = 0;
34093409
HighsInt num_ok_infeasible_bound = 0;
34103410
const bool has_integrality = lp.integrality_.size() > 0;
3411+
bool performed_inward_integer_rounding = false;
34113412
// Lambda for assessing infeasible bounds
3412-
auto assessInfeasibleBound = [&](const std::string type, const HighsInt iX,
3413-
double& lower, double& upper) {
3413+
auto infeasibleBoundOk = [&](const std::string type, const HighsInt iX,
3414+
double& lower, double& upper) {
34143415
double range = upper - lower;
3416+
// Should only be called if lower > upper, so range < 0
3417+
assert(range < 0);
34153418
if (range >= 0) return true;
34163419
if (range > -this->options_.primal_feasibility_tolerance) {
3420+
// Infeasibility is less than feasibility tolerance, so fix
3421+
// bounds at lower (upper) if lower (upper) is an integer - and
3422+
// both can't be integer, otherwise the range <= -1 - otherwise
3423+
// fix at 0.5 * (lower + upper)
34173424
num_ok_infeasible_bound++;
34183425
bool report = num_ok_infeasible_bound <= 10;
34193426
bool integer_lower = lower == std::floor(lower + 0.5);
@@ -3445,29 +3452,63 @@ bool Highs::infeasibleBoundsOk() {
34453452
}
34463453
return true;
34473454
}
3455+
// Infeasibility is greater than feasibility tolerance, so report
3456+
// this (up to 10 times)
34483457
num_true_infeasible_bound++;
34493458
if (num_true_infeasible_bound <= 10)
3450-
highsLogUser(log_options, HighsLogType::kInfo,
3451-
"%s %d bounds [%g, %g] have excessive infeasibility = %g\n",
3452-
type.c_str(), int(iX), lower, upper, range);
3459+
highsLogUser(
3460+
log_options, HighsLogType::kInfo,
3461+
"%s %d bounds [%g, %g] have excessive infeasibility = %g%s\n",
3462+
type.c_str(), int(iX), lower, upper, range,
3463+
performed_inward_integer_rounding ? " due to inward integer rounding"
3464+
: "");
34533465
return false;
34543466
};
34553467

3468+
const bool perform_inward_integer_rounding = !this->options_.solve_relaxation;
34563469
for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) {
3470+
performed_inward_integer_rounding = false;
3471+
double lower = lp.col_lower_[iCol];
3472+
double upper = lp.col_upper_[iCol];
34573473
if (has_integrality) {
3458-
// Semi-variables can have inconsistent bounds
3474+
// Semi-variables cannot have inconsistent bounds
34593475
if (lp.integrality_[iCol] == HighsVarType::kSemiContinuous ||
34603476
lp.integrality_[iCol] == HighsVarType::kSemiInteger)
34613477
continue;
3478+
if (perform_inward_integer_rounding &&
3479+
lp.integrality_[iCol] == HighsVarType::kInteger) {
3480+
// Assess bounds after inward integer rounding
3481+
double feastol = this->options_.mip_feasibility_tolerance;
3482+
double integer_lower = std::ceil(lower - feastol);
3483+
double integer_upper = std::floor(upper + feastol);
3484+
assert(integer_lower >= lower);
3485+
assert(integer_upper <= upper);
3486+
performed_inward_integer_rounding =
3487+
integer_lower > lower || integer_upper < upper;
3488+
lower = integer_lower;
3489+
upper = integer_upper;
3490+
}
3491+
}
3492+
//
3493+
if (lower > upper) {
3494+
if (infeasibleBoundOk("Column", iCol, lower, upper)) {
3495+
// Bound infeasibility is OK (less than the tolerance), so can
3496+
// change the model data
3497+
lp.col_lower_[iCol] = lower;
3498+
lp.col_upper_[iCol] = upper;
3499+
}
34623500
}
3463-
if (lp.col_lower_[iCol] > lp.col_upper_[iCol])
3464-
assessInfeasibleBound("Column", iCol, lp.col_lower_[iCol],
3465-
lp.col_upper_[iCol]);
3501+
// Note that any inward integer rounding can't be used to change
3502+
// the model data, since it may be a significant change and make
3503+
// the relaxation infeasible when previously it was feasible. In
3504+
// particular, when inward integer rounding leads to inconsistent
3505+
// bounds being propagated to the relaxation, this can prevent a
3506+
// dual ray from being constructed
34663507
}
3508+
performed_inward_integer_rounding = false;
34673509
for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) {
34683510
if (lp.row_lower_[iRow] > lp.row_upper_[iRow])
3469-
assessInfeasibleBound("Row", iRow, lp.row_lower_[iRow],
3470-
lp.row_upper_[iRow]);
3511+
infeasibleBoundOk("Row", iRow, lp.row_lower_[iRow], lp.row_upper_[iRow]);
34713512
}
34723513
if (num_ok_infeasible_bound > 0)
34733514
highsLogUser(log_options, HighsLogType::kInfo,
@@ -3598,7 +3639,7 @@ HighsStatus Highs::multiobjectiveSolve() {
35983639
"%2d %11.6g %11.6g %11.6g %11.6g %11d ", int(iObj),
35993640
linear_objective.weight, linear_objective.offset,
36003641
linear_objective.abs_tolerance, linear_objective.rel_tolerance,
3601-
linear_objective.priority);
3642+
int(linear_objective.priority));
36023643
if (lp.num_col_ < coeff_logging_size_limit) {
36033644
for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++)
36043645
*multi_objective_log << highsFormatToString(

highs/lp_data/HighsOptions.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ struct HighsOptionsStruct {
421421
// Options for MIP solver
422422
bool mip_detect_symmetry;
423423
bool mip_allow_restart;
424+
bool mip_allow_feasibility_jump;
424425
HighsInt mip_max_nodes;
425426
HighsInt mip_max_stall_nodes;
426427
HighsInt mip_max_start_nodes;
@@ -568,6 +569,7 @@ struct HighsOptionsStruct {
568569
icrash_breakpoints(false),
569570
mip_detect_symmetry(false),
570571
mip_allow_restart(false),
572+
mip_allow_feasibility_jump(false),
571573
mip_max_nodes(0),
572574
mip_max_stall_nodes(0),
573575
mip_max_start_nodes(0),
@@ -996,6 +998,12 @@ class HighsOptions : public HighsOptionsStruct {
996998
advanced, &mip_allow_restart, true);
997999
records.push_back(record_bool);
9981000

1001+
record_bool =
1002+
new OptionRecordBool("mip_allow_feasibility_jump",
1003+
"Whether MIP feasibility jump is permitted",
1004+
advanced, &mip_allow_feasibility_jump, true);
1005+
records.push_back(record_bool);
1006+
9991007
record_int = new OptionRecordInt("mip_max_nodes",
10001008
"MIP solver max number of nodes", advanced,
10011009
&mip_max_nodes, 0, kHighsIInf, kHighsIInf);

0 commit comments

Comments
 (0)