Skip to content

Commit 8145eba

Browse files
authored
Merge pull request #3135 from ERGO-Code/fix-3118-2
Fix 3118
2 parents 780d824 + 2299abe commit 8145eba

8 files changed

Lines changed: 299 additions & 76 deletions

File tree

check/TestCheckSolution.cpp

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
7878
std::string(HIGHS_DIR) + "/check/instances/" + model + ".mps";
7979
Highs highs;
8080
const HighsInfo& info = highs.getInfo();
81-
if (dev_run) printf("\n********************\nSolving from scratch\n");
81+
if (dev_run)
82+
printf(
83+
"\n********************\nSolving from scratch\n********************\n");
8284
highs.setOptionValue("output_flag", dev_run);
8385
highs.readModel(model_file);
8486
HighsLp lp = highs.getLp();
@@ -92,7 +94,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
9294

9395
std::string solution_file = test_name + model + ".sol";
9496
if (dev_run) REQUIRE(highs.writeSolution("") == HighsStatus::kOk);
95-
;
97+
9698
REQUIRE(highs.writeSolution(solution_file) == HighsStatus::kOk);
9799

98100
highs.clear();
@@ -102,7 +104,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
102104
bool valid, integral, feasible;
103105
if (test0) {
104106
if (dev_run)
105-
printf("\n***************************\nSolving from saved solution\n");
107+
printf(
108+
"\n***************************\nSolving from saved "
109+
"solution\n***************************\n");
106110
highs.setOptionValue("output_flag", dev_run);
107111
highs.readModel(model_file);
108112

@@ -124,7 +128,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
124128
const bool test1 = other_tests;
125129
if (test1) {
126130
if (dev_run)
127-
printf("\n***************************\nSolving from solution file\n");
131+
printf(
132+
"\n**************************\nSolving from solution "
133+
"file\n**************************\n");
128134
highs.setOptionValue("output_flag", dev_run);
129135
highs.readModel(model_file);
130136

@@ -147,8 +153,8 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
147153
if (test2) {
148154
if (dev_run)
149155
printf(
150-
"\n***************************\nSolving from saved integer "
151-
"solution\n");
156+
"\n***********************************\nSolving from saved integer "
157+
"solution\n***********************************\n");
152158
highs.setOptionValue("output_flag", dev_run);
153159
highs.readModel(model_file);
154160

@@ -180,7 +186,8 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
180186
if (test3) {
181187
if (dev_run)
182188
printf(
183-
"\n***************************\nSolving from column solution file\n");
189+
"\n*********************************\nSolving from column solution "
190+
"file\n*********************************\n");
184191
std::string column_solution_file =
185192
std::string(HIGHS_DIR) + "/check/instances/flugpl_integer.sol";
186193

@@ -206,8 +213,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
206213
if (test4) {
207214
if (dev_run)
208215
printf(
209-
"\n***************************\nSolving from illegal column solution "
210-
"file\n");
216+
"\n*****************************************\nSolving from illegal "
217+
"column solution "
218+
"file\n*****************************************\n");
211219
std::string column_solution_file =
212220
std::string(HIGHS_DIR) + "/check/instances/flugpl_illegal_integer.sol";
213221

@@ -225,8 +233,9 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
225233
HighsSolution starting_solution = optimal_solution;
226234
if (dev_run)
227235
printf(
228-
"\n***************************\nSolving from partial integer "
229-
"solution\n");
236+
"\n*************************************\nSolving from partial "
237+
"integer "
238+
"solution\n*************************************\n");
230239
highs.setOptionValue("output_flag", dev_run);
231240
highs.readModel(model_file);
232241

@@ -256,8 +265,8 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
256265
if (test6) {
257266
if (dev_run)
258267
printf(
259-
"\n***************************\nSolving from sparse integer "
260-
"solution\n");
268+
"\n************************************\nSolving from sparse integer "
269+
"solution\n************************************\n");
261270
HighsInt num_integer_variable = 0;
262271
for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++)
263272
if (lp.integrality_[iCol] == HighsVarType::kInteger)

