fix(breaks): stop a required break interrupting a job - #16
Open
hutchinsp01 wants to merge 1 commit into
Open
Conversation
A required break is modelled as reserved time, which was applied on top of whatever the vehicle happened to be doing at the break's `latest` time. When that moment fell inside a job's service, the break duration was added to the activity's departure, so the break was reported in the middle of the job. Place the break around the service instead. It is now taken at the first moment it becomes due without interrupting work: * while the vehicle is idle at a stop, before the service starts * on the road, when it becomes due while driving * once the service in progress is finished, delaying the departure The new `place_reserved_time` holds this rule and is shared by the scheduler and the solution writer, so the reported break cannot drift from the schedule the solver computed. `earliest` is honoured as a floor rather than ignored, so the search is driven by the moment a break becomes due instead of its deadline. When a break cannot be taken within its time window without interrupting a service, the insertion is rejected: the job moves to another vehicle or is reported as unassigned. The departure time does not depend on where the break sits, so routes which were already valid keep their schedules and statistics. Also fix the routing checker, which assumed a transit break sat at the very start of a leg and validated each half of it independently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Problem
A required break is modelled as reserved time rather than as a job, and was applied on top of whatever the vehicle happened to be doing at the break's
latesttime. When that moment fell inside a job's service, the break duration was simply added to the activity's departure, so the break was reported in the middle of the job:Total duration was correct, but the work was shown as interrupted.
Rule
A required break is now taken at the first moment it becomes due without interrupting work:
If deferring past the service in progress would push the break beyond its
latest, the insertion is rejected: the job moves to another vehicle, or is reported as unassigned withTIME_WINDOW_CONSTRAINT. A required break is still never dropped and is always taken inside its window.The departure time does not depend on where the break sits (
service_start + service_duration + break_durationeither way), so routes which were already valid keep their schedules and statistics.Changes
place_reserved_timeinreserved_time.rsholds the placement rule and returnsBeforeService/AfterService/None(not feasible).estimate_departuredelegates to it, andNonebecomes aControlFlow::Break, which the transport feature already turns into a constraint violation.earliestacts as a floor rather than being ignored.break_writer.rsderives the break's position from the core route using that same shared function, rather than re-deriving it from the API stop schedules. The old logic that extended the overlapping activity'stime.end— which drew the break inside the job — is gone.checker/routing.rsassumed a transit break sat at the very start of a leg and validated each half independently; it now validates the leg as a whole.checker/breaks.rsexpected a required break only if its window ended before the tour ended; it now expects one if the break becomes due before the vehicle departs its last stop.Behaviour changes to be aware of
Breaks move earlier. Honouring
earliestmeans a break is taken at the start of its window rather than pinned tolatest. A window of[12:00, 13:00]that previously resolved to 13:00 now resolves to 12:00, and if the vehicle is driving at 12:00 the result is a transit stop where a break at a stop was reported before.earliest == latestis effectively unusable. Pinned to a single instant there is no slack to defer into, so any job whose service spans that instant becomes infeasible. Required breaks need a real window to be satisfiable around jobs.Testing
Full workspace suite passes (all 15 targets). New coverage:
can_place_reserved_time(5 cases) — the placement rule directly, including both infeasible outcomescan_defer_break_until_service_is_finished— the reported problem: break follows the service instead of splitting itcan_take_break_before_service_when_it_is_due_on_arrivalcan_take_break_during_waiting_time— break absorbed by idle time, with the waiting statistic and cost adjustedcan_take_break_on_the_road_when_it_is_due_while_drivingcan_reject_job_which_cannot_be_served_around_break— job reported unassigned rather than interruptedThree existing tests encoded the old behaviour and were rewritten; the rest are unchanged.
🤖 Generated with Claude Code