From 15532ba74b8ec3642d10a099de9aa32d24eb5937 Mon Sep 17 00:00:00 2001 From: Julian Hall Date: Tue, 3 Nov 2026 16:14:03 +0000 Subject: [PATCH 1/4] Removed presolve_light option, and introduced presolve_mode option --- check/TestPresolve.cpp | 6 +- highs/lp_data/HConst.h | 12 ++- highs/lp_data/HighsOptions.cpp | 3 - highs/lp_data/HighsOptions.h | 18 ++--- highs/presolve/HPresolve.cpp | 129 ++++++++++++++++++++------------- 5 files changed, 100 insertions(+), 68 deletions(-) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index 9f4374a4976..993743590ef 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -951,7 +951,7 @@ TEST_CASE("presolve-light", "[highs_test_presolve]") { printf("%s presolved LP has %d columns; %d rows and %d nonzeros\n", k == 0 ? "Fully" : "Lightly", int(presolved_num_col), int(presolved_num_row), int(presolved_num_nz)); - highs.setOptionValue("presolve_light", kHighsOnString); + highs.setOptionValue("presolve_mode", kPresolveModeLight); } highs.resetThreadScheduler(true); @@ -1030,7 +1030,7 @@ TEST_CASE("presolve-initial-sweep-all", "[highs_test_presolve]") { // Tests all cases in initial sweep Highs highs; highs.setOptionValue("output_flag", dev_run); - highs.setOptionValue("presolve_light", kHighsOnString); + highs.setOptionValue("presolve_mode", kPresolveModeLight); if (dev_run) highs.setOptionValue("presolve_rule_logging", true); HighsLp lp; lp.num_col_ = 7; @@ -1279,7 +1279,7 @@ TEST_CASE("presolve-light-no-crossover", "[highs_test_presolve]") { REQUIRE(h.readModel(filename) == HighsStatus::kOk); - REQUIRE(h.setOptionValue("presolve_light", kHighsOnString) == + REQUIRE(h.setOptionValue("presolve_mode", kPresolveModeLight) == HighsStatus::kOk); REQUIRE(h.setOptionValue("solve_relaxation", true) == HighsStatus::kOk); testAllLpSolversCrossoverChoose(h, HighsModelStatus::kOptimal); diff --git a/highs/lp_data/HConst.h b/highs/lp_data/HConst.h index e57a2a09794..63b7839c591 100644 --- a/highs/lp_data/HConst.h +++ b/highs/lp_data/HConst.h @@ -273,7 +273,17 @@ enum class HighsBasisStatus : uint8_t { // and postsolve }; -// Types of LP presolve rules +// Types of presolve modes +enum PresolveMode : uint8_t { + kPresolveModeMin = 0, + kPresolveModeDefault = kPresolveModeMin, + kPresolveModeInitialSweep, + kPresolveModeLight, + kPresolveModeFull, + kPresolveModeMax = kPresolveModeFull +}; + +// Types of presolve rules enum PresolveRuleType : int { kPresolveRuleIllegal = -1, kPresolveRuleMin = 0, diff --git a/highs/lp_data/HighsOptions.cpp b/highs/lp_data/HighsOptions.cpp index 15a60687499..c66e346fa6a 100644 --- a/highs/lp_data/HighsOptions.cpp +++ b/highs/lp_data/HighsOptions.cpp @@ -455,9 +455,6 @@ OptionStatus checkOptionValue(const HighsLogOptions& report_log_options, } else if (option.name == kHipoFactorString) { if (!optionHipoFactorOk(report_log_options, value)) return OptionStatus::kIllegalValue; - } else if (option.name == kPresolveLightString) { - if (!optionOffChooseOnOk(report_log_options, option.name, value)) - return OptionStatus::kIllegalValue; } return OptionStatus::kOk; } diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index 9f283b3583c..b0c08f70c5b 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -291,7 +291,6 @@ const string kModelFileString = "model_file"; const string kReadBasisFileString = "read_basis_file"; const string kWriteBasisFileString = "write_basis_file"; const string kPresolveString = "presolve"; -const string kPresolveLightString = "presolve_light"; const string kSolverString = "solver"; const string kParallelString = "parallel"; const string kThreadsString = "threads"; @@ -444,7 +443,6 @@ struct HighsOptionsStruct { bool lp_presolve_requires_basis_postsolve; bool mps_parser_type_free; bool use_warm_start; - std::string presolve_light; bool write_matrix_image; bool write_hessian_image; HighsInt keep_n_rows; @@ -458,6 +456,7 @@ struct HighsOptionsStruct { HighsInt max_dual_simplex_phase1_cleanup_level; HighsInt simplex_price_strategy; HighsInt simplex_unscaled_solution_strategy; + HighsInt presolve_mode; HighsInt presolve_reduction_limit; HighsInt restart_presolve_reduction_limit; HighsInt presolve_substitution_maxfillin; @@ -621,7 +620,6 @@ struct HighsOptionsStruct { lp_presolve_requires_basis_postsolve(false), mps_parser_type_free(false), use_warm_start(true), - presolve_light(""), write_matrix_image(false), write_hessian_image(false), keep_n_rows(0), @@ -635,6 +633,7 @@ struct HighsOptionsStruct { max_dual_simplex_phase1_cleanup_level(0), simplex_price_strategy(0), simplex_unscaled_solution_strategy(0), + presolve_mode(0), presolve_reduction_limit(0), restart_presolve_reduction_limit(0), presolve_substitution_maxfillin(0), @@ -1524,12 +1523,6 @@ class HighsOptions : public HighsOptionsStruct { advanced, &use_warm_start, true); records.push_back(record_bool); - record_string = new OptionRecordString( - kPresolveLightString, - "Use only low-cost presolve rules: \"off\", \"choose\" or \"on\"", - advanced, &presolve_light, kHighsChooseString); - records.push_back(record_string); - record_bool = new OptionRecordBool( "write_matrix_image", "Write an image of the constraint matrix to a file", advanced, @@ -1666,6 +1659,13 @@ class HighsOptions : public HighsOptionsStruct { kMaxPivotThreshold); records.push_back(record_double); + record_int = new OptionRecordInt( + "presolve_mode", + "Presolve mode 0 => default; 1 => initial sweep; 2 => light; 3 => full", + advanced, &presolve_mode, kPresolveModeMin, kPresolveModeDefault, + kPresolveModeMax); + records.push_back(record_int); + record_int = new OptionRecordInt( "presolve_reduction_limit", "Limit on number of presolve reductions -1 => no limit", advanced, diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 5742fe4f33f..746150c0c8a 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -366,26 +366,55 @@ bool HPresolve::isImpliedEquationAtUpper(HighsInt row) const { void HPresolve::chooseRules() { const bool silent = silentLog(); - // By default all presolve rules are allowed - this->allow_rule_.assign(kPresolveRuleCount, true); + + auto setRulesForInitialSweepMode = [&]() { + // All rules off by default + allow_rule_.assign(kPresolveRuleCount, false); + // Switch on the rules used in initial sweep + allow_rule_[kPresolveRuleEmptyRow] = true; + allow_rule_[kPresolveRuleSingletonRow] = true; + allow_rule_[kPresolveRuleRedundantRow] = true; + allow_rule_[kPresolveRuleEmptyCol] = true; + allow_rule_[kPresolveRuleFixedCol] = true; + // Switch on initial sweep! + allow_rule_[kPresolveRuleInitialSweep] = true; + }; // All rules can be switched off, although the most fundamental can // only be switched off in initial sweep - auto allowedOffInInitialSweep = [&](const HighsInt rule_type) { - if (rule_type == kPresolveRuleEmptyRow) return true; - if (rule_type == kPresolveRuleSingletonRow) return true; - if (rule_type == kPresolveRuleRedundantRow) return true; - if (rule_type == kPresolveRuleEmptyCol) return true; - if (rule_type == kPresolveRuleFixedCol) return true; + auto allowedOffInInitialSweep = [&](const HighsInt rule) { + if (rule == kPresolveRuleEmptyRow) return true; + if (rule == kPresolveRuleSingletonRow) return true; + if (rule == kPresolveRuleRedundantRow) return true; + if (rule == kPresolveRuleEmptyCol) return true; + if (rule == kPresolveRuleFixedCol) return true; return false; }; - auto logRule = [&](const HighsInt rule_type) { + auto setRulesForLightMode = [&]() { + // All rules on by default + allow_rule_.assign(kPresolveRuleCount, true); + // Switch off the rules not used in presolve light + allow_rule_[kPresolveRuleDominatedCol] = false; + allow_rule_[kPresolveRuleDependentEquations] = false; + allow_rule_[kPresolveRuleDependentFreeCols] = false; + allow_rule_[kPresolveRuleAggregator] = false; + allow_rule_[kPresolveRuleParallelRowsAndCols] = false; + allow_rule_[kPresolveRuleSparsify] = false; + allow_rule_[kPresolveRuleProbing] = false; + allow_rule_[kPresolveRuleEnumeration] = false; + allow_rule_[kPresolveRuleDualFixing] = false; + allow_rule_[kPresolveRuleZeroCostSingleton] = false; + allow_rule_[kPresolveRuleColStuffing] = false; + allow_rule_[kPresolveRuleFourierMotzkin] = false; + }; + + auto logRule = [&](const HighsInt rule) { highsLogUser(options->log_options, HighsLogType::kInfo, " %1s Rule %2d (set bit %2d = %7d): %s\n", - allowedOffInInitialSweep(rule_type) ? "*" : " ", - int(rule_type), int(rule_type), int(1 << rule_type), - utilPresolveRuleTypeToString(rule_type).c_str()); + allowedOffInInitialSweep(rule) ? "*" : " ", int(rule), + int(rule), int(1 << rule), + utilPresolveRuleTypeToString(rule).c_str()); }; auto ruleOffForPostsolve = [&](const std::string& postsolve_type, @@ -406,64 +435,60 @@ void HPresolve::chooseRules() { highsLogUser(options->log_options, HighsLogType::kInfo, "Permitted suppression of presolve rules via " "presolve_rule_off option:\n"); - for (HighsInt rule_type = kPresolveRuleMin; rule_type < kPresolveRuleCount; - rule_type++) - logRule(rule_type); + for (HighsInt rule = kPresolveRuleMin; rule < kPresolveRuleCount; rule++) + logRule(rule); highsLogUser(options->log_options, HighsLogType::kInfo, " * Only in initial sweep\n"); } - if (options->presolve_light == kHighsOnString) { - // Switch off the rules not used in presolve_light mode - allow_rule_[kPresolveRuleDominatedCol] = false; - allow_rule_[kPresolveRuleDependentEquations] = false; - allow_rule_[kPresolveRuleDependentFreeCols] = false; - allow_rule_[kPresolveRuleAggregator] = false; - allow_rule_[kPresolveRuleParallelRowsAndCols] = false; - allow_rule_[kPresolveRuleSparsify] = false; - allow_rule_[kPresolveRuleProbing] = false; - allow_rule_[kPresolveRuleEnumeration] = false; - allow_rule_[kPresolveRuleDualFixing] = false; - allow_rule_[kPresolveRuleZeroCostSingleton] = false; - allow_rule_[kPresolveRuleColStuffing] = false; - allow_rule_[kPresolveRuleFourierMotzkin] = false; + // Set the pre-defined rules on/off according to the mode + if (options->presolve_mode == kPresolveModeDefault) { + // By default all presolve rules are allowed, but subsets for + // specific solvers (esp PDLP) can be set + if (usePdlp(options->solver)) { + setRulesForLightMode(); + } else { + this->allow_rule_.assign(kPresolveRuleCount, true); + } + } else if (options->presolve_mode == kPresolveModeInitialSweep) { + setRulesForInitialSweepMode(); + } else if (options->presolve_mode == kPresolveModeLight) { + setRulesForLightMode(); + } else if (options->presolve_mode == kPresolveModeFull) { + // Full presolve uses all presolve rules + this->allow_rule_.assign(kPresolveRuleCount, true); + } else { + highsLogUser(options->log_options, HighsLogType::kError, + "Unrecognised presolve mode %d\n", + int(options->presolve_mode)); + assert(1 == 0); } - + // Possibly switch off specific rules defined by the user via + // presolve_rule_off if (options->presolve_rule_off) { - // Some presolve rules are off - // - // Transform options->presolve_rule_off into logical settings in + // Transform presolve_rule_off into logical settings in // allow_rule_[*], commenting on the rules switched off if (!silent) highsLogUser(options->log_options, HighsLogType::kInfo, "Presolve rules switched off:\n"); + // Keep track of whether to add the qualifier "* Only in initial + // sweep" to the logging! bool off_in_initial_sweep = false; - for (HighsInt rule_type = kPresolveRuleMin; rule_type < kPresolveRuleCount; - rule_type++) { - HighsInt bit = 1 << rule_type; - // Identify whether this rule is allowed + for (HighsInt rule = kPresolveRuleMin; rule < kPresolveRuleCount; rule++) { + HighsInt bit = 1 << rule; if (!(options->presolve_rule_off & bit)) continue; - if (rule_type != kPresolveRuleDominatedCol) { - off_in_initial_sweep = - allowedOffInInitialSweep(rule_type) || off_in_initial_sweep; - allow_rule_[rule_type] = false; - if (!silent) logRule(rule_type); - } else { - // This is a rule that cannot be switched off so, if an - // attempt is made, don't allow it to be off and possibly - // comment negatively - if (!silent) - highsLogUser(options->log_options, HighsLogType::kWarning, - "Cannot disallow rule %2d (bit %2d = %5d): %s\n", - int(rule_type), int(rule_type), int(bit), - utilPresolveRuleTypeToString(rule_type).c_str()); - } + off_in_initial_sweep = + allowedOffInInitialSweep(rule) || off_in_initial_sweep; + allow_rule_[rule] = false; + if (!silent) logRule(rule); } if (!silent && off_in_initial_sweep) highsLogUser(options->log_options, HighsLogType::kInfo, " * Only in initial sweep\n"); } + // Finally switch off rules that are incompatible with dual or basis + // postsolve may_require_basis_postsolve_ = false; may_require_dual_postsolve_ = false; From 4fd289eba2a25f20954d21fed095e4cbc6e3eaa7 Mon Sep 17 00:00:00 2001 From: JAJHall Date: Wed, 4 Nov 2026 21:14:09 +0000 Subject: [PATCH 2/4] Updated HighsOptions.h and used it to create docs/src/options/definitions.md --- docs/src/options/definitions.md | 25 ++++++++++++++------ highs/lp_data/HighsOptions.h | 42 ++++++++++++++++----------------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/docs/src/options/definitions.md b/docs/src/options/definitions.md index 07024f2ae10..cd437aec540 100644 --- a/docs/src/options/definitions.md +++ b/docs/src/options/definitions.md @@ -6,7 +6,7 @@ - Default: "choose" ## [solver](@id option-solver) -- LP/QP solver: "choose", "simplex", "ipm", "ipx", "hipo", "pdlp", "hipdlp" or "qpasm" +- LP/QP solver: "choose", "simplex", "ipm", "ipx", "hipo", "pdlp", "qpasm" or "hipdlp", - Type: string - Default: "choose" @@ -200,9 +200,9 @@ - Default: "false" ## [write\_solution\_style](@id option-write-solution-style) -- Style of solution file (raw = computer-readable, pretty = human-readable): -1 => HiGHS old raw (deprecated); 0 => HiGHS raw; 1 => HiGHS pretty; 2 => Glpsol raw; 3 => Glpsol pretty; 4 => HiGHS sparse raw +- Style of solution file (raw = computer-readable, pretty = human-readable): -1 => HiGHS old raw (deprecated); 0 => HiGHS raw; 1 => HiGHS pretty; 2 => Glpsol raw; 3 => Glpsol pretty; 4 => HiGHS sparse raw; 5 => HiGHS partial raw - Type: integer -- Range: {-1, 4} +- Range: {-1, 5} - Default: 0 ## [glpsol\_cost\_row\_location](@id option-glpsol-cost-row-location) @@ -246,6 +246,12 @@ - Type: string - Default: "" +## [presolve\_mode](@id option-presolve-mode) +- Presolve mode 0 => default; 1 => initial sweep; 2 => light; 3 => full +- Type: integer +- Range: {0, 3} +- Default: 0 + ## [mip\_detect\_symmetry](@id option-mip-detect-symmetry) - Whether MIP symmetry should be detected - Type: boolean @@ -445,13 +451,13 @@ - Default: "choose" ## [hipo\_parallel\_force](@id option-hipo-parallel-force) -- Bit map to force the use of parallel techniques in HiPO +- Bit mask to force parallel techniques in HiPO - Type: integer - Range: {0, 1023} - Default: 0 ## [hipo\_parallel\_forbid](@id option-hipo-parallel-forbid) -- Bit map to forbid the use of parallel techniques in HiPO +- Bit mask to forbid parallel techniques in HiPO - Type: integer - Range: {0, 1023} - Default: 0 @@ -461,8 +467,8 @@ - Type: string - Default: "choose" -## [hipo\_fatcor](@id option-hipo-factor) -- HiPO matrix factorisation: "choose", "multifrontal", or "uplooking" +## [hipo\_factor](@id option-hipo-factor) +- HiPO matrix factorisation: "choose", "multifrontal" or "uplooking" - Type: string - Default: "choose" @@ -519,6 +525,11 @@ - Range: [1e-10, inf] - Default: 1e-07 +## [test\_qp\_oracle](@id option-test-qp-oracle) +- Use an oracle for the QP Hessian +- Type: boolean +- Default: "false" + ## [qp\_allow\_hot\_start](@id option-qp-allow-hot-start) - Allow the active set QP solver to hot start - Type: boolean diff --git a/highs/lp_data/HighsOptions.h b/highs/lp_data/HighsOptions.h index b0c08f70c5b..832070eb80f 100644 --- a/highs/lp_data/HighsOptions.h +++ b/highs/lp_data/HighsOptions.h @@ -1020,7 +1020,7 @@ class HighsOptions : public HighsOptionsStruct { "pretty = human-readable): " "-1 => HiGHS old raw (deprecated); 0 => HiGHS raw; " "1 => HiGHS pretty; 2 => Glpsol raw; 3 => Glpsol pretty; " - "4 => HiGHS sparse raw", + "4 => HiGHS sparse raw; 5 => HiGHS partial raw", advanced, &write_solution_style, kSolutionStyleMin, kSolutionStyleRaw, kSolutionStyleMax); records.push_back(record_int); @@ -1109,6 +1109,13 @@ class HighsOptions : public HighsOptionsStruct { &write_iis_model_file, kHighsFilenameDefault); records.push_back(record_string); + record_int = new OptionRecordInt( + "presolve_mode", + "Presolve mode 0 => default; 1 => initial sweep; 2 => light; 3 => full", + advanced, &presolve_mode, kPresolveModeMin, kPresolveModeDefault, + kPresolveModeMax); + records.push_back(record_int); + record_bool = new OptionRecordBool( "mip_detect_symmetry", "Whether MIP symmetry should be detected", advanced, &mip_detect_symmetry, true); @@ -1337,6 +1344,19 @@ class HighsOptions : public HighsOptionsStruct { advanced, &hipo_parallel_type, kHighsChooseString); records.push_back(record_string); + record_int = new OptionRecordInt( + "hipo_parallel_force", "Bit mask to force parallel techniques in HiPO", + advanced, &hipo_parallel_force, 0, 0, + static_cast(hipo::ParallelTechnique::kMaxSum)); + records.push_back(record_int); + + record_int = + new OptionRecordInt("hipo_parallel_forbid", + "Bit mask to forbid parallel techniques in HiPO", + advanced, &hipo_parallel_forbid, 0, 0, + static_cast(hipo::ParallelTechnique::kMaxSum)); + records.push_back(record_int); + record_string = new OptionRecordString( kHipoOrderingString, "HiPO matrix reordering: \"choose\", \"metis\", \"amd\" or \"rcm\"", @@ -1355,19 +1375,6 @@ class HighsOptions : public HighsOptionsStruct { advanced, &hipo_block_size, 0, 128, kHighsIInf); records.push_back(record_int); - record_int = new OptionRecordInt( - "hipo_parallel_force", "Bit mask to force parallel techniques in HiPO", - advanced, &hipo_parallel_force, 0, 0, - static_cast(hipo::ParallelTechnique::kMaxSum)); - records.push_back(record_int); - - record_int = - new OptionRecordInt("hipo_parallel_forbid", - "Bit mask to forbid parallel techniques in HiPO", - advanced, &hipo_parallel_forbid, 0, 0, - static_cast(hipo::ParallelTechnique::kMaxSum)); - records.push_back(record_int); - record_bool = new OptionRecordBool( "hipo_serial_blas", "Force OpenBLAS to run in serial when used by HiPO. This setting is " @@ -1659,13 +1666,6 @@ class HighsOptions : public HighsOptionsStruct { kMaxPivotThreshold); records.push_back(record_double); - record_int = new OptionRecordInt( - "presolve_mode", - "Presolve mode 0 => default; 1 => initial sweep; 2 => light; 3 => full", - advanced, &presolve_mode, kPresolveModeMin, kPresolveModeDefault, - kPresolveModeMax); - records.push_back(record_int); - record_int = new OptionRecordInt( "presolve_reduction_limit", "Limit on number of presolve reductions -1 => no limit", advanced, From e7ea36b8aa44e55531f28311635391d788889be1 Mon Sep 17 00:00:00 2001 From: JAJHall Date: Wed, 4 Nov 2026 21:44:27 +0000 Subject: [PATCH 3/4] Added presolve-mode unit test --- check/TestPresolve.cpp | 43 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index 845339aa26d..f7b12deb166 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -2288,3 +2288,46 @@ TEST_CASE("hipo-ipx-pdlp-no-crossover", "[highs_test_presolve]") { } #endif + +TEST_CASE("presolve-mode", "[highs_test_presolve]") { + std::string model_file = + std::string(HIGHS_DIR) + "/check/instances/egout-ac.mps"; + Highs h; + h.setOptionValue("output_flag", dev_run); + REQUIRE(h.setOptionValue("presolve_rule_logging", true) == HighsStatus::kOk); + REQUIRE(h.readModel(model_file) == HighsStatus::kOk); + + std::vector presolve_modes = {"default", "initial sweep", "light", "full"}; + h.setOptionValue("", dev_run); + + HighsInt initial_sweep_num_row = 0; + HighsInt initial_sweep_num_col = 0; + HighsInt initial_sweep_num_nz = 0; + const HighsLp& presolved_lp = h.getPresolvedLp(); + for (HighsInt iMode = kPresolveModeMin; iMode <= kPresolveModeMax; iMode++) { + if (dev_run) { + std::string text = "Testing presolve mode " + std::to_string(iMode) + " (" + presolve_modes[iMode] + ")"; + highsPrintStringAsHeading(text); + } + REQUIRE(h.setOptionValue("presolve_mode", iMode) == HighsStatus::kOk); + REQUIRE(h.presolve() == HighsStatus::kOk); + if (dev_run) + printf("Presolved LP has %d rows, %d columns and %d nonzeros\n", + int(presolved_lp.num_row_), int(presolved_lp.num_col_), int(presolved_lp.numNz())); + if (iMode == kPresolveModeDefault || iMode == kPresolveModeFull) { + REQUIRE(presolved_lp.num_row_ == 0); + REQUIRE(presolved_lp.num_col_ == 0); + REQUIRE(presolved_lp.numNz() == 0); + } else if (iMode == kPresolveModeInitialSweep) { + initial_sweep_num_row = presolved_lp.num_row_; + initial_sweep_num_col = presolved_lp.num_col_; + initial_sweep_num_nz = presolved_lp.numNz(); + } else { + REQUIRE(initial_sweep_num_row > presolved_lp.num_row_); + REQUIRE(initial_sweep_num_col > presolved_lp.num_col_); + REQUIRE(initial_sweep_num_nz > presolved_lp.numNz()); + } + } + h.resetThreadScheduler(true); +} + From 08bf374e72ef595bf809dfcbc03447e547edda02 Mon Sep 17 00:00:00 2001 From: JAJHall Date: Wed, 4 Nov 2026 22:24:12 +0000 Subject: [PATCH 4/4] Added setRulesForFullMode to HPresolve::chooseRules() and documentation for presolve_mode --- check/TestPresolve.cpp | 10 ++++---- docs/src/guide/further.md | 8 ++++++- highs/presolve/HPresolve.cpp | 46 ++++++++++++++++++++---------------- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/check/TestPresolve.cpp b/check/TestPresolve.cpp index f7b12deb166..b08030bc9d3 100644 --- a/check/TestPresolve.cpp +++ b/check/TestPresolve.cpp @@ -2297,7 +2297,8 @@ TEST_CASE("presolve-mode", "[highs_test_presolve]") { REQUIRE(h.setOptionValue("presolve_rule_logging", true) == HighsStatus::kOk); REQUIRE(h.readModel(model_file) == HighsStatus::kOk); - std::vector presolve_modes = {"default", "initial sweep", "light", "full"}; + std::vector presolve_modes = {"default", "initial sweep", + "light", "full"}; h.setOptionValue("", dev_run); HighsInt initial_sweep_num_row = 0; @@ -2306,14 +2307,16 @@ TEST_CASE("presolve-mode", "[highs_test_presolve]") { const HighsLp& presolved_lp = h.getPresolvedLp(); for (HighsInt iMode = kPresolveModeMin; iMode <= kPresolveModeMax; iMode++) { if (dev_run) { - std::string text = "Testing presolve mode " + std::to_string(iMode) + " (" + presolve_modes[iMode] + ")"; + std::string text = "Testing presolve mode " + std::to_string(iMode) + + " (" + presolve_modes[iMode] + ")"; highsPrintStringAsHeading(text); } REQUIRE(h.setOptionValue("presolve_mode", iMode) == HighsStatus::kOk); REQUIRE(h.presolve() == HighsStatus::kOk); if (dev_run) printf("Presolved LP has %d rows, %d columns and %d nonzeros\n", - int(presolved_lp.num_row_), int(presolved_lp.num_col_), int(presolved_lp.numNz())); + int(presolved_lp.num_row_), int(presolved_lp.num_col_), + int(presolved_lp.numNz())); if (iMode == kPresolveModeDefault || iMode == kPresolveModeFull) { REQUIRE(presolved_lp.num_row_ == 0); REQUIRE(presolved_lp.num_col_ == 0); @@ -2330,4 +2333,3 @@ TEST_CASE("presolve-mode", "[highs_test_presolve]") { } h.resetThreadScheduler(true); } - diff --git a/docs/src/guide/further.md b/docs/src/guide/further.md index 557e974e451..22c90cedfb0 100644 --- a/docs/src/guide/further.md +++ b/docs/src/guide/further.md @@ -8,13 +8,19 @@ cases, the time saved by solving the reduced model is very much greater than the time taken to perform presolve. Once the presolved model is solved, a postsolve procedure (of minimal computational cost) deduces the optimal solution to the original model. Hence presolve is -performed by default. The only exception occus when there is a valid +performed by default. The only exception occurs when there is a valid basis for an LP and the simplex solver is used. In this case the original LP is solved, starting from this basis. In cases where the use of presolve is found not to be advantageous, its use can be switched off by setting the [presolve](@ref option-presolve) option to "off". +HiGHS has four presolve modes for LPs, selected by setting the value of the [presolve\_mode](@ref option-presolve-mode) option +* Default (`presolve_mode=0`): HiGHS chooses the appropriate presolve rules for the problem to be solved +* Initial sweep (`presolve_mode=1`): This mode removes variables that are fixed or have no coefficients in the constraint matrix, and constraints that have no coefficients in the constraint matrix, a single coefficient in the constraint matrix, or are satisfied by virtue of bounds on the constraint activity corresponding to bounds on the variables. +* Light (`presolve_mode=2`): This mode applies the "cheap" presolve rules +* Full (`presolve_mode=3`): This mode applies all presolve rules + HiGHS has a method [presolve](@ref Presolve/postsolve) that performs presolve on the incumbent model, allowing the presolved model to be extracted or written to a file. This is intended for users who have their own diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 746150c0c8a..88e74fb4806 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -367,6 +367,29 @@ bool HPresolve::isImpliedEquationAtUpper(HighsInt row) const { void HPresolve::chooseRules() { const bool silent = silentLog(); + auto setRulesForFullMode = [&]() { + // All rules on in full mode + this->allow_rule_.assign(kPresolveRuleCount, true); + }; + + auto setRulesForLightMode = [&]() { + // All rules on by default + setRulesForFullMode(); + // Switch off the rules not used in presolve light + allow_rule_[kPresolveRuleDominatedCol] = false; + allow_rule_[kPresolveRuleDependentEquations] = false; + allow_rule_[kPresolveRuleDependentFreeCols] = false; + allow_rule_[kPresolveRuleAggregator] = false; + allow_rule_[kPresolveRuleParallelRowsAndCols] = false; + allow_rule_[kPresolveRuleSparsify] = false; + allow_rule_[kPresolveRuleProbing] = false; + allow_rule_[kPresolveRuleEnumeration] = false; + allow_rule_[kPresolveRuleDualFixing] = false; + allow_rule_[kPresolveRuleZeroCostSingleton] = false; + allow_rule_[kPresolveRuleColStuffing] = false; + allow_rule_[kPresolveRuleFourierMotzkin] = false; + }; + auto setRulesForInitialSweepMode = [&]() { // All rules off by default allow_rule_.assign(kPresolveRuleCount, false); @@ -391,24 +414,6 @@ void HPresolve::chooseRules() { return false; }; - auto setRulesForLightMode = [&]() { - // All rules on by default - allow_rule_.assign(kPresolveRuleCount, true); - // Switch off the rules not used in presolve light - allow_rule_[kPresolveRuleDominatedCol] = false; - allow_rule_[kPresolveRuleDependentEquations] = false; - allow_rule_[kPresolveRuleDependentFreeCols] = false; - allow_rule_[kPresolveRuleAggregator] = false; - allow_rule_[kPresolveRuleParallelRowsAndCols] = false; - allow_rule_[kPresolveRuleSparsify] = false; - allow_rule_[kPresolveRuleProbing] = false; - allow_rule_[kPresolveRuleEnumeration] = false; - allow_rule_[kPresolveRuleDualFixing] = false; - allow_rule_[kPresolveRuleZeroCostSingleton] = false; - allow_rule_[kPresolveRuleColStuffing] = false; - allow_rule_[kPresolveRuleFourierMotzkin] = false; - }; - auto logRule = [&](const HighsInt rule) { highsLogUser(options->log_options, HighsLogType::kInfo, " %1s Rule %2d (set bit %2d = %7d): %s\n", @@ -448,15 +453,14 @@ void HPresolve::chooseRules() { if (usePdlp(options->solver)) { setRulesForLightMode(); } else { - this->allow_rule_.assign(kPresolveRuleCount, true); + setRulesForFullMode(); } } else if (options->presolve_mode == kPresolveModeInitialSweep) { setRulesForInitialSweepMode(); } else if (options->presolve_mode == kPresolveModeLight) { setRulesForLightMode(); } else if (options->presolve_mode == kPresolveModeFull) { - // Full presolve uses all presolve rules - this->allow_rule_.assign(kPresolveRuleCount, true); + setRulesForFullMode(); } else { highsLogUser(options->log_options, HighsLogType::kError, "Unrecognised presolve mode %d\n",