check/TestMipSolver.cpp

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,3 +1483,125 @@ TEST_CASE("issue-2975", "[highs_test_mip_solver]") {
14831483

14841484
highs.resetGlobalScheduler(true);
14851485
}
1486+
1487+
TEST_CASE("issue-3118", "[highs_test_mip_solver]") {
1488+
const double M = 1e10;
1489+
// min x + y
1490+
// s.t. x + M*y = 1
1491+
// M*x + y = 1
1492+
// x, y binary
1493+
// Initial "solution" x = y = 1/M
1494+
//
1495+
// This problem is found infeasible in presolve, but the point x = y
1496+
// = 1/M is integer feasible to within the tolerances, so is
1497+
// acccepted as an optimal solution to the problem
1498+
Highs highs;
1499+
highs.setOptionValue("output_flag", dev_run);
1500+
1501+
HighsInt x = 0;
1502+
HighsInt y = 1;
1503+
HighsLp lp;
1504+
lp.num_col_ = 2;
1505+
lp.num_row_ = 2;
1506+
lp.col_lower_ = {0., 0.};
1507+
lp.col_upper_ = {1., 1.};
1508+
lp.col_cost_ = {1., 1.};
1509+
lp.integrality_ = {HighsVarType::kInteger, HighsVarType::kInteger};
1510+
lp.row_lower_ = {1., 1.};
1511+
lp.row_upper_ = {1., 1.};
1512+
lp.a_matrix_.format_ = MatrixFormat::kRowwise;
1513+
lp.a_matrix_.start_ = {0, 2, 4};
1514+
lp.a_matrix_.index_ = {x, y, x, y};
1515+
lp.a_matrix_.value_ = {1., M, M, 1};
1516+
highs.passModel(lp);
1517+
1518+
std::vector<double> solution_values(lp.num_col_, 1 / M);
1519+
highs.setSolution(2, nullptr, solution_values.data());
1520+
1521+
highs.run();
1522+
REQUIRE(highs.getModelStatus() == HighsModelStatus::kOptimal);
1523+
REQUIRE(std::abs(2 / M - highs.getInfo().objective_function_value) < 1e-9);
1524+
if (dev_run) highs.writeSolution("", 1);
1525+
1526+
highs.resetGlobalScheduler(true);
1527+
}
1528+
1529+
TEST_CASE("issue-3118a", "[highs_test_mip_solver]") {
1530+
const double M = 1e10;
1531+
// max f = x
1532+
// s.t. x - M*y = -1
1533+
// x <= b
1534+
// x, y integer in [0, 2]x[0, 1]
1535+
//
1536+
// The MIP is not feasible over the integers for any b
1537+
//
1538+
// With initial "solution" x = 0; y = 1/M
1539+
//
1540+
// For b = 0: (0, 1/M) with f = 0 is the only feasible solution of
1541+
// the relaxation, and it's also feasible for the MIP, so claiming
1542+
// it is optimal when presolve identifies infeasibility is clearly
1543+
// justified
1544+
//
1545+
// For b = 1: (1, 2/M) with f = 1 is the optimal solution of the
1546+
// relaxation, and it's also feasible for the MIP. However, claiming
1547+
// that the initial "solution" (0, 1/M) with f = 0 is optimal when
1548+
// presolve identifies infeasibility is still justified, because
1549+
// it's a point that is feasible for a MIP that is deemed infeasible
1550+
// by presolve
1551+
Highs highs;
1552+
highs.setOptionValue("output_flag", dev_run);
1553+
1554+
HighsInt x = 0;
1555+
HighsInt y = 1;
1556+
HighsLp lp;
1557+
lp.sense_ = ObjSense::kMaximize;
1558+
lp.num_col_ = 2;
1559+
lp.num_row_ = 2;
1560+
lp.col_lower_ = {0, 0};
1561+
lp.col_upper_ = {2, 1};
1562+
lp.col_cost_ = {1, 0};
1563+
lp.integrality_ = {HighsVarType::kInteger, HighsVarType::kInteger};
1564+
lp.row_lower_ = {-1, -kHighsInf};
1565+
lp.row_upper_ = {-1, 0};
1566+
lp.a_matrix_.format_ = MatrixFormat::kRowwise;
1567+
lp.a_matrix_.start_ = {0, 2, 3};
1568+
lp.a_matrix_.index_ = {x, y, x};
1569+
lp.a_matrix_.value_ = {1., -M, 1};
1570+
1571+
for (HighsInt k = 0; k < 2; k++) {
1572+
HighsInt b = k == 0 ? 0 : 1;
1573+
lp.row_upper_[1] = b;
1574+
1575+
highs.passModel(lp);
1576+
1577+
// Solve as MIP
1578+
if (dev_run)
1579+
printf("================\nCase b = %d (MIP)\n================\n", int(b));
1580+
highs.setOptionValue("solve_relaxation", false);
1581+
std::vector<double> solution_values = {0, 1 / M};
1582+
highs.setSolution(2, nullptr, solution_values.data());
1583+
1584+
highs.run();
1585+
if (dev_run) highs.writeSolution("", 1);
1586+
REQUIRE(highs.getModelStatus() == HighsModelStatus::kOptimal);
1587+
1588+
// Solve as LP
1589+
if (dev_run)
1590+
printf("===============\nCase b = %d (LP)\n===============\n", int(b));
1591+
highs.setOptionValue("solve_relaxation", true);
1592+
highs.clearSolver();
1593+
highs.run();
1594+
highs.writeSolution("", 1);
1595+
REQUIRE(highs.getModelStatus() == HighsModelStatus::kOptimal);
1596+
double lp_objective_value = highs.getInfo().objective_function_value;
1597+
1598+
solution_values = highs.getSolution().col_value;
1599+
highs.setSolution(2, nullptr, solution_values.data());
1600+
1601+
bool valid, integral, feasible;
1602+
REQUIRE(highs.assessPrimalSolution(valid, integral, feasible) ==
1603+
HighsStatus::kOk);
1604+
}
1605+
1606+
highs.resetGlobalScheduler(true);
1607+
}

