From 4b55a3ced4d51fbe3ce6b769778bf1752fe09118 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 14 Apr 2026 18:34:09 +0200 Subject: [PATCH 01/13] Implement single machine schedule sepa --- cmake/sources-python.cmake | 2 + cmake/sources.cmake | 2 + highs/meson.build | 1 + highs/mip/HighsMachineSchedSeparator.cpp | 248 +++++++++++++++++++++++ highs/mip/HighsMachineSchedSeparator.h | 60 ++++++ highs/mip/HighsMipAnalysis.cpp | 2 + highs/mip/HighsSeparation.cpp | 2 + highs/mip/HighsSeparator.h | 2 + highs/mip/MipTimer.h | 5 +- highs/presolve/HPresolve.cpp | 19 -- 10 files changed, 323 insertions(+), 20 deletions(-) create mode 100644 highs/mip/HighsMachineSchedSeparator.cpp create mode 100644 highs/mip/HighsMachineSchedSeparator.h diff --git a/cmake/sources-python.cmake b/cmake/sources-python.cmake index ac1c94c7e4d..33a3081c72a 100644 --- a/cmake/sources-python.cmake +++ b/cmake/sources-python.cmake @@ -215,6 +215,7 @@ set(highs_sources_python highs/mip/HighsImplications.cpp highs/mip/HighsLpAggregator.cpp highs/mip/HighsLpRelaxation.cpp + highs/mip/HighsMachineSchedSeparator.cpp highs/mip/HighsMipAnalysis.cpp highs/mip/HighsMipSolver.cpp highs/mip/HighsMipSolverData.cpp @@ -339,6 +340,7 @@ set(highs_headers_python highs/mip/HighsImplications.h highs/mip/HighsLpAggregator.h highs/mip/HighsLpRelaxation.h + highs/mip/HighsMachineSchedSeparator.h highs/mip/HighsMipAnalysis.h highs/mip/HighsMipSolver.h highs/mip/HighsMipSolverData.h diff --git a/cmake/sources.cmake b/cmake/sources.cmake index 89897224c9b..1b54006ef4f 100644 --- a/cmake/sources.cmake +++ b/cmake/sources.cmake @@ -371,6 +371,7 @@ set(highs_sources mip/HighsImplications.cpp mip/HighsLpAggregator.cpp mip/HighsLpRelaxation.cpp + mip/HighsMachineSchedSeparator.cpp mip/HighsMipAnalysis.cpp mip/HighsMipSolver.cpp mip/HighsMipSolverData.cpp @@ -498,6 +499,7 @@ set(highs_headers mip/HighsImplications.h mip/HighsLpAggregator.h mip/HighsLpRelaxation.h + mip/HighsMachineSchedSeparator.h mip/HighsMipAnalysis.h mip/HighsMipSolver.h mip/HighsMipSolverData.h diff --git a/highs/meson.build b/highs/meson.build index 93b4a6f0c82..c164f195ef6 100644 --- a/highs/meson.build +++ b/highs/meson.build @@ -232,6 +232,7 @@ _srcs = [ 'mip/HighsImplications.cpp', 'mip/HighsLpAggregator.cpp', 'mip/HighsLpRelaxation.cpp', + 'mip/HighsMachineSchedSeparator.cpp', 'mip/HighsMipAnalysis.cpp', 'mip/HighsMipSolver.cpp', 'mip/HighsMipSolverData.cpp', diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp new file mode 100644 index 00000000000..04b56360174 --- /dev/null +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -0,0 +1,248 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* */ +/* This file is part of the HiGHS linear optimization suite */ +/* */ +/* Available as open-source under the MIT License */ +/* */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/**@file mip/HighsMachineSchedSeparator.cpp + */ + +#include "mip/HighsMachineSchedSeparator.h" + +#include + +#include "mip/HighsCutGeneration.h" +#include "mip/HighsLpRelaxation.h" +#include "mip/HighsMipSolverData.h" +#include "mip/HighsTransformedLp.h" + +bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( + std::vector>& vals, + std::vector>& inds, std::vector& rhss, + double& releasedate, const HighsMipSolver& mipsolver) { + struct pair_hash { + size_t operator()(const std::pair& p) const { + return HighsHashHelpers::hash(p); + } + }; + enum class ArcType { + kIfBinOne, // 0 + kIfBinZero, // 1 + }; + HighsInt largestDegree = 0; + HighsInt largestDegreeCol = -1; + std::vector degrees(mipsolver.numCol()); + std::unordered_map, + std::tuple, pair_hash> + adjacency; + + auto addEntry = [&](HighsInt posCol, HighsInt negCol, HighsInt binCol, + double p, ArcType t) { + // My_ji + s_i - s_j >= p_ji + // y_ji = val -> s_i >= s_j + p_ji - M * val + // Make an arc from negCol (j) to posCol (i) + if (p < 0) return; + auto it = adjacency.find({negCol, posCol}); + if (it != adjacency.end()) { + if (std::get<0>(it->second) < p) { + std::get<0>(it->second) = p; + std::get<1>(it->second) = binCol; + std::get<2>(it->second) = t; + } + } else { + degrees[posCol]++; + if (degrees[posCol] > largestDegree || + (degrees[posCol] == largestDegree && posCol < largestDegreeCol)) { + largestDegreeCol = posCol; + largestDegree = degrees[posCol]; + } + adjacency.emplace(std::make_pair(negCol, posCol), + std::make_tuple(p, binCol, t)); + } + }; + + HighsInt numRows = 0; + HighsInt maxRows = std::min(1000, 2 * mipsolver.numRow()); + adjacency.reserve(maxRows); + for (HighsInt row = 0; row != mipsolver.numRow(); row++) { + double rowLower = mipsolver.model_->row_lower_[row]; + double rowUpper = mipsolver.model_->row_upper_[row]; + if (rowLower == rowUpper) continue; + HighsInt start = mipsolver.mipdata_->ARstart_[row]; + HighsInt end = mipsolver.mipdata_->ARstart_[row + 1]; + if (end - start != 3) continue; + bool machineSchedRow = true; + HighsInt posContCol = -1; + HighsInt negContCol = -1; + HighsInt binCol = -1; + double binCoef = 0; + for (HighsInt i = start; i != end; i++) { + HighsInt col = mipsolver.mipdata_->ARindex_[i]; + if (mipsolver.mipdata_->domain.isBinary(col)) { + if (binCol != -1) { + machineSchedRow = false; + break; + } + binCol = col; + binCoef = mipsolver.mipdata_->ARvalue_[i]; + } else if (mipsolver.mipdata_->ARvalue_[i] == -1) { + negContCol = col; + } else if (mipsolver.mipdata_->ARvalue_[i] == 1) { + posContCol = col; + } else { + machineSchedRow = false; + break; + } + } + if (!machineSchedRow || binCol == -1 || negContCol == -1 || + posContCol == -1 || posContCol == negContCol) + continue; + // We want to put the row into form: + // Mx_ji + s_i - s_j >= p_ji, p_ji >= 0 + // x_ji = 1 -> s_i >= s_j + p_ij - M + if (rowUpper != kHighsInf) { + // Given My_ij + s_i - s_j <= d + // The row becomes (after multiplying by -1): + // -My_ij + s_j - s_i >= -d + // Add implication s_j >= s_i + p_ij + M, p_ij + M > 0 when binCol = 1 + // Add implication s_j >= s_i + p_ij, p, p_ij > 0 when binCol = 0 + double rhs_0 = -rowUpper; + double rhs_1 = -rowUpper + binCoef; + // TODO: If both rhs_0 > 0 && rhs_1 > 0 then strengthen the claim, i.e., + // TODO: min{rhs_0, rhs_1} + y_ji * (max{rhs_0, rhs_1} - min{rhs_0, rhs_1} + if (rhs_0 > 0 || rhs_1 > 0) { + if (rhs_0 > 0) { + addEntry(negContCol, posContCol, binCol, rhs_0, ArcType::kIfBinZero); + } else { + addEntry(negContCol, posContCol, binCol, rhs_1, ArcType::kIfBinOne); + } + numRows++; + } + } + if (rowLower != -kHighsInf) { + // Given Mx_ij + s_i - s_j >= d + // Add implication s_i >= s_j + pji - M, p_ji - M > 0 when binCol = 1 + // Add implication s_i >= s_j + pji, p_ji > 0 when binCol = 0 + double rhs_0 = rowLower; + double rhs_1 = rowLower - binCoef; + if (rhs_0 > 0 || rhs_1 > 0) { + if (rhs_0 > 0) { + addEntry(posContCol, negContCol, binCol, rhs_0, ArcType::kIfBinZero); + } else if (rhs_1 > 0) { + addEntry(posContCol, negContCol, binCol, rhs_1, ArcType::kIfBinOne); + } + numRows++; + } + } + if (numRows > maxRows) break; + } + + // A clique of size 3 needs at least 6 arcs + if (numRows <= 5) return false; + + // Greedily search neighbours of largest degree column for a double-sided + // clique (corresponds to a single machine schedule) + std::vector potentialNeighbours; + potentialNeighbours.reserve(largestDegree); + for (const auto& arc : adjacency) { + HighsInt col = std::get<1>(arc.first); + if (col == largestDegreeCol) { + potentialNeighbours.emplace_back(std::get<0>(arc.first)); + } + } + std::vector neighbours; + neighbours.reserve(largestDegree + 1); + neighbours.emplace_back(largestDegreeCol); + releasedate = mipsolver.mipdata_->domain.col_lower_[largestDegreeCol]; + std::vector processingTimes; + processingTimes.resize(largestDegree + 1, kHighsInf); + // Iterate over potential neighbours and check validity + for (HighsInt col : potentialNeighbours) { + bool valid_neighbour = true; + for (HighsInt neighbour : neighbours) { + auto fromArc = adjacency.find({col, neighbour}); + auto toArc = adjacency.find({neighbour, col}); + // Need to verify that the to and fromArc exist and are opposites + if (toArc == adjacency.end() || fromArc == adjacency.end() || + std::get<1>(fromArc->second) != std::get<1>(toArc->second) || + std::get<2>(fromArc->second) == std::get<2>(toArc->second)) { + valid_neighbour = false; + break; + } + } + if (!valid_neighbour) continue; + size_t newNeighbourIndex = neighbours.size(); + // Extract the processing times from the arcs + for (size_t i = 0; i != neighbours.size(); ++i) { + HighsInt neighbour = neighbours[i]; + const auto fromArc = adjacency.find({col, neighbour}); + const auto toArc = adjacency.find({neighbour, col}); + processingTimes[newNeighbourIndex] = std::min( + processingTimes[newNeighbourIndex], std::get<0>(fromArc->second)); + processingTimes[i] = + std::min(processingTimes[i], std::get<0>(toArc->second)); + } + releasedate = + std::min(mipsolver.mipdata_->domain.col_lower_[col], releasedate); + neighbours.emplace_back(col); + } + if (neighbours.size() < 3) return false; + + // Now populate the actual inequalities + vals.resize(neighbours.size(), std::vector(neighbours.size())); + inds.resize(neighbours.size(), std::vector(neighbours.size())); + rhss.resize(neighbours.size()); + for (size_t i = 0; i != neighbours.size(); ++i) { + rhss[i] -= releasedate; + HighsInt col = neighbours[i]; + for (size_t j = 0; j != neighbours.size(); ++j) { + size_t jj = j >= i ? j + 1 : j; + if (jj >= neighbours.size()) continue; + HighsInt neighbour = neighbours[jj]; + const auto toArc = adjacency.find({neighbour, col}); + assert(toArc != adjacency.end()); + inds[i][j] = std::get<1>(toArc->second); + vals[i][j] = processingTimes[jj]; + if (std::get<2>(toArc->second) == ArcType::kIfBinZero) { + rhss[i] -= vals[i][j]; + vals[i][j] *= -1; + } + } + // Put the job start time on the LHS + inds[i].back() = col; + vals[i].back() = -1; + } + + return true; +} + +void HighsMachineSchedSeparator::separateLpSolution( + HighsLpRelaxation& lpRelaxation, HighsLpAggregator& lpAggregator, + HighsTransformedLp& transLp, HighsCutPool& cutpool) { + if (separated) return; + const HighsMipSolver& mip = lpRelaxation.getMipSolver(); + std::vector> vals; + std::vector> inds; + std::vector rhss; + double releasedate; + has_single_machine_schedule = + findSingleMachineScheduleClique(vals, inds, rhss, releasedate, mip); + if (!has_single_machine_schedule) { + separated = true; + return; + } + + // Load the cuts!!! + const std::vector& lpSolution = lpRelaxation.getSolution().col_value; + for (size_t i = 0; i != inds.size(); ++i) { + double viol = -rhss[i]; + for (size_t j = 0; j != inds[i].size(); j++) { + viol += lpSolution[inds[i][j]] * vals[i][j]; + } + if (viol >= 10 * mip.mipdata_->feastol) + cutpool.addCut(mip, inds[i].data(), vals[i].data(), inds[i].size(), + rhss[i]); + } + separated = true; +} diff --git a/highs/mip/HighsMachineSchedSeparator.h b/highs/mip/HighsMachineSchedSeparator.h new file mode 100644 index 00000000000..ae59c592598 --- /dev/null +++ b/highs/mip/HighsMachineSchedSeparator.h @@ -0,0 +1,60 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* */ +/* This file is part of the HiGHS linear optimization suite */ +/* */ +/* Available as open-source under the MIT License */ +/* */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/**@file mip/HighsMachineSchedSeparator.h + * @brief Class for separating release-date cuts, see Section 4.1 in + * Recognition and Exploitation of Single-Machine Scheduling Subproblems in + * Mixed Integer Programs, Reinout Lambertus Henricus Wijfjes, Msc Thesis, 2022, + * or Cutting Plane Algorithm for the Single Machine Scheduling Problem + * with Release Times, G. L. Nemhauser and M. W. P. Savelsbergh, 1992. + * + * Given a set of jobs N with start times s_j, processing times p_ij, + * release times r_j, and binary dependency variables y_ij, + * where y = 1 -> s_i - s_j < 0. + * To simplify the problem: We set p_j = min{p_ji : i \in N / j} + * A valid inequality is then: + * s_j >= min{r_{i} : i \in N} + \sum_{i \in N / j} p_i y_ij + * + */ + +#ifndef MIP_HIGHS_MACHINE_SCHED_SEPARATOR_H_ +#define MIP_HIGHS_MACHINE_SCHED_SEPARATOR_H_ + +#include "mip/HighsMipSolver.h" +#include "mip/HighsMipSolverData.h" +#include "mip/HighsSeparator.h" +#include "util/HighsRandom.h" + +/// Helper class to compute single-row relaxations from the current LP +/// relaxation by substituting bounds and aggregating rows +class HighsMachineSchedSeparator : public HighsSeparator { + private: + HighsRandom randgen; + bool has_single_machine_schedule = false; + bool separated = false; + + public: + void separateLpSolution(HighsLpRelaxation& lpRelaxation, + HighsLpAggregator& lpAggregator, + HighsTransformedLp& transLp, + HighsCutPool& cutpool) override; + + bool findSingleMachineScheduleClique(std::vector>& vals, + std::vector>& inds, + std::vector& rhss, + double& releasedate, + const HighsMipSolver& mipsolver); + + HighsMachineSchedSeparator(const HighsMipSolver& mipsolver) + : HighsSeparator(mipsolver, kMachineSchedSepaString) { + randgen.initialise(mipsolver.options_mip_->random_seed); + has_single_machine_schedule = false; + separated = false; + } +}; + +#endif diff --git a/highs/mip/HighsMipAnalysis.cpp b/highs/mip/HighsMipAnalysis.cpp index 2f36e01ec78..2a69c69bc88 100644 --- a/highs/mip/HighsMipAnalysis.cpp +++ b/highs/mip/HighsMipAnalysis.cpp @@ -42,6 +42,8 @@ void HighsMipAnalysis::setupMipTime(const HighsOptions& options) { std::make_pair(kPathAggrSepaString, kMipClockPathAggrSepa)); sepa_name_clock.push_back( std::make_pair(kModKSepaString, kMipClockModKSepa)); + sepa_name_clock.push_back( + std::make_pair(kMachineSchedSepaString, kMipClockMachineSchedSepa)); } } diff --git a/highs/mip/HighsSeparation.cpp b/highs/mip/HighsSeparation.cpp index a85691a1d02..fd316d46099 100644 --- a/highs/mip/HighsSeparation.cpp +++ b/highs/mip/HighsSeparation.cpp @@ -16,6 +16,7 @@ #include "mip/HighsImplications.h" #include "mip/HighsLpAggregator.h" #include "mip/HighsLpRelaxation.h" +#include "mip/HighsMachineSchedSeparator.h" #include "mip/HighsMipSolverData.h" #include "mip/HighsModkSeparator.h" #include "mip/HighsPathSeparator.h" @@ -31,6 +32,7 @@ HighsSeparation::HighsSeparation(const HighsMipSolver& mipsolver) { separators.emplace_back(new HighsTableauSeparator(mipsolver)); separators.emplace_back(new HighsPathSeparator(mipsolver)); separators.emplace_back(new HighsModkSeparator(mipsolver)); + separators.emplace_back(new HighsMachineSchedSeparator(mipsolver)); } HighsInt HighsSeparation::separationRound(HighsDomain& propdomain, diff --git a/highs/mip/HighsSeparator.h b/highs/mip/HighsSeparator.h index 75fb1a41e07..2eb5d60cdcd 100644 --- a/highs/mip/HighsSeparator.h +++ b/highs/mip/HighsSeparator.h @@ -22,6 +22,8 @@ const std::string kCliqueSepaString = "Separation: Clique"; const std::string kTableauSepaString = "Separation: Tableau"; const std::string kPathAggrSepaString = "Separation: Path aggregation"; const std::string kModKSepaString = "Separation: Mod-k"; +const std::string kMachineSchedSepaString = + "Separation: Single Machine Scheduling"; class HighsLpRelaxation; class HighsTransformedLp; diff --git a/highs/mip/MipTimer.h b/highs/mip/MipTimer.h index f1044de0658..4f82373190d 100644 --- a/highs/mip/MipTimer.h +++ b/highs/mip/MipTimer.h @@ -95,6 +95,7 @@ enum iClockMip { kMipClockTableauSepa, kMipClockPathAggrSepa, kMipClockModKSepa, + kMipClockMachineSchedSepa, // LP solves kMipClockDuSimplexBasisSolveLp, @@ -233,6 +234,8 @@ class MipTimer { timer_pointer->clock_def(kPathAggrSepaString.c_str()); clock[kMipClockModKSepa] = timer_pointer->clock_def(kModKSepaString.c_str()); + clock[kMipClockMachineSchedSepa] = + timer_pointer->clock_def(kMachineSchedSepaString.c_str()); // Presolve - Should correspond to kMipClockRunPresolve clock[kMipClockProbingPresolve] = @@ -488,7 +491,7 @@ class MipTimer { void reportMipSeparationClock(const HighsTimerClock& mip_timer_clock) { const std::vector mip_clock_list{ kMipClockImplboundSepa, kMipClockCliqueSepa, kMipClockTableauSepa, - kMipClockPathAggrSepa, kMipClockModKSepa}; + kMipClockPathAggrSepa, kMipClockModKSepa, kMipClockMachineSchedSepa}; reportMipClockList("MipSeparation", mip_clock_list, mip_timer_clock, kMipClockTotal); //, tolerance_percent_report); }; diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 1ebf6d9ca52..c7092f96f43 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -4465,25 +4465,6 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, // check if variable is implied integer HPRESOLVE_CHECKED_CALL(static_cast(convertImpliedInteger(col))); - - // shift integral variables to have a lower bound of zero - if (model->integrality_[col] != HighsVarType::kContinuous && - model->col_lower_[col] != 0.0 && - (model->col_lower_[col] != -kHighsInf || - model->col_upper_[col] != kHighsInf) && - model->col_upper_[col] - model->col_lower_[col] > 0.5) { - // substitute with the bound that is smaller in magnitude and only - // substitute if bound is not large for an integer - if (std::abs(model->col_upper_[col]) > std::abs(model->col_lower_[col])) { - if (std::abs(model->col_lower_[col]) < 1000.5) - HPRESOLVE_CHECKED_CALL(transformColumn(postsolve_stack, col, 1.0, - model->col_lower_[col])); - } else { - if (std::abs(model->col_upper_[col]) < 1000.5) - HPRESOLVE_CHECKED_CALL(transformColumn(postsolve_stack, col, -1.0, - model->col_upper_[col])); - } - } } // dual fixing From 856e4d7158ad03f0f68207b5d4efd4ebc9205588 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 23 Apr 2026 11:17:14 +0200 Subject: [PATCH 02/13] Don't handle infinite lower bounds --- highs/mip/HighsMachineSchedSeparator.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp index 04b56360174..f2bf2c2ac0e 100644 --- a/highs/mip/HighsMachineSchedSeparator.cpp +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -64,7 +64,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( HighsInt numRows = 0; HighsInt maxRows = std::min(1000, 2 * mipsolver.numRow()); - adjacency.reserve(maxRows); + adjacency.reserve(maxRows + 2); for (HighsInt row = 0; row != mipsolver.numRow(); row++) { double rowLower = mipsolver.model_->row_lower_[row]; double rowUpper = mipsolver.model_->row_upper_[row]; @@ -79,6 +79,11 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( double binCoef = 0; for (HighsInt i = start; i != end; i++) { HighsInt col = mipsolver.mipdata_->ARindex_[i]; + if (mipsolver.mipdata_->domain.col_lower_[col] == -kHighsInf) { + // TODO: Could some jobs be modelled in reverse? + machineSchedRow = false; + break; + } if (mipsolver.mipdata_->domain.isBinary(col)) { if (binCol != -1) { machineSchedRow = false; @@ -135,7 +140,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( numRows++; } } - if (numRows > maxRows) break; + if (numRows >= maxRows) break; } // A clique of size 3 needs at least 6 arcs From 85406e5be7abdc9ff741022fd2c37769a6c49fce Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 23 Apr 2026 11:55:44 +0200 Subject: [PATCH 03/13] Sort potential neighbours greedily --- highs/mip/HighsMachineSchedSeparator.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp index f2bf2c2ac0e..3038f79146d 100644 --- a/highs/mip/HighsMachineSchedSeparator.cpp +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -12,6 +12,7 @@ #include +#include "../extern/pdqsort/pdqsort.h" #include "mip/HighsCutGeneration.h" #include "mip/HighsLpRelaxation.h" #include "mip/HighsMipSolverData.h" @@ -156,6 +157,11 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( potentialNeighbours.emplace_back(std::get<0>(arc.first)); } } + pdqsort(potentialNeighbours.begin(), potentialNeighbours.end(), + [&](const HighsInt c1, const HighsInt c2) { + return degrees[c1] > degrees[c2]; + }); + std::vector neighbours; neighbours.reserve(largestDegree + 1); neighbours.emplace_back(largestDegreeCol); From 8a4cacad6f6ca22f263a35d1ef9f0be27a9214d4 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 24 Apr 2026 10:37:59 +0200 Subject: [PATCH 04/13] Add back restricted version of integer variable shifting --- highs/presolve/HPresolve.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index c7092f96f43..86478dd398b 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -4467,6 +4467,26 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, HPRESOLVE_CHECKED_CALL(static_cast(convertImpliedInteger(col))); } + // shift "binary" variables to have a lower bound of zero + if (model->integrality_[col] != HighsVarType::kContinuous && + model->col_lower_[col] != 0.0 && + (model->col_lower_[col] != -kHighsInf || + model->col_upper_[col] != kHighsInf) && + model->col_upper_[col] - model->col_lower_[col] > 0.5 && + model->col_upper_[col] - model->col_lower_[col] < 1.5) { + // substitute with the bound that is smaller in magnitude and only + // substitute if bound is not large for an integer + if (std::abs(model->col_upper_[col]) > std::abs(model->col_lower_[col])) { + if (std::abs(model->col_lower_[col]) < 1000.5) + HPRESOLVE_CHECKED_CALL( + transformColumn(postsolve_stack, col, 1.0, model->col_lower_[col])); + } else { + if (std::abs(model->col_upper_[col]) < 1000.5) + HPRESOLVE_CHECKED_CALL(transformColumn(postsolve_stack, col, -1.0, + model->col_upper_[col])); + } + } + // dual fixing HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col)); if (colDeleted[col]) return Result::kOk; From b1b8bb5c0368ca3938ce89357f5d89b6a449f680 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 24 Apr 2026 10:42:04 +0200 Subject: [PATCH 05/13] Add it back to the correct spot.... --- highs/presolve/HPresolve.cpp | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/highs/presolve/HPresolve.cpp b/highs/presolve/HPresolve.cpp index 86478dd398b..107bd95ff1e 100644 --- a/highs/presolve/HPresolve.cpp +++ b/highs/presolve/HPresolve.cpp @@ -4465,25 +4465,25 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack, // check if variable is implied integer HPRESOLVE_CHECKED_CALL(static_cast(convertImpliedInteger(col))); - } - // shift "binary" variables to have a lower bound of zero - if (model->integrality_[col] != HighsVarType::kContinuous && - model->col_lower_[col] != 0.0 && - (model->col_lower_[col] != -kHighsInf || - model->col_upper_[col] != kHighsInf) && - model->col_upper_[col] - model->col_lower_[col] > 0.5 && - model->col_upper_[col] - model->col_lower_[col] < 1.5) { - // substitute with the bound that is smaller in magnitude and only - // substitute if bound is not large for an integer - if (std::abs(model->col_upper_[col]) > std::abs(model->col_lower_[col])) { - if (std::abs(model->col_lower_[col]) < 1000.5) - HPRESOLVE_CHECKED_CALL( - transformColumn(postsolve_stack, col, 1.0, model->col_lower_[col])); - } else { - if (std::abs(model->col_upper_[col]) < 1000.5) - HPRESOLVE_CHECKED_CALL(transformColumn(postsolve_stack, col, -1.0, - model->col_upper_[col])); + // shift "binary" variables to have a lower bound of zero + if (model->integrality_[col] != HighsVarType::kContinuous && + model->col_lower_[col] != 0.0 && + (model->col_lower_[col] != -kHighsInf || + model->col_upper_[col] != kHighsInf) && + model->col_upper_[col] - model->col_lower_[col] > 0.5 && + model->col_upper_[col] - model->col_lower_[col] < 1.5) { + // substitute with the bound that is smaller in magnitude and only + // substitute if bound is not large for an integer + if (std::abs(model->col_upper_[col]) > std::abs(model->col_lower_[col])) { + if (std::abs(model->col_lower_[col]) < 1000.5) + HPRESOLVE_CHECKED_CALL(transformColumn(postsolve_stack, col, 1.0, + model->col_lower_[col])); + } else { + if (std::abs(model->col_upper_[col]) < 1000.5) + HPRESOLVE_CHECKED_CALL(transformColumn(postsolve_stack, col, -1.0, + model->col_upper_[col])); + } } } From c1eed97d17903002ba150a2872b7714d2066bf40 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 29 Apr 2026 17:10:49 +0200 Subject: [PATCH 06/13] Make std::min not look at different types --- highs/mip/HighsMachineSchedSeparator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp index 3038f79146d..d91b2f01e6a 100644 --- a/highs/mip/HighsMachineSchedSeparator.cpp +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -64,7 +64,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( }; HighsInt numRows = 0; - HighsInt maxRows = std::min(1000, 2 * mipsolver.numRow()); + const HighsInt maxRows = std::min(HighsInt{1000}, 2 * mipsolver.numRow()); adjacency.reserve(maxRows + 2); for (HighsInt row = 0; row != mipsolver.numRow(); row++) { double rowLower = mipsolver.model_->row_lower_[row]; From 27e9cd529ea01d2013106c0e26756b2cfb95e601 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 4 May 2026 12:48:13 +0200 Subject: [PATCH 07/13] Make formatter happy --- highs/mip/MipTimer.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/MipTimer.h b/highs/mip/MipTimer.h index 4f82373190d..0f6641119a6 100644 --- a/highs/mip/MipTimer.h +++ b/highs/mip/MipTimer.h @@ -491,7 +491,7 @@ class MipTimer { void reportMipSeparationClock(const HighsTimerClock& mip_timer_clock) { const std::vector mip_clock_list{ kMipClockImplboundSepa, kMipClockCliqueSepa, kMipClockTableauSepa, - kMipClockPathAggrSepa, kMipClockModKSepa, kMipClockMachineSchedSepa}; + kMipClockPathAggrSepa, kMipClockModKSepa, kMipClockMachineSchedSepa}; reportMipClockList("MipSeparation", mip_clock_list, mip_timer_clock, kMipClockTotal); //, tolerance_percent_report); }; From 8f6de97377a7fe951e700b1252eeb2770a6a3545 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 13 May 2026 14:39:34 +0200 Subject: [PATCH 08/13] Add clique detection --- highs/mip/HighsMachineSchedSeparator.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp index d91b2f01e6a..84ef4d7e474 100644 --- a/highs/mip/HighsMachineSchedSeparator.cpp +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -27,6 +27,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( return HighsHashHelpers::hash(p); } }; + std::vector clique(2); enum class ArcType { kIfBinOne, // 0 kIfBinZero, // 1 @@ -46,6 +47,14 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( if (p < 0) return; auto it = adjacency.find({negCol, posCol}); if (it != adjacency.end()) { + // If there's two overlapping arcs y_ji and y'_ji then we can conclude + // if y_ji -> y'_ji, i.e., y_ji + ~y'_ji <= 1 + clique[0] = HighsCliqueTable::CliqueVar( + std::get<1>(it->second), + std::get<2>(it->second) == ArcType::kIfBinOne); + clique[1] = + HighsCliqueTable::CliqueVar(binCol, t == ArcType::kIfBinOne ? 0 : 1); + mipsolver.mipdata_->cliquetable.addClique(mipsolver, clique.data(), 2); if (std::get<0>(it->second) < p) { std::get<0>(it->second) = p; std::get<1>(it->second) = binCol; @@ -64,7 +73,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( }; HighsInt numRows = 0; - const HighsInt maxRows = std::min(HighsInt{1000}, 2 * mipsolver.numRow()); + const HighsInt maxRows = std::min(HighsInt{5000}, 2 * mipsolver.numRow()); adjacency.reserve(maxRows + 2); for (HighsInt row = 0; row != mipsolver.numRow(); row++) { double rowLower = mipsolver.model_->row_lower_[row]; From 202fb62fdde4db1e9649a7fd0a108d41441665c5 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 13 May 2026 15:09:05 +0200 Subject: [PATCH 09/13] Remove releasedate as parameter --- highs/mip/HighsMachineSchedSeparator.cpp | 7 +++---- highs/mip/HighsMachineSchedSeparator.h | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp index 84ef4d7e474..615adbe26fb 100644 --- a/highs/mip/HighsMachineSchedSeparator.cpp +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -21,7 +21,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( std::vector>& vals, std::vector>& inds, std::vector& rhss, - double& releasedate, const HighsMipSolver& mipsolver) { + const HighsMipSolver& mipsolver) { struct pair_hash { size_t operator()(const std::pair& p) const { return HighsHashHelpers::hash(p); @@ -174,7 +174,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( std::vector neighbours; neighbours.reserve(largestDegree + 1); neighbours.emplace_back(largestDegreeCol); - releasedate = mipsolver.mipdata_->domain.col_lower_[largestDegreeCol]; + double releasedate = mipsolver.mipdata_->domain.col_lower_[largestDegreeCol]; std::vector processingTimes; processingTimes.resize(largestDegree + 1, kHighsInf); // Iterate over potential neighbours and check validity @@ -245,9 +245,8 @@ void HighsMachineSchedSeparator::separateLpSolution( std::vector> vals; std::vector> inds; std::vector rhss; - double releasedate; has_single_machine_schedule = - findSingleMachineScheduleClique(vals, inds, rhss, releasedate, mip); + findSingleMachineScheduleClique(vals, inds, rhss, mip); if (!has_single_machine_schedule) { separated = true; return; diff --git a/highs/mip/HighsMachineSchedSeparator.h b/highs/mip/HighsMachineSchedSeparator.h index ae59c592598..55d6b33f137 100644 --- a/highs/mip/HighsMachineSchedSeparator.h +++ b/highs/mip/HighsMachineSchedSeparator.h @@ -46,7 +46,6 @@ class HighsMachineSchedSeparator : public HighsSeparator { bool findSingleMachineScheduleClique(std::vector>& vals, std::vector>& inds, std::vector& rhss, - double& releasedate, const HighsMipSolver& mipsolver); HighsMachineSchedSeparator(const HighsMipSolver& mipsolver) From ea4ee189b82ca2d64176408b5842c07edaa04963 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 22 Jun 2026 12:49:37 +0200 Subject: [PATCH 10/13] Use HighsHashTree instead of unordered_map. Remove incorrect cliques --- highs/mip/HighsMachineSchedSeparator.cpp | 110 ++++++++++------------- highs/mip/HighsMachineSchedSeparator.h | 12 +-- 2 files changed, 51 insertions(+), 71 deletions(-) diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp index 615adbe26fb..6b4c321174b 100644 --- a/highs/mip/HighsMachineSchedSeparator.cpp +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -10,8 +10,6 @@ #include "mip/HighsMachineSchedSeparator.h" -#include - #include "../extern/pdqsort/pdqsort.h" #include "mip/HighsCutGeneration.h" #include "mip/HighsLpRelaxation.h" @@ -22,22 +20,18 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( std::vector>& vals, std::vector>& inds, std::vector& rhss, const HighsMipSolver& mipsolver) { - struct pair_hash { - size_t operator()(const std::pair& p) const { - return HighsHashHelpers::hash(p); - } - }; std::vector clique(2); enum class ArcType { - kIfBinOne, // 0 - kIfBinZero, // 1 + kIfBinOne, + kIfBinZero, }; HighsInt largestDegree = 0; HighsInt largestDegreeCol = -1; std::vector degrees(mipsolver.numCol()); - std::unordered_map, - std::tuple, pair_hash> - adjacency; + const HighsInt maxRows = std::min(HighsInt{50000}, 2 * mipsolver.numRow()); + HighsHashTable, + std::tuple> + adjacency(maxRows + 2); auto addEntry = [&](HighsInt posCol, HighsInt negCol, HighsInt binCol, double p, ArcType t) { @@ -45,20 +39,10 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( // y_ji = val -> s_i >= s_j + p_ji - M * val // Make an arc from negCol (j) to posCol (i) if (p < 0) return; - auto it = adjacency.find({negCol, posCol}); - if (it != adjacency.end()) { - // If there's two overlapping arcs y_ji and y'_ji then we can conclude - // if y_ji -> y'_ji, i.e., y_ji + ~y'_ji <= 1 - clique[0] = HighsCliqueTable::CliqueVar( - std::get<1>(it->second), - std::get<2>(it->second) == ArcType::kIfBinOne); - clique[1] = - HighsCliqueTable::CliqueVar(binCol, t == ArcType::kIfBinOne ? 0 : 1); - mipsolver.mipdata_->cliquetable.addClique(mipsolver, clique.data(), 2); - if (std::get<0>(it->second) < p) { - std::get<0>(it->second) = p; - std::get<1>(it->second) = binCol; - std::get<2>(it->second) = t; + const auto it = adjacency.find({negCol, posCol}); + if (it != nullptr) { + if (std::get<0>(*it) > p) { + adjacency[{negCol, posCol}] = std::make_tuple(p, binCol, t); } } else { degrees[posCol]++; @@ -67,20 +51,18 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( largestDegreeCol = posCol; largestDegree = degrees[posCol]; } - adjacency.emplace(std::make_pair(negCol, posCol), - std::make_tuple(p, binCol, t)); + adjacency.insert(std::make_pair(negCol, posCol), + std::make_tuple(p, binCol, t)); } }; HighsInt numRows = 0; - const HighsInt maxRows = std::min(HighsInt{5000}, 2 * mipsolver.numRow()); - adjacency.reserve(maxRows + 2); for (HighsInt row = 0; row != mipsolver.numRow(); row++) { - double rowLower = mipsolver.model_->row_lower_[row]; - double rowUpper = mipsolver.model_->row_upper_[row]; + const double rowLower = mipsolver.model_->row_lower_[row]; + const double rowUpper = mipsolver.model_->row_upper_[row]; if (rowLower == rowUpper) continue; - HighsInt start = mipsolver.mipdata_->ARstart_[row]; - HighsInt end = mipsolver.mipdata_->ARstart_[row + 1]; + const HighsInt start = mipsolver.mipdata_->ARstart_[row]; + const HighsInt end = mipsolver.mipdata_->ARstart_[row + 1]; if (end - start != 3) continue; bool machineSchedRow = true; HighsInt posContCol = -1; @@ -90,7 +72,6 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( for (HighsInt i = start; i != end; i++) { HighsInt col = mipsolver.mipdata_->ARindex_[i]; if (mipsolver.mipdata_->domain.col_lower_[col] == -kHighsInf) { - // TODO: Could some jobs be modelled in reverse? machineSchedRow = false; break; } @@ -122,10 +103,8 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( // -My_ij + s_j - s_i >= -d // Add implication s_j >= s_i + p_ij + M, p_ij + M > 0 when binCol = 1 // Add implication s_j >= s_i + p_ij, p, p_ij > 0 when binCol = 0 - double rhs_0 = -rowUpper; - double rhs_1 = -rowUpper + binCoef; - // TODO: If both rhs_0 > 0 && rhs_1 > 0 then strengthen the claim, i.e., - // TODO: min{rhs_0, rhs_1} + y_ji * (max{rhs_0, rhs_1} - min{rhs_0, rhs_1} + const double rhs_0 = -rowUpper; + const double rhs_1 = -rowUpper + binCoef; if (rhs_0 > 0 || rhs_1 > 0) { if (rhs_0 > 0) { addEntry(negContCol, posContCol, binCol, rhs_0, ArcType::kIfBinZero); @@ -139,12 +118,12 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( // Given Mx_ij + s_i - s_j >= d // Add implication s_i >= s_j + pji - M, p_ji - M > 0 when binCol = 1 // Add implication s_i >= s_j + pji, p_ji > 0 when binCol = 0 - double rhs_0 = rowLower; - double rhs_1 = rowLower - binCoef; + const double rhs_0 = rowLower; + const double rhs_1 = rowLower - binCoef; if (rhs_0 > 0 || rhs_1 > 0) { if (rhs_0 > 0) { addEntry(posContCol, negContCol, binCol, rhs_0, ArcType::kIfBinZero); - } else if (rhs_1 > 0) { + } else { addEntry(posContCol, negContCol, binCol, rhs_1, ArcType::kIfBinOne); } numRows++; @@ -160,10 +139,11 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( // clique (corresponds to a single machine schedule) std::vector potentialNeighbours; potentialNeighbours.reserve(largestDegree); - for (const auto& arc : adjacency) { - HighsInt col = std::get<1>(arc.first); + for (const auto& entry : adjacency) { + auto arc = entry.key(); + const HighsInt col = std::get<1>(arc); if (col == largestDegreeCol) { - potentialNeighbours.emplace_back(std::get<0>(arc.first)); + potentialNeighbours.emplace_back(std::get<0>(arc)); } } pdqsort(potentialNeighbours.begin(), potentialNeighbours.end(), @@ -174,37 +154,36 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( std::vector neighbours; neighbours.reserve(largestDegree + 1); neighbours.emplace_back(largestDegreeCol); - double releasedate = mipsolver.mipdata_->domain.col_lower_[largestDegreeCol]; + double releaseDate = mipsolver.mipdata_->domain.col_lower_[largestDegreeCol]; std::vector processingTimes; processingTimes.resize(largestDegree + 1, kHighsInf); // Iterate over potential neighbours and check validity for (HighsInt col : potentialNeighbours) { bool valid_neighbour = true; for (HighsInt neighbour : neighbours) { - auto fromArc = adjacency.find({col, neighbour}); - auto toArc = adjacency.find({neighbour, col}); + const auto fromArc = adjacency.find({col, neighbour}); + const auto toArc = adjacency.find({neighbour, col}); // Need to verify that the to and fromArc exist and are opposites - if (toArc == adjacency.end() || fromArc == adjacency.end() || - std::get<1>(fromArc->second) != std::get<1>(toArc->second) || - std::get<2>(fromArc->second) == std::get<2>(toArc->second)) { + if (toArc == nullptr || fromArc == nullptr || + std::get<1>(*fromArc) != std::get<1>(*toArc) || + std::get<2>(*fromArc) == std::get<2>(*toArc)) { valid_neighbour = false; break; } } if (!valid_neighbour) continue; - size_t newNeighbourIndex = neighbours.size(); + const size_t newNeighbourIndex = neighbours.size(); // Extract the processing times from the arcs for (size_t i = 0; i != neighbours.size(); ++i) { HighsInt neighbour = neighbours[i]; const auto fromArc = adjacency.find({col, neighbour}); const auto toArc = adjacency.find({neighbour, col}); - processingTimes[newNeighbourIndex] = std::min( - processingTimes[newNeighbourIndex], std::get<0>(fromArc->second)); - processingTimes[i] = - std::min(processingTimes[i], std::get<0>(toArc->second)); + processingTimes[newNeighbourIndex] = + std::min(processingTimes[newNeighbourIndex], std::get<0>(*fromArc)); + processingTimes[i] = std::min(processingTimes[i], std::get<0>(*toArc)); } - releasedate = - std::min(mipsolver.mipdata_->domain.col_lower_[col], releasedate); + releaseDate = + std::min(mipsolver.mipdata_->domain.col_lower_[col], releaseDate); neighbours.emplace_back(col); } if (neighbours.size() < 3) return false; @@ -214,17 +193,17 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( inds.resize(neighbours.size(), std::vector(neighbours.size())); rhss.resize(neighbours.size()); for (size_t i = 0; i != neighbours.size(); ++i) { - rhss[i] -= releasedate; + rhss[i] -= releaseDate; HighsInt col = neighbours[i]; for (size_t j = 0; j != neighbours.size(); ++j) { size_t jj = j >= i ? j + 1 : j; if (jj >= neighbours.size()) continue; HighsInt neighbour = neighbours[jj]; const auto toArc = adjacency.find({neighbour, col}); - assert(toArc != adjacency.end()); - inds[i][j] = std::get<1>(toArc->second); + assert(toArc != nullptr); + inds[i][j] = std::get<1>(*toArc); vals[i][j] = processingTimes[jj]; - if (std::get<2>(toArc->second) == ArcType::kIfBinZero) { + if (std::get<2>(*toArc) == ArcType::kIfBinZero) { rhss[i] -= vals[i][j]; vals[i][j] *= -1; } @@ -240,6 +219,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( void HighsMachineSchedSeparator::separateLpSolution( HighsLpRelaxation& lpRelaxation, HighsLpAggregator& lpAggregator, HighsTransformedLp& transLp, HighsCutPool& cutpool) { + // Only separate once if (separated) return; const HighsMipSolver& mip = lpRelaxation.getMipSolver(); std::vector> vals; @@ -252,7 +232,7 @@ void HighsMachineSchedSeparator::separateLpSolution( return; } - // Load the cuts!!! + // Load the cuts const std::vector& lpSolution = lpRelaxation.getSolution().col_value; for (size_t i = 0; i != inds.size(); ++i) { double viol = -rhss[i]; @@ -260,8 +240,8 @@ void HighsMachineSchedSeparator::separateLpSolution( viol += lpSolution[inds[i][j]] * vals[i][j]; } if (viol >= 10 * mip.mipdata_->feastol) - cutpool.addCut(mip, inds[i].data(), vals[i].data(), inds[i].size(), - rhss[i]); + cutpool.addCut(mip, inds[i].data(), vals[i].data(), + static_cast(inds[i].size()), rhss[i]); } separated = true; } diff --git a/highs/mip/HighsMachineSchedSeparator.h b/highs/mip/HighsMachineSchedSeparator.h index 55d6b33f137..b9468329176 100644 --- a/highs/mip/HighsMachineSchedSeparator.h +++ b/highs/mip/HighsMachineSchedSeparator.h @@ -14,7 +14,7 @@ * * Given a set of jobs N with start times s_j, processing times p_ij, * release times r_j, and binary dependency variables y_ij, - * where y = 1 -> s_i - s_j < 0. + * where y = 1 -> s_i - s_j < p_ij. * To simplify the problem: We set p_j = min{p_ji : i \in N / j} * A valid inequality is then: * s_j >= min{r_{i} : i \in N} + \sum_{i \in N / j} p_i y_ij @@ -37,17 +37,17 @@ class HighsMachineSchedSeparator : public HighsSeparator { bool has_single_machine_schedule = false; bool separated = false; + bool findSingleMachineScheduleClique(std::vector>& vals, + std::vector>& inds, + std::vector& rhss, + const HighsMipSolver& mipsolver); + public: void separateLpSolution(HighsLpRelaxation& lpRelaxation, HighsLpAggregator& lpAggregator, HighsTransformedLp& transLp, HighsCutPool& cutpool) override; - bool findSingleMachineScheduleClique(std::vector>& vals, - std::vector>& inds, - std::vector& rhss, - const HighsMipSolver& mipsolver); - HighsMachineSchedSeparator(const HighsMipSolver& mipsolver) : HighsSeparator(mipsolver, kMachineSchedSepaString) { randgen.initialise(mipsolver.options_mip_->random_seed); From 7bd158969d1c952bfbbfa72bfb671575d8b2d24c Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 23 Jun 2026 14:59:54 +0200 Subject: [PATCH 11/13] Add clique extraction --- highs/mip/HighsMachineSchedSeparator.cpp | 52 +++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp index 6b4c321174b..b2ae8fdb8fa 100644 --- a/highs/mip/HighsMachineSchedSeparator.cpp +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -20,7 +20,6 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( std::vector>& vals, std::vector>& inds, std::vector& rhss, const HighsMipSolver& mipsolver) { - std::vector clique(2); enum class ArcType { kIfBinOne, kIfBinZero, @@ -32,6 +31,9 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( HighsHashTable, std::tuple> adjacency(maxRows + 2); + HighsHashTable, std::vector> arcToBin; + // 1 -> (i,j) y = 0, 2 -> (i,j) y = 1, 4 -> (j,i) y = 0, 8 -> (j,i) y = 1 + HighsHashTable, uint8_t> jobOrder; auto addEntry = [&](HighsInt posCol, HighsInt negCol, HighsInt binCol, double p, ArcType t) { @@ -54,6 +56,19 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( adjacency.insert(std::make_pair(negCol, posCol), std::make_tuple(p, binCol, t)); } + // Store binaries to later check if any two binaries + // imply the same ordering of two jobs. + HighsInt u = std::min(negCol, posCol); + HighsInt v = std::max(negCol, posCol); + if (jobOrder.find({u, v, binCol}) == nullptr) { + jobOrder[{u, v, binCol}] = 0; + } + if (negCol < posCol) { + arcToBin[{negCol, posCol}].emplace_back(binCol); + jobOrder[{negCol, posCol, binCol}] |= t == ArcType::kIfBinOne ? 2 : 1; + } else { + jobOrder[{posCol, negCol, binCol}] |= t == ArcType::kIfBinOne ? 8 : 4; + } }; HighsInt numRows = 0; @@ -132,6 +147,41 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( if (numRows >= maxRows) break; } + // Extract binary variables that imply the same job ordering + std::vector clique(2); + for (const auto& entry : arcToBin) { + std::pair arc = entry.key(); + HighsInt baseBinCol = -1; + bool baseForward = false; + for (const HighsInt& binCol : entry.value()) { + bool forward = false; + bool impliesOrder = false; + if ((jobOrder[{arc.first, arc.second, binCol}] & 9) == 9) { + impliesOrder = true; + } else if ((jobOrder[{arc.first, arc.second, binCol}] & 6) == 6) { + impliesOrder = true; + forward = true; + } + if (!impliesOrder) continue; + if (baseBinCol == -1) { + baseBinCol = binCol; + baseForward = forward; + continue; + } + HighsCliqueTable::CliqueVar stayCliqueVar = + HighsCliqueTable::CliqueVar(baseBinCol, baseForward); + HighsCliqueTable::CliqueVar substCliqueVar = + HighsCliqueTable::CliqueVar(binCol, forward); + clique[0] = stayCliqueVar; + clique[1] = substCliqueVar.complement(); + mipsolver.mipdata_->cliquetable.addClique(mipsolver, clique.data(), 2); + clique[0] = stayCliqueVar.complement(); + clique[1] = substCliqueVar; + mipsolver.mipdata_->cliquetable.addClique(mipsolver, clique.data(), 2); + if (mipsolver.mipdata_->domain.infeasible()) return false; + } + } + // A clique of size 3 needs at least 6 arcs if (numRows <= 5) return false; From 0e10e49d699e64ad25433e5b461c13ba3a999c84 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 24 Jun 2026 14:04:22 +0200 Subject: [PATCH 12/13] Skip dupplicate baseBinCol entries --- highs/mip/HighsMachineSchedSeparator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp index b2ae8fdb8fa..2a9d33e8900 100644 --- a/highs/mip/HighsMachineSchedSeparator.cpp +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -162,7 +162,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( impliesOrder = true; forward = true; } - if (!impliesOrder) continue; + if (!impliesOrder || binCol == baseBinCol) continue; if (baseBinCol == -1) { baseBinCol = binCol; baseForward = forward; From 4953df6ce6232243f2b5bb02a60f3e39a68b85c8 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 1 Jul 2026 15:26:30 +0200 Subject: [PATCH 13/13] Add comments from Franz --- highs/mip/HighsMachineSchedSeparator.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/highs/mip/HighsMachineSchedSeparator.cpp b/highs/mip/HighsMachineSchedSeparator.cpp index 3db97ade4b5..bc93044b83d 100644 --- a/highs/mip/HighsMachineSchedSeparator.cpp +++ b/highs/mip/HighsMachineSchedSeparator.cpp @@ -43,7 +43,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( if (p < 0) return; const auto it = adjacency.find({negCol, posCol}); if (it != nullptr) { - if (std::get<0>(*it) > p) { + if (std::get<0>(*it) < p) { adjacency[{negCol, posCol}] = std::make_tuple(p, binCol, t); } } else { @@ -121,7 +121,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( const double rhs_0 = -rowUpper; const double rhs_1 = -rowUpper + binCoef; if (rhs_0 > 0 || rhs_1 > 0) { - if (rhs_0 > 0) { + if (rhs_0 > rhs_1) { addEntry(negContCol, posContCol, binCol, rhs_0, ArcType::kIfBinZero); } else { addEntry(negContCol, posContCol, binCol, rhs_1, ArcType::kIfBinOne); @@ -136,7 +136,7 @@ bool HighsMachineSchedSeparator::findSingleMachineScheduleClique( const double rhs_0 = rowLower; const double rhs_1 = rowLower - binCoef; if (rhs_0 > 0 || rhs_1 > 0) { - if (rhs_0 > 0) { + if (rhs_0 > rhs_1) { addEntry(posContCol, negContCol, binCol, rhs_0, ArcType::kIfBinZero); } else { addEntry(posContCol, negContCol, binCol, rhs_1, ArcType::kIfBinOne);