Skip to content

Commit 02ab767

Browse files
authored
Merge pull request ERGO-Code#1774 from ERGO-Code/fix-1583-1689
QP solver logging less chatty; modifing Hessian when adding or deleting columns
2 parents 65fc061 + 7bd0035 commit 02ab767

16 files changed

+549
-139
lines changed

check/TestCAPI.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,13 +1141,13 @@ void full_api_qp() {
11411141
return_status = Highs_addCol(highs, -1.0, -inf, inf, 0, NULL, NULL);
11421142
assert( return_status == kHighsStatusOk );
11431143
num_col++;
1144-
// Cannot solve the model until the Hessian has been replaced
1144+
// Can solve the model before the Hessian has been replaced
11451145
return_status = Highs_run(highs);
1146-
assert( return_status == kHighsStatusError );
1147-
assertIntValuesEqual("Run status for 2-d QP with illegal Hessian", return_status, -1);
1146+
assert( return_status == kHighsStatusOk );
1147+
assertIntValuesEqual("Run status for 2-d QP with OK Hessian", return_status, 0);
11481148

11491149
model_status = Highs_getModelStatus(highs);
1150-
assertIntValuesEqual("Model status for 2-d QP with illegal Hessian", model_status, 2);
1150+
assertIntValuesEqual("Model status for this 2-d QP with OK Hessian", model_status, kHighsModelStatusUnbounded);
11511151

11521152
free(q_start);
11531153
free(q_index);
@@ -1775,6 +1775,6 @@ int main() {
17751775
test_passHessian();
17761776
test_ranging();
17771777
test_getModel();
1778-
// test_setSolution();
17791778
return 0;
17801779
}
1780+
// test_setSolution();

check/TestCheckSolution.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ TEST_CASE("check-set-mip-solution", "[highs_check_solution]") {
8383
if (dev_run) printf("Num nodes = %d\n", int(scratch_num_nodes));
8484

8585
std::string solution_file = model + ".sol";
86-
// if (dev_run) return_status = highs.writeSolution("");
86+
if (dev_run) return_status = highs.writeSolution("");
8787
return_status = highs.writeSolution(solution_file);
8888
REQUIRE(return_status == HighsStatus::kOk);
8989

check/TestMipSolver.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,7 @@ TEST_CASE("MIP-get-saved-solutions-presolve", "[highs_test_mip_solver]") {
656656
TEST_CASE("IP-with-fract-bounds-no-presolve", "[highs_test_mip_solver]") {
657657
Highs highs;
658658
// No presolve
659+
highs.setOptionValue("output_flag", dev_run);
659660
highs.setOptionValue("presolve", "off");
660661

661662
// IP without constraints and fractional bounds on variables

check/TestQpSolver.cpp

Lines changed: 276 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include <algorithm>
12
#include <cstdio>
23

34
#include "HCheckConfig.h"
@@ -219,12 +220,12 @@ TEST_CASE("test-qod", "[qpsolver]") {
219220
highs.addCol(-1, -inf, inf, 0, NULL, NULL);
220221
if (dev_run) highs.writeModel("");
221222

222-
// Cannot solve the model until the Hessian has been replaced
223+
// Can solve the model before the Hessian has been replaced
223224
return_status = highs.run();
224-
REQUIRE(return_status == HighsStatus::kError);
225+
REQUIRE(return_status == HighsStatus::kOk);
225226

226227
model_status = highs.getModelStatus();
227-
REQUIRE(model_status == HighsModelStatus::kModelError);
228+
REQUIRE(model_status == HighsModelStatus::kUnbounded);
228229

229230
// Pass the new Hessian
230231
hessian.dim_ = 2;
@@ -600,3 +601,275 @@ TEST_CASE("test-semi-definite2", "[qpsolver]") {
600601
REQUIRE(fabs(solution.col_value[0] + 1) < double_equal_tolerance);
601602
REQUIRE(fabs(solution.col_value[1] - 2) < double_equal_tolerance);
602603
}
604+
605+
void hessianProduct(const HighsHessian& hessian, const std::vector<double>& arg,
606+
std::vector<double>& result) {
607+
HighsInt dim = hessian.dim_;
608+
assert(HighsInt(arg.size()) == dim);
609+
result.resize(dim);
610+
for (HighsInt iCol = 0; iCol < dim; iCol++) {
611+
double sum = 0;
612+
for (HighsInt iEl = hessian.start_[iCol]; iEl < hessian.start_[iCol + 1];
613+
iEl++)
614+
sum += hessian.value_[iEl] * arg[hessian.index_[iEl]];
615+
result[iCol] = sum;
616+
}
617+
}
618+
619+
TEST_CASE("test-qp-modification", "[qpsolver]") {
620+
// HighsStatus return_status;
621+
// HighsModelStatus model_status;
622+
// double required_objective_function_value;
623+
624+
HighsModel model;
625+
626+
HighsLp& lp = model.lp_;
627+
HighsHessian& hessian = model.hessian_;
628+
629+
lp.num_col_ = 2;
630+
lp.num_row_ = 1;
631+
lp.col_cost_ = {1.0, -1.0};
632+
lp.col_lower_ = {-inf, -inf};
633+
lp.col_upper_ = {inf, inf};
634+
lp.sense_ = ObjSense::kMinimize;
635+
lp.offset_ = 0;
636+
lp.row_lower_ = {-inf};
637+
lp.row_upper_ = {1};
638+
lp.a_matrix_.format_ = MatrixFormat::kRowwise;
639+
lp.a_matrix_.start_ = {0, 2};
640+
lp.a_matrix_.index_ = {0, 1};
641+
lp.a_matrix_.value_ = {1.0, 1.0};
642+
hessian.dim_ = 1;
643+
hessian.start_ = {0, 1};
644+
hessian.value_ = {1.0};
645+
646+
Highs highs;
647+
highs.setOptionValue("output_flag", dev_run);
648+
const HighsModel& incumbent_model = highs.getModel();
649+
// Cannot have Hessian with index exceeding hessian.dim_-1
650+
hessian.index_ = {1};
651+
REQUIRE(highs.passModel(model) == HighsStatus::kError);
652+
653+
// Correct the Hessian index
654+
hessian.index_[0] = 0;
655+
REQUIRE(highs.passModel(model) == HighsStatus::kOk);
656+
if (dev_run) {
657+
printf("\nNow solve the QP\n\n");
658+
incumbent_model.hessian_.print();
659+
}
660+
highs.run();
661+
if (dev_run) highs.writeSolution("", kSolutionStylePretty);
662+
// Add a new variables and ensure that the Hessian dimension is correct
663+
std::vector<HighsInt> index = {0};
664+
std::vector<double> value = {1};
665+
REQUIRE(highs.addCol(-1, 0, 1, 1, index.data(), value.data()) ==
666+
HighsStatus::kOk);
667+
REQUIRE((incumbent_model.hessian_.dim_ == 0 ||
668+
incumbent_model.hessian_.dim_ == incumbent_model.lp_.num_col_));
669+
if (dev_run) {
670+
printf("\nNow solve the QP after adding new variable\n\n");
671+
incumbent_model.hessian_.print();
672+
}
673+
highs.run();
674+
if (dev_run) highs.writeSolution("", kSolutionStylePretty);
675+
676+
HighsInt dim = incumbent_model.lp_.num_col_;
677+
std::vector<double> arg0;
678+
std::vector<double> arg1;
679+
std::vector<double> result0;
680+
std::vector<double> result1;
681+
arg0.resize(dim);
682+
HighsRandom random;
683+
for (HighsInt iCol = 0; iCol < dim; iCol++) arg0[iCol] = random.fraction();
684+
HighsHessian hessian0 = incumbent_model.hessian_;
685+
arg1 = arg0;
686+
687+
// Deleting column 1 removes no nonzeros from the Hessian
688+
HighsInt delete_col = 1;
689+
REQUIRE(highs.deleteCols(delete_col, delete_col) == HighsStatus::kOk);
690+
REQUIRE((incumbent_model.hessian_.dim_ == 0 ||
691+
incumbent_model.hessian_.dim_ == incumbent_model.lp_.num_col_));
692+
if (dev_run) {
693+
printf("\nNow solve the QP after deleting column 1\n\n");
694+
incumbent_model.hessian_.print();
695+
}
696+
highs.run();
697+
if (dev_run) highs.writeSolution("", kSolutionStylePretty);
698+
699+
dim--;
700+
for (HighsInt iCol = delete_col; iCol < dim; iCol++)
701+
arg1[iCol] = arg1[iCol + 1];
702+
arg0[delete_col] = 0;
703+
hessianProduct(hessian0, arg0, result0);
704+
for (HighsInt iCol = delete_col; iCol < dim; iCol++)
705+
result0[iCol] = result0[iCol + 1];
706+
707+
arg1.resize(dim);
708+
hessianProduct(incumbent_model.hessian_, arg1, result1);
709+
for (HighsInt iCol = 0; iCol < dim; iCol++)
710+
REQUIRE(result0[iCol] == result1[iCol]);
711+
712+
// Deleting column 0 removes only nonzero from the Hessian, so problem is an
713+
// LP
714+
delete_col = 0;
715+
REQUIRE(highs.deleteCols(delete_col, delete_col) == HighsStatus::kOk);
716+
REQUIRE((incumbent_model.hessian_.dim_ == 0 ||
717+
incumbent_model.hessian_.dim_ == incumbent_model.lp_.num_col_));
718+
if (dev_run) {
719+
printf("\nNow solve the LP after deleting column 0\n\n");
720+
incumbent_model.hessian_.print();
721+
}
722+
highs.run();
723+
if (dev_run) highs.writeSolution("", kSolutionStylePretty);
724+
}
725+
726+
TEST_CASE("test-qp-delete-col", "[qpsolver]") {
727+
HighsModel model;
728+
HighsLp& lp = model.lp_;
729+
HighsHessian& hessian = model.hessian_;
730+
731+
lp.num_col_ = 5;
732+
lp.num_row_ = 1;
733+
lp.col_cost_ = {-2, -1, 0, 1, 2};
734+
lp.col_lower_ = {0, 0, 0, 0, 0};
735+
lp.col_upper_ = {inf, inf, inf, inf, inf};
736+
lp.sense_ = ObjSense::kMinimize;
737+
lp.offset_ = 0;
738+
lp.row_lower_ = {-inf};
739+
lp.row_upper_ = {1};
740+
lp.a_matrix_.format_ = MatrixFormat::kRowwise;
741+
lp.a_matrix_.start_ = {0, 5};
742+
lp.a_matrix_.index_ = {0, 1, 2, 3, 4};
743+
lp.a_matrix_.value_ = {1, 1, 1, 1, 1};
744+
hessian.dim_ = 5;
745+
hessian.start_ = {0, 4, 7, 10, 11, 12};
746+
hessian.index_ = {0, 1, 3, 4, 1, 2, 4, 2, 3, 4, 3, 4};
747+
hessian.value_ = {11, 21, 41, 51, 22, 32, 52, 33, 43, 53, 44, 55};
748+
749+
Highs highs;
750+
highs.setOptionValue("output_flag", dev_run);
751+
const HighsModel& incumbent_model = highs.getModel();
752+
REQUIRE(highs.passModel(model) == HighsStatus::kOk);
753+
if (dev_run) incumbent_model.hessian_.print();
754+
755+
HighsInt dim = incumbent_model.lp_.num_col_;
756+
std::vector<double> arg0;
757+
std::vector<double> arg1;
758+
std::vector<double> result0;
759+
std::vector<double> result1;
760+
arg0.resize(dim);
761+
HighsRandom random;
762+
for (HighsInt iCol = 0; iCol < dim; iCol++) arg0[iCol] = random.fraction();
763+
HighsHessian hessian0 = incumbent_model.hessian_;
764+
arg1 = arg0;
765+
766+
std::vector<HighsInt> set = {1, 3};
767+
REQUIRE(highs.deleteCols(2, set.data()) == HighsStatus::kOk);
768+
if (dev_run) incumbent_model.hessian_.print();
769+
770+
arg1[1] = arg1[2];
771+
arg1[2] = arg1[4];
772+
arg0[1] = 0;
773+
arg0[3] = 0;
774+
hessianProduct(hessian0, arg0, result0);
775+
result0[1] = result0[2];
776+
result0[2] = result0[4];
777+
778+
dim = 3;
779+
arg1.resize(dim);
780+
hessianProduct(incumbent_model.hessian_, arg1, result1);
781+
for (HighsInt iCol = 0; iCol < dim; iCol++)
782+
REQUIRE(result0[iCol] == result1[iCol]);
783+
784+
dim = 100;
785+
lp.clear();
786+
lp.num_col_ = dim;
787+
lp.num_row_ = 1;
788+
789+
lp.col_cost_.resize(dim);
790+
lp.col_lower_.resize(dim);
791+
lp.col_upper_.resize(dim);
792+
lp.a_matrix_.index_.resize(dim);
793+
lp.a_matrix_.value_.resize(dim);
794+
795+
for (HighsInt iCol = 0; iCol < dim; iCol++) {
796+
lp.col_cost_[iCol] = double(iCol);
797+
lp.col_lower_[iCol] = 0;
798+
lp.col_upper_[iCol] = inf;
799+
lp.a_matrix_.index_[iCol] = iCol;
800+
lp.a_matrix_.value_[iCol] = 1;
801+
}
802+
lp.a_matrix_.start_.push_back(dim);
803+
804+
lp.sense_ = ObjSense::kMinimize;
805+
lp.offset_ = 0;
806+
lp.row_lower_ = {-inf};
807+
lp.row_upper_ = {1};
808+
lp.a_matrix_.format_ = MatrixFormat::kRowwise;
809+
810+
hessian.clear();
811+
hessian.dim_ = dim;
812+
std::vector<double> hessian_col(dim);
813+
for (HighsInt iCol = 0; iCol < dim; iCol++) {
814+
hessian_col.assign(dim, 0);
815+
HighsInt kmax = std::max(HighsInt(1), (dim - iCol) / 3);
816+
hessian_col[iCol] = dim * iCol + iCol;
817+
if (iCol < dim - 1) {
818+
for (HighsInt k = 0; k < kmax; k++) {
819+
HighsInt iRow = iCol + 1 + random.integer(dim - iCol - 1);
820+
assert(iRow >= 0);
821+
assert(iRow < dim);
822+
hessian_col[iRow] = dim * iCol + iRow;
823+
}
824+
}
825+
for (HighsInt iRow = iCol; iRow < dim; iRow++) {
826+
if (hessian_col[iRow]) {
827+
hessian.index_.push_back(iRow);
828+
hessian.value_.push_back(hessian_col[iRow]);
829+
}
830+
}
831+
hessian.start_.push_back(HighsInt(hessian.index_.size()));
832+
}
833+
834+
REQUIRE(highs.passModel(model) == HighsStatus::kOk);
835+
if (dev_run) incumbent_model.hessian_.print();
836+
837+
arg0.resize(dim);
838+
for (HighsInt iCol = 0; iCol < dim; iCol++) arg0[iCol] = random.fraction();
839+
hessian0 = incumbent_model.hessian_;
840+
arg1 = arg0;
841+
842+
std::vector<HighsInt> mask;
843+
mask.assign(dim, 0);
844+
845+
HighsInt kmax = std::max(HighsInt(1), dim / 3);
846+
for (HighsInt k = 0; k < kmax; k++) {
847+
HighsInt iRow = random.integer(dim);
848+
assert(iRow >= 0);
849+
assert(iRow < dim);
850+
mask[iRow] = 1;
851+
}
852+
highs.deleteCols(mask.data());
853+
if (dev_run) incumbent_model.hessian_.print();
854+
855+
for (HighsInt iCol = 0; iCol < dim; iCol++) {
856+
HighsInt iRow = mask[iCol];
857+
if (iRow < 0) {
858+
arg0[iCol] = 0;
859+
} else {
860+
arg1[iRow] = arg1[iCol];
861+
}
862+
}
863+
hessianProduct(hessian0, arg0, result0);
864+
for (HighsInt iCol = 0; iCol < dim; iCol++) {
865+
HighsInt iRow = mask[iCol];
866+
if (iRow >= 0) result0[iRow] = result0[iCol];
867+
}
868+
dim = incumbent_model.hessian_.dim_;
869+
arg1.resize(dim);
870+
hessianProduct(incumbent_model.hessian_, arg1, result1);
871+
872+
for (HighsInt iCol = 0; iCol < dim; iCol++) {
873+
REQUIRE(result0[iCol] == result1[iCol]);
874+
}
875+
}

src/Highs.h

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -987,8 +987,9 @@ class Highs {
987987

988988
/**
989989
* @brief Delete multiple columns from the incumbent model given by
990-
* a mask (full length array with 1 => change; 0 => not). New index
991-
* of any column not deleted is returned in place of the value 0.
990+
* a mask (full length array with 1 => delete; 0 => keep). New index
991+
* of any column kept is returned in place of the value 0. For
992+
* deleted columns, a value of -1 is returned.
992993
*/
993994
HighsStatus deleteCols(HighsInt* mask);
994995

@@ -1008,9 +1009,10 @@ class Highs {
10081009
}
10091010

10101011
/**
1011-
* @brief Delete multiple variables from the incumbent model given by
1012-
* a mask (full length array with 1 => change; 0 => not). New index
1013-
* of any variable not deleted is returned in place of the value 0.
1012+
* @brief Delete multiple variables from the incumbent model given
1013+
* by a mask (full length array with 1 => delete; 0 => keep). New
1014+
* index of any variable not deleted is returned in place of the
1015+
* value 0. For deleted variables, a value of -1 is returned.
10141016
*/
10151017
HighsStatus deleteVars(HighsInt* mask) { return deleteCols(mask); }
10161018

@@ -1027,8 +1029,9 @@ class Highs {
10271029

10281030
/**
10291031
* @brief Delete multiple rows from the incumbent model given by a
1030-
* mask (full length array with 1 => change; 0 => not). New index of
1031-
* any row not deleted is returned in place of the value 0.
1032+
* mask (full length array with 1 => delete; 0 => keep). New index
1033+
* of any row not deleted is returned in place of the value 0. For
1034+
* deleted rows, a value of -1 is returned.
10321035
*/
10331036
HighsStatus deleteRows(HighsInt* mask);
10341037

0 commit comments

Comments
 (0)