highs/Highs.h

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,11 @@ class Highs {
266266

267267
/**
268268
* @brief Assess the validity, integrality and feasibility of the
269-
* current primal solution. Of value after calling
270-
* Highs::readSolution
269+
* current primal solution. Row values are computed and checked
270+
* against what's in Highs::solution_.row_value and, if the
271+
* differences exceed a tolerance, valid returns false. If any of
272+
* valid, integral or feasible is false, then assessPrimalSolution
273+
* returns HighsStatus::kWarning.
271274
*/
272275
HighsStatus assessPrimalSolution(bool& valid, bool& integral,
273276
bool& feasible) const;
@@ -1245,7 +1248,15 @@ class Highs {
12451248
HighsStatus setSolution(const HighsSolution& solution);
12461249

12471250
/**
1248-
* @brief Pass a sparse primal solution
1251+
* @brief Pass a primal solution. If index is not a null pointer,
1252+
* then it is assumed that value contains num_entries of packed
1253+
* values, with index defing the corresponding primal solution
1254+
* components. If index is a null pointer, then value is assumed to
1255+
* be a full primal solution.
1256+
*
1257+
* It allows a full primal solution to be passed (for MIPs) without
1258+
* requiring either a HighsSolution that contains (empty vectors of)
1259+
* spurious dual information, or a full list of indices
12491260
*/
12501261
HighsStatus setSolution(const HighsInt num_entries, const HighsInt* index,
12511262
const double* value);

highs/lp_data/Highs.cpp

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,15 +2575,37 @@ HighsStatus Highs::getColOrRowName(const HighsLp& lp, const bool is_col,
25752575

25762576
HighsStatus Highs::setSolution(const HighsInt num_entries,
25772577
const HighsInt* index, const double* value) {
2578+
const bool packed = index != nullptr;
2579+
if (packed) {
2580+
const bool ok_num_entries = num_entries >= 0;
2581+
if (!ok_num_entries) {
2582+
highsLogUser(options_.log_options, HighsLogType::kError,
2583+
"setSolution: Packed user solution has %d < 0 entries\n",
2584+
int(num_entries));
2585+
assert(ok_num_entries);
2586+
return HighsStatus::kError;
2587+
}
2588+
} else {
2589+
const bool ok_num_entries = num_entries == model_.lp_.num_col_;
2590+
if (!ok_num_entries) {
2591+
highsLogUser(options_.log_options, HighsLogType::kError,
2592+
"setSolution: Packed user solution has %d != %d entries\n",
2593+
int(num_entries), int(model_.lp_.num_col_));
2594+
assert(ok_num_entries);
2595+
return HighsStatus::kError;
2596+
}
2597+
}
25782598
HighsStatus return_status = HighsStatus::kOk;
25792599
if (model_.lp_.num_col_ == 0) return return_status;
25802600
// Warn about duplicates in index
25812601
HighsInt num_duplicates = 0;
25822602
std::vector<bool> is_set;
25832603
is_set.assign(model_.lp_.num_col_, false);
2584-
for (HighsInt iX = 0; iX < num_entries; iX++) {
2585-
HighsInt iCol = index[iX];
2604+
const HighsInt to_ix = packed ? num_entries : model_.lp_.num_col_;
2605+
for (HighsInt iX = 0; iX < to_ix; iX++) {
2606+
HighsInt iCol = packed ? index[iX] : iX;
25862607
if (iCol < 0 || iCol >= model_.lp_.num_col_) {
2608+
assert(packed);
25872609
highsLogUser(options_.log_options, HighsLogType::kError,
25882610
"setSolution: User solution index %d has value %d out of "
25892611
"range [0, %d)\n",
@@ -2605,6 +2627,7 @@ HighsStatus Highs::setSolution(const HighsInt num_entries,
26052627
is_set[iCol] = true;
26062628
}
26072629
if (num_duplicates > 0) {
2630+
assert(packed);
26082631
highsLogUser(options_.log_options, HighsLogType::kWarning,
26092632
"setSolution: User set of indices has %d duplicate%s: last "
26102633
"value used\n",
@@ -2616,8 +2639,8 @@ HighsStatus Highs::setSolution(const HighsInt num_entries,
26162639
// user, and insert the values determined by the user
26172640
HighsSolution new_solution;
26182641
new_solution.col_value.assign(model_.lp_.num_col_, kHighsUndefined);
2619-
for (HighsInt iX = 0; iX < num_entries; iX++) {
2620-
HighsInt iCol = index[iX];
2642+
for (HighsInt iX = 0; iX < to_ix; iX++) {
2643+
HighsInt iCol = packed ? index[iX] : iX;
26212644
new_solution.col_value[iCol] = value[iX];
26222645
}
26232646
return interpretCallStatus(options_.log_options, setSolution(new_solution),
@@ -3990,6 +4013,7 @@ HighsStatus Highs::completeSolutionFromDiscreteAssignment() {
39904013
HighsStatus return_status = assessLpPrimalSolution(
39914014
"", options_, lp, solution_, valid, integral, feasible);
39924015
assert(return_status != HighsStatus::kError);
4016+
if (return_status == HighsStatus::kError) return HighsStatus::kError;
39934017
assert(valid);
39944018
// If the current solution is integer feasible, then it can be
39954019
// used by MIP solver to get a primal bound
@@ -4050,7 +4074,8 @@ HighsStatus Highs::completeSolutionFromDiscreteAssignment() {
40504074
// There are no continuous variables, so no feasible solution can be
40514075
// deduced
40524076
highsLogUser(options_.log_options, HighsLogType::kInfo,
4053-
"User-supplied values of discrete variables cannot yield "
4077+
"No continuous variables, so user-supplied values of "
4078+
"discrete variables cannot yield "
40544079
"feasible solution\n");
40554080
call_run = false;
40564081
} else {

0 commit comments

Comments
 (0)