Skip to content

Add single machine scheduling separator - #2990

Merged
Opt-Mucca merged 23 commits into
latestfrom
machine-schedule-sepa
Aug 4, 2026
Merged

Add single machine scheduling separator#2990
Opt-Mucca merged 23 commits into
latestfrom
machine-schedule-sepa

Conversation

@Opt-Mucca

Copy link
Copy Markdown
Collaborator

This PR implements identification of natural-date single-machine scheduling subproblems and derives valid inequalities. See the description of highs/mip/HighsMachineSchedSeparator.h for the exact description.

Motivation: The instance neos-3046615-murg from MIPLIB 2017 benchmark now solves well within time limit, and neos-3046601-motu from MIPLIB 2017 collection now solves in a second or two (both were previously unsolvable within a few hours). This change should introduce some tiny epsilon and very positively affect some instances.

Changes:

  • Add highs/mip/HighsMachineSchedSeparator.h and highs/mip/HighsMachineSchedSeparator.cpp. Currently the separator is only called once, and the cuts are only added to the pool. This is efficient, and I noticed no real change by trying to separate more frequently.
  • I have removed some presolve code that shifts integer columns to have a lower bound of zero. Such a shift completely destroys any structure of this subproblem and makes it impossible to efficiently find. I've tested this change in isolation locally, and there was only some (positive) performance noise. I don't believe it is done for any purpose other than cosmetics + potentially making it easier to transform the LP when generating cuts, so it should be fine to remove. @fwesselm I may be missing something obvious there....

Potential improvements that I don't think need to be made:

  • Someone could model jobs from [-inf, 0] instead of [0, inf]. I'd currently not spot these. I'd also not spot a row where both binary columns share the same coefficient value (different signs), although I think presolve is going to scale these rows most of the time already.
  • Computing these inequalities requires finding a clique. I implemented the most naive greedy approach possible. I don't think we have any max-clique code outside of HighsCliqueTable, which I don't think was efficient to use in this case (could be wrong). There's likely going to be some instances that this code could improve but fails to because of this naive approach, however it ensures near-zero overhead is added to all other instances. An example: I suspect supportcase26 could be improved with some better detection.

This should not be merged until a more thorough performance check has been made! @fwesselm This is a way lower priority than the MIP Worker Refactor to review / test.

@Opt-Mucca
Opt-Mucca requested a review from fwesselm April 23, 2026 10:02
@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.12245% with 86 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.89%. Comparing base (187c624) to head (7f1b433).
⚠️ Report is 127 commits behind head on latest.

Files with missing lines Patch % Lines
highs/mip/HighsMachineSchedSeparator.cpp 54.25% 85 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           latest    #2990      +/-   ##
==========================================
- Coverage   72.92%   72.89%   -0.04%     
==========================================
  Files         436      438       +2     
  Lines      106038   106283     +245     
  Branches    17071    17118      +47     
==========================================
+ Hits        77329    77470     +141     
- Misses      28433    28536     +103     
- Partials      276      277       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Opt-Mucca

Copy link
Copy Markdown
Collaborator Author

I've added some basic clique extraction (was the largest TODO). It now solves the benchmark instance neos-5107597-kakapo within time limit reliably.
That instance had overlapping binary variables, e.g.,

x = 1 -> y_0 - y_1 >= b_0
x = 0 -> y_1 - y_0 >= b_1
y = 1 -> y_0 - y_1 >= b_2
y = 0 -> y_1 - y_0 >= b_3
b_i >= 0

From this you can conclude that x=y. I'm not sure how to efficiently store a multi-graph and find these overlaps though, so I only find one half of it at a time, i.e., x + ~y <= 1.

@Opt-Mucca
Opt-Mucca marked this pull request as draft June 21, 2026 08:49
@jajhall jajhall added the MIP label Jul 2, 2026

@BenChampion BenChampion left a comment

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.

Thanks for asking me to review this change! It was very educational. (The quantity of comments reflects that: I added the comments as I worked through it.)

