DOCS Extensions

P-step CVRP and VRPTW extension

Documentation index

baldes::extensions::pstep implements the compact continuous relaxation in Proposition 1 of A p-step Formulation for the Capacitated Vehicle Routing Problem, Dollevoet, Munari and Spliet.

The extension is available without Cargo features. Solving requires an implementation of Baldes's LpMaster, such as Clp (--features clp).

Terminal
cargo build --release --features clp --bin baldes
./target/release/baldes pstep instances/cvrplib/P-n16-k8.vrp --p 2 --threads 4
./target/release/baldes pstep instances/cvrplib/P-n16-k8.vrp --p 3 --threads 4 --enumerate --json pstep.json

Use baldes pstep --help for time, iteration and label limits. --enumerate checks the bound against the complete column pool; use it on small cases. The JSON separates a converged full-model lp_bound from a potentially nonconverged restricted_objective. A work-limit error cannot certify a bound. Neither output is an integer routing solution. The fleet is unlimited even if the CVRPLIB instance name or header specifies a conventional truck count.

Rust callers can use Problem::from_instance on a parsed inputs::Instance. It preserves the input's distance rounding. Then call Problem::solve_column_generation with an empty LpMaster and ColumnGenerationOptions; pricing.p and pricing.threads control the step size and CPU parallelism; pricing.method selects Auto (default), Enumeration, or Bucket. The older examples/pstep.rs illustrates full-pool construction, and examples/pstep_cvrp.rs produces CSV benchmark records.

Create a Problem with customer demands, vehicle capacity and a directed cost matrix. Matrix indices are start depot 0, customers 1..=n, end depot n+1. For a conventional single-depot instance, copy depot-to-customer costs into row 0 and customer-to-depot costs into column n+1. None means no arc. Customer demands must be strictly positive; capacity and supplied costs must be finite, with nonnegative costs. The model has an unlimited homogeneous fleet.

Problem::enumerate builds the complete column pool for a supplied positive p. Every partial path is elementary and capacity feasible. Paths have exactly p arcs, except those starting at the depot, which may be shorter. Empty depot routes are omitted. At large p, the pool contains full routes and depot prefixes; flow conservation forces incomplete prefixes to zero.

The compact prior-demand choices are zero for depot starts, maximum feasible prior demand for depot ends, and both extremes for internal paths. Coincident extremes are stored once. A junction customer's demand belongs to both adjacent paths; the load coefficients account for this overlap.

Formulation::entries exposes sparse columns in this row order:

Rows Coefficient Sense / RHS
0..n Customer degree (1 at endpoints, 2 internally) = 2
n..2n Start +1, end -1 = 0
2n..3n Start d + q_i, end -(d + q(path)) >= 0

Formulation::into_master populates an empty LP backend with nonnegative continuous columns. Solve through the backend's status and time-limit API. An optimal solve gives the full p-step LP bound to solver tolerance. At p=1 this has the single-commodity flow bound; at p>=n+1 it has the set-partitioning bound. Bounds need not increase at every successive p; the paper guarantees nondecrease when p is replaced by an integer multiple.

The reference enumerate implementation has combinatorial cost. Both column count and search work are bounded by EnumerationOptions. Hitting a limit returns an error instead of returning a restricted pool as a complete model. It is intended as an exact column-pool reference for small instances and small p.

price_steps solves independent jobs for each start, end and prior-demand extreme. The default Auto method uses streaming elementary enumeration when p<=6 and the dense prefix estimate 1+n+...+n^(p-1) is at most 100,000; otherwise it uses the core bucket-graph labeling engine. This is a bounded size heuristic, not a prediction that one backend always wins. Capacity and missing arcs prune enumeration further. Enumeration retains just the best path per job, so it does not build the full column pool. Both methods solve the same exact pricing problem and enforce work/time limits.

Use --pricing enumeration or --pricing bucket to compare a specific case; --enumerate remains the independent full-pool LP cross-check. --labels limits generated labels for bucket pricing and attempted extensions for streaming enumeration. JSON reports these separately as generated_labels and enumerated_extensions, and records the requested and selected methods. Exceeding either budget aborts pricing without certifying convergence.

For bucket pricing, hop count is an equality resource, preventing artificial waiting from creating extra arcs. Capacity is disposable: every load window has lower bound zero, so waiting never helps and a cheaper label using less capacity safely dominates. Reduced costs use actual customer demands, not any resource waiting. Customer starts require exactly p arcs; depot starts allow 1..p. The fixed endpoints and extreme make the reduced cost additive along arcs, including degree, endpoint and load duals. Each returned path is replayed against the master coefficients.

