Coupled split delivery with CoupledRequestFlexLoad

Experimental feature. Split delivery with CoupledRequestFlexLoad is 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:

GroupLocationTotal demandSplit?
CologneCologne city centre30 palletsMandatory: 30 > truck capacity (20)
LeverkusenLeverkusen10 palletsOptional: 10 fits in one truck
DortmundDortmund city centre20 palletsOptional: 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.

ParameterValue in this scenarioNotes
Truck capacity20Cologne total (30) exceeds this, so a split is mandatory
Initial supply load20Start value per supply node; the optimizer changes it
isFuzzyVisittrueLets a truck deliver a part of a share
isIgnoreOnZeroLoadtrueOn all split nodes and supply nodes
JOptWeight.Capacity10See Choosing JOptWeight.Capacity
JOpt.Algorithm.PreOptimization.SA.NumIterations500,000Iterations of simulated annealing
JOptExitCondition.JOptGenerationCount20,000Generations 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.

SettingOutcome
two-way, fuzzy visits, weight 10all delivered
two-way, fuzzy visits, weight 200all delivered
two-way, no fuzzy visits, weight 10under-delivery of 20 units, in two of two runs
two-way, no fuzzy visits, weight 200all delivered, small order not consolidated (3 + 7)
three-way, fuzzy visits, weight 10under-delivery of 15 units
three-way, fuzzy visits, weight 200all 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.


Added: 2026-04-09 (JOpt core 7.5.3-j17) | Last updated: 2026-10-19