DNA //evolutions

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.


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:

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.


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

ParameterValueNotes
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 the next section
SA iterations500,000Pre-optimization phase
GE generations20,000Genetic 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.

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.


  • PNDBakeryChainFlexLoadExample: SupplyFlexLoad with a chain of bakery supply stops.
  • PNDRequestAndSupplyFlexLoadExample: basic RequestFlexLoad and SupplyFlexLoad setup.