fix(objectives): ignore work prefer-early-tours cannot move - #18
Open
hutchinsp01 wants to merge 2 commits into
Open
fix(objectives): ignore work prefer-early-tours cannot move#18hutchinsp01 wants to merge 2 commits into
hutchinsp01 wants to merge 2 commits into
Conversation
hutchinsp01
force-pushed
the
fix/early-tours-ignore-pinned-work
branch
from
September 4, 2026 06:13
5b24b14 to
36ce610
Compare
The objective scored every shift in use, so a shift held open by work the solver cannot move counted as already paid for. Given an appointment pinned to a later shift by a relation, putting a new job on that shift scored delay(late) while putting it on an earlier one scored delay(early) + delay(late) - so the later shift always won, and new work was drawn onto whichever late day the standing commitments happened to have opened. The opposite of the objective's purpose. Score shifts carrying new work instead. Only the first such job on a shift is charged, so the per-tour behaviour is unchanged, and a plan with no committed work behaves exactly as before. Two kinds of job are not new work. The caller passes the existing jobs - those already committed to a shift before the solve - which the pragmatic reader takes from the problem's locks, covering every relation type. That is read from the locks rather than from SolutionContext::locked, which is not the same set: an any relation lands in reserved instead, and locked is additionally extended at runtime with reload and recharge jobs. Beyond those, a job naming a vehicle is skipped. VehicleId is a core job dimension set only on the break, reload and recharge jobs generated for a single vehicle shift, none of which can be taken on another day, so the rule belongs in core rather than in the format reader. Breaks are why it matters: one is required as soon as its route exists and the tour's span covers the break window, so counting it priced the break at its shift's delay - and since this objective outranks cost, the solver preferred to drop the break rather than pay it. Ranking minimize-tours above this objective still consolidates new work onto a shift a committed job has opened, since that saves a tour. Noted on the enum variant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two vrp-core unit tests over a shift holding an appointment: one that the delay is charged for a job which could have gone elsewhere and not for an existing or vehicle-bound one, and one over fitness, whose last two cases are the regression pair - joining the appointment's shift scores four days against one for opening an earlier shift, where before it scored four against five and joining won. One vrp-pragmatic test end to end: a relation pins an appointment to the later shift, which is parked next to the jobs so cost prefers joining it. Asserts the new job still lands on the earliest shift and the break on the pinned shift is still taken. Checked against the unfixed code: with neither exclusion both crates fail, and with existing jobs excluded but vehicle-bound ones still counted they still fail on the break. minimize-tours is left out of the objective list on purpose - ranked above, it consolidates onto the pinned shift whatever this objective prefers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hutchinsp01
force-pushed
the
fix/early-tours-ignore-pinned-work
branch
from
September 4, 2026 06:35
36ce610 to
6ec84bb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
prefer-early-toursscored every shift in use. A shift held open by work the solver cannot move therefore counted as already paid for, and new work was drawn onto it in preference to an earlier empty day — the opposite of what the objective is for.With Monday the fleet's earliest shift and a standing appointment pinned to Thursday by a relation:
Joining the late shift always wins, and it is not only the insertion estimate —
fitnessalready charged Thursday unconditionally, so adding Tuesday could only make the sum worse.This scores shifts carrying work the solver could have placed elsewhere instead. Only the first such job on a shift is charged, so the per-tour behaviour from #17 is unchanged, and a plan with no pinned work behaves exactly as before.
What is not new work
The feature takes the existing jobs — those already committed to a shift before the solve:
The pragmatic reader fills that from the problem's locks, which covers every relation type. It reads the locks rather than
SolutionContext::locked, which is not the same set: onlystrictandsequencerelations reach the solver as locked jobs, ananyrelation lands inreservedinstead, andlockedis additionally extended at runtime with reload and recharge jobs. Reading it would both miss real pins and invent ones that are not there. There is a test onstrictspecifically because it exercises the pathanydoes not.Separately, core skips any job that names a vehicle.
VehicleIdis a core job dimension set only on the break, reload and recharge jobs generated for a single vehicle shift, so this is a rule core can state for itself rather than something the format has to describe. None of those jobs can be taken on another day.Breaks are why that second rule earns its place: one is required as soon as its route exists and the tour's span covers the break window, so counting it priced the break at its shift's delay — and since this objective outranks cost, the solver preferred to drop the break rather than pay it.
departure, standing, arrival— break silently droppeddeparture, [standing, break], arrivalSo a fleet using breaks alongside this objective was losing them.
Ordering caveat
Unchanged from #17, but pinned work makes it much easier to hit: ranked below
minimize-tours, new work still consolidates onto a shift a pinned job has already opened, because that saves a tour. The behavioural tests here leaveminimize-toursout for exactly that reason. Noted on the enum variant.Trade-off
New work will now open an earlier empty shift rather than joining a later one that is already staffed, which can cost an extra vehicle day. That is the intent — rank
minimize-toursabove if consolidating is worth more than earliness.Testing
Full workspace suite passes. Coverage is deliberately thin — two vrp-core unit tests and one end-to-end pragmatic test — but each was checked against the unfixed code rather than just asserted to pass:
The core fitness test's last two cases are the regression pair: joining the appointment's shift scores four days against one for opening an earlier shift, where previously it scored four against five and joining won.
No version bump is included in this branch.
🤖 Generated with Claude Code