Workers share immutable instance costs and duals. Each owns its search storage and results. Results are merged in fixed job order and all threads join before the main thread updates the LP. threads=1 runs the same pricing algorithm, making serial/parallel results reproducible. Parallel overhead may outweigh the benefit on very small cases.

The solver starts with degree artificials, uses exact pricing to complete phase I, and then switches to travel costs. Convergence requires every pricing job to finish without a negative reduced-cost step at the requested tolerance. Steps are deduplicated by node order and prior demand. LP columns are added in batches. Restricted-master objectives are not full-model lower bounds until pricing converges. Reported bounds are numerical, not independently certified.

For p<=3 the path topology itself enforces elementarity. For p=4, a relaxed bucket solve is sufficient when its minimum is elementary. Otherwise, pricing partitions by the first arc and forbids later returns to its head. Each resulting four-arc path is elementary; every feasible elementary step belongs to one partition. The relaxed minimum also bounds these partitions. Longer steps use exact cycle exclusion around bucket solves: a cyclic relaxed minimum branches on forbidding each arc of a detected cycle. Every elementary path lies in at least one child; relaxed costs bound each child. This avoids the core's 15-element local NG mask limit. It can still require exponential work for larger p. PricingOptions bounds total labels and bucket subproblems per endpoint job, as well as worker count and time. Exceeding a pricing limit returns an error.

Time windows, cuts, integer solving and integration with Baldes's branch-and-price tree are not implemented here. For integer optimization of this compact model, impose binary aggregate arc flows, as in equations (7)–(9); making extreme p-step columns binary is not equivalent. The exposed paths and coefficients provide the data needed for a separate arc-flow integrality implementation.

Run node scripts/benchmarks/benchmark_pstep.mjs ./target/release/baldes from the repository root to repeat the known-instance validation and serial/parallel timing checks. It writes raw JSON and CSV records under results/pstep/ (override with PSTEP_OUT) and compares every bound with full column enumeration.

Add --grid to run p=2,3,4 on all three bundled CVRPLIB instances, with four workers, 120 seconds per solve and five million labels per endpoint job. Grid records go under results/pstep-20261020/p234/. Reference enumeration is omitted for the larger p=4 pools and A-n32-k5 at p=3, whose pool exceeds the CLI's column limit. Pricing stops are recorded explicitly, without a claimed LP bound.

The validation runs gave these numerical bounds (four workers, development profile). The normal CVRP column uses default root NG-route pricing; the integer values were obtained separately with the normal tree solver.

Instance p=2 LP p=3 LP p=4 LP Normal CVRP root LP Integer optimum
P-n16-k8 408.968169 434.947541 441.000000 441.000000 450
E-n22-k4 336.229510 344.079301 351.662163 373.500000 375
A-n32-k5 672.464631 678.388776 Time limit 757.983333 784

The A-n32-k5 p=4 run stopped at 120 seconds without a full LP bound. The other eight cases converged; six also matched complete-column enumeration within 1e-5. The A-n32-k5 integer solve used capacity cuts, rank-one cuts and a one-second HGS warm start before closing the gap at 784.

Compare backends with node scripts/benchmarks/benchmark_pstep_pricing.mjs. It alternates backend order over three repetitions, checks converged bounds agree, and writes raw JSON and CSV to results/pstep-pricing/ (or the second positional argument). Set PSTEP_RUNS, PSTEP_SECONDS, or PSTEP_THREADS to change repetitions, per-solve limits, or worker count. An optional binary path and output directory are accepted.

The 2026-10-21 release comparison used an AMD Ryzen 9 9950X, four workers, three repetitions with alternating backend order, no warmup runs, and five million labels/extensions per job. The binary was built with cargo build --release --features clp --bin baldes (opt-level 3, thin LTO). These are end-to-end solve medians, including the LP; they are local timings, not a guarantee for larger or different instances.

Instance p Bucket seconds Streaming enumeration seconds
P-n16-k8 2 0.0262 0.0033
P-n16-k8 3 0.1658 0.0044
P-n16-k8 4 0.5595 0.0089
E-n22-k4 2 0.1812 0.0372
E-n22-k4 3 1.7307 0.0658
E-n22-k4 4 18.36 0.32
A-n32-k5 2 0.6490 0.1771
A-n32-k5 3 10.7781 0.3635
A-n32-k5 4 Time limit 2.47

The initial run used a 15-second limit; the extended run allowed 20 seconds. E-n22 p=4 needed the extended limit. Converged backend bounds agreed within 1e-5. Enumeration also converged for A-n32 p=4 at 691.463021739; this is a numerical p-step LP bound. The larger p=4 full-pool model was not independently solved for that instance. Raw records, binary/input hashes, and compiler metadata are under results/pstep-pricing-20261021/final/ and verified/.

Experimental CUDA pricing

Build with an NVIDIA CUDA toolkit (nvcc on PATH) and a working CUDA driver:

Terminal
cargo build --release --features clp,cuda --bin baldes
./target/release/baldes pstep instances/cvrplib/P-n16-k8.vrp --p 3 --gpu --rounding none
cargo test --release --features cuda --lib extensions::pstep
cargo run --release --features cuda --example pstep_gpu_bench
python3 scripts/benchmarks/benchmark_pstep_cuda.py

--gpu is the CLI shorthand for --pricing cuda.

PricingMethod::Cuda runs exact elementary enumeration for each endpoint/load extreme on an NVIDIA GPU. It supports effective p <= 6, uses double precision, and preserves CPU enumeration traversal and tie order. It is opt-in; Auto keeps selecting CPU methods. The LP and reduced-cost replay remain on CPU. threads applies to CPU methods; CUDA reports zero CPU pricing workers.

The prototype allocates and transfers data each round. Timing includes these costs, synchronization, and result replay. Runtime errors and extension limits abort the entire round. The deadline is checked after synchronization: an active kernel is not preempted, and an over-budget result is discarded. Large p or large dense instances can therefore overrun a requested wall-clock limit.

The synthetic benchmark warms every configuration once, then alternates five measured rounds on identical problems and duals, comparing every returned path. The CVRPLIB script uses three fresh-process runs per configuration, alternates CPU/CUDA order, fixes rounding to none, and checks converged LP bounds.

Time windows

The CLI automatically enforces finite service-start windows in Solomon inputs:

Terminal
cargo build --release --features clp,cuda --bin baldes
./target/release/baldes pstep instances/Solomon/C201.txt --p 3 --gpu --rounding none --json c201-gpu.json
./target/release/baldes pstep instances/Solomon/C201.txt --p 3 --pricing enumeration --threads 16 --rounding none --json c201-cpu.json
cargo test --features cuda,clp --test pstep_vrptw
cargo run --release --features cuda --example pstep_vrptw_bench -- instances/Solomon/C201.txt instances/Solomon/C202.txt

Rust callers use TimeWindowProblem::from_instance. TimeWindowProblem wraps the existing CVRP data and adds windows and service durations for both depot copies and every customer. Travel duration equals travel cost, in the same units as the windows. Finite ordered windows and nonnegative service times are required. Auto uses CPU enumeration; Cuda uses the GPU; bucket pricing is currently unsupported for this model. CUDA still requires effective p <= 6.

The master has four blocks of n rows: degree, endpoint flow, cumulative load, and service-start flow. A timed step contributes its start time positively at its first customer and its end time negatively at its last customer. Service starts of outgoing steps must therefore cover incoming end times. This follows the endpoint-time extension in Vaes (2022), sections 3.2–3.3.

For a fixed path, propagation represents its earliest end service time as max(start + transit, barrier). transit includes service on each arc's tail; barrier accounts for waiting. Latest feasible starts are bounded by every visited due time. Pricing considers the nondominated earliest scheduling breakpoint and the latest feasible start, together with the existing load extremes. This retains compatible time assignments in the master instead of only filtering individually feasible segments.

The result remains an unlimited-fleet LP relaxation, with no integer route certificate. The JSON identifies problem: VRPTW and time_windows_enforced. A converged LP bound is distinct from a restricted-master objective. GPU/CPU comparisons must use identical rounding; the examples explicitly use none.

Measured Solomon comparison

On a Ryzen 9 9950X and RTX 5090, three fresh-process runs with all 100 customers and --rounding none gave these median LP solve times. P-step CPU pricing used 16 workers; the normal solver used its default settings.

Instance Default solver P-step CPU p=2 P-step GPU p=2 P-step CPU p=3 P-step GPU p=3
C201 0.127 s 1.031 s 1.187 s 2.612 s 2.053 s
C202 8.449 s 2.489 s 2.719 s 7.523 s 5.677 s

All CPU/GPU p-step pairs converged to matching bounds. The default solver obtained 591.55655667 for both cases. C201 p-step obtained the same bound; C202 p-step obtained 590.78102398 at p=2 and 591.54926228 at p=3, so its faster times do not represent equally strong bounds. GPU startup outweighed pricing savings at p=2. At p=3, warmed, matched-dual pricing rounds were 6.96x (C201) and 7.25x (C202) faster on GPU; whole-solve improvements were 1.27x and 1.33x because LP solving remained on CPU.

Machine details, input/binary hashes, timing ranges and LP bounds record the measurements. These timings precede the --gpu alias and integration of the unrelated imported-pricing update; the measured CUDA and VRPTW algorithms are identical. The normal solver comparison uses its stronger route LP.

BALDES · Rust tools for vehicle routing · MIT license