Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4b55a3c
Implement single machine schedule sepa
Opt-Mucca Apr 14, 2026
856e4d7
Don't handle infinite lower bounds
Opt-Mucca Apr 23, 2026
85406e5
Sort potential neighbours greedily
Opt-Mucca Apr 23, 2026
8a4caca
Add back restricted version of integer variable shifting
Opt-Mucca Apr 24, 2026
b1b8bb5
Add it back to the correct spot....
Opt-Mucca Apr 24, 2026
c1eed97
Make std::min not look at different types
Opt-Mucca Apr 29, 2026
27e9cd5
Make formatter happy
Opt-Mucca May 4, 2026
8f6de97
Add clique detection
Opt-Mucca May 13, 2026
202fb62
Remove releasedate as parameter
Opt-Mucca May 13, 2026
d24cd51
Merge latest into branch
Opt-Mucca Jun 20, 2026
ea4ee18
Use HighsHashTree instead of unordered_map. Remove incorrect cliques
Opt-Mucca Jun 22, 2026
7bd1589
Add clique extraction
Opt-Mucca Jun 23, 2026
0e10e49
Skip dupplicate baseBinCol entries
Opt-Mucca Jun 24, 2026
2bb6401
Merge latest into branch"
Opt-Mucca Jun 24, 2026
9ada144
Merge branch 'latest' of https://github.com/ERGO-Code/HiGHS into mach…
fwesselm Jun 30, 2026
8c4cbb9
Merge branch 'latest' of https://github.com/ERGO-Code/HiGHS into mach…
fwesselm Jul 1, 2026
4953df6
Add comments from Franz
Opt-Mucca Jul 1, 2026
e2b1319
Merge branch 'machine-schedule-sepa' of https://github.com/ERGO-Code/…
fwesselm Jul 1, 2026
0ebbf1a
Add most comments from Ben
Opt-Mucca Jul 30, 2026
e0e8bd5
Clean up loop
Opt-Mucca Jul 31, 2026
5ba06a2
Merge branch 'machine-schedule-sepa' of https://github.com/ERGO-Code/…
fwesselm Aug 3, 2026
d5febde
Merge branch 'latest' of https://github.com/ERGO-Code/HiGHS into mach…
fwesselm Aug 3, 2026
7f1b433
Add lambda suggestion from Franz
Opt-Mucca Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmake/sources.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ set(highs_sources
mip/HighsImplications.cpp
mip/HighsLpAggregator.cpp
mip/HighsLpRelaxation.cpp
mip/HighsMachineSchedSeparator.cpp
mip/HighsMipSolver.cpp
mip/HighsMipSolverData.cpp
mip/HighsMipWorker.cpp
Expand Down Expand Up @@ -517,6 +518,7 @@ set(highs_headers
mip/HighsImplications.h
mip/HighsLpAggregator.h
mip/HighsLpRelaxation.h
mip/HighsMachineSchedSeparator.h
mip/HighsMipSolver.h
mip/HighsMipSolverData.h
mip/HighsMipWorker.h
Expand Down
1 change: 1 addition & 0 deletions highs/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ _srcs = [
'mip/HighsImplications.cpp',
'mip/HighsLpAggregator.cpp',
'mip/HighsLpRelaxation.cpp',
'mip/HighsMachineSchedSeparator.cpp',
'mip/HighsMipSolver.cpp',
'mip/HighsMipSolverData.cpp',
'mip/HighsMipWorker.cpp',
Expand Down
323 changes: 323 additions & 0 deletions highs/mip/HighsMachineSchedSeparator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* */
/* 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 "../extern/pdqsort/pdqsort.h"
#include "mip/HighsCutGeneration.h"
#include "mip/HighsLpRelaxation.h"
#include "mip/HighsMipSolverData.h"
#include "mip/HighsTransformedLp.h"

bool HighsMachineSchedSeparator::findSingleMachineScheduleClique(
std::vector<std::vector<double>>& vals,
std::vector<std::vector<HighsInt>>& inds, std::vector<double>& rhss,
const HighsDomain& globaldom, const HighsMipSolver& mipsolver) {
enum class ArcType {
kImplicationWhenOne,
kImplicationWhenZero,
};
HighsInt largestDegree = 0;
HighsInt largestDegreeCol = -1;
std::vector<HighsInt> degrees(mipsolver.numCol());
const HighsInt maxRows = std::min(HighsInt{50000}, 2 * mipsolver.numRow());
// keys are (j,i) to values (p_ji, y_ji, implication-when-one-or-zero)
// entries map: y_ji = val -> s_i >= s_j + p_ji - M * val
HighsHashTable<std::pair<HighsInt, HighsInt>,
std::tuple<double, HighsInt, ArcType>>
adjacency(maxRows + 2);
// Used to track binaries that imply the same order in both directions
HighsHashTable<std::pair<HighsInt, HighsInt>, std::vector<HighsInt>> arcToBin;
Comment thread
Opt-Mucca marked this conversation as resolved.
// The keys in this map are triples of column indices representing the arc
// endpoints (continuous variables) i, j and the binary variable y.
// The values are bit-fields representing which values of y imply
// which dependency relationships between i and j, as follows:
// 1 -> (i,j) y = 0, 2 -> (i,j) y = 1, 4 -> (j,i) y = 0, 8 -> (j,i) y = 1
Comment thread
Opt-Mucca marked this conversation as resolved.
HighsHashTable<std::tuple<HighsInt, HighsInt, HighsInt>, uint8_t> jobOrder;

auto addEntry = [&](HighsInt posCol, HighsInt negCol, HighsInt binCol,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
auto addEntry = [&](HighsInt posCol, HighsInt negCol, HighsInt binCol,
auto addEntry = [](HighsInt posCol, HighsInt negCol, HighsInt binCol,

For the things you modify, either add explicit captures, or use pass-by-reference.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? It's easier to just add the & here instead of passing three more references.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's much harder to reason about what a function is doing if it can modify any value in scope rather than just three specific things.

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;
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];
if (degrees[posCol] > largestDegree ||
(degrees[posCol] == largestDegree && posCol < largestDegreeCol)) {
largestDegreeCol = posCol;
largestDegree = degrees[posCol];
}
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);
Comment thread
fwesselm marked this conversation as resolved.
jobOrder[{negCol, posCol, binCol}] |=
t == ArcType::kImplicationWhenOne ? 2 : 1;
} else {
jobOrder[{posCol, negCol, binCol}] |=
t == ArcType::kImplicationWhenOne ? 8 : 4;
}
};

HighsInt numRows = 0;
for (HighsInt row = 0; row != mipsolver.numRow(); row++) {
const double rowLower = mipsolver.model_->row_lower_[row];
const double rowUpper = mipsolver.model_->row_upper_[row];
if (rowLower == rowUpper) continue;
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;
HighsInt negContCol = -1;
HighsInt binCol = -1;
double binCoef = 0;
for (HighsInt i = start; i != end; i++) {
HighsInt col = mipsolver.mipdata_->ARindex_[i];
if (globaldom.col_lower_[col] == -kHighsInf) {
machineSchedRow = false;
break;
}
if (globaldom.isBinary(col)) {
if (binCol != -1) {
machineSchedRow = false;
break;
}
binCol = col;
binCoef = mipsolver.mipdata_->ARvalue_[i];
} else if (mipsolver.mipdata_->ARvalue_[i] == -1) {
Comment thread
fwesselm marked this conversation as resolved.
if (negContCol != -1) {
machineSchedRow = false;
break;
}
negContCol = col;
} else if (mipsolver.mipdata_->ARvalue_[i] == 1) {
if (posContCol != -1) {
machineSchedRow = false;
break;
}
posContCol = col;
Comment thread
BenChampion marked this conversation as resolved.
} else {
machineSchedRow = false;
break;
}
}
if (!machineSchedRow || binCol == -1 || negContCol == -1 ||
posContCol == -1)
continue;
// We want to put the row into form:
// My_ji + s_i - s_j >= p_ji, p_ji >= 0
// y_ji = 1 -> s_i >= s_j + p_ji - M
if (rowUpper != kHighsInf) {
Comment thread
Opt-Mucca marked this conversation as resolved.
// 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_ij > 0 when binCol = 0
const double rhs_0 = -rowUpper;
const double rhs_1 = -rowUpper + binCoef;
if (rhs_0 > 0 || rhs_1 > 0) {
if (rhs_0 > rhs_1) {
addEntry(negContCol, posContCol, binCol, rhs_0,
ArcType::kImplicationWhenZero);
} else {
addEntry(negContCol, posContCol, binCol, rhs_1,
ArcType::kImplicationWhenOne);
}
++numRows;
}
}
if (rowLower != -kHighsInf) {
// Given My_ij + s_i - s_j >= d
// Add implication s_i >= s_j + p_ji - M, p_ji - M > 0 when binCol = 1
// Add implication s_i >= s_j + p_ji, p_ji > 0 when binCol = 0
const double rhs_0 = rowLower;
const double rhs_1 = rowLower - binCoef;
if (rhs_0 > 0 || rhs_1 > 0) {
if (rhs_0 > rhs_1) {
addEntry(posContCol, negContCol, binCol, rhs_0,
ArcType::kImplicationWhenZero);
} else {
addEntry(posContCol, negContCol, binCol, rhs_1,
ArcType::kImplicationWhenOne);
}
++numRows;
}
}
if (numRows >= maxRows) break;
}

// Extract binary variables that imply the same job ordering
if (!mipsolver.mipdata_->parallelLockActive()) {
std::vector<HighsCliqueTable::CliqueVar> clique(2);
for (const auto& entry : arcToBin) {
std::pair<HighsInt, HighsInt> arc = entry.key();
HighsInt baseBinCol = -1;
bool baseArcImplicationWhenOne = false;
for (const HighsInt& binCol : entry.value()) {
bool arcImplicationWhenOne = 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) {
Comment thread
BenChampion marked this conversation as resolved.
impliesOrder = true;
arcImplicationWhenOne = true;
}
if (!impliesOrder || binCol == baseBinCol) continue;
if (baseBinCol == -1) {
baseBinCol = binCol;
baseArcImplicationWhenOne = arcImplicationWhenOne;
continue;
}
// Add cliques x1 + ~x2 <= 1 and ~x1 + x2 <= 1 which together imply x1
// == x2 (depending on signs may also have x1 == ~x2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This last addition is a little confusing but I think I know what you mean (that the original variables may already be complemented with respect to each other)

HighsCliqueTable::CliqueVar stayCliqueVar =
HighsCliqueTable::CliqueVar(baseBinCol, baseArcImplicationWhenOne);
HighsCliqueTable::CliqueVar substCliqueVar =
HighsCliqueTable::CliqueVar(binCol, arcImplicationWhenOne);
clique[0] = stayCliqueVar;
clique[1] = substCliqueVar.complement();
mipsolver.mipdata_->cliquetable.addClique(mipsolver, clique.data(), 2);
Comment thread
fwesselm marked this conversation as resolved.
clique[0] = stayCliqueVar.complement();
clique[1] = substCliqueVar;
mipsolver.mipdata_->cliquetable.addClique(mipsolver, clique.data(), 2);
Comment thread
BenChampion marked this conversation as resolved.
if (globaldom.infeasible()) return false;
Comment thread
BenChampion marked this conversation as resolved.
}
}
}

// Skip any clique smaller than size 3
if (numRows <= 5) return false;

// Greedily search neighbours of largest degree column for a double-sided
// clique (corresponds to a single machine schedule)
Comment on lines +204 to +205

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say what you mean by a double-sided clique? (You aren't using any clique data structures here).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this as a directed graph, where a double-sided clique is two arcs of different direction inbetween every two members (nodes)

std::vector<HighsInt> potentialNeighbours;
potentialNeighbours.reserve(largestDegree);
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));
}
}
pdqsort(potentialNeighbours.begin(), potentialNeighbours.end(),
[&](const HighsInt c1, const HighsInt c2) {
return degrees[c1] > degrees[c2];
});

std::vector<HighsInt> neighbours;
neighbours.reserve(largestDegree + 1);
neighbours.emplace_back(largestDegreeCol);
double releaseDate = globaldom.col_lower_[largestDegreeCol];
std::vector<double> processingTimes(largestDegree + 1, kHighsInf);
// Iterate over potential neighbours and check validity
// Greedily add neighbours if they're valid
for (HighsInt col : potentialNeighbours) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth explaining in more detail your strategy here? (In particular, you are enlarging neighbors with every iteration.) I had to backtrack multiple times to figure out what this loop was doing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a small comment

bool valid_neighbour = true;
for (HighsInt neighbour : neighbours) {
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we do this earlier?

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;
double minProcessingTime = kHighsInf;
// Extract the processing times from the arcs
Comment thread
Opt-Mucca marked this conversation as resolved.
// Turn pair-wise dependent times p_ji into p_j = min{p_ji : i \in N / j}
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});
minProcessingTime = std::min(minProcessingTime, std::get<0>(*fromArc));
processingTimes[i] = std::min(processingTimes[i], std::get<0>(*toArc));
}
releaseDate = std::min(globaldom.col_lower_[col], releaseDate);
processingTimes[neighbours.size()] = minProcessingTime;
neighbours.emplace_back(col);
}
if (neighbours.size() < 3) return false;
Comment thread
BenChampion marked this conversation as resolved.

// Now populate the actual inequalities
vals.resize(neighbours.size());
inds.resize(neighbours.size());
rhss.resize(neighbours.size());
for (size_t i = 0; i != neighbours.size(); ++i) {
vals[i].reserve(neighbours.size());
inds[i].reserve(neighbours.size());
rhss[i] -= releaseDate;
HighsInt col = neighbours[i];
for (size_t j = 0; j != neighbours.size(); ++j) {
if (i == j) continue;
HighsInt neighbour = neighbours[j];
const auto toArc = adjacency.find({neighbour, col});
assert(toArc != nullptr);
inds[i].emplace_back(std::get<1>(*toArc));
vals[i].emplace_back(processingTimes[j]);
if (std::get<2>(*toArc) == ArcType::kImplicationWhenZero) {
rhss[i] -= vals[i].back();
vals[i].back() *= -1;
}
}
Comment thread
Opt-Mucca marked this conversation as resolved.
// Put the job start time on the LHS
inds[i].emplace_back(col);
vals[i].emplace_back(-1);
}

return true;
}

void HighsMachineSchedSeparator::separateLpSolution(
HighsLpRelaxation& lpRelaxation, HighsLpAggregator& lpAggregator,
HighsTransformedLp& transLp, HighsCutPool& cutpool) {
// Only try to separate once
if (already_tried) return;
const HighsMipSolver& mip = lpRelaxation.getMipSolver();
std::vector<std::vector<double>> vals;
std::vector<std::vector<HighsInt>> inds;
std::vector<double> rhss;
has_single_machine_schedule = findSingleMachineScheduleClique(
vals, inds, rhss, transLp.getGlobaldom(), mip);
if (!has_single_machine_schedule) {
already_tried = true;
return;
}

// Load the cuts
const std::vector<double>& 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(),
static_cast<HighsInt>(inds[i].size()), rhss[i]);
}
already_tried = true;
}
Loading
Loading