Solver features
The Stateless API exposes SWAT's VRP engine directly. Everything the engine does is switched on by a field in the request payload — nothing is configured or stored on the SWAT side.
This page is a map: what the engine can do, and which field controls it. The API reference has the complete field list with types and defaults, and the linked concept pages explain individual features in depth.
Nodes, vehicles, a routing engine and a scheduling mode are already enough to produce a valid plan. Add features one at a time and compare the returned cost and rejected_bookings between runs — that is far more reliable than switching on several constraints at once and trying to work out which one changed the result.
How a request is organised
All engine configuration lives under engine_settings, split into four blocks:
| Block | Controls | Examples |
|---|---|---|
model_parameters | What the model is — the objective and the rules that apply fleet-wide | optimize_quantity, booking_penalty, mutually_exclusive_groups, compound_zones |
calculation_parameters | How this particular run behaves | scheduling_mode, calculations_mode, allow_vehicle_late, use_path_equalizer |
solver_parameters | How hard and how long the solver searches | first_solution_strategy, time_limit_ms, use_local_search_metaheuristic |
routing_engine | How travel times and distances are obtained | routing_engine_name, road_network, time_factor |
Anything that varies per vehicle or per stop is set on the vehicles and nodes objects themselves, and usually overrides the fleet-wide value.
The API drops fields it does not recognise instead of returning an error. A parameter placed in the wrong block is accepted and then silently does nothing. If a feature appears to have no effect, check which block it belongs to before investigating anything else.
Choosing a model
Two scheduling modes are supported, set with calculation_parameters.scheduling_mode:
prebook— pickup and delivery with time windows. Each booking has its own pickup and dropoff node, linked bybooking_uid.prebook_cvrptw— capacitated routing from a single warehouse or depot. Pickup nodes are merged, which greatly reduces problem size, at the cost of one trip per vehicle.
See CVRPTW vs. PDP scheduling modes for how to choose, and the Introduction for complete example payloads of both.
Between them these two modes cover a superset of the common VRP variants, so most textbook problems reduce to one of them — a plain TSP or a distance-constrained CVRP is a CVRPTW with constraints left unset.
Where CVRPTW and VRPPDTW sit in the VRP family
Objective value and penalties
The solver minimises a single number — the objective value. Every cost and every soft-constraint violation contributes to it, which is what allows the engine to trade a small detour against a broken preference and pick the better plan overall.
| Cost term | Fields |
|---|---|
| Travel time or distance | model_parameters.optimize_quantity |
| Using a vehicle at all | model_parameters.vehicle_costs, vehicle.vehicle_cost, vehicle.amortized_linear_cost_factor, vehicle.amortized_quadratic_cost_factor |
| Distance, time and stop rates | vehicle.cost_per_km; cost_per_hour, cost_per_stop, overtime_cost_per_hour, overtime_threshold_duration (fleet-wide in model_parameters, overridable per vehicle) |
| Leaving a booking unserved | model_parameters.booking_penalty, node.penalty |
| Duration of each pickup-to-dropoff trip | node.trip_cost |
| Idle waiting | model_parameters.slack_cost_factor, calculation_parameters.waiting_time_cost_factor |
| Importance of an individual stop | calculation_parameters.use_node_weights_cost with node.weight |
| Soft-constraint violations | group_crossing_penalty, node.lifo_order_penalty, vehicle_late_penalty_coefficient, the penalties inside vehicle.efficiency |
| Workload fairness | path_equalizer_weight, working_hours_equalizer |
| Route compactness | model_parameters.travel_matrix_operator, model_parameters.cluster_cost |
Most constraints in the engine can be hard or soft. A hard constraint is never violated — the solver would rather return no solution at all. A soft constraint is violated at a price: a fixed penalty, a penalty proportional to the size of the violation, or both. Soft is usually the safer choice, because a hard constraint that cannot be met turns into rejected bookings rather than a slightly worse plan.
Penalties compete with travel cost, and travel cost is measured in seconds or metres. A penalty of 100 is invisible next to a route of 20,000 seconds; the default booking_penalty is 10000 for that reason. When a soft constraint is being ignored, the penalty is usually too small rather than broken.
Bookings the solver could not serve come back in rejected_bookings with a reason code, and the response's cost is the objective value it settled on — start with those two when a plan is not what you expected.
Constraints
Vehicle
| Capability | Fields | Details |
|---|---|---|
| Multi-dimensional capacity and demand | vehicle.capacity, node.demand — dictionaries keyed by dimension, e.g. {"weight": 2000, "volume": 10} | Vehicle capacity and node demand |
| Required equipment, numeric ranges | vehicle.characteristics, node.vehicle_characteristics | Vehicle characteristics |
| Boolean label logic (AND / OR / NOT) | vehicle.labels, node.vehicle_labels | Vehicle and order labels |
| Minimum load during a trip, maximum load on return | vehicle.efficiency | Vehicle efficiency |
| Trips back to the depot, physical stop count | vehicle.number_of_trips with node.end_of_trip, vehicle.max_physical_stops | Maximum trips per vehicle |
| Consecutive pickups before a dropoff, and the reverse | vehicle.max_pickup_locations, vehicle.max_dropoff_locations | Maximum pickup and dropoff locations |
| Route length and duration ceilings | vehicle.max_trip_length, vehicle.max_trip_duration, node.max_trip_duration | Maximum trip duration |
| Route directness | solver_parameters.total_to_furthest_distance_ratio | Distance ratio constraint |
| Start and end locations, fixed route prefixes | vehicle.lat/lon, vehicle.partial_route, vehicle.partial_route_end | Vehicle start and end locations |
| Depot handling | node_type: depot, node.end_of_trip | Vehicle depot |
Time
| Capability | Fields | Details |
|---|---|---|
| Time windows | node.open_time_ts, node.close_time_ts, node.close_time_ts_dynamic | Time windows in CVRPTW problems |
| Several windows on one stop | node.time_windows | Multiple time windows |
| Allow lateness at a price | calculation_parameters.allow_vehicle_late, max_possible_lateness, vehicle_late_penalty_coefficient, node.allow_late | Lateness configuration |
| Driver breaks, single or recurring | vehicle.dynamic_break_duration and the other dynamic_break_* fields | Driver breaks |
| Working hours | vehicle.start_time, vehicle.end_time, calculation_parameters.start_time_end_time_limitations | Vehicle working hours |
| Limit idle waiting | model_parameters.max_slack, node.max_slack, model_parameters.path_constraints_mode | Vehicle slack |
| Gate, queue and docking time on entry or exit | model_parameters.compound_zones | Compound zones |
| Vehicles served at once at a depot | model_parameters.cumulative_limitations | Limiting warehouse capacity |
| Earliest vs. latest scheduled arrival | node.finalization_type | Finalization type |
Orders and sequence
| Capability | Fields | Details |
|---|---|---|
| Orders that must not share a vehicle | model_parameters.mutually_exclusive_groups, strictly_exclusive_groups, group_crossing_penalty, node.groups | Mutually exclusive groups |
Visit order between groups — prebook_cvrptw only | model_parameters.groups_order | Prioritising a dropoff or pickup |
| Last-in-first-out unloading | model_parameters.use_lifo_order_check, lifo_order_check_on_all_vehicles, node.lifo_order_check, node.lifo_order_penalty | API reference |
| Cost of dropping an order | model_parameters.booking_penalty, node.penalty | Booking penalty |
| Protect a route already in progress | vehicle.assigned_nodes, vehicle.assigned_nodes_protection_interval, calculation_parameters.allow_jump | Route sequence locking |
Geofences
Assign geofences to both sides of the match — vehicle.geofence_ids and node.geofence_ids — and a vehicle will only serve nodes whose geofences it covers. An empty list on a vehicle means no restriction.
Both fields are lists, and that is what makes cross-border work: give a node near a boundary the ids of both adjacent zones and either side's vehicles may serve it. Without this, strict zone borders force long detours for stops that happen to sit just on the wrong side.
Route quality and workload balance
Cheapest is not always operationally acceptable. These features shape the plan a dispatcher actually receives.
| Capability | Fields | Details |
|---|---|---|
| Even out the number of stops per vehicle | calculation_parameters.use_path_equalizer, path_equalizer_weight | Equalizers |
| Even out shift times across the fleet | model_parameters.working_hours_equalizer | Equalizers |
| Penalise long arcs so routes stay compact | model_parameters.travel_matrix_operator | Route compactness |
| Keep each vehicle's stops geographically clustered | model_parameters.cluster_cost, vehicle.cluster_cost_factor | Route compactness |
| Charge for how long each booking spends on the vehicle | node.trip_cost | Trip cost |
| Squeeze idle time out of the schedule | model_parameters.slack_cost_factor, vehicle.logistics_optimize_slack (both require path_constraints_mode: logistics) | Vehicle slack |
Travel times and the road network
Travel times and distances come from the routing engine configured in engine_settings.routing_engine — SWAT's self-hosted engine, with the road profile chosen by road_network. A secondary_routing_engine can be configured alongside it, which is how straight-line distance is mixed into a time-based objective for compactness.
Mixed fleets. Set model_parameters.mixed_fleet to true when vehicles differ in routing profile or capacity, then give each vehicle its own vehicle.routing_engine. Small vans and heavy trucks then get different accessible roads and different speeds within a single request. Per-vehicle routing_engine is only read when mixed_fleet is true.
Driver-level speed. routing_engine.time_factor scales the travel times the engine returns, so a cautious driver can be modelled with a factor above 1 and a fast one below. Setting use_speed_in_routing with speed replaces map speeds with a fixed value instead.
Time-dependent travel. Per-node matrix_timestamp lets each stop's transits be computed for the time of day it is actually served, so peak-hour congestion is reflected in the plan rather than averaged away. model_parameters.time_dependent_transit accompanies this; note that it forces the objective to total_distance regardless of optimize_quantity.
Pre-computed matrices. Supply your own travel matrices in matrices and reference them per vehicle with vehicle.matrix_id to skip routing engine calls entirely — useful for repeated solves over a stable location set.
Search behaviour
The solver runs in two phases. First it builds an initial feasible solution using the strategy named in solver_parameters.first_solution_strategy; then, if use_local_search_metaheuristic is enabled, it improves that solution until time_limit_ms runs out. The time limit is therefore a quality dial, not just a timeout: more time means more improvement, and the right value depends on problem size, constraint count and how good the first solution was.
First solution strategy
first_solution_strategy takes a numeric code. You do not need to understand the algorithms behind them — these are the ones worth choosing between:
| Code | Strategy | Reach for it when |
|---|---|---|
0 | AUTOMATIC | Default. Start here and only change it if the result or the runtime is unsatisfactory. |
3 | PATH_CHEAPEST_ARC | A dependable general-purpose alternative when AUTOMATIC underperforms. |
8 | PARALLEL_CHEAPEST_INSERTION | Many stops are optional or penalised, and you want each placed where it costs least overall. |
9 | LOCAL_CHEAPEST_INSERTION | Faster than 8 on large payloads, where building the first solution is itself the bottleneck. |
14, 16 | SEQUENTIAL_ / ADJUSTABLE_PARALLEL_CHEAPEST_INSERTION | Incremental re-solves. Both build on the previous run's result, so keep it in vehicle.assigned_nodes. |
17 | LOGISTICS_PARALLEL_CHEAPEST_INSERTION | Single-warehouse logistics in prebook_cvrptw mode. |
5 | EVALUATOR_STRATEGY | You want to supply the starting routes yourself. |
18, 19 | CHOOSE_FIRST_VALID / CHOOSE_BEST | No single strategy is reliable across your payloads. See below. |
The remaining codes are classical academic heuristics — Savings, Sweep, Christofides and the arc-based variants. They are accepted and occasionally useful for comparison, but rarely the best choice for a real fleet. The API reference lists every code with its description.
11 (SWEEP) is not supportedThe solver build currently in use does not implement it.
Cascading from a known solution. With code 5, the initial solution is the union of every vehicle's first_solution list — useful for chaining runs, where one solve seeds the next, and for reproducing an exact starting configuration in testing. Nodes you leave out, and nodes that violate another constraint, start in rejected_bookings, though the solver may still bring them back in. Unlike partial_route, a first_solution list is a starting point rather than a fixture: the solver is free to change it. Populating first_solution without setting first_solution_strategy to 5 raises an exception.
Falling through several strategies. Set first_solution_strategy to 18 or 19 and list candidates in first_solution_strategies. 18 tries them in the given order and keeps the first that produces a valid solution — [2, 7, 1, 8] falls through to 1 if 2 and 7 fail. 19 runs them all and keeps the best result. first_solution_strategies_time_limit_ms bounds each attempt, while time_limit_ms still bounds the whole run.
Improvement operators
During the second phase the solver improves the plan by repeatedly trying small rearrangements. solver_parameters.use_all_local_search_operators enables the full set; use_local_search_operators switches on individual extras by name:
logistics_relocate_pair— moves a pickup and its dropoff together, and treats stops that share a physical location as one cluster that can be moved as a unit. Worth enabling for PDP logistics where many pickups or dropoffs sit at the same address: without it the solver spends time on reorderings within a single location that cannot change the cost, and can leave a vehicle looping back to an address it has already visited.extended_swap_active— improves solutions when waypoints are used.
Robustness at scale
Unroutable stops. By default, when the map cannot connect a node the engine falls back to a straight-line approximation, so the run still completes. Set calculation_parameters.exclude_unroutable_bookings to true if you would rather have those bookings returned as unserved than routed on an estimate. Routability depends on the map, the vehicle profile and any temporary restrictions in force.
Large payloads. calculation_parameters.partition_nodes splits a payload into consecutive time partitions, solves them in sequence and stitches the results together. It requires partitioning to be enabled server-side; when it is not, the flag is ignored and the payload is solved whole.
Understanding a result. Every response carries the final cost, the per-vehicle plan, and a rejected_bookings list in which each entry names the reason it could not be served. Between those, most "why did it do that?" questions can be answered without changing the payload — check them before adjusting parameters.