To summarize my feedback, some parts of this were much harder to understand than others. It might help to delineate as much as possible

  • code that identifies the single machine scheduling structure, ensuring we don't try to build the cuts where they won't be helpful
  • code that computes the data we need once we've identified the structure
  • the complete preconditions for the valid inequalities
  • ideas for each step, intents behind data structures, and your algorithmic strategy

In due course, testing results would also be good to see.

Once you've had a chance to digest my feedback I'd be happy to chat. (It would certainly help me to fill any gaps in my understanding.)

Comment thread highs/mip/HighsMachineSchedSeparator.cpp
Comment thread highs/mip/HighsMachineSchedSeparator.cpp Outdated
Comment thread highs/mip/HighsMachineSchedSeparator.cpp Outdated
HPRESOLVE_CHECKED_CALL(static_cast<Result>(convertImpliedInteger(col)));

// shift integral variables to have a lower bound of zero
// shift "binary" variables to have a lower bound of zero

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.

I see you have an outstanding comment to @fwesselm about this; I don't think I know enough yet to evaluate the implications of changing this.

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.

It is in general nice to have variables have a lower bound of zero. I don't see this making any mathematical difference though, and it completely changes the structure. I don't believe it's done in other solvers. The advantage would be that there's less changes needed to be done in stuff like cut transformation as they already have a lb of 0.
Consider that HiGHS has proven that one job has to start at at-least time 5. It will shift this 0. The job variable no longer represents the start time though, it now represents the start time + 5, which is impossible to recover easily. It can no longer be used to make the inequality.

Comment thread highs/mip/HighsMachineSchedSeparator.h
Comment thread highs/mip/HighsMachineSchedSeparator.cpp Outdated
Comment thread highs/mip/HighsMachineSchedSeparator.cpp
std::vector<double> processingTimes;
processingTimes.resize(largestDegree + 1, kHighsInf);
// Iterate over potential neighbours and check validity
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

Comment thread highs/mip/HighsMachineSchedSeparator.cpp
Comment thread highs/mip/HighsMachineSchedSeparator.cpp

@BenChampion BenChampion left a comment

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.

Thanks for addressing my comments. This looks good to me now modulo some new comments/continuing discussions, and proper testing.

That just leaves the clique size at least three thing: why can't we form the inequality for two jobs? Is it just not useful? Or is it something else? (EDIT: this is only for my understanding)

// 1 -> (i,j) y = 0, 2 -> (i,j) y = 1, 4 -> (j,i) y = 0, 8 -> (j,i) y = 1
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.

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.

Comment thread highs/mip/HighsMachineSchedSeparator.cpp
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)

Comment thread highs/mip/HighsMachineSchedSeparator.cpp
Comment on lines +130 to +131
// My_ji + s_i - s_j >= p_ji, p_ji >= 0
// y_ji = 1 -> s_i >= s_j + p_jj - M

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
// My_ji + s_i - s_j >= p_ji, p_ji >= 0
// y_ji = 1 -> s_i >= s_j + p_jj - M
// My_ji + s_i - s_j >= p_ji, p_ji >= 0
// y_ji = 1 -> s_i >= s_j + p_ji - M

Also, don't you want M(1-y_ji)?

@Opt-Mucca Opt-Mucca Jul 31, 2026

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.

It's just re-arranging and setting y_ji = 1 so that it becomes M. Edit: Fixed the p_jj error

Comment thread highs/mip/HighsMachineSchedSeparator.cpp

@fwesselm fwesselm left a comment

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.

Looks good, @Opt-Mucca!

Comment thread highs/mip/HighsMachineSchedSeparator.cpp
Comment thread highs/mip/HighsMachineSchedSeparator.cpp
Comment thread highs/mip/HighsMachineSchedSeparator.cpp
Comment thread highs/mip/HighsMachineSchedSeparator.cpp
Comment thread highs/mip/MipTimer.h
@Opt-Mucca

Copy link
Copy Markdown
Collaborator Author

This improves performance by 1% or so on the test set of @fwesselm I'm now going to merge it

@Opt-Mucca
Opt-Mucca merged commit dc83470 into latest Aug 4, 2026
534 of 536 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants