Coupled Split Delivery with CoupledRequestFlexLoad
Experimental feature. Split delivery with
CoupledRequestFlexLoadis experimental: you can use it, and its API or behaviour may still change.
What this page shows
This page shows how JOpt handles split delivery with CoupledRequestFlexLoad, using a small scenario. A split delivery fulfils one order with more than one visit. Each visit delivers a part of the order. The visits of a split can come from different trucks or from one truck that reloads in between.
The coupling keeps the total of each order group: the sum of the start shares of its split nodes. Delivery of the full quantity is not guaranteed by the coupling. It is a pseudo-constraint: a weighted cost, not a hard limit. A result can contain under-delivery, and a share can stay above the vehicle capacity, so check the violation summary of every result.
Two cases appear side by side, a mandatory split and an optional split, across three order groups:
| Group | Location | Total demand | Split? |
|---|---|---|---|
| Cologne | Cologne city centre | 30 pallets | Mandatory: 30 > truck capacity (20) |
| Leverkusen | Leverkusen | 10 pallets | Optional: 10 fits in one truck |
| Dortmund | Dortmund city centre | 20 pallets | Optional: exactly fills one truck |
The Cologne order does not fit in one truck load, so it needs at least two visits. The Leverkusen and Dortmund orders fit on a single truck, so the optimizer can consolidate each of them to one stop.
Setup
Customer groups
Each group consists of two split-nodes (S0 and S1) placed at the same geographic location. Each split node holds a CoupledRequestFlexLoad with its share of the order. The loads of a group are coupled: the optimizer moves quantity from one split node to another, and the group total stays the same.
The total of a group is the sum of the start shares of its split nodes, so set the start shares to add up to the ordered quantity (for example 15 + 15 = 30). Shares are whole numbers. setCoupledLoadValue stores the order total: set it to the real total. The group total in a run comes from the start shares.
Any split-node assigned zero pallets is automatically suppressed from routing via isIgnoreOnZeroLoad = true, so it does not appear as an unnecessary stop.
Supply nodes
Four SupplyFlexLoad nodes are placed in Aachen, each starting with 20 pallets. Trucks start empty and must reload at a supply node before serving customers. Once a supply node's load reaches zero it disappears from routing (isIgnoreOnZeroLoad = true).
Resources
Two trucks, both based in Aachen (same coordinates as the supply nodes). Each truck carries a maximum of 20 pallets.
Fuzzy visits: isFuzzyVisit = true
CoupledRequestFlexLoad is created with isFuzzyVisit = true:
new CoupledRequestFlexLoad("Pallets", requestPerSplit, /* isFuzzyVisit */ true);
isFuzzyVisit = true lets a vehicle deliver a part of a share. In small test runs with two split nodes per order, the runs with fuzzy visits delivered everything. Without fuzzy visits, a capacity weight of 10 left part of the demand undelivered, and a weight of 200 delivered everything but did not consolidate the small order on one stop. The table under "Choosing JOptWeight.Capacity" lists these runs.
How the coupling map works
Each CoupledRequestFlexLoad gets a coupling map, set with setCoupledDepotMap. The map names the load's siblings, not the load itself: the node id of each sibling to the ids of that sibling's node depots. For a group of any size, build the map of every node of the group to its node depot id, then give each node that map without its own entry:
// Full node-id -> depot-id map for the group
Map<String, List<String>> allDepots = group.stream()
.collect(Collectors.toMap(INode::getId, n -> List.of(depotLabel.apply(n))));
// Each node's coupling map = all entries except its own
Map<String, List<String>> siblingDepots = new HashMap<>(allDepots);
siblingDepots.entrySet().removeIf(e -> e.getValue().contains(ownDepotId));
All split nodes of a group and the supply nodes use the same load ID ("Pallets" here), and the truck's resource depot must declare a capacity for that same ID.
Result
One result of this scenario:
Truck_Aachen_0:
Supply_Aachen_0 -> load 20 pallets
Cologne_S0 -> deliver 20 pallets (Cologne split, part 1)
Supply_Aachen_2 -> reload 20 pallets
Dortmund_S0 -> deliver 20 pallets (Dortmund, consolidated)
Truck_Aachen_1:
Supply_Aachen_1 -> load 20 pallets
Cologne_S1 -> deliver 10 pallets (Cologne split, part 2)
Leverkusen_S1 -> deliver 10 pallets (Leverkusen, consolidated)
No ratio is aimed at. The shares follow from the route cost, so another run can split Cologne differently.
Cologne: split across two trucks, 20 + 10 = 30. In this result no visit delivers more than 20 pallets. The coupling itself does not limit a share to the vehicle capacity.
Leverkusen: consolidated to S1 (10 pallets). S0 is suppressed.
Dortmund: consolidated to S0 (20 pallets). S1 is suppressed.
Three of the four supply nodes (Supply_Aachen_0 and Supply_Aachen_2 for truck 0; Supply_Aachen_1 for truck 1) are used. Supply_Aachen_3 is unused and suppressed.
Key parameters
| Parameter | Value | Notes |
|---|---|---|
| Truck capacity | 20 | Cologne total (30) exceeds this, so a split is mandatory |
| Initial supply load | 20 | Start value per supply node; the optimizer changes it |
isFuzzyVisit | true | Lets a truck deliver a part of a share |
isIgnoreOnZeroLoad | true | On all split-nodes and supply nodes |
JOptWeight.Capacity | 10 | See the next section |
| SA iterations | 500,000 | Pre-optimization phase |
| GE generations | 20,000 | Genetic evolution phase |
Choosing JOptWeight.Capacity
JOptWeight.Capacity is the weight of the capacity cost. Full delivery is a pseudo-constraint of this cost, not a hard limit. A visit that cannot deliver its share shows in the result as CONSTRAINTVIOLATION/CAPACITY/UNDERLOAD with the missing quantity, and the violation summary counts it. Whether a result is free of under-delivery depends on JOptWeight.Capacity and on the search. Check the violation summary of every result.
Small test runs (two order groups, two trucks of capacity 20, one run per setting) gave these outcomes. Two-way means two split nodes per order, three-way means three. The optimizer is randomized: a single run shows that something can happen, not how often.
| Setting | Outcome |
|---|---|
| two-way, fuzzy visits, weight 10 | all delivered |
| two-way, fuzzy visits, weight 200 | all delivered |
| two-way, no fuzzy visits, weight 10 | under-delivery of 20 units, in two of two runs |
| two-way, no fuzzy visits, weight 200 | all delivered, small order not consolidated (3 + 7) |
| three-way, fuzzy visits, weight 10 | under-delivery of 15 units |
| three-way, fuzzy visits, weight 200 | all delivered |
The weight of 10 in this scenario was not enough in the three-way runs. If a result shows under-delivery, raise the weight and run again.
Related examples
PNDBakeryChainFlexLoadExample: SupplyFlexLoad with a chain of bakery supply stops.PNDRequestAndSupplyFlexLoadExample: basic RequestFlexLoad and SupplyFlexLoad setup.
Clustering During Construction — High-Quality Starting Solutions at Scale
This document explains JOpt TourOptimizer’s clustering construction capabilities and how to reproduce and inspect them using the provided Advanced Java examples.
ExtraInfo — Attaching Domain Metadata to Nodes and Resources
In real-world optimization systems you almost always need to carry domain metadata alongside routing entities, for example: