Coupled split delivery with CoupledRequestFlexLoad
Experimental feature. Split delivery with
CoupledRequestFlexLoadis experimental: you can use it, and its API or behaviour may still change.
This page shows split delivery with CoupledRequestFlexLoad in a small scenario with three pallet orders, two trucks and four supply nodes. It covers the split nodes of an order, the coupling map, fuzzy visits, the weight JOptWeight.Capacity and how to read a result. It is written for developers who model pickup and delivery problems with JOpt as a Java library. Read the pickup and delivery guide first: it explains the node depots, loads, resource depots and flexible load types that this page builds on.
What this page shows
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. The coupling does not guarantee delivery of the full quantity. 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.
Scenario
Customer groups
Each group consists of two split nodes (S0 and S1) at the same 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.
Each split node is set with setIgnoreOnZeroLoad(true): a split node assigned zero pallets is suppressed from routing and does not appear as an unnecessary stop.
Supply nodes
Four supply nodes in Aachen each hold a SupplyFlexLoad that starts at 20 pallets. Trucks start empty and must load at a supply node before they serve customers. A supply node whose load reaches zero is suppressed from routing as well, with the same setting.
Resources
The scenario has two trucks, both based in Aachen at the coordinates of the supply nodes. Each truck carries a maximum of 20 pallets.
Key parameters
The optimization properties page lists the property keys by category, with their defaults.
| Parameter | Value in this scenario | 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 Choosing JOptWeight.Capacity |
JOpt.Algorithm.PreOptimization.SA.NumIterations | 500,000 | Iterations of simulated annealing |
JOptExitCondition.JOptGenerationCount | 20,000 | Generations of genetic evolution |
How the code works
Fuzzy visits: isFuzzyVisit = true
In this scenario each CoupledRequestFlexLoad is created with isFuzzyVisit = true, the third argument of the constructor:
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 in 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));
In this snippet, group is the list of the nodes of one order group, depotLabel returns the node depot id of a node, and ownDepotId is the node depot id of the node that gets the map.
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.
Output and what to look for
A sample 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)
The optimizer does not aim at a fixed ratio: 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.
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. The pickup and delivery guide describes the types of capacity violations. 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:
SupplyFlexLoadat two bakeries; the optimizer sets how much bread each bakery prepares - PNDRequestAndSupplyFlexLoadExample: a basic
RequestFlexLoadandSupplyFlexLoadsetup - Pickup and delivery examples: the pickup and delivery examples explained
Related pages
- Pickup and delivery: node depots, loads, resource depots, the load types and the capacity violations
- Manufacturing planning: flexible loads in pickup and delivery, with a section on split delivery
- Optimization properties: the property keys by category, with their defaults
- How JOpt.TourOptimizer works: the construction algorithm, simulated annealing and genetic evolution
- License guide: the free and the full licenses and their element limits
Added: 2026-04-09 (JOpt core 7.5.3-j17) | Last updated: 2026-10-19