mamut_routing_tools.campaign.ladder¶
Which city gets which size, and which sourcing method.
The previous design drew n, method and the city independently and then
discovered, during generation, that the city could not serve the draw. A POI
request the amenities could not fill was quietly completed with sampled road
points and reclassified as hybrid: the instance still reached its size, so
nothing failed, and the published set ended up with a third of the sourcing it
was designed for.
This module inverts that. Sizes come first, as a ladder -- one distinct n
per instance, rising smoothly, the way Uchoa et al.'s X set moves from 101 to
1001 -- and each rung is then matched to a city that can demonstrably supply it.
The matching reads the measured POI capacity from :mod:~.poi_capacity, so a
rung asking for 800 real amenities is only ever given to a city that has them.
Two consequences worth stating, because they are the point:
- A rung that cannot be filled is reported, not silently downgraded. The unfillable list says what each orphan rung needed and what the best remaining city offered, which is a precise specification for how many more extracts to fetch rather than a vague "get some bigger cities".
- Size stops being a factor and becomes a covariate. With one instance per
nyou can regress solver performance on size; with fourteen instances piled onn = 100you can only compare buckets.
What this module does not decide is what the instance is like -- demands,
depot placement, clustering. Those stay with the descriptor-driven selector in
:mod:~.select, which now chooses among candidates that share a pinned
(city, n, method) triple.
RungAssignment
dataclass
¶
RungAssignment(index: int, n: int, method: str, avg_route_size: int, city: str, osm_file: str, distortion_stratum: str | None, required: int, available: int)
One ladder position, and the city that will supply it.
rung_key
property
¶
Stable identifier for quota'ing exactly one instance per rung.
The route-size band belongs in it. The key is what the candidate-pool cache is fingerprinted on, and a plan that differs only in its bands is a different design producing different demands and capacities -- but would hash identically without this, and silently reuse the other one's pool.
UnfilledRung
dataclass
¶
UnfilledRung(index: int, n: int, method: str, required: int, best_available: int, best_city: str | None)
A rung no remaining city could supply, and by how much it fell short.
LadderPlan
dataclass
¶
LadderPlan(assignments: list[RungAssignment] = list(), unfilled: list[UnfilledRung] = list(), method_weights: dict[str, float] = dict(), headroom: float = DEFAULT_HEADROOM, min_routes: int = CAMPAIGN_MIN_ROUTES)
The complete size/method/city assignment, plus what it could not fill.
band_size_corr
property
¶
corr(log n, band) over the plan -- the decorrelation guard's result.
Reported rather than asserted: admissibility puts a hard floor under it (band 6 cannot appear below n = 300), so the honest thing is to publish what the assignment achieved, not to pretend the axes are independent.
guaranteed_min_routes
property
¶
Fewest routes any assigned rung can produce, over every legal draw.
admissible_route_size_bands
¶
Route-size bands that keep an n-customer instance a real VRP.
A band is admissible only if its whole interval fits in
[3, n / min_routes]. Testing the band rather than the realized draw is
the point: r is drawn uniformly inside the band at generation time, so
admitting a band that only partly fits would make soundness depend on the
draw, which is exactly the coin-flip v2 lost. Full containment means the
guarantee holds for every seed.
Band 7 (50-200 customers per route) therefore never appears below n = 1200, which independently reproduces CVRPLIB's own convention: XL introduced that band together with n >= 1000, and it is only this repository that had it reachable from n = 100.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/ladder.py
assign_route_size_bands
¶
assign_route_size_bands(sizes: Sequence[int], rng: Generator, *, min_routes: int = CAMPAIGN_MIN_ROUTES) -> list[int]
One route-size band per rung, spread as evenly as admissibility allows.
The bands available to a rung are a prefix of the vocabulary -- a bigger
instance can carry every band a smaller one can, plus longer ones -- so the
rungs with the fewest choices are the ones that constrain the outcome.
Serving them first, each taking the least-used band it is allowed, is what
lets the scarce long bands land on the large rungs that are the only ones
able to hold them. Ties are broken with rng rather than by index, which
keeps the assignment from correlating band with n any more than
admissibility already forces.
Returns bands in the order of sizes.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/ladder.py
band_size_correlation
¶
Pearson correlation between log n and the assigned band.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/ladder.py
size_ladder
¶
k distinct sizes rising smoothly from n_min to n_max.
Geometrically spaced, so the ladder spends its resolution where instances differ most: the step from 100 to 110 changes a CVRP far more than the step from 900 to 910. Rounding collides at the bottom, where consecutive rungs are less than a customer apart, so a repair pass walks the ladder and pushes each rung at least one past its predecessor. Both endpoints are exact.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/ladder.py
weighted_column
¶
length labels in the requested proportions, shuffled.
Exact rather than sampled: 50/25/25 of 100 is 50, 25 and 25, not "about that". Remainders from the largest-remainder apportionment go to the heaviest weights first, so a 50 % share is never the one left short.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/ladder.py
required_supply
¶
required_supply(n: int, method: str, *, hybrid_share: float, headroom: float, vertex_factor: float) -> tuple[str, int]
(resource, amount) a rung of this size and method demands.
resource is "poi" or "vertices" -- the two are not
interchangeable, and comparing a POI rung against a road-graph size is how
the old design talked itself into instances it could not build.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/ladder.py
assign_rungs
¶
assign_rungs(ladder: Sequence[int], capacities: Iterable[PoiCapacity], profiles: Iterable[CityProfile] = (), *, method_weights: Mapping[str, float] | None = None, hybrid_share: float = 0.5, headroom: float = DEFAULT_HEADROOM, vertex_factor: float = DEFAULT_VERTEX_FACTOR, min_routes: int = CAMPAIGN_MIN_ROUTES, base_seed: int = 0) -> LadderPlan
Match every rung to a city that can demonstrably supply it.
Cheapest sufficient city, hardest rung first. Rungs are processed in decreasing order of what they demand, and each takes the unused city that satisfies it while costing the pool the least amenity capacity.
Amenity capacity is the scarce resource and road vertices are not: every city has plenty of road and only some have plenty of shops. So the cost of spending a city is its POI capacity, whatever the rung happens to need. For a POI or hybrid rung that is ordinary best fit -- the smallest capacity that still clears the bar, which keeps Tokyo available for the rungs that need Tokyo. For a parametric rung, which consumes no amenities at all, it means deliberately spending the amenity-poorest city that has enough road.
That second case is not a refinement, it is the difference between filling the ladder and not. Ranking parametric rungs by road vertices instead makes a city with 563 000 vertices and 57 amenities the last candidate for the only kind of rung it can serve, so it is never used -- and the ladder then runs out of cities while five amenity-dead extracts sit unspent.
Ties among equally sufficient cities break toward the distortion stratum that is currently most under-represented, so the road-network diversity the study depends on survives a matching that is otherwise driven entirely by amenity counts.
One instance per city: a city is removed from the pool once used.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/ladder.py
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 | |
plan_summary
¶
Counts a human can scan to see whether the design came out as asked.