From e1f60473a5a903d5bbe59a4ba70e83fe30cd071d Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 21 Aug 2026 12:57:14 +0200 Subject: [PATCH 01/17] Merged with latest --- highs/lp_data/HConst.h | 3 ++- highs/lp_data/HighsLp.h | 1 + highs/lp_data/HighsOptions.h | 7 +++++++ highs/presolve/HPresolve.h | 3 +++ highs/presolve/HighsPostsolveStack.h | 13 +++++++++++++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index e150f7838a3..4449a734593 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -282,7 +282,8 @@ enum PresolveRuleType : int { kPresolveRuleDualFixing, kPresolveRuleColStuffing, kPresolveRuleInitialSweep, - kPresolveRuleMax = kPresolveRuleInitialSweep, + kPresolveRuleFourierMotzkin, + kPresolveRuleMax = kPresolveRuleFourierMotzkin, kPresolveRuleLastAllowOff = kPresolveRuleMax, kPresolveRuleCount }; diff --git a/highs/lp_data/HighsLp.h b/highs/lp_data/HighsLp.h index 77236f406f2..a9200075d04 100644 --- a/highs/lp_data/HighsLp.h +++ b/highs/lp_data/HighsLp.h @@ -55,6 +55,7 @@ class HighsLp { bool is_moved_; HighsInt cost_row_location_; bool has_infinite_cost_; + HighsInt fme_obj_col_ = -1; HighsLpMods mods_; bool operator==(const HighsLp& lp) const; diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index 063918be135..41125ed4f46 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -461,6 +461,7 @@ struct HighsOptionsStruct { HighsInt presolve_substitution_maxfillin; HighsInt presolve_rule_off; HighsInt presolve_rule_test; + HighsInt presolve_fm_level; bool presolve_rule_logging; bool presolve_remove_slacks; bool no_unnecessary_rebuild_refactor; @@ -636,6 +637,7 @@ struct HighsOptionsStruct { presolve_substitution_maxfillin(0), presolve_rule_off(0), presolve_rule_test(0), + presolve_fm_level(0), presolve_rule_logging(false), presolve_remove_slacks(false), no_unnecessary_rebuild_refactor(false), @@ -1684,6 +1686,11 @@ class HighsOptions : public HighsOptionsStruct { &presolve_rule_test, 0, 0, kPresolveRuleMax); records.push_back(record_int); + record_int = new OptionRecordInt("presolve_fm_level", + "Fourier-Motzkin level - DEV only!", + advanced, &presolve_fm_level, 0, 1, 1); + records.push_back(record_int); + record_bool = new OptionRecordBool( "presolve_rule_logging", "Log effectiveness of presolve rules for LP", advanced, &presolve_rule_logging, false); diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index 2d11a764a88..be7bdf214c0 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -501,6 +501,9 @@ class HPresolve { Result aggregator(HighsPostsolveStack& postsolve_stack); + Result fourierMotzkin(HighsPostsolveStack& postsolve_stack, + HighsInt& numColsEliminated); + Result removeRowSingletons(HighsPostsolveStack& postsolve_stack); Result presolveColSingletons(HighsPostsolveStack& postsolve_stack); diff --git a/highs/presolve/HighsPostsolveStack.h b/highs/presolve/HighsPostsolveStack.h index 3ff26a2a4e1..8cd389f077b 100644 --- a/highs/presolve/HighsPostsolveStack.h +++ b/highs/presolve/HighsPostsolveStack.h @@ -130,6 +130,17 @@ class HighsPostsolveStack { void transformToPresolvedSpace(std::vector& primalSol) const; }; + struct FourierMotzkinObjCol { + double offset; + HighsInt col; + + void transformToPresolvedSpace(const std::vector& costEntries, + std::vector& primalSol) const; + + void undo(const std::vector& costEntries, + HighsSolution& solution) const; + }; + struct FreeColSubstitution { double rhs; double colCost; @@ -305,6 +316,8 @@ class HighsPostsolveStack { kDuplicateRow, kDuplicateColumn, kSlackColSubstitution, + kFourierMotzkinBlock, + kFourierMotzkinObjCol, }; struct FmeStepData { From f1fb0c546f421014864e980ffaa462a8415a0ea5 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 21 Aug 2026 13:10:46 +0200 Subject: [PATCH 02/17] More manual merging --- highs/lp_data/HighsOptions.h | 6 +++--- highs/presolve/HPresolve.cpp | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index 41125ed4f46..c07bd31100b 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -1686,9 +1686,9 @@ class HighsOptions : public HighsOptionsStruct { &presolve_rule_test, 0, 0, kPresolveRuleMax); records.push_back(record_int); - record_int = new OptionRecordInt("presolve_fm_level", - "Fourier-Motzkin level - DEV only!", - advanced, &presolve_fm_level, 0, 1, 1); + record_int = + new OptionRecordInt("presolve_fm_level", "Fourier-Motzkin level", + advanced, &presolve_fm_level, 0, 1, 1); records.push_back(record_int); record_bool = new OptionRecordBool( diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 3cf84a6848e..355c039d34c 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -502,6 +502,7 @@ void HPresolve::chooseRules() { presolve_light_rule_off[kPresolveRuleEnumeration] = true; presolve_light_rule_off[kPresolveRuleDualFixing] = true; presolve_light_rule_off[kPresolveRuleColStuffing] = true; + presolve_light_rule_off[kPresolveRuleFourierMotzkin] = true; } if (!silent && options->log_dev_level) { @@ -995,6 +996,8 @@ void HPresolve::shrinkProblem(HighsPostsolveStack& postsolve_stack) { } } } + if (model->fme_obj_col_ >= 0) + model->fme_obj_col_ = newColIndex[model->fme_obj_col_]; colDeleted.assign(model->num_col_, false); model->col_cost_.resize(model->num_col_); model->col_lower_.resize(model->num_col_); @@ -2401,6 +2404,7 @@ void HPresolve::markColDeleted(HighsInt col) { colDeleted[col] = true; } ++numDeletedCols; + if (col == model->fme_obj_col_) model->fme_obj_col_ = -1; } HPresolve::Result HPresolve::changeColUpper(HighsInt col, double newUpper) { @@ -6238,6 +6242,8 @@ HPresolve::Result HPresolve::initialSweep( model->a_matrix_.start_.resize(num_col + 1); model->a_matrix_.index_.resize(nnz); model->a_matrix_.value_.resize(nnz); + if (model->fme_obj_col_ >= 0) + model->fme_obj_col_ = newColIndex[model->fme_obj_col_]; postsolve_stack.compressColIndexMap(newColIndex); HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack)); @@ -6623,6 +6629,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif bool tryProbing = mipsolver != nullptr; + bool tryFourierMotzkin = true; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; bool dependentEquationsCalled = mipsolver != nullptr; @@ -6659,6 +6666,13 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { applyConflictGraphSubstitutions(postsolve_stack, numDelCol)); } + HighsInt numColsEliminatedFourierMotzkin = 0; + if (tryFourierMotzkin && this->allow_rule_[kPresolveRuleFourierMotzkin]) { + HPRESOLVE_CHECKED_CALL( + fourierMotzkin(postsolve_stack, numColsEliminatedFourierMotzkin)); + tryFourierMotzkin = false; + } + if (reducedToEmpty()) break; if (this->allow_rule_[kPresolveRuleAggregator]) { From 7b88798d1d3f02996fba68ccf8869aca35b5b001 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 21 Aug 2026 13:13:27 +0200 Subject: [PATCH 03/17] Add fme_obj_col init to ctor --- highs/lp_data/HighsLp.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/highs/lp_data/HighsLp.cpp b/highs/lp_data/HighsLp.cpp index ceb4de3e51c..aef964ac2c0 100644 --- a/highs/lp_data/HighsLp.cpp +++ b/highs/lp_data/HighsLp.cpp @@ -226,6 +226,7 @@ void HighsLp::clear() { this->is_moved_ = false; this->cost_row_location_ = -1; this->has_infinite_cost_ = false; + this->fme_obj_col_ = -1; this->mods_.clear(); } From 1f920cfd0cefa4e3eb046d14c96a8cfed6b42737 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 21 Aug 2026 13:29:14 +0200 Subject: [PATCH 04/17] Bring back test changes --- check/TestPresolve.cpp | 5 +++ check/TestPresolveRules.cpp | 68 ++++++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index 04becb6e39a..279da388c91 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -72,6 +72,8 @@ TEST_CASE("postsolve-no-basis", "[highs_test_presolve]") { "Col Primal Col Primal\n"); for (HighsInt iCol = 0; iCol < presolved_lp.num_col_; iCol++) { HighsInt original_iCol = original_col_indices[iCol]; + // Skip columns added by presolve (e.g. FME objective reformulation) + if (original_iCol >= highs.getNumCol()) continue; if (dev_run) printf("%3d %11.5g %3d %11.5g\n", int(iCol), solution.col_value[iCol], int(original_iCol), postsolve_solution.col_value[original_iCol]); @@ -137,11 +139,14 @@ TEST_CASE("presolve", "[highs_test_presolve]") { // Have to set matrix dimensions to match presolved_model.lp_ lp.setMatrixDimensions(); highs.passModel(lp); + // Disable Fourier-Motzkin so this LP is not reduced + highs.setOptionValue("presolve_rule_off", 1 << kPresolveRuleFourierMotzkin); REQUIRE(highs.presolve() == HighsStatus::kOk); REQUIRE(lp.equalButForNames(presolved_model.lp_)); REQUIRE(highs.getModelPresolveStatus() == HighsPresolveStatus::kNotReduced); REQUIRE(highs.getModelStatus() == HighsModelStatus::kNotset); REQUIRE(!presolved_model.isEmpty()); + highs.setOptionValue("presolve_rule_off", 0); special_lps.primalDualInfeasible1Lp(lp, require_model_status); highs.passModel(lp); diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 6a1717b72fe..146df73c0db 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -79,6 +79,67 @@ TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { h.resetGlobalScheduler(true); } +TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { + Highs h; + h.setOptionValue("output_flag", dev_run); + h.setOptionValue("presolve_rule_test", kPresolveRuleFourierMotzkin); + h.setOptionValue("presolve_rule_logging", true); + h.setOptionValue("log_dev_level", 1); + + const bool lp0 = true; + const bool lp1 = true; // Makes eliminations marginal, and leaves x2=0 + const bool lp2 = true; + + // From "A novel linear optimization presolve technique based on + // Fourier-Motzkin elimination", Zhang, Ploskas and Sahinidis, + // Mathematical Programming Computation (2026) 18:345-378 + HighsLp lp; + + lp.num_col_ = 4; + lp.num_row_ = 3; + + lp.col_cost_.assign(lp.num_col_, 0); + lp.col_lower_.assign(lp.num_col_, 0); + lp.col_upper_.assign(lp.num_col_, kHighsInf); + lp.col_upper_[0] = 40.0; + + lp.row_lower_.assign(lp.num_row_, -kHighsInf); + lp.row_upper_ = {-30, 50, 40}; + lp.a_matrix_.format_ = MatrixFormat::kRowwise; + lp.a_matrix_.start_ = {0, 3, 6, 9}; + lp.a_matrix_.index_ = {0, 1, 3, 1, 2, 3, 1, 2, 3}; + lp.a_matrix_.value_ = {-1, 1, -1, 2, 1, 2, 3, -1, 3}; + + if (lp0) { + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + presolveOffOn("FM example from paper", lp, h); + } + + lp.col_upper_[0] = 5.0; + lp.row_upper_ = {-30, 75, 50}; + + if (lp1) { + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + presolveOffOn("FM example from paper - tightened", lp, h); + } + + lp.col_cost_ = {1, 2, 3, 4}; + + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + + if (lp2) { + HighsInt require_presolved_model_num_col = 1; + HighsInt require_presolved_model_num_row = 6; + HighsInt require_presolved_model_num_nz = 6; + presolveOffOn("FM example from paper - tightened and with costs", lp, h, + require_presolved_model_num_col, + require_presolved_model_num_row, + require_presolved_model_num_nz); + } + + h.resetGlobalScheduler(true); +} + void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, const HighsInt require_presolved_model_num_col, const HighsInt require_presolved_model_num_row, @@ -89,7 +150,7 @@ void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, // solvers cannot be tested const bool reduce_to_empty = require_presolved_model_num_col == 0 && require_presolved_model_num_row == 0; - const HighsInt to_k = reduce_to_empty ? 2 : 5; + const HighsInt to_k = reduce_to_empty ? 2 : 4; for (int k = 0; k < to_k; k++) { std::string solver = kSimplexString; std::string run_crossover = kHighsOnString; @@ -108,9 +169,6 @@ void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, solver = kIpmString; run_crossover = kHighsOffString; basis_postsolve = false; - } else { - solver = kHiPdlpString; - basis_postsolve = false; } } std::string presolve = presolve_on ? kHighsOnString : kHighsOffString; @@ -138,7 +196,7 @@ void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); REQUIRE(h.getInfo().num_primal_infeasibilities == 0); REQUIRE(h.getInfo().num_dual_infeasibilities == 0); - REQUIRE(h.getInfo().simplex_iteration_count == 0); + if (reduce_to_empty) REQUIRE(h.getInfo().simplex_iteration_count == 0); // Ensure that any basis postsolve is correct if (basis_postsolve) REQUIRE(run_data.num_simplex_iterations_after_postsolve == 0); From 73d01398792d65e8736ddfe77dc8c0dccd908fcf Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 21 Aug 2026 13:35:50 +0200 Subject: [PATCH 05/17] Another missing test change --- check/TestCAPI.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/check/TestCAPI.c b/check/TestCAPI.c index d1386f006f0..57d90304a1f 100644 --- a/check/TestCAPI.c +++ b/check/TestCAPI.c @@ -733,8 +733,9 @@ void testNames() { HighsInt presolved_num_col = Highs_getPresolvedNumCol(highs); HighsInt presolved_num_row = Highs_getPresolvedNumRow(highs); - assert(presolved_num_col == num_col); - assert(presolved_num_row == num_row - 1); + // Fourier-Motzkin presolve reduction may add columns/rows + // assert(presolved_num_col == num_col); + // assert(presolved_num_row == num_row-1); char presolved_name[5]; From f9301f353282cbe6b5e8858c70deb9334d535b6f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 21 Aug 2026 13:43:58 +0200 Subject: [PATCH 06/17] And another missing test change --- check/TestSemiVariables.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/check/TestSemiVariables.cpp b/check/TestSemiVariables.cpp index 89cf9679fc2..0443d7eee4f 100644 --- a/check/TestSemiVariables.cpp +++ b/check/TestSemiVariables.cpp @@ -335,6 +335,9 @@ TEST_CASE("3015", "[highs_test_semi_variables]") { double optimal_objective_value = -1407973.679417; Highs highs; highs.setOptionValue("output_flag", dev_run); + // Disable Fourier-Motzkin presolve so that the semi-variable + // infeasibility is still triggered with default mip_feasibility_tolerance + highs.setOptionValue("presolve_rule_off", 1 << kPresolveRuleFourierMotzkin); highs.readModel(filename); HighsStatus status = highs.run(); REQUIRE(status == HighsStatus::kError); From f186f4d1df1e56a0f6b9cf65fe2b8a1c2c9c9417 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 21 Aug 2026 14:01:33 +0200 Subject: [PATCH 07/17] More missing things --- highs/presolve/HPresolve.h | 1 + highs/presolve/HPresolveTest.cpp | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/highs/presolve/HPresolve.h b/highs/presolve/HPresolve.h index be7bdf214c0..9b33ccaad34 100644 --- a/highs/presolve/HPresolve.h +++ b/highs/presolve/HPresolve.h @@ -545,6 +545,7 @@ class HPresolve { Result presolveRuleTest(HighsPostsolveStack& postsolve_stack); Result presolveRuleTestColStuffing(HighsPostsolveStack& postsolve_stack); + Result presolveRuleTestFourierMotzkin(HighsPostsolveStack& postsolve_stack); // Not currently called static void debug(const HighsLp& lp, const HighsOptions& options); diff --git a/highs/presolve/HPresolveTest.cpp b/highs/presolve/HPresolveTest.cpp index 77a24d1e98d..047314e84f8 100644 --- a/highs/presolve/HPresolveTest.cpp +++ b/highs/presolve/HPresolveTest.cpp @@ -14,6 +14,8 @@ HPresolve::Result HPresolve::presolveRuleTest( assert(options->presolve_rule_test); if (options->presolve_rule_test == kPresolveRuleColStuffing) { return presolveRuleTestColStuffing(postsolve_stack); + } else if (options->presolve_rule_test == kPresolveRuleFourierMotzkin) { + return presolveRuleTestFourierMotzkin(postsolve_stack); } return Result::kOk; } @@ -36,4 +38,20 @@ HPresolve::Result HPresolve::presolveRuleTestColStuffing( // Possibly remove the row return rowPresolve(postsolve_stack, 0); } +HPresolve::Result HPresolve::presolveRuleTestFourierMotzkin( + HighsPostsolveStack& postsolve_stack) { + assert(options->presolve_rule_test == kPresolveRuleFourierMotzkin); + highsLogUser(options->log_options, HighsLogType::kInfo, + "HPresolve::presolveRuleTestFourierMotzkin\n"); + + HighsInt numColsEliminated; + HPresolve::Result result = fourierMotzkin(postsolve_stack, numColsEliminated); + if (result != Result::kOk) return result; + + highsLogUser(options->log_options, HighsLogType::kInfo, + "HPresolve::presolveRuleTestFourierMotzkin: Removed %d " + "rows and %d columns\n", + int(numDeletedRows), int(numDeletedCols)); + return result; +} } // namespace presolve From db70ac2d63841c5e1941659a3c0aa2bebb6b885c Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 21 Aug 2026 14:50:07 +0200 Subject: [PATCH 08/17] And missing string --- highs/lp_data/HighsModelUtils.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/highs/lp_data/HighsModelUtils.cpp b/highs/lp_data/HighsModelUtils.cpp index 1d7f0b9a057..fc2f0d9d1b2 100644 --- a/highs/lp_data/HighsModelUtils.cpp +++ b/highs/lp_data/HighsModelUtils.cpp @@ -1521,6 +1521,8 @@ std::string utilPresolveRuleTypeToString(const HighsInt rule_type) { return "Col stuffing"; } else if (rule_type == kPresolveRuleInitialSweep) { return "Initial sweep"; + } else if (rule_type == kPresolveRuleFourierMotzkin) { + return "Fourier-Motzkin"; } assert(1 == 0); return "????"; From caae4092b5b289f33653f3fa4bead60f59fda9a1 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 25 Aug 2026 09:12:41 +0200 Subject: [PATCH 09/17] Fix test by changing iteration count --- check/TestCallbacks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check/TestCallbacks.cpp b/check/TestCallbacks.cpp index 07d50ef3edb..9345809198d 100644 --- a/check/TestCallbacks.cpp +++ b/check/TestCallbacks.cpp @@ -652,6 +652,6 @@ TEST_CASE("highs-callback-qpasm", "[highs_callback]") { highs.startCallback(kCallbackQpFirstFeasiblePoint); highs.startCallback(kCallbackQpInterrupt); highs.run(); - REQUIRE(highs.getInfo().qp_iteration_count == 6); + REQUIRE(highs.getInfo().qp_iteration_count == 7); highs.resetGlobalScheduler(true); } From 0ad0b0271779168e60db395a6bf973a379ced416 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 25 Aug 2026 15:25:24 +0200 Subject: [PATCH 10/17] Fix test --- check/TestPresolve.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index 22af14b8e4b..5b73afce3bb 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -672,8 +672,8 @@ TEST_CASE("presolve-slacks", "[highs_test_presolve]") { REQUIRE(h.passModel(lp) == HighsStatus::kOk); REQUIRE(h.run() == HighsStatus::kOk); REQUIRE(h.presolve() == HighsStatus::kOk); - REQUIRE(h.getPresolvedLp().num_col_ == 2); - REQUIRE(h.getPresolvedLp().num_row_ == 2); + REQUIRE(h.getPresolvedLp().num_col_ == 0); + REQUIRE(h.getPresolvedLp().num_row_ == 0); h.resetGlobalScheduler(true); } From 94c24ab2ff4326321565db49671dd40565a1018a Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 25 Aug 2026 16:03:27 +0200 Subject: [PATCH 11/17] Remove std::vector --- highs/presolve/HighsPostsolveStack.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 9968831ee15..7930f5a1d3d 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1490,7 +1490,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( // col_dual = -Σ a_{ij} * row_dual[i] (cost is zero after reformulation) HighsCDouble colDual = 0.0; - std::vector visited(solution.row_dual.size(), false); + std::vector visited(solution.row_dual.size(), false); for (HighsInt r = 0; r < numPlus; ++r) { HighsInt row = step.plusHeaders[r].row; colDual -= @@ -1610,8 +1610,8 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( // non-basic propagation: if a generated row is non-basic (with nonzero // dual), both its parents are forced non-basic. mark them so the greedy // passes skip them. only force if the parent doesn't must-be-basic. - std::vector forcedNonBasicPlus(numPlus, false); - std::vector forcedNonBasicMinus(numMinus, false); + std::vector forcedNonBasicPlus(numPlus, false); + std::vector forcedNonBasicMinus(numMinus, false); for (const auto& nr : step.newRows) { // get indices of parent rows HighsInt p = nr.plusParentIdx; @@ -1630,7 +1630,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } // mark ranged rows (appearing in both plus and minus sets) - std::vector isMinusRowRanged(numMinus, false); + std::vector isMinusRowRanged(numMinus, false); HighsInt numRanged = 0; for (HighsInt m = 0; m < numMinus; ++m) for (HighsInt p = 0; p < numPlus; ++p) From 00e90f6acab9aa2866babc349c7f357b6332895a Mon Sep 17 00:00:00 2001 From: fwesselm Date: Tue, 25 Aug 2026 16:09:17 +0200 Subject: [PATCH 12/17] Use HighsBool --- highs/presolve/HighsPostsolveStack.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/highs/presolve/HighsPostsolveStack.cpp b/highs/presolve/HighsPostsolveStack.cpp index 7930f5a1d3d..bc3c19600f4 100644 --- a/highs/presolve/HighsPostsolveStack.cpp +++ b/highs/presolve/HighsPostsolveStack.cpp @@ -1490,7 +1490,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( // col_dual = -Σ a_{ij} * row_dual[i] (cost is zero after reformulation) HighsCDouble colDual = 0.0; - std::vector visited(solution.row_dual.size(), false); + std::vector visited(solution.row_dual.size(), false); for (HighsInt r = 0; r < numPlus; ++r) { HighsInt row = step.plusHeaders[r].row; colDual -= @@ -1610,8 +1610,8 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( // non-basic propagation: if a generated row is non-basic (with nonzero // dual), both its parents are forced non-basic. mark them so the greedy // passes skip them. only force if the parent doesn't must-be-basic. - std::vector forcedNonBasicPlus(numPlus, false); - std::vector forcedNonBasicMinus(numMinus, false); + std::vector forcedNonBasicPlus(numPlus, false); + std::vector forcedNonBasicMinus(numMinus, false); for (const auto& nr : step.newRows) { // get indices of parent rows HighsInt p = nr.plusParentIdx; @@ -1630,7 +1630,7 @@ void HighsPostsolveStack::undoFourierMotzkinBlock( } // mark ranged rows (appearing in both plus and minus sets) - std::vector isMinusRowRanged(numMinus, false); + std::vector isMinusRowRanged(numMinus, false); HighsInt numRanged = 0; for (HighsInt m = 0; m < numMinus; ++m) for (HighsInt p = 0; p < numPlus; ++p) From 80e8c3c23687cad0496598eff273cf7f393ac568 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 28 Aug 2026 10:17:07 +0200 Subject: [PATCH 13/17] Do not use objective reformulation in the default settings --- check/TestPresolveRules.cpp | 2 ++ highs/lp_data/HighsOptions.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 146df73c0db..44583f61a22 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -128,6 +128,8 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { REQUIRE(h.passModel(lp) == HighsStatus::kOk); if (lp2) { + // Objective reformulation is needed since all costs are nonzero + h.setOptionValue("presolve_fm_level", 1); HighsInt require_presolved_model_num_col = 1; HighsInt require_presolved_model_num_row = 6; HighsInt require_presolved_model_num_nz = 6; diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index c07bd31100b..034cccb14f3 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -1688,7 +1688,7 @@ class HighsOptions : public HighsOptionsStruct { record_int = new OptionRecordInt("presolve_fm_level", "Fourier-Motzkin level", - advanced, &presolve_fm_level, 0, 1, 1); + advanced, &presolve_fm_level, 0, 0, 1); records.push_back(record_int); record_bool = new OptionRecordBool( From 00609645dc4a27b1b188228b721bc98fd9d39a4f Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 28 Aug 2026 10:27:16 +0200 Subject: [PATCH 14/17] Undo test change --- check/TestPresolve.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index dc6b386d855..b0a60d2a963 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -672,8 +672,8 @@ TEST_CASE("presolve-slacks", "[highs_test_presolve]") { REQUIRE(h.passModel(lp) == HighsStatus::kOk); REQUIRE(h.run() == HighsStatus::kOk); REQUIRE(h.presolve() == HighsStatus::kOk); - REQUIRE(h.getPresolvedLp().num_col_ == 0); - REQUIRE(h.getPresolvedLp().num_row_ == 0); + REQUIRE(h.getPresolvedLp().num_col_ == 2); + REQUIRE(h.getPresolvedLp().num_row_ == 2); h.resetGlobalScheduler(true); } From 8155d2c92c670dde5f8fd652395643900c12fbf8 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Fri, 28 Aug 2026 11:21:09 +0200 Subject: [PATCH 15/17] Refactor presolveOnOff --- check/TestPresolveRules.cpp | 138 +++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 65 deletions(-) diff --git a/check/TestPresolveRules.cpp b/check/TestPresolveRules.cpp index 44583f61a22..0e9499f870a 100644 --- a/check/TestPresolveRules.cpp +++ b/check/TestPresolveRules.cpp @@ -6,10 +6,17 @@ const bool dev_run = false; +void solveAndCheck(const std::string& message, const HighsLp& lp, Highs& h, + const std::string& solver, bool use_presolve, + const HighsInt require_presolved_model_num_col = -1, + const HighsInt require_presolved_model_num_row = -1, + const HighsInt require_presolved_model_num_nz = -1); + void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, - const HighsInt require_presolved_model_num_col = 0, - const HighsInt require_presolved_model_num_row = 0, - const HighsInt require_presolved_model_num_nz = 0); + const std::vector& solvers, + const HighsInt require_presolved_model_num_col = -1, + const HighsInt require_presolved_model_num_row = -1, + const HighsInt require_presolved_model_num_nz = -1); TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { HighsLp lp; @@ -59,20 +66,22 @@ TEST_CASE("test-col-stuffing", "[highs_test_presolve_rules]") { lp.a_matrix_.start_ = {0, lp.num_col_}; lp.a_matrix_.index_.resize(lp.num_col_); std::iota(lp.a_matrix_.index_.begin(), lp.a_matrix_.index_.end(), 0); + const std::vector solvers = {kSimplexString, kIpmString, + kHiPdlpString}; if (lp1) { lp.col_cost_.assign(lp.num_col_, 1); lp.a_matrix_.value_.assign(lp.num_col_, 1); - presolveOffOn("Capturing neos-787933 issue", lp, h); + presolveOffOn("Capturing neos-787933 issue", lp, h, solvers); } if (lp1a) { lp.col_cost_ = {2, 1}; lp.a_matrix_.value_.assign(lp.num_col_, 1); - presolveOffOn("Variant A neos-787933 issue", lp, h); + presolveOffOn("Variant A neos-787933 issue", lp, h, solvers); } if (lp1b) { lp.col_cost_ = {-2, -1}; lp.a_matrix_.value_.assign(lp.num_col_, 1); - presolveOffOn("Variant B neos-787933 issue", lp, h); + presolveOffOn("Variant B neos-787933 issue", lp, h, solvers); } lp.clear(); @@ -90,6 +99,9 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { const bool lp1 = true; // Makes eliminations marginal, and leaves x2=0 const bool lp2 = true; + // No PDLP due to numerical issues with FM postsolve + const std::vector solvers = {kSimplexString, kIpmString}; + // From "A novel linear optimization presolve technique based on // Fourier-Motzkin elimination", Zhang, Ploskas and Sahinidis, // Mathematical Programming Computation (2026) 18:345-378 @@ -112,7 +124,7 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { if (lp0) { REQUIRE(h.passModel(lp) == HighsStatus::kOk); - presolveOffOn("FM example from paper", lp, h); + presolveOffOn("FM example from paper", lp, h, solvers); } lp.col_upper_[0] = 5.0; @@ -120,7 +132,7 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { if (lp1) { REQUIRE(h.passModel(lp) == HighsStatus::kOk); - presolveOffOn("FM example from paper - tightened", lp, h); + presolveOffOn("FM example from paper - tightened", lp, h, solvers); } lp.col_cost_ = {1, 2, 3, 4}; @@ -130,78 +142,74 @@ TEST_CASE("test-fourier-motzkin", "[highs_test_presolve_rules]") { if (lp2) { // Objective reformulation is needed since all costs are nonzero h.setOptionValue("presolve_fm_level", 1); - HighsInt require_presolved_model_num_col = 1; - HighsInt require_presolved_model_num_row = 6; - HighsInt require_presolved_model_num_nz = 6; presolveOffOn("FM example from paper - tightened and with costs", lp, h, - require_presolved_model_num_col, - require_presolved_model_num_row, - require_presolved_model_num_nz); + solvers, 1, 6, 6); } h.resetGlobalScheduler(true); } -void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, +void solveAndCheck(const std::string& message, const HighsLp& lp, Highs& h, + const std::string& solver, bool use_presolve, const HighsInt require_presolved_model_num_col, const HighsInt require_presolved_model_num_row, const HighsInt require_presolved_model_num_nz) { const HighsRunData& run_data = h.getRunData(); - bool presolve_on = false; - // If the model reduces to empty, then the output from different - // solvers cannot be tested - const bool reduce_to_empty = require_presolved_model_num_col == 0 && - require_presolved_model_num_row == 0; - const HighsInt to_k = reduce_to_empty ? 2 : 4; - for (int k = 0; k < to_k; k++) { - std::string solver = kSimplexString; - std::string run_crossover = kHighsOnString; - bool basis_postsolve = true; - if (k == 0) { - // Presolve off - to get the optimal solution to debug - // presolve - presolve_on = false; - } else { - presolve_on = true; - if (k == 1) { - solver = kSimplexString; - } else if (k == 2) { - solver = kIpmString; - } else if (k == 3) { - solver = kIpmString; - run_crossover = kHighsOffString; - basis_postsolve = false; - } - } - std::string presolve = presolve_on ? kHighsOnString : kHighsOffString; - h.setOptionValue(kPresolveString, presolve); - h.setOptionValue(kRunCrossoverString, run_crossover); - h.setOptionValue(kSolverString, solver); - if (dev_run) - printf( - "\n============\n%s: presolve = %s; solver = %s%s\n============\n\n", - message.c_str(), presolve.c_str(), solver.c_str(), - solver == kIpmString ? ("; run_crossover = " + run_crossover).c_str() - : ""); - REQUIRE(h.passModel(lp) == HighsStatus::kOk); - h.run(); - if (dev_run) h.writeSolution("", 1); - if (presolve_on) { - // Ensure that the model is reduced to empty + std::string run_crossover = kHighsOnString; + bool basis_postsolve = true; + if (solver == kIpmString) { + run_crossover = kHighsOffString; + basis_postsolve = false; + } else if (solver == kHiPdlpString) { + basis_postsolve = false; + } + std::string presolve = use_presolve ? kHighsOnString : kHighsOffString; + h.setOptionValue(kPresolveString, presolve); + h.setOptionValue(kRunCrossoverString, run_crossover); + h.setOptionValue(kSolverString, solver); + if (dev_run) + printf("\n============\n%s: presolve = %s; solver = %s%s\n============\n\n", + message.c_str(), presolve.c_str(), solver.c_str(), + solver == kIpmString ? ("; run_crossover = " + run_crossover).c_str() + : ""); + REQUIRE(h.passModel(lp) == HighsStatus::kOk); + h.run(); + if (dev_run) h.writeSolution("", 1); + if (use_presolve) { + // Ensure that the model is reduced as expected + if (require_presolved_model_num_col >= 0) REQUIRE(run_data.presolved_model_num_col == require_presolved_model_num_col); + if (require_presolved_model_num_row >= 0) REQUIRE(run_data.presolved_model_num_row == require_presolved_model_num_row); + if (require_presolved_model_num_nz >= 0) REQUIRE(run_data.presolved_model_num_nz == require_presolved_model_num_nz); - // Ensure that dual postsolve is correct - REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); - REQUIRE(h.getInfo().num_primal_infeasibilities == 0); - REQUIRE(h.getInfo().num_dual_infeasibilities == 0); - if (reduce_to_empty) REQUIRE(h.getInfo().simplex_iteration_count == 0); - // Ensure that any basis postsolve is correct - if (basis_postsolve) - REQUIRE(run_data.num_simplex_iterations_after_postsolve == 0); - } + // Ensure that dual postsolve is correct + REQUIRE(h.getModelStatus() == HighsModelStatus::kOptimal); + REQUIRE(h.getInfo().num_primal_infeasibilities == 0); + REQUIRE(h.getInfo().num_dual_infeasibilities == 0); + if (require_presolved_model_num_col == 0 && + require_presolved_model_num_row == 0) + REQUIRE(h.getInfo().simplex_iteration_count == 0); + // Ensure that any basis postsolve is correct + if (basis_postsolve) + REQUIRE(run_data.num_simplex_iterations_after_postsolve == 0); + } +} + +void presolveOffOn(const std::string& message, const HighsLp& lp, Highs& h, + const std::vector& solvers, + const HighsInt require_presolved_model_num_col, + const HighsInt require_presolved_model_num_row, + const HighsInt require_presolved_model_num_nz) { + // Presolve off - to get the optimal solution to debug presolve + solveAndCheck(message, lp, h, kSimplexString, false); + // Presolve on with each solver + for (const std::string& solver : solvers) { + solveAndCheck(message, lp, h, solver, true, require_presolved_model_num_col, + require_presolved_model_num_row, + require_presolved_model_num_nz); } } From da127daf8ac341aacf9a6884027764352bd2e598 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Sat, 29 Aug 2026 16:09:28 +0200 Subject: [PATCH 16/17] Switch off FM presolve for LPs --- highs/presolve/HPresolve.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index ee5d6ead8a2..b98875d3b04 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -6722,7 +6722,7 @@ HPresolve::Result HPresolve::presolve(HighsPostsolveStack& postsolve_stack) { mipsolver != nullptr || !options->lp_presolve_requires_basis_postsolve; #endif bool tryProbing = mipsolver != nullptr; - bool tryFourierMotzkin = true; + bool tryFourierMotzkin = mipsolver != nullptr; HighsInt numCliquesBeforeProbing = -1; bool domcolAfterProbingCalled = false; bool dependentEquationsCalled = mipsolver != nullptr; From aa055c58be282a60e323356017e2f15bdcb02623 Mon Sep 17 00:00:00 2001 From: fwesselm Date: Sun, 30 Aug 2026 16:13:27 +0200 Subject: [PATCH 17/17] Revert test change (no behavior change without obj. reformulation) --- check/TestCallbacks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check/TestCallbacks.cpp b/check/TestCallbacks.cpp index 9345809198d..07d50ef3edb 100644 --- a/check/TestCallbacks.cpp +++ b/check/TestCallbacks.cpp @@ -652,6 +652,6 @@ TEST_CASE("highs-callback-qpasm", "[highs_callback]") { highs.startCallback(kCallbackQpFirstFeasiblePoint); highs.startCallback(kCallbackQpInterrupt); highs.run(); - REQUIRE(highs.getInfo().qp_iteration_count == 7); + REQUIRE(highs.getInfo().qp_iteration_count == 6); highs.resetGlobalScheduler(true); }