From 6e30386f32be1ac3800c56853e90a1c049d5e68e Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Mon, 31 Aug 2026 15:37:44 +0100 Subject: [PATCH 1/4] Added unit test exposing error in flipping duals after postsolve --- check/TestPresolve.cpp | 58 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index 3669449c23..3d90f3bd65 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -1154,6 +1154,64 @@ TEST_CASE("add-to-matrix", "[highs_test_presolve]") { h.resetGlobalScheduler(true); } +TEST_CASE("max-lp-dual-postsolve", "[highs_test_presolve]") { + Highs h; + // h.setOptionValue("output_flag", dev_run); + + // Use blending problem + HighsLp lp; + lp.model_name_ = "blending"; + lp.num_col_ = 2; + lp.num_row_ = 2; + lp.col_cost_ = {8, 10}; + lp.col_lower_ = {0, 0}; + lp.col_upper_ = {inf, inf}; + lp.row_lower_ = {-inf, -inf}; + lp.row_upper_ = {120, 210}; + lp.a_matrix_.format_ = MatrixFormat::kColwise; + lp.a_matrix_.start_ = {0, 2, 4}; + lp.a_matrix_.index_ = {0, 1, 0, 1}; + lp.a_matrix_.value_ = {0.3, 0.7, 0.5, 0.5}; + lp.sense_ = ObjSense::kMaximize; + lp.offset_ = 0; + h.passModel(lp); + + REQUIRE(h.setOptionValue("solver", kHipoString) == HighsStatus::kOk); + REQUIRE(h.setOptionValue("run_crossover", kHighsOffString) == HighsStatus::kOk); + // REQUIRE(h.setOptionValue("presolve_reduction_limit", 7) == HighsStatus::kOk); + // REQUIRE(h.setOptionValue("presolve_rule_logging", true) == HighsStatus::kOk); + // REQUIRE(h.setOptionValue("log_dev_level", 1) == HighsStatus::kOk); + + h.run(); + // if (dev_run) + h.writeSolution("", kSolutionStylePretty); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + + double optimal_objective = h.getObjectiveValue(); + HighsSolution solution = h.getSolution(); + + // Add a redundant constraint that's removed in presolve so that + // postsolve must be performed + std::vector index = {0, 1}; + std::vector value = {1, 1}; + REQUIRE(h.addRow(-inf, 1000, 2, index.data(), value.data()) == HighsStatus::kOk); + h.clearSolver(); + + h.run(); + // if (dev_run) + h.writeSolution("", kSolutionStylePretty); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + + REQUIRE(doubleEqual(optimal_objective, h.getObjectiveValue())); + for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) + REQUIRE(doubleEqual(solution.col_dual[iCol], h.getSolution().col_dual[iCol])); + for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) + REQUIRE(doubleEqual(solution.row_dual[iRow], h.getSolution().row_dual[iRow])); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + + h.resetGlobalScheduler(true); +} + TEST_CASE("issue-3140", "[highs_test_presolve]") { Highs highs; highs.setOptionValue("output_flag", dev_run); From a0d37a415db2c40bab185b6e47a1c08c8dba6ac0 Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Mon, 31 Aug 2026 16:07:15 +0100 Subject: [PATCH 2/4] Silenced unit test max-lp-dual-postsolve; removed redundant PresolveComponent::negateReducedLpColDuals() ; formatted --- check/TestPresolve.cpp | 31 ++++++++++++++-------------- highs/lp_data/Highs.cpp | 9 ++++++-- highs/presolve/PresolveComponent.cpp | 7 ------- highs/presolve/PresolveComponent.h | 2 -- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index 3d90f3bd65..a8a1e4ec0e 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -1156,9 +1156,9 @@ TEST_CASE("add-to-matrix", "[highs_test_presolve]") { TEST_CASE("max-lp-dual-postsolve", "[highs_test_presolve]") { Highs h; - // h.setOptionValue("output_flag", dev_run); + h.setOptionValue("output_flag", dev_run); - // Use blending problem + // Start with blending problem HighsLp lp; lp.model_name_ = "blending"; lp.num_col_ = 2; @@ -1177,16 +1177,13 @@ TEST_CASE("max-lp-dual-postsolve", "[highs_test_presolve]") { h.passModel(lp); REQUIRE(h.setOptionValue("solver", kHipoString) == HighsStatus::kOk); - REQUIRE(h.setOptionValue("run_crossover", kHighsOffString) == HighsStatus::kOk); - // REQUIRE(h.setOptionValue("presolve_reduction_limit", 7) == HighsStatus::kOk); - // REQUIRE(h.setOptionValue("presolve_rule_logging", true) == HighsStatus::kOk); - // REQUIRE(h.setOptionValue("log_dev_level", 1) == HighsStatus::kOk); + REQUIRE(h.setOptionValue("run_crossover", kHighsOffString) == + HighsStatus::kOk); h.run(); - // if (dev_run) - h.writeSolution("", kSolutionStylePretty); + if (dev_run) h.writeSolution("", kSolutionStylePretty); REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); - + double optimal_objective = h.getObjectiveValue(); HighsSolution solution = h.getSolution(); @@ -1194,19 +1191,23 @@ TEST_CASE("max-lp-dual-postsolve", "[highs_test_presolve]") { // postsolve must be performed std::vector index = {0, 1}; std::vector value = {1, 1}; - REQUIRE(h.addRow(-inf, 1000, 2, index.data(), value.data()) == HighsStatus::kOk); + REQUIRE(h.addRow(-inf, 1000, 2, index.data(), value.data()) == + HighsStatus::kOk); h.clearSolver(); h.run(); - // if (dev_run) - h.writeSolution("", kSolutionStylePretty); + if (dev_run) h.writeSolution("", kSolutionStylePretty); REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); - REQUIRE(doubleEqual(optimal_objective, h.getObjectiveValue())); + REQUIRE(doubleEqual(optimal_objective, h.getObjectiveValue())); + // Check that the dual values are unchanged for the original problem for (HighsInt iCol = 0; iCol < lp.num_col_; iCol++) - REQUIRE(doubleEqual(solution.col_dual[iCol], h.getSolution().col_dual[iCol])); + REQUIRE( + doubleEqual(solution.col_dual[iCol], h.getSolution().col_dual[iCol])); for (HighsInt iRow = 0; iRow < lp.num_row_; iRow++) - REQUIRE(doubleEqual(solution.row_dual[iRow], h.getSolution().row_dual[iRow])); + REQUIRE( + doubleEqual(solution.row_dual[iRow], h.getSolution().row_dual[iRow])); + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); h.resetGlobalScheduler(true); diff --git a/highs/lp_data/Highs.cpp b/highs/lp_data/Highs.cpp index dece15d525..182a5a5c19 100644 --- a/highs/lp_data/Highs.cpp +++ b/highs/lp_data/Highs.cpp @@ -4009,8 +4009,13 @@ HighsPostsolveStatus Highs::runPostsolve() { assert(model_.lp_.a_matrix_.isColwise()); calculateRowValuesQuad(model_.lp_, presolve_.data_.recovered_solution_); - if (have_dual_solution && model_.lp_.sense_ == ObjSense::kMaximize) - presolve_.negateReducedLpColDuals(); + if (have_dual_solution && model_.lp_.sense_ == ObjSense::kMaximize) { + // Negate the dual values since the incumbent LP is a maximization + for (HighsInt iCol = 0; iCol < model_.lp_.num_col_; iCol++) + presolve_.data_.recovered_solution_.col_dual[iCol] *= -1; + for (HighsInt iRow = 0; iRow < model_.lp_.num_row_; iRow++) + presolve_.data_.recovered_solution_.row_dual[iRow] *= -1; + } // Ensure that the postsolve status is used to set // presolve_.postsolve_status_, as well as being returned diff --git a/highs/presolve/PresolveComponent.cpp b/highs/presolve/PresolveComponent.cpp index 91bc1381ff..36de501fe5 100644 --- a/highs/presolve/PresolveComponent.cpp +++ b/highs/presolve/PresolveComponent.cpp @@ -21,13 +21,6 @@ HighsStatus PresolveComponent::init(const HighsLp& lp, HighsTimer& timer, return HighsStatus::kOk; } -void PresolveComponent::negateReducedLpColDuals() { - for (HighsInt col = 0; col < data_.reduced_lp_.num_col_; col++) - data_.recovered_solution_.col_dual[col] = - -data_.recovered_solution_.col_dual[col]; - return; -} - HighsPresolveStatus PresolveComponent::run() { presolve::HPresolve presolve; presolve.setInput(data_.reduced_lp_, *options_, diff --git a/highs/presolve/PresolveComponent.h b/highs/presolve/PresolveComponent.h index d331719f19..987c81965b 100644 --- a/highs/presolve/PresolveComponent.h +++ b/highs/presolve/PresolveComponent.h @@ -75,8 +75,6 @@ class PresolveComponent : public HighsComponent { HighsLp& getReducedProblem() { return data_.reduced_lp_; } HighsPresolveLog& getPresolveLog() { return data_.presolve_log_; } - void negateReducedLpColDuals(); - PresolveComponentInfo info_; PresolveComponentData data_; const HighsOptions* options_; From ff3f47e8e7fe7b1df27e20b8da7bf8d3c884cfb7 Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Mon, 31 Aug 2026 16:11:41 +0100 Subject: [PATCH 3/4] Corrected successfull to successful in highs/ipm/hipo/ipm/Solver.cpp --- highs/ipm/hipo/ipm/Solver.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/highs/ipm/hipo/ipm/Solver.cpp b/highs/ipm/hipo/ipm/Solver.cpp index 01a15ad5b7..6ad050f1d3 100644 --- a/highs/ipm/hipo/ipm/Solver.cpp +++ b/highs/ipm/hipo/ipm/Solver.cpp @@ -1213,7 +1213,7 @@ isSuccess Solver::checkTerminationKkt() { highs_solution, Hoptions_, "During HiPO solve"); if (model_status == HighsModelStatus::kOptimal) { - logger_.printInfo("Check successfull\n"); + logger_.printInfo("Check successful\n"); return true; } else logger_.printInfo("Check failed\n"); @@ -1282,7 +1282,7 @@ isFailure Solver::initialiseLinearSolver() { } isSuccess Solver::switchToMultifrontal() { - bool switch_successfull = false; + bool switch_successful = false; if (LS_->type() == kUpLookingType && options_.factor == kHighsChooseString) { LS_.reset(new FactorHighsSolver(*kkt_, options_, model_, regul_, info_, @@ -1297,11 +1297,11 @@ isSuccess Solver::switchToMultifrontal() { it_->largest_dx_x_ = 0; it_->largest_dy_y_ = 0; info_.error = kOk; - switch_successfull = true; + switch_successful = true; } } - return switch_successfull; + return switch_successful; } void Solver::printHeader() const { From cb317c0ae4e146db0130fe2d22ca9e8077ff5e91 Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Mon, 31 Aug 2026 16:28:06 +0100 Subject: [PATCH 4/4] Using IPX rather than HiPO in max-lp-dual-postsolve - since Bazel CI test fails --- check/TestPresolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index a8a1e4ec0e..663b6ad747 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -1176,7 +1176,7 @@ TEST_CASE("max-lp-dual-postsolve", "[highs_test_presolve]") { lp.offset_ = 0; h.passModel(lp); - REQUIRE(h.setOptionValue("solver", kHipoString) == HighsStatus::kOk); + REQUIRE(h.setOptionValue("solver", kIpxString) == HighsStatus::kOk); REQUIRE(h.setOptionValue("run_crossover", kHighsOffString) == HighsStatus::kOk);