Conversation
The forward and backward margin updates repeat the same three scheduling rules in several places: setup applies only when arriving from a different location, a stop takes the first time window whose end it can still reach, and a break absorbs any wait into the remaining travel of the current leg. Each rule was written out again at every site, so a change to scheduling semantics had to be made consistently in ten places. Pull them into file-local helpers (action_time_for, first_reachable_tw, apply_break_earliest and its existing backward twin apply_break_latest) and call those from fwd_update_earliest_from, bwd_update_latest_from, update_last_latest_date, fwd_update_action_time_from, order_choice and replace. No behavior change: output is byte-identical to master on 116 benchmark instances, with two exceptions on lc106 where both binaries produce both outputs, which is the solver's existing run-to-run nondeterminism rather than a difference between them.
format_route emits the breaks scheduled before a stop in two places, once between jobs and once before the route end, and the two blocks are the same 77 lines. Anything that changes how a break is timed has to be changed in both. Pull them into a file-local lambda taking the rank to emit before. No behavior change: output is byte-identical to master on the benchmark set.
Some shipments carry a bound on how long the load may stay on the vehicle: a contractual delivery time measured from collection, perishable goods, or time on board for passenger transport. Add an optional max_transit_time key on the shipment object, in seconds, carried on both halves of the pair and const after input setup. Presence of the field forces the VRPTW solver path, as time windows do: enforcement needs the scheduling state that only TWRoute keeps, so a constrained input must never solve on the CVRP path. Input::has_max_transit_time() lets later work skip every code path this feature adds when no shipment carries a cap. add_shipment rejects a pair whose two halves disagree on the value, which the JSON parser cannot produce (it reads one value per shipment) but the library API can. No enforcement yet: the field parses, validates and is visible to the solver, and output is unchanged.
Plan mode already checks user-supplied routes against every other hard constraint and reports what they break. Give max_transit_time the same treatment: record each constrained pickup's departure while walking the route, and at the matching delivery report the amount by which the elapsed time exceeds the cap. Transit time is measured from the end of the pickup action to the start of delivery service, so waiting, delivery-side setup and any break taken while the load is on board all count. A delivery ordered before its pickup already carries a PRECEDENCE violation, so no transit time is reported for it. The excess is aggregated per route and per summary like lead_time and delay, and surfaces as a violation with cause "max_transit_time" and the excess as its duration. Solve mode is untouched. This makes existing plans auditable against a cap before anything enforces one.
Two sound rejection screens, both of which can only turn down a shipment or a move that no schedule could have served anyway. At input setup, a shipment whose minimum conceivable transit time already exceeds its cap is marked incompatible with every vehicle and surfaces as unassigned, exactly like a skill mismatch. That minimum is the larger of a travel bound and the wait its own time windows force, measured from the latest achievable pickup departure to the start of the delivery's first window. The travel bound holds for arbitrary, including non-metric, matrices: any routing from pickup to delivery either takes the direct leg or leaves the pickup on some edge and enters the delivery on some edge, so the smaller of the direct leg and the cheapest such pair bounds it from below. The edge scan only runs when the direct leg alone would reject. Inside route validity, the forward simulation now records the stops and breaks it walks, and a candidate-path lower bound sums travel and intermediate action time between a pickup and its delivery along the actual candidate sequence. That sum is a floor on every schedule of that sequence, so exceeding the cap proves the move infeasible. Both screens are one-sided: they reject only what they can prove. A move that survives them may still break a cap, and nothing here accepts a move as compliant. Exact enforcement follows. Routes carrying no constrained pickup skip all of it through a counter maintained by replace(), and inputs with no cap anywhere keep the existing code path exactly, byte-identical on the benchmark set.
The two screens in place reject only what they can prove; neither can accept a move as compliant. This makes enforcement exact, so every route the search commits to is backed by a schedule meeting every cap. Two things decide a candidate the screens leave open. The forward simulation already produces an as-soon-as-possible schedule. If that schedule meets every cap on the route, it is a witness: a concrete compliant schedule, so the move is feasible and no further work is needed. Every pair is checked, including pairs wholly inside the unmodified prefix, because a committed route can be compliant only through a delayed schedule, and with multiple time windows that delay can overshoot into a later window and push every stop after it. A pair whose delivery falls in the unmodified suffix has no exact ASAP time, so it disqualifies the witness rather than being assumed compliant. The witness may only accept. Everything else goes to the scheduling engine. Given a fixed stop sequence it looks for a schedule satisfying every time window, break and cap. When an ASAP schedule breaks a cap, the excess is usually waiting time sitting after the pickup with the load on board, which could equally sit before it. The engine shifts pickups later within the room their own windows and the stops after them allow, re-simulates, and re-checks, until a compliant schedule appears or none can. Slack analysis only proposes a shift; a full re-simulation decides. Delays carry across time-window gaps. The engine decides against the committed break layout: breaks keep the positions the search gave them, so a delay needing a different break placement is a conservative rejection rather than a relayout. Acceptance is always backed by a schedule re-verified from scratch against raw input, so it cannot accept unsoundly; rejection is conservative only when the hot-path iteration budget of 2P+2 runs out for P constrained pairs. Termination does not rest on that bound, since service starts only move later and are capped by their windows. Output timestamps for constrained routes are the engine's schedule rather than raw ASAP times, which places waiting before pickups where that meets a cap. Break service times come from the engine too, so output timing has a single authority. Realization runs once per route off the hot path and retries without an iteration bound if the hot-path budget was exhausted. Solve mode never emits a cap-violating route and never reports violations. All candidate evaluation works on stack and thread-local scratch; no mutable state is added to shared Job objects.
Plan mode already reports transit-time violations, but its ETA choice is blind to them: given freedom in when to start service, it can pick times that break a cap where other times in the same feasible set would not. Give the existing MIP a soft term per constrained pair: an excess variable bounded below by zero, one row tying it to the gap between the delivery start and the pickup departure, and the same makespan weighting the delay terms already use. The excess sum is pinned in the second phase like the other violation sums. The action time in the row uses the same formula the solver enforces, so a schedule produced by solve mode is a zero-excess point of this model, and whatever excess remains is reported as the violation duration. The model is untouched when no shipment carries a cap: with the field absent, plan-mode output is byte-identical to before. With P constrained pairs on a route it gains P columns and P+1 rows of three nonzeros.
drogers0
force-pushed
the
feature/max-transit-time
branch
from
August 24, 2026 17:30
11b40b6 to
d22a237
Compare
Collaborator
|
Thanks for opening a PR! As this is a huge change we'll have to dig deep into the proposed approach in order to provide feedback. |
Author
I understand, I did my best to make it traceable but it is still a monolith. Thanks for taking a look! |
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.
Hi @jcoupey — this is another attempt at #703, so I want to be upfront that I've read why the previous ones didn't work and tried to build around those objections rather than past them. Happy to be told it's still the wrong shape. I put detailed design notes and performance findings in the collapsed review note to try and make the review easier for you.
Some shipments carry a hard bound on how long the load may stay on the vehicle:
The requests behind this span several domains: a contractual delivery time measured from collection (#1241), perishable and temperature-sensitive goods (#870), cargo with a usable lifetime (#1275), and passenger transport, where the bound is time on board (#703, #1330, #1339). What they share is a limit on elapsed time between the two ends of one shipment, rather than on when either end happens.
Transit time runs from the end of the pickup action to the start of delivery service, so travel, waiting, delivery-side setup and any break taken while the load is on the vehicle all count.
The bound couples the times of two stops on the same route, which absolute per-step time windows cannot express. The usual workaround shaves the delivery window down to
pickup_early + max_transit_time. That never breaks the bound, but it assumes the worst case: whenever the pickup actually happens later thanpickup_early, the shipment gets less transit time than it is entitled to, and servable requests become unassignable. On identical instances the shave serves 886 of 1532 shipments; exact enforcement serves all 1532, both fully cap-compliant. That gap is the reason for the feature.A transit bound would turn into a backward arc if a delivery's latest date were allowed to constrain its pickup's latest date, which is the cycle #703 describes. Nothing here propagates the constraint backward, so the forward and backward margin passes keep the shape they have today. Enforcement is layered instead: each candidate move pays the cheapest test that can decide it, and an exact scheduler settles only what the cheap tests cannot. The cap stays a hard validity constraint rather than a cost, so operator gain evaluation is untouched, there are no penalty magnitudes to tune, and solve mode never reports violations.
1,686 insertions and 299 deletions across 18 files, 72% of the insertions in
tw_route.{h,cpp}.Commits
Each builds on its own and was verified before the next was applied.
lc106, which produces several different outputs across repeated runs of the unchanged binary toolc106caveat)-creports the excess at the delivery step and in the summary-creports no transit violationsCommits 1, 2 and 4 are individually useful. The first two are behavior-neutral refactors of existing code, both byte-identical against master, and are meant to be read apart from the feature. Plan-mode reporting makes existing plans auditable against a cap before anything enforces one. Commit 5 adds only sound rejection: it turns down shipments and moves no schedule could have served, and accepts nothing. Commit 6 is where exactness and its cost live.
Commit 6 carries output realization rather than deferring it, since splitting them would leave a tree whose search enforces caps but whose printed timestamps can still show a violation for a route that is in fact feasible.
Happy to split this into separate pull requests if that reads better.
What it costs
max_transit_timeanywhere keep the existing code path and produce byte-identical output.What it does not do
Design note:
max_transit_timefor shipmentsBaseline: upstream
master@07be776fProblem
Some shipments carry a hard bound on how long the load may stay on the
vehicle:
The requests behind this span several domains: a contractual delivery time
measured from collection (#1241), perishable and temperature-sensitive
goods (#870), cargo with a usable lifetime (#1275), and passenger
transport, where the bound is time on board (#703, #1330, #1339). What they
share is a limit on elapsed time between the two ends of one shipment,
rather than on when either end happens.
This couples the times of two steps of the same shipment. VROOM's time
windows are absolute per-step constraints, so the bound is not expressible
through them. The standard workaround is to shave the delivery window down to
pickup_early + max_transit_time, assuming the worst case (earliest possiblepickup). That never violates the bound, but it is lossy: whenever the pickup
actually happens later than
pickup_early, the shipment is granted lesstransit time than it is entitled to, which can make legally servable requests
unassignable.
Prior art
time_gapfeature request. Maintainer's objection: exact enforcement adds a backward
arc (delivery latest constrains pickup latest) to the scheduling dependency
graph, creating a cycle; the linear forward/backward margin propagation that
makes validity checks cheap no longer suffices.
requester's soft-cost alternative, a per-hour shipment cost starting after a
free gap, closed as not actionable. Maintainer's objection: moving the idea
from validity to cost evaluation does not help, because the reason the gap
cannot be asserted also prevents measuring the penalty; only margins are
stored during search and actual ETAs are decided when formatting the
solution.
max_shipment_timerequest, closed as duplicate of Relative time window for shipments #703.max_lifetimeattempt, closed: soft penalty with ad-hoc magnitudes and a
mutablefield onshared
Jobstate (data race under multithreaded solving).presentation for passenger transport. Maintainer suggestion: schedule
pickups as late as possible to compress on-board dwell. Solve-mode output
realization below implements that policy for constrained routes.
max_onboard_timerequest, redirected to Relative time window for shipments #703.Semantics
max_transit_time(integer seconds,>= 0) on ashipment. The measure iswall-clock:
The clock starts when the pickup action ends (setup plus service, where
setup is skipped when the vehicle is already at that location) and stops when
delivery service begins. Everything in between counts: travel, waiting,
delivery-side setup, and any break the vehicle takes while the cargo is on
board. Enforcement, output
timestamps, plan-mode ETA selection and violation reporting all use this one
definition.
Design: layered exact enforcement
A transit bound turns into a backward arc as soon as a delivery's latest
date is allowed to constrain its pickup's latest date, which is the cycle
#703 describes. Nothing here propagates the constraint backward through
route margins, so the forward and backward passes keep the shape they have
today. Enforcement is layered instead: each candidate move pays the
cheapest test that can decide it, and an exact scheduler settles what the
cheap tests cannot. Every move the search accepts is backed by a schedule
meeting every cap. Plan mode reports violations on user-supplied routes,
like
lead_time/delay.The cap stays a hard validity constraint rather than a cost, which is where
the soft-penalty attempts in #870 and #1275 ran into trouble. Operator gain
evaluation is untouched: no penalty magnitudes to tune, no unit clash with
vehicle.costs, no violations in solve mode, and none of theoperator-gain surface catalogued in
#1266 to modify.
Green nodes with heavy borders are added by this feature; every other color
marks the pipeline stage of existing VROOM machinery (blue input, amber
solve, violet output, rose plan mode; teal/red for the accept and reject
outcomes).
flowchart TD subgraph SG_IN["Input setup"] parse["parse + validate input"] --> pre{"shipment can never meet its cap?"} pre -- "yes" --> unas["mark unassignable, report like skill incompatibility"] pre -- "no" --> disp["dispatch to VRPTW solver"] end subgraph SG_SOLVE["Solve: construction + local search"] heur["construction heuristics build initial routes"] --> ls["local search operators propose candidate moves"] ls --> tw["TW / load / break feasibility simulation"] tw -- "infeasible" --> reject["reject move"] tw -- "feasible" --> gate{"route carries a constrained pickup?"} gate -- "no" --> accept["accept move"] gate -- "yes" --> lb{"path lower bound exceeds a cap?"} lb -- "yes" --> reject lb -- "no" --> wit{"ASAP schedule meets every cap?"} wit -- "yes" --> accept wit -- "no" --> eng{"exact engine finds a compliant schedule?"} eng -- "yes" --> accept eng -- "no" --> reject accept --> commit["replace(): update committed route and cached margins"] commit -. "repeat until no improving move" .-> ls end subgraph SG_OUT["Output"] sol["best solution"] --> real["realize cap-compliant timestamps"] real --> fmt["format_route: emit solution"] end subgraph SG_PLAN["Plan mode (-c)"] mip["choose_ETA MIP selects service times"] --> exc["soft transit-excess terms"] exc --> viol["report max_transit_time violations"] end disp --> heur ls -- "search exhausted" --> sol disp -.-> mip classDef added fill:#c9e5a5,stroke:#3f7d1c,color:#1d330b,stroke-width:2.5px; classDef inStage fill:#dbe8fa,stroke:#4a7ab5,color:#16324f; classDef solveStage fill:#fdf2d0,stroke:#b8933a,color:#4a3a10; classDef outStage fill:#e8def2,stroke:#8a63ad,color:#32204a; classDef planStage fill:#fadde3,stroke:#b55a72,color:#4a1e2a; classDef acceptNode fill:#d2ede4,stroke:#3d8a70,color:#123528; classDef rejectNode fill:#f5d9d9,stroke:#a84848,color:#3d1414; class pre,unas,gate,lb,wit,eng,real,exc,viol added; class parse,disp inStage; class heur,ls,tw,commit solveStage; class sol,fmt outStage; class mip planStage; class accept acceptNode; class reject rejectNode; style SG_IN fill:#f7fafe,stroke:#a8c4e4; style SG_SOLVE fill:#fffdf4,stroke:#d9c58a; style SG_OUT fill:#faf7fd,stroke:#c3aed6; style SG_PLAN fill:#fdf6f8,stroke:#d9a8b5;Input-time rejection
A shipment whose minimum conceivable transit time already exceeds its cap
can never be served. That minimum is the larger of a travel bound and the
wait forced by its own time windows, measured from the latest achievable
pickup departure (end of the pickup's last window plus the largest
setup-plus-service among compatible vehicles) to the start of the delivery's
first window. The travel bound stays valid for arbitrary, including
non-metric, matrices: it is the smaller of the direct pickup-to-delivery leg
and the cheapest edge out of the pickup plus the cheapest edge into the
delivery, minimized over vehicle profiles. Such
shipments are marked incompatible with every vehicle during input setup and
surface immediately as unassigned, exactly like skill incompatibility. Both
bound terms deliberately underestimate, so no servable shipment is rejected.
Presence of
max_transit_timeforces the VRPTW solver path (like time windowsdo): enforcement machinery lives in
TWRoute, and constrained inputs mustnever solve on the CVRP path.
Cheap screen inside route validity
TWRoute::is_valid_addition_for_tw, the choke point for constructionheuristics and local-search operators, evaluates constrained pairs on the
candidate route:
intermediate action time along the actual candidate sequence between pickup
and delivery lower-bounds every schedule's transit time, for arbitrary
(including non-metric) duration matrices. Exceeding the cap proves the move
infeasible.
a concrete as-soon-as-possible schedule; if that schedule meets every cap
on the route, it is a proof by example that the move is feasible and no
further work is needed. Every pair is checked, including pairs entirely in
the unmodified prefix: the committed route may be compliant only through
a delayed schedule, and with multiple time windows that delay can
overshoot into a later window and push every downstream stop. Pairs whose
delivery lies in the unmodified suffix have no exact ASAP time, so they
disqualify the witness. This check may only accept, never reject.
This choke point covers the whole search: every construction heuristic and
every local-search operator, SWAP* included, validates candidate routes
through
is_valid_addition_for_tw(SWAP* additionally exchanges singlejobs only, so it can never separate a pickup from its delivery). Removals
go through the same choke point (
is_valid_removalis an empty-rangeaddition), so they pay the same screens and engine; removing a stop can
invalidate a route on non-metric matrices, which is why that check already
exists upstream.
Route-level gating keeps all of this off routes that carry no constrained
pickup (a counter maintained in
replace(), mirroring the existingper-vehicle capability gating), and a single
Input::has_max_transit_time()flag keeps unconstrained inputs on the exact master code path: their
behavior and performance are unchanged.
Exact scheduling engine
Candidates the screen cannot decide go to the engine
(
compute_cap_compliant_schedule): given a fixed stop sequence, it searchesfor a full schedule satisfying every time window, break and cap. Its
contract is asymmetric by design: acceptance is always backed by a concrete
schedule re-verified from scratch against raw input, so it can never accept
unsoundly; rejection is conservative only when the hot-path iteration
budget runs out, quantified below.
The engine decides against the committed break layout: breaks keep the
positions the search assigned them, and a delay that would need a different
break placement to stay feasible is a conservative rejection, not a
relayout. Exactness below is therefore stated relative to that layout. How
much this loses is not measured: the break-bearing stress set shows zero cap
violations, which is a soundness result and says nothing about how many
servable shipments a relayout would recover.
When evaluating a candidate move, the engine follows the codebase's
incremental idiom: it seeds its forward simulation from the committed route's
cached schedule arrays at the edit boundary instead of re-simulating the
unchanged prefix, exactly as
fwd_update_earliest_fromdoes. Any schedule itreturns is still re-verified from scratch against raw input, so seeding
cannot introduce false acceptances.
The core idea: when an as-soon-as-possible schedule breaks a cap, the excess
is usually waiting time sitting after the pickup, with the cargo on
board, that could equally sit before it. The engine shifts pickups later
within the room their own windows and every downstream stop allow (their
"forward slack", the classic DARP scheduling device), then re-simulates the
whole route and re-checks every cap, repeating until a compliant schedule
appears or none can. Slack analysis only ever proposes a shift; the
decision is always a full re-simulation. When a delay pushes a pickup past
its current time window, the engine advances to the next window and starts
at the later of the window start and the required delay, so the computed
delay is carried across window gaps.
On the hot path iterations are bounded at 2P+2 for P constrained pairs.
Termination does not depend on the bound (service starts only ever move
later and are capped by their time windows); the bound turns eventual
convergence into a linear-in-P cost guarantee, sized so each pair can be
delayed once and revisited once after another pair's delay propagates
through it. Budget exhaustion is a conservative rejection, measured at
0.007% of 95 million engine calls (see the complexity section).
Each delay is the smallest one that can remove the violation, and a later
start never makes any other start earlier, so the trajectory is the minimal
one: the engine is exact up to the iteration budget. To check that, the engine is
compared against a brute-force search that enumerates every schedule a
route admits. Over 32500 randomized routes carrying two to five
constrained pairs with multiple time windows (14058 of them feasible on
windows alone), calling the engine from scratch on each, there is no
schedule the brute-force search found that the engine missed, and none the
engine accepted that breaks a cap. A second comparison drives the full
validity entry point (
is_valid_addition_for_tw) with random committedroutes and random insertions: over 1369 insertions every accept and every
reject matched the brute-force answer, so the entry point turned away
nothing it could have served. Two
configurations are pinned as regression tests: one that requires carrying
the delay across a window gap, and one where a prefix pair's window-gap
overshoot makes an insertion infeasible that the ASAP schedule alone would
accept.
The brute-force comparisons use break-free routes. Breaks are still
verified on every accepted schedule, so what's untested is over-rejection,
not correctness.
Output realization and plan mode
Solve-mode timestamps for constrained routes are the engine's compliant
schedule rather than raw ASAP times, which moves waiting from after a
pickup to before it wherever that is what meets a cap.
It applies to the whole route: a stop with no cap that shares a vehicle
with a constrained pair follows the compliant schedule's timing too, which
can place waiting earlier than the backward-minimized policy used on
unconstrained routes. Break service times come from the engine as well, so
output timing has a single authority rather than a second placement rule to
keep in step with the certified schedule.
This is narrower than the "schedule pickups as late as possible" policy
suggested in #1330. The engine delays a pickup by exactly the amount that
clears its cap and then stops, so on-board time is compressed only for a
pair that a cap would otherwise catch. A pair whose ASAP schedule already
fits its cap is left where it is, and a shipment with no cap is never
affected. Minimizing on-board time in general is a separate change.
Every accepted move was proven by the witness (the ASAP schedule itself, which
is the engine's starting point) or by the engine, so a compliant schedule
exists for every committed route. Realization runs once per route off the
hot path: if the hot-path budget is exhausted it reruns the fixpoint with
no iteration bound, which terminates because service starts are monotone
and bounded by window ends. A failure past that point would be an internal
invariant breach and throws: solve mode never emits a cap-violating route
and never reports violations — those are plan mode's job on user-supplied
routes. No test or benchmark set has reached that path.
Plan mode (
-c) biases ETA selection with soft transit-time excess terms inthe existing MIP. The LP is untouched when no shipment carries a cap; with P
constrained pairs on a route it gains P excess variables and P+1 rows of
three nonzeros each, weighted at the same makespan scale as the existing
delay terms. The excess uses the identical action-time formula as the
engine, so a solve-mode schedule is a zero-excess point of this MIP;
residual excess is reported as a
max_transit_timeviolation with itsduration, aggregated like
lead_time/delay. Feeding every solve-moderoute from the single- and multi-window benchmark sets back through plan
mode reports zero
max_transit_timeviolations. The soft terms do not moveLP solve time measurably (capped vs caps-stripped plan inputs: within 2% at
100 tasks, within 1% at 400 tasks, about a millisecond in absolute terms),
and with the field absent plan-mode output is byte-identical to upstream
master.
All candidate evaluation works on stack and thread-local scratch state; no
mutable state is ever added to shared
Jobobjects, and solving is safe atany thread count.
Performance characteristics
Li&Lim PDPTW benchmarks (single time window per stop unless noted), caps
synthesized time-window-aware so instances are cap-feasible by
construction. Timing is single-threaded for run-to-run determinism, except
the default-thread pass noted below; matched builds, identical flags. Every
measured claim below comes from a script kept with the test assets rather
than from a hand-run: the instance generators, a two-binary output
comparator that reruns each mismatch and prints the set of output hashes
per binary, so run-to-run nondeterminism cannot be mistaken for a
behavioral difference, the plan-mode timing and parity harness, and the
brute-force comparisons described above:
with no measurable overhead, at every tested size (100–1000 tasks).
higher routing cost than the same instance solved with the caps removed:
median 1.34x, range 1.02x to 1.75x across 29 instances. That is the price
of the constraint itself, not of the machinery enforcing it.
workloads (20% of shipments capped, a realistic mixed shape);
fully capped sets pay more, geometric mean ~1.95x (1.3–3.9x per instance):
once a route's committed pairs comply only through delayed pickups, the
ASAP witness can no longer vouch for it and every candidate on that route
goes to the engine. A witness built on the committed compliant schedule
rather than ASAP would recover much of this; it is not implemented. At
the default thread count the difference is not resolvable at these
instance sizes: capped median 336ms vs 384ms uncapped across the
fully-capped set, within the run-to-run spread the solver already shows at
this scale. Zero violations across repeated runs.
shaving serves 886 of 1532 shipments (58%), exact enforcement serves all
1532, both fully cap-compliant. Against the other workaround, solving
without caps and dropping every shipment whose transit exceeds its cap,
872 of 1532 survive. This gap is the reason for the feature.
split into two disjoint windows with a gap, every third delivery given a
second later window): 1529 of 1532 served, zero cap violations.
minimum transit, nonzero setup times, mandatory mid-shift and end-of-shift
breaks on every vehicle): zero cap violations and zero break-window
violations on every instance. Solve time is 0.4–1.6x the same
instances without caps (geometric mean ~0.9x), but the two runs do not do
equal work: the capped run leaves 654 shipments unassigned, so this is not
a like-for-like overhead measurement. Each of those 654 is also unassigned
when solved alone on a dedicated vehicle from the same instance. That is a
self-consistency check rather than an independent infeasibility proof,
since the same engine answers both times.
Worst-case complexity per validity check
For a candidate route of length L with P constrained pairs, B breaks and at
most W time windows per stop (stock validity simulation: O((L+B)W)):
field pay nothing anywhere.
spanning the edit, from one O(L) path walk per spanning pair. S is bounded
by the vehicle's simultaneous on-board shipment count, worst case O(L).
per constrained pickup whose delivery is not in the trace (the lookup that
separates "delivery in the suffix", which needs the engine, from "delivery
absent from the candidate", which constrains nothing): O(S·L) worst case,
same S as above.
runs at most 2P+2 iterations, each an O(P) worst-pair scan plus one
O((L+B)W) re-simulation; the pair list build adds O(P·L); prefix seeding
reduces the simulated span but not the class.
So the worst case per validity check is O(P·(L+B)·W), a factor P over the
stock check, reaching O(L²) when every second stop is a constrained pickup.
A synthetic scaling harness confirms the classes empirically (log-log slopes
over route doublings up to L=1024): engine-saturated checks scale at slope
~2 (quadratic, ~2ms per check at L=1024), screen-plus-one-engine-pass checks
at slope ~2, witness-decided checks at slope ~1 (linear, ~3µs at L=1024).
At the adversarial corner the per-check cost compounds with the number of
candidates: a single vehicle, so nothing bounds route length, and every
second stop a constrained pickup, so P grows with the route. Solving the
same instances with the caps removed separates what the caps cost from what
the shape costs.
This is the deliberate worst case, not a realistic instance (one
vehicle, every second stop a constrained pickup, caps at 1.5x the minimum
leg):
Single runs, single-threaded. Both columns grow steeply: a forced single
route is a quadratic move neighborhood for the stock solver too. The ratio
between them roughly doubles as the route doubles, which is the one extra
factor of P the per-check bound predicts, turning up in wall-clock time.
Real workloads sit far from this corner: a fleet bounds route length, P per
route is small, and the cheap screens decide most candidates. The benchmark
sections above reflect that gap. Instrumented across 95 million engine calls (fully-capped,
mixed and adversarial benchmark sets), the fixpoint converged in 0 or 1
iterations for 93% of calls, never used more than 11, and exhausted its
2P+2 budget in 0.007% of calls overall (0% on the mixed set), each
exhaustion resolving to a conservative rejection.
Code footprint
1,686 insertions and 299 deletions across 18 files. 72% of the insertions
are in
tw_route.{h,cpp}, whose main file grows from 1,512 to 2,525lines; the rest is spread thinly, no other file gaining more than 171
lines.
The change is not confined to new code. The engine's forward pass and the
stock one need the same three scheduling rules — setup suppression, forward
time-window selection, and the break step with its wait absorbed into
remaining leg travel — so those rules are extracted into file-local
primitives that
fwd_update_earliest_fromandbwd_update_latest_fromnowcall. That is a deliberate trade: the existing margin machinery changes
shape, but no second copy of the scheduling semantics exists to drift out of
sync with the machinery the engine certifies. The extraction is
behavior-preserving: output is byte-identical across all four benchmark
sets, 116 instances, apart from lc106. On that instance the unchanged
binary also produces several different outputs across repeated runs, so it
is nondeterministic on its own account, independent of this change.
format_routegets the same treatment for a smaller reason: it emitted thebreaks scheduled before a stop in two places, between jobs and before the
route end, as the same 77 lines. Timing a break now happens in one lambda
rather than two copies. That extraction is byte-identical to master as
well.
On top of those primitives the feature adds one file-local forward simulator
(
cap_asap_forward), the candidate-path screen, one engine implementation(the committed-route entry point is a two-line delegate of the candidate
one) with the realization wrapper, and this state:
Input::has_max_transit_time()andJob::max_transit_time, both constafter input setup and never mutated during solving;
TWRoute::constrained_job_count_, the route-level gating counter,reconciled against route contents by an assert at realization time, off
the hot path, so a mutation path bypassing its bookkeeping cannot disable
enforcement silently, and the private
TraceEventrecord;across threads and never referenced past a call;
Violations::transit_time_excess, trailing and defaulted so existingconstructor calls keep compiling, and
VIOLATION::MAX_TRANSIT_TIME.Every schedule the engine returns is self-verified from scratch against raw
input before acceptance. The brute-force comparisons and the pinned
regression configurations guard future changes to scheduling semantics.