mamut_routing_tools.campaign.design¶
The candidate pool: what to consider generating, and what each one would be.
A campaign that wants measured diversity cannot pick parameters and hope. It
enumerates far more candidates than it needs, works out what each one would
look like without paying for it, and lets :mod:~.select keep the spread.
:class:CandidateSpec is a complete, deterministic recipe for one instance --
everything needed to rebuild it byte for byte later, which is what makes the
"propose" and "generate" phases separable.
:func:evaluate_candidates is the cheap half of generation: it builds the
customer selection and draws the demands exactly as the real generator will,
then stops. No distance matrix is computed, which is the whole point -- the
Dijkstra passes are what cost hours, and they are only spent on winners.
CandidateSpec
dataclass
¶
CandidateSpec(city: str, osm_path: Path, n: int, method: str, demand_type: int, avg_route_size: int, depot_mode: str, customer_mode: str, cluster_seeds: int, cluster_decay_meters: float, hybrid_poi_share: float, poi_attach_mode: str = CAMPAIGN_POI_ATTACH_MODE, categories: tuple[str, ...] = CAMPAIGN_POI_CATEGORIES, distortion_stratum: str | None = None, tier: str = 'main', rung_key: str = '')
A complete, deterministic recipe for one base instance.
EvaluatedCandidate
dataclass
¶
EvaluatedCandidate(spec: CandidateSpec, descriptors: InstanceDescriptors, resolved_n: int, effective_method: str = '', poi_fraction: float = 0.0)
A candidate together with what it would look like if generated.
is_usable
¶
Servable at the requested size, a genuine VRP, and fully described.
The size check is not pedantry. A city whose amenities are sparse serves a POI request short -- Nairobi's extract has 27 amenities in total -- and the generator only warns. Accepting the short instance would put a candidate selected for the 701-1000 bucket into the tree at n=198, which is exactly the kind of silent drift a designed benchmark must not have.
Neither is the route-count check. This gate read lb_cap >= 2 in v2 --
"not a TSP" -- and 22 of the 110 published bases cleared it while still
being trivially partitioned, 19 of them into exactly two routes. Two is
not a fleet. The band admissibility rule in :mod:~.ladder is what makes
the guarantee hold by construction; this is the backstop that catches the
one case it cannot, where Q = ceil(r * mean demand) rounds up far
enough to cost a route at the very edge of a band.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/design.py
stable_seed
¶
A seed derived from what the instance is, never from a counter.
Mirrors family.family._stable_seed: re-running the campaign, or
regenerating one instance on its own, must land on the same draw.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/design.py
enumerate_for_rungs
¶
enumerate_for_rungs(assignments: Sequence[Any], *, osm_dir: str | Path = 'osmdata', per_rung: int = 6, base_seed: int = 0, tier: str = 'main', categories: Sequence[str] = CAMPAIGN_POI_CATEGORIES, poi_attach_mode: str = CAMPAIGN_POI_ATTACH_MODE) -> list[CandidateSpec]
per_rung candidates for each ladder position.
The counterpart of :func:enumerate_candidates for a campaign that has
already decided what each instance is -- city, size and sourcing method
come from a :class:~.ladder.RungAssignment and are not up for negotiation
here. What is still free is what the instance is like: how demand is
distributed, how tightly the vehicles are loaded, where the depot sits and
whether the customers cluster. Those are drawn as balanced columns across
the whole pool exactly as before, so the selector still gets a spread to
choose from on every axis it can still move.
Splitting it this way is what stops the two decisions from fighting. Drawn together, a size the city could not serve was silently downgraded; drawn in order, the size is guaranteed feasible before any descriptor is computed.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/design.py
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | |
enumerate_candidates
¶
enumerate_candidates(cities: Sequence[tuple[str, Path]], profiles: Sequence[CityProfile] = (), *, per_city: int = 8, size_grid: Sequence[int] = MAIN_SIZE_GRID, methods: Sequence[str] = METHODS, size_ceiling: Mapping[str, int] | None = None, tier: str = 'main', base_seed: int = 0) -> list[CandidateSpec]
per_city candidates for every city, balanced on every design axis.
methods narrows the sourcing axis. The POI-only tier passes a single
method because "every customer is a real amenity" is the point of that tier,
not one option among three.
size_ceiling caps n per city. Sizes are drawn from a grid that knows
nothing about how many customers a given city can actually serve, so without
it a POI-only tier proposes n = 5000 in a city holding 1800 amenities and
discovers the shortfall only after paying for the evaluation. The cap
replaces an over-large draw with the largest grid size that still fits, and
drops the candidate when even the smallest does not.
The route-size band is drawn as a balanced column and then repaired against
the size, because which bands leave a real fleet depends on n: a target
of 50-200 customers per route is an ordinary instance at n = 4000 and a
two-route degenerate at n = 272. A draw the size cannot support falls back to
the largest admissible band, which keeps the column's ordering -- longer
draws still get longer routes -- without letting it off the end.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/design.py
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 | |
evaluate_candidate
¶
What this candidate would be, short of its distance matrices.
Rebuilds the customer selection and draws the demands through exactly the
calls generate_single_instance makes, from exactly the same seed, so the
descriptors describe the instance that generation will actually produce.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/design.py
evaluate_candidates
¶
evaluate_candidates(specs: Sequence[CandidateSpec], *, jobs: int = 1, drop_unusable: bool = True, on_progress: Callable[[str, int, int], None] | None = None) -> list[EvaluatedCandidate]
Evaluate a whole pool, one process per city.
drop_unusable=False returns everything that evaluated, letting the
caller apply :meth:EvaluatedCandidate.is_usable itself. That is what a
campaign wants for its saved pool: usability depends on the feature space,
and re-deciding it must not cost another pass over every OSM extract.
Source code in MAMUT-routing-tools/src/mamut_routing_tools/campaign/design.py
resolve_cities
¶
(city_slug, osm_path) for the extracts on disk, slugged like the tree.
Aix-en-Provence.osm becomes aix_en_provence, matching the place slugs
the benchmark tree and the website already use.