Need technical support for GMPRO's advanced features? 👋 Say Hello! to start working with Afi Labs.

GMPRO docs: Territory optimization and route planning

How to use the costsPerVehicle parameter to build a territory optimization system with GMPRO.

GMPRO docs: Territory optimization and route planning

In this short tutorial, I'm going to show you how to use GMPRO's costsPerVehicle parameter to build a territory optimization and route planning system for your logistics operation.

Part 1: GMPRO: Google Maps Platform route optimization API
Part 2: GMPRO TSP solver: Google Maps with more than 25 waypoints
Part 3: Google Maps route optimization: multi vehicle
Part 4: Fleet routing app - free Google Maps route planner for multiple stops
Part 5: GMPRO docs: Fixed vehicle costs
Part 6: GMPRO docs: Territory optimization and route planning (this article)
Part 7: GMPRO docs: Solving the VRP with route clustering and soft constraints

What is territory optimization?

Territory optimization is the strategic process of dividing a geographic area or market into specific regions, or "territories". It is commonly used by sales teams, logistics companies and field service operators (e.g. telecommunications, HVAC, or utility companies) to balance workloads, maximize coverage and improve geographic efficiency so that each territory is geographically compact and easily accessible, without overlap.

Territory optimization involves fairly dividing a work area into distinct regions
Territory optimization involves fairly dividing a work area into distinct regions

Territory optimization works by assigning each stop to a specific region. To determine the most efficient sequence of stops for each vehicle, you'll still need a route optimization API, like GMPRO

How territory optimization works in GMPRO

There are two main ways to incorporate territory optimization into a route optimization system. The first is by applying hard constraints. For instance, if you want to designate a downtown territory that only cargo bikes in your fleet can service, you should add a downtown constraint to the loadDemands field of shipments located downtown:

{
    "loadDemands": {
        "downtown": {
            "amount": "1"
        }
    }
}

In the corresponding vehicles object, you would add that same constraint to the loadLimits field like so:

{
    "loadLimits": {
        "downtown": {
            "maxLoad": 5
        }
    }
}

This ensures that only vehicles with a downtown loadLimit can handle downtown deliveries.

The second way to approach territory optimization is to use soft constraints, which is the method I prefer.

💡
GMPRO is only responsible for assigning vehicles to stops within their designated territories. If you want to define and visualize territories on a map, refer to this Mapbox case study to see how territories are mapped and assigned at Sotargruppen.

Applying soft constraints to territory optimization in GMPRO

Unlike a hard constraint which will leave a shipment unserved if there are no suitable vehicles to deliver it, a soft constraint will try to find alternative vehicles to do the job, even if the resulting route solution isn't optimal. It's like saying, "Well, this isn't perfect, but this will just have to do." Applied to territory optimization, this means using the costsPerVehicle and costsPerVehicleIndices fields to selectively apply costs to vehicles that are unsuitable to handle the shipment.

👨‍💻
Screenshots in this blog post were taken with GMPRO-viewer , using data imported via GMPRO-json-converter. Both tools are free to use.

For example, let's say you have two regions, downtown Vancouver (downtown) and North Vancouver (north-vancouver) as shown in the map below.

You have a single shipment to deliver downtown, and two vehicles, veh-cargobike which is a cargo bike and veh-sprintervan which is a large Mercedes Sprinter Van. The package is quite small, so either vehicle would work but the main problem is parking - it is difficult and time consuming for the sprinter van to find curbside parking downtown, so you would rather the sprinter van not do the delivery. To model this, first attach both vehicles to the GMPRO request like so:

{
    "vehicles": [
        {
            "travelMode": "DRIVING",
            "startLocation": {
                "latitude": 49.2808422,
                "longitude": -123.0827831
            },
            "startTimeWindows": [
                {
                    "startTime": "2024-07-08T16:00:00Z"
                }
            ],
            "endTimeWindows": [
                {
                    "endTime": "2024-07-08T18:00:00Z"
                }
            ],
            "loadLimits": {
                "weight": {
                    "maxLoad": 5
                }
            },
            "label": "veh-cargobike",
            "costPerKilometer": 1
        },
        {
            "travelMode": "DRIVING",
            "startLocation": {
                "latitude": 49.2658769,
                "longitude": -123.0815619
            },
            "startTimeWindows": [
                {
                    "startTime": "2024-07-08T16:00:00Z"
                }
            ],
            "endTimeWindows": [
                {
                    "endTime": "2024-07-08T18:00:00Z"
                }
            ],
            "loadLimits": {
                "weight": {
                    "maxLoad": 5
                }
            },
            "label": "veh-sprintervan",
            "costPerKilometer": 1
        }
    ]
}

Note that veh-cargobike is in position 0 of the vehicles array and veh-sprintervan is in position 1.

Similarly, on the shipments array, we need to set up the costsPerVehicles and costsPerVehicleIndices array like this:

{
    "costsPerVehicle": [
        0,
        50
    ],
    "costsPerVehicleIndices": [
        0,
        1
    ]
}

Doing so attaches an additional cost of 50 to the vehicle in position 1 of the vehicles array (veh-sprintervan), making it less likely that GMPRO will assign veh-sprintervan to do the delivery. This is exactly what we want.

costPerVehicle / costPerVehicleIndices worked example

Putting everything together, here's the full GMPRO request for our 1 shipment / 2 vehicle example.

Territory optimization applied to downtown Vancouver for a single vehicle
Territory optimization applied to downtown Vancouver for a single vehicle

Input

{
    "model": {
        "shipments": [
            {
                "deliveries": [
                    {
                        "arrivalLocation": {
                            "latitude": 49.2912037,
                            "longitude": -123.1362474
                        },
                        "timeWindows": [
                            {
                                "startTime": "2024-07-08T16:00:00Z",
                                "endTime": "2024-07-08T18:00:00Z"
                            }
                        ],
                        "duration": "600s"
                    }
                ],
                "loadDemands": {
                    "weight": {
                        "amount": 1
                    }
                },
                "costsPerVehicle": [
                    0,
                    50
                ],
                "costsPerVehicleIndices": [
                    0,
                    1
                ],
                "label": "dt123"
            }
        ],
        "vehicles": [
            {
                "travelMode": "DRIVING",
                "startLocation": {
                    "latitude": 49.2668756,
                    "longitude": -123.0800413
                },
                "startTimeWindows": [
                    {
                        "startTime": "2024-07-08T16:00:00Z"
                    }
                ],
                "endTimeWindows": [
                    {
                        "endTime": "2024-07-08T18:00:00Z"
                    }
                ],
                "loadLimits": {
                    "weight": {
                        "maxLoad": 5
                    }
                },
                "label": "veh-cargobike",
                "costPerKilometer": 1
            },
            {
                "travelMode": "DRIVING",
                "startLocation": {
                    "latitude": 49.2808422,
                    "longitude": -123.0802082
                },
                "startTimeWindows": [
                    {
                        "startTime": "2024-07-08T16:00:00Z"
                    }
                ],
                "endTimeWindows": [
                    {
                        "endTime": "2024-07-08T18:00:00Z"
                    }
                ],
                "loadLimits": {
                    "weight": {
                        "maxLoad": 5
                    }
                },
                "label": "veh-sprintervan",
                "costPerKilometer": 1
            }
        ],
        "globalStartTime": "2024-07-08T16:00:00Z",
        "globalEndTime": "2024-07-09T02:59:59Z"
    },
    "populatePolylines": true
}

Output

{
    "routes": [
        {
            "vehicleLabel": "veh-cargobike",
            "vehicleStartTime": "2024-07-08T16:00:00Z",
            "vehicleEndTime": "2024-07-08T16:27:46Z",
            "visits": [
                {
                    "startTime": "2024-07-08T16:17:46Z",
                    "detour": "0s",
                    "shipmentLabel": "dt123",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                }
            ],
            "transitions": [
                {
                    "travelDuration": "1066s",
                    "travelDistanceMeters": 6141,
                    "waitDuration": "0s",
                    "totalDuration": "1066s",
                    "startTime": "2024-07-08T16:00:00Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "1"
                        }
                    }
                },
                {
                    "travelDuration": "0s",
                    "waitDuration": "0s",
                    "totalDuration": "0s",
                    "startTime": "2024-07-08T16:27:46Z",
                    "vehicleLoads": {
                        "weight": {}
                    }
                }
            ],
            "routePolyline": {
                "points": "ijukHdbfnV[pDG|@Z?J?FA|@EHAV?J?xA@`@@?TAVCXCVUfBOxAQtAOvAALOpACN?@CNQ`B]~CK|@OpAIv@QbBEXEXIt@?DCNShBQtA?BABK~@EXCXA`@A\\ATAV?@?T?N?tC?v@At@?bB?b@A\\?@?f@?J?lAAb@?`B?z@?nAA\\?xC?t@?f@A`@AVAPE`@CNCPAHI^ADEVI\\KZKZKXCBKRyA|CcAtBm@t@}@lBa@z@MTIRITKX?@ERK`@qBEw@EM?QAQ?s@Ca@?WCEAOAOEKAGCKE[Oo@YOG[OQGMCKCKAWCM?eBE]Ai@AM?m@AG?cACE?iCEkAAUAsFIsBEY?m@Aa@Au@A@b@BlB?R@~@BbB?l@@t@@v@@\\?Z?@@t@@J@t@?N@X?@?Z?X?t@?H?XAv@Al@Cv@Ad@Eh@GdAC`@OdBCLGb@GXWtAS~@Qn@IVGRA@CFWt@a@~@IV]l@[f@c@|@[h@IPMRUb@INIRIJGHIJc@t@CFA@AH_@p@i@|@s@nA_BjCINS^OXKPININMPCFMTINKPWd@_A`Bg@z@Wd@}@dBIJa@v@GJGLcAbBCFa@t@MRKRw@tACDUb@m@`Aa@t@]j@m@bAOXUb@i@|@IP{@zAc@z@M\\GREPCLAJEZGp@CXKrACTETERA@GVEJEHMVYl@_@n@INOVILYd@q@nAYd@?@Wd@gAhBIRCFyAfCaAfBCDQXGHCNAL?F@D?B@B@@@DBBBDh@~@r@jADHRZFNHNDF?@v@rAdAhBl@bA@@LRJPYd@q@lAgB~CiBbDCDc@t@EFsCbFGJ[l@yAjCa@r@o@jAiB~Cs@lAo@lAb@r@b@t@Wf@"
            },
            "metrics": {
                "performedShipmentCount": 1,
                "travelDuration": "1066s",
                "waitDuration": "0s",
                "delayDuration": "0s",
                "breakDuration": "0s",
                "visitDuration": "600s",
                "totalDuration": "1666s",
                "travelDistanceMeters": 6141,
                "maxLoads": {
                    "weight": {
                        "amount": "1"
                    }
                }
            },
            "routeCosts": {
                "model.vehicles.cost_per_kilometer": 6.141
            },
            "routeTotalCost": 6.141
        },
        {
            "vehicleIndex": 1,
            "vehicleLabel": "veh-sprintervan"
        }
    ],
    "metrics": {
        "aggregatedRouteMetrics": {
            "performedShipmentCount": 1,
            "travelDuration": "1066s",
            "waitDuration": "0s",
            "delayDuration": "0s",
            "breakDuration": "0s",
            "visitDuration": "600s",
            "totalDuration": "1666s",
            "travelDistanceMeters": 6141,
            "maxLoads": {
                "weight": {
                    "amount": "1"
                }
            }
        },
        "usedVehicleCount": 1,
        "earliestVehicleStartTime": "2024-07-08T16:00:00Z",
        "latestVehicleEndTime": "2024-07-08T16:27:46Z",
        "totalCost": 6.141,
        "costs": {
            "model.vehicles.cost_per_kilometer": 6.141
        }
    }
}

As expected, GMPRO assigns veh-cargovan for the delivery. Now, let's explore a more practical example of how territory optimization works in a real-world scenario.

GMPRO territory optimization worked example

Just as in the earlier example, we'll be using our 2 vehicles, veh-cargobike and veh-sprintervan. But this time, we are going to give them 8 shipments to deliver. 3 in downtown Vancouver, 3 in North Vancouver and 2 "on the way". The shipments in downtown Vancouver and North Vancouver have had their costPerVehicle array set up to prefer veh-cargobike and veh-sprintervan respectively, while the remaining 2 shipments are "open" (they do not contain a costPerVehicle array), and can be delivered by either vehicle.

Territory optimization applied to downtown and North Vancouver for two vehicles
Territory optimization applied to downtown and North Vancouver for two vehicles

Input

{
  "model": {
    "shipments": [
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.31374169999999,
              "longitude": -123.0514387
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            50,
            0
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "nv789"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.343481,
              "longitude": -123.0863414
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            50,
            0
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "nv987"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.309186,
              "longitude": -123.033168
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            50,
            0
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "nv123"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2912037,
              "longitude": -123.1362474
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            0,
            50
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "dt123"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2786079,
              "longitude": -123.1141059
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            0,
            50
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "dt456"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2771031,
              "longitude": -123.1088116
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            0,
            50
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "dt789"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2812657,
              "longitude": -123.0280715
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "label": "ev123"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2580983,
              "longitude": -123.1146481
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "label": "ev456"
      }
    ],
    "vehicles": [
      {
        "travelMode": "DRIVING",
        "startLocation": {
          "latitude": 49.2668756,
          "longitude": -123.0800413
        },
        "startTimeWindows": [
          {
            "startTime": "2024-07-08T16:00:00Z"
          }
        ],
        "endTimeWindows": [
          {
            "endTime": "2024-07-08T18:00:00Z"
          }
        ],
        "loadLimits": {
          "weight": {
            "maxLoad": 5
          }
        },
        "label": "veh-cargobike",
        "costPerKilometer": 1
      },
      {
        "travelMode": "DRIVING",
        "startLocation": {
          "latitude": 49.2808422,
          "longitude": -123.0802082
        },
        "startTimeWindows": [
          {
            "startTime": "2024-07-08T16:00:00Z"
          }
        ],
        "endTimeWindows": [
          {
            "endTime": "2024-07-08T18:00:00Z"
          }
        ],
        "loadLimits": {
          "weight": {
            "maxLoad": 5
          }
        },
        "label": "veh-sprintervan",
        "costPerKilometer": 1
      }
    ],
    "globalStartTime": "2024-07-08T16:00:00Z",
    "globalEndTime": "2024-07-09T02:59:59Z"
  },
  "populatePolylines": true
}

Output

{
    "routes": [
        {
            "vehicleLabel": "veh-cargobike",
            "vehicleStartTime": "2024-07-08T16:00:00Z",
            "vehicleEndTime": "2024-07-08T17:10:29Z",
            "visits": [
                {
                    "shipmentIndex": 7,
                    "startTime": "2024-07-08T16:10:08Z",
                    "detour": "0s",
                    "shipmentLabel": "ev456",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 5,
                    "startTime": "2024-07-08T16:25:58Z",
                    "detour": "1010s",
                    "shipmentLabel": "dt789",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 4,
                    "startTime": "2024-07-08T16:40:24Z",
                    "detour": "1835s",
                    "shipmentLabel": "dt456",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 3,
                    "startTime": "2024-07-08T17:00:29Z",
                    "detour": "2563s",
                    "shipmentLabel": "dt123",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                }
            ],
            "transitions": [
                {
                    "travelDuration": "608s",
                    "travelDistanceMeters": 3818,
                    "waitDuration": "0s",
                    "totalDuration": "608s",
                    "startTime": "2024-07-08T16:00:00Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "4"
                        }
                    }
                },
                {
                    "travelDuration": "350s",
                    "travelDistanceMeters": 2313,
                    "waitDuration": "0s",
                    "totalDuration": "350s",
                    "startTime": "2024-07-08T16:20:08Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "3"
                        }
                    }
                },
                {
                    "travelDuration": "266s",
                    "travelDistanceMeters": 1355,
                    "waitDuration": "0s",
                    "totalDuration": "266s",
                    "startTime": "2024-07-08T16:35:58Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "2"
                        }
                    }
                },
                {
                    "travelDuration": "605s",
                    "travelDistanceMeters": 2737,
                    "waitDuration": "0s",
                    "totalDuration": "605s",
                    "startTime": "2024-07-08T16:50:24Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "1"
                        }
                    }
                },
                {
                    "travelDuration": "0s",
                    "waitDuration": "0s",
                    "totalDuration": "0s",
                    "startTime": "2024-07-08T17:10:29Z",
                    "vehicleLoads": {
                        "weight": {}
                    }
                }
            ],
            "routePolyline": {
                "points": "ijukHdbfnV[pDG|@Z?J?FA|@EHAV?J?xA@`@@nDI?@?@?@?@@??@?@?@@??@?@@??@@??@@?@??@@?@??A@?@??A@??A@A?A@??A?A?A?A?Ab@?h@@l@?n@@xA@z@@@?`@?AdK?r@CnI?`@?J?f@?fAA^?l@@hAKZ?@?@Af@?vA?tB?jB?jAP@zA@JADA@?bA@zAAP@x@?H@zA@|A?xA@@?vA@zA@rA@VDR@Z@@@DBFBBBNHDBNHTJb@`@f@b@VPVBXD?j@CzA?bAAv@?JA`BAnAAlC?n@AtBC`D?x@CxAAjA?dAAxA?xB?bB?dDClB?zAAbC?lB?h@@hAApA?fA?LCfFAzDAvCAl@CxCEtE?V?RGvFGrFC`EAfBAbCAbCCfCEpEA^APAr@?t@{A?}AAs@@c@@@kAAjAQ?g@Ac@?qCGQCA?g@CkB@KAQ@uAAgAAaACQ?M?y@GQA[Ac@Au@AIAc@Am@CUAo@?eB?aBAsAEkAAq@DU@c@?S?m@@e@@G?I?K?cA?}@B_AB]B]@o@@o@B_BBG@q@@kA@G?e@GsDGeAA_@?C?E?E?QAQ?C?yAEqACYAW?i@Ag@E]?]AWMMEMAIAMAA?OACAKAIAEAEAKESKMGKKKKEEACACIKMWGMEKEOGQCGq@_CKWEOEMCICKGMGMGEMKq@mBIUIUACGQGM?AIQi@wAEIKU_@aAEMGOUi@O_@k@wAMa@Si@M[IQIWGQIi@I_@E[C]CYCe@[AM?E?oAC_@A[?MAU?c@Ac@CE?EREPARCRAP?N?P?N@P@NBNBT@DBNDR@@DPbB`EPb@BHBH@PFTBH?@BL@J?H@B?H@F?P@R?JAJAVCZCZAV?X?VBXBXFVNp@FVHVLXLZN\\HRJRNTBFBDHJHF@BNJBBHDPDPDr@PHBFFHHb@l@PVRXFJJPNTUJSJKHSRKLQNKPIPGJCBEJCHMTg@|@[l@k@`Ak@_AgBaD_A_BYe@_@q@e@y@d@x@^p@Xd@~@~AfB`Dj@~@MRQXa@p@Wf@[n@w@pAMX_AbB}@|Ag@|@[h@a@r@EHQXm@fAQX}@~A_A`BMVEFo@nA{@|AOVQX[f@Yd@S\\INEFEF}@~AeAnBMROREHGHEFA@GFGHIFGFIDIFIDGDE@UH_@JQHQFQHOLMJA@KLC@MRORQZdAjB\\h@Zj@NPw@pAm@hAs@lAS`@iB`Ds@lA?@Yd@_CbEGNo@jAoBhDAAAACAA??@C?AB?BAB?B?B?B@BQZWf@s@lAYf@Wf@mArB?@w@tAUZy@|Ao@jAa@r@u@rAEHm@dA}CrFeAeBgAkBk@aAYg@Wf@"
            },
            "metrics": {
                "performedShipmentCount": 4,
                "travelDuration": "1829s",
                "waitDuration": "0s",
                "delayDuration": "0s",
                "breakDuration": "0s",
                "visitDuration": "2400s",
                "totalDuration": "4229s",
                "travelDistanceMeters": 10223,
                "maxLoads": {
                    "weight": {
                        "amount": "4"
                    }
                }
            },
            "routeCosts": {
                "model.vehicles.cost_per_kilometer": 10.223
            },
            "routeTotalCost": 10.223
        },
        {
            "vehicleIndex": 1,
            "vehicleLabel": "veh-sprintervan",
            "vehicleStartTime": "2024-07-08T16:00:00Z",
            "vehicleEndTime": "2024-07-08T17:09:55Z",
            "visits": [
                {
                    "shipmentIndex": 6,
                    "startTime": "2024-07-08T16:09:00Z",
                    "detour": "0s",
                    "shipmentLabel": "ev123",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 2,
                    "startTime": "2024-07-08T16:25:32Z",
                    "detour": "741s",
                    "shipmentLabel": "nv123",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "startTime": "2024-07-08T16:40:02Z",
                    "detour": "1578s",
                    "shipmentLabel": "nv789",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 1,
                    "startTime": "2024-07-08T16:59:55Z",
                    "detour": "2555s",
                    "shipmentLabel": "nv987",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                }
            ],
            "transitions": [
                {
                    "travelDuration": "540s",
                    "travelDistanceMeters": 3810,
                    "waitDuration": "0s",
                    "totalDuration": "540s",
                    "startTime": "2024-07-08T16:00:00Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "4"
                        }
                    }
                },
                {
                    "travelDuration": "392s",
                    "travelDistanceMeters": 5041,
                    "waitDuration": "0s",
                    "totalDuration": "392s",
                    "startTime": "2024-07-08T16:19:00Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "3"
                        }
                    }
                },
                {
                    "travelDuration": "270s",
                    "travelDistanceMeters": 1902,
                    "waitDuration": "0s",
                    "totalDuration": "270s",
                    "startTime": "2024-07-08T16:35:32Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "2"
                        }
                    }
                },
                {
                    "travelDuration": "593s",
                    "travelDistanceMeters": 7110,
                    "waitDuration": "0s",
                    "totalDuration": "593s",
                    "startTime": "2024-07-08T16:50:02Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "1"
                        }
                    }
                },
                {
                    "travelDuration": "0s",
                    "waitDuration": "0s",
                    "totalDuration": "0s",
                    "startTime": "2024-07-08T17:09:55Z",
                    "vehicleLoads": {
                        "weight": {}
                    }
                }
            ],
            "routePolyline": {
                "points": "kdxkH|afnVDCDG@K?Ai@S?SAICWCa@Gc@B_@EWQ}@CMAKAME[AS?IAQAc@?g@?kA?_@@_@?[BsF?C?w@@w@?iEBc@D_GBiE@eD?u@?G?_@?y@@w@?U?S?W?S?m@?O?uB?[@mC@wC?e@@eB@mBA_FA_DA_@Ki@?g@@sG?u@@sG?w@@_K@yD?s@?_@?uE@}@?w@?q@@G@K?iF?kA?aE?w@?y@?mH?_@?sA?cA?oA?G?A?w@?{EAm@DKB_@@eB@kE?qE?mB?e@?U?U@oBAqA?AIi@AcB?}A?Y?i@?Y?i@?K?_@?W?]?W?_@?oA?_A?kB?eE?w@?mB?w@?]?uE?q@?K?mBFg@@_@?E?}@?_A@}A?cB?yA?Y?[AgA@s@?sE?c@Ae@Gi@@qA?qA?]?O?N?\\?pAApAIl@?x@A~A?`BAp@Ol@IVINMP[?i@?oB@mB?}A?k@?O?U?k@?wD@i@?g@?a@@Y?A?_@C]CI?UG[ISIMGMGOGOIOKIGIGKIWSWW[][]QSQAY_@a@i@OSA?QUQUSWQUCEKMSUQUOUQUQSU[OQIMGGOSUYMOQUMOU]QSQUe@m@QUQU_@e@CCOQUUWWe@[ECOGYKUEUEk@Ee@AS?C?u@@e@?a@@_@?E?m@@{@@o@?S@[?W?Q?e@@e@?_@?E?c@@]?C?e@@g@?Y@E?a@?s@@e@?c@@a@?O?_A@eA@k@@{@?{@@u@@_@?{@@m@?W@S?M?U?W@S?cA?cEDI?c@?[@i@?W@_@?[@Q?o@?a@@Q?e@@Y@M@M@YBQBa@FKBKBODMBKD[Hc@Re@Pc@XOJYPQJSLc@XWLOHQFOFIBYJSHMDMBQDSBUBWBk@@[?Y?O?C?K?Y?G?o@AUAS?_BCO?o@AY?k@AW?U?SAi@?]Ai@AW?k@AW?S?UA[[C?EAUCEAOCSCQEMCKEOGSMSUCEACCCCGEIAG?AAAAKAA?EACAM?KAAEMEOi@sBe@eAY]QUMEECQIUQ[K?`@BjA@RFtADv@BhAD`@B\\DTNv@Lj@DNFVFVLd@Nn@FZFT?BDTFf@?p@?ZALAJCTEPITELGLEFEFGHEFABABABAD?De@TMFMFWNKLGLKPEJGPM\\IRQn@Md@CNITA@ETEJAHANAZA^^CR@^B\\@d@B`@?N?L?J@F?|A?pAEtA?`Cm@t@QPE`AU?fD?b@?c@?gDaATQDu@PaCl@uA?qAD}A?G?KAM?O?a@?e@C]A_@CSA_@BIh@El@?X?B?hA?D?X@|@A^?b@@lBC`F?X?JCbKAzAAPEr@?BAr@?@?J?j@?h@?bA?d@Ab@ArBAvA?v@?t@?~@?p@?lECbA?lA?N@HBJ?v@?X?^Av@AfBAbCAjDA`ACtD?nBAhB?ZApAAnB?v@A~@MWKSAA@@JRLV@_A?w@@oB@qA?[@iB?oBBuD@aA@kD@cC@gB@w@?_@?Y?w@BG@E@EBQ?eA?i@BeD?M?aB@mABcDAcB?K?a@@a@QCi@IWICC_@OAAGEQKEE[Wg@q@c@w@Wg@ACIOOYO[q@mAYg@_@q@k@iAi@gASVi@dACF]h@A@CFKLABY^i@p@_@d@i@d@ABABQh@_BjAoA|@aAr@}@n@}@n@{@l@e@\\UNiAx@[TWP{@l@q@f@{@l@w@j@UP}@l@i@^e@\\oA|@YREB]V[Ty@j@c@\\[Ri@`@w@h@IHOLc@^e@f@]`@UZEFYd@QZMT_@x@Sf@ELGNQd@K\\IZGRI`@A@Kh@I`@IZG\\Qz@Kh@Or@Mx@UdAI`@SdAKb@ERGXEPI^GTGTWz@KVQf@KXMZO\\[p@S`@INOXILOTKLMPORILOPQRSVQPQPWTYXA@KHSP]XQNURA@URQNONURQP_@b@Y^SVCBOROVQTEHGL]j@O\\INWh@ABM\\M\\O`@ELITGTGNQl@K`@K`@K`@G^I`@EPCPO~@CTGb@ANCL?@Gj@C`@CPAPEh@A\\ATAZ?^?DA\\?`@@d@?b@@d@?^@L?X@~@BlA@bA@R@v@@jABhA?N?T@^@h@?`@@d@@`@?x@?d@?b@?r@?~@?^?n@AD?v@?l@?d@?zAAvA?F?z@@fA?j@?dA@bBAbA?lAAzA?r@?n@?R?H?h@?@A`A?l@?bA?j@A\\?V?n@?|@?l@AjA?j@?\\ArD?fA?p@?TAfA?hBCnBAlAAnAAz@AdAApAAp@?B?l@?^?fA?h@@X@tC@t@?P?P?H@t@@fA?x@@f@?p@OZA@?BExB?DCtACx@?@?n@A\\?B?X?nA?B@l@?p@?j@?XATA|@Ox@CLCDCDEHQTi@?cCDQPkA@wBCeAAA?iACO?w@?iB?_BCeACA?cBCS?A?wC@U?wAAq@Be@@]AMAIAICGCIEICGEGEIGiCcBuDcCcBkAkAu@gAo@qAy@On@It@Iz@INCH"
            },
            "metrics": {
                "performedShipmentCount": 4,
                "travelDuration": "1795s",
                "waitDuration": "0s",
                "delayDuration": "0s",
                "breakDuration": "0s",
                "visitDuration": "2400s",
                "totalDuration": "4195s",
                "travelDistanceMeters": 17863,
                "maxLoads": {
                    "weight": {
                        "amount": "4"
                    }
                }
            },
            "routeCosts": {
                "model.vehicles.cost_per_kilometer": 17.863
            },
            "routeTotalCost": 17.863
        }
    ],
    "metrics": {
        "aggregatedRouteMetrics": {
            "performedShipmentCount": 8,
            "travelDuration": "3624s",
            "waitDuration": "0s",
            "delayDuration": "0s",
            "breakDuration": "0s",
            "visitDuration": "4800s",
            "totalDuration": "8424s",
            "travelDistanceMeters": 28086,
            "maxLoads": {
                "weight": {
                    "amount": "4"
                }
            }
        },
        "usedVehicleCount": 2,
        "earliestVehicleStartTime": "2024-07-08T16:00:00Z",
        "latestVehicleEndTime": "2024-07-08T17:10:29Z",
        "totalCost": 28.086,
        "costs": {
            "model.vehicles.cost_per_kilometer": 28.086
        }
    }
}

As we hoped, both the downtown and north-vancouver regions are served by veh-cargobike and veh-sprintervan respectively. Of the remaining 2 deliveries in East Vancouver, one was assigned to veh-cargobike and the other to veh-sprintervan, depending on which was passing through.

Testing territory optimization soft constraints in GMPRO

At the beginning of this post, I mentioned that I prefer territory optimization with soft constraints because it allows drivers to assist in other territories when needed. Here's the same 8 shipment / 2 vehicle example but with a capacity constraint on veh-cargobike so that it can only do 2 deliveries instead of the original 5.

{
    "loadLimits": {
        "weight": {
            "maxLoad": 2
        }
    },
    "label": "veh-cargobike"
}

Input

{
  "model": {
    "shipments": [
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.31374169999999,
              "longitude": -123.0514387
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            50,
            0
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "nv789"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.343481,
              "longitude": -123.0863414
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            50,
            0
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "nv987"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.309186,
              "longitude": -123.033168
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            50,
            0
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "nv123"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2912037,
              "longitude": -123.1362474
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            0,
            50
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "dt123"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2786079,
              "longitude": -123.1141059
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            0,
            50
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "dt456"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2771031,
              "longitude": -123.1088116
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "costsPerVehicle": [
            0,
            50
        ],
        "costsPerVehicleIndices": [
            0,
            1
        ],
        "label": "dt789"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2812657,
              "longitude": -123.0280715
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "label": "ev123"
      },
      {
        "deliveries": [
          {
            "arrivalLocation": {
              "latitude": 49.2580983,
              "longitude": -123.1146481
            },
            "timeWindows": [
              {
                "startTime": "2024-07-08T16:00:00Z",
                "endTime": "2024-07-08T18:00:00Z"
              }
            ],
            "duration": "600s"
          }
        ],
        "loadDemands": {
          "weight": {
            "amount": 1
          }
        },
        "label": "ev456"
      }
    ],
    "vehicles": [
      {
        "travelMode": "DRIVING",
        "startLocation": {
          "latitude": 49.2668756,
          "longitude": -123.0800413
        },
        "startTimeWindows": [
          {
            "startTime": "2024-07-08T16:00:00Z"
          }
        ],
        "endTimeWindows": [
          {
            "endTime": "2024-07-08T18:00:00Z"
          }
        ],
        "loadLimits": {
          "weight": {
            "maxLoad": 2
          }
        },
        "label": "veh-cargobike",
        "costPerKilometer": 1
      },
      {
        "travelMode": "DRIVING",
        "startLocation": {
          "latitude": 49.2808422,
          "longitude": -123.0802082
        },
        "startTimeWindows": [
          {
            "startTime": "2024-07-08T16:00:00Z"
          }
        ],
        "endTimeWindows": [
          {
            "endTime": "2024-07-08T18:00:00Z"
          }
        ],
        "loadLimits": {
          "weight": {
            "maxLoad": 6
          }
        },
        "label": "veh-sprintervan",
        "costPerKilometer": 1
      }
    ],
    "globalStartTime": "2024-07-08T16:00:00Z",
    "globalEndTime": "2024-07-09T02:59:59Z"
  },
  "populatePolylines": true
}

Output

{
    "routes": [
        {
            "vehicleLabel": "veh-cargobike",
            "vehicleStartTime": "2024-07-08T16:00:00Z",
            "vehicleEndTime": "2024-07-08T16:39:54Z",
            "visits": [
                {
                    "shipmentIndex": 4,
                    "startTime": "2024-07-08T16:09:49Z",
                    "detour": "0s",
                    "shipmentLabel": "dt456",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 3,
                    "startTime": "2024-07-08T16:29:54Z",
                    "detour": "728s",
                    "shipmentLabel": "dt123",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                }
            ],
            "transitions": [
                {
                    "travelDuration": "589s",
                    "travelDistanceMeters": 3955,
                    "waitDuration": "0s",
                    "totalDuration": "589s",
                    "startTime": "2024-07-08T16:00:00Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "2"
                        }
                    }
                },
                {
                    "travelDuration": "605s",
                    "travelDistanceMeters": 2737,
                    "waitDuration": "0s",
                    "totalDuration": "605s",
                    "startTime": "2024-07-08T16:19:49Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "1"
                        }
                    }
                },
                {
                    "travelDuration": "0s",
                    "waitDuration": "0s",
                    "totalDuration": "0s",
                    "startTime": "2024-07-08T16:39:54Z",
                    "vehicleLoads": {
                        "weight": {}
                    }
                }
            ],
            "routePolyline": {
                "points": "ijukHdbfnV[pDG|@Z?J?FA|@EHAV?J?xA@`@@?TAVCXCVUfBOxAQtAOvAALOpACN?@CNQ`B]~CK|@OpAIv@QbBEXEXIt@?DCNShBQtA?BABK~@EXCXA`@A\\ATAV?@?T?N?tC?v@At@?bB?b@A\\?@?f@?J?lAAb@?`B?z@?nAA\\?xC?t@?f@A`@AVAPE`@CNCPAHI^ADEVI\\KZKZKXCBKRyA|CcAtBm@t@}@lBa@z@MTIRITKX?@ERK`@qBEw@EM?QAQ?s@Ca@?WCEAOAOEKAGCKE[Oo@YOG[OQGMCKCKAWCM?eBE]Ai@AM?m@AG?cACE?iCEkAAUAsFIsBEY?m@Aa@Au@A@b@BlB?R@~@BbB?l@@t@@v@@\\?Z?@@t@@J@t@?N@X?@?Z?X?t@?H?XAv@Al@Cv@Ad@Eh@GdAC`@OdBCLGb@GXWtAS~@Qn@IVGRA@CFWt@a@~@IV]l@[f@c@|@[h@IPMRUb@INIRIJGHIJc@t@CFA@AH_@p@i@|@s@nAfAnB~AnCZf@\\h@d@x@DHXf@x@tAd@x@^p@Xd@~@~AfB`Dj@~@MRQXa@p@Wf@[n@w@pAMX_AbB}@|Ag@|@[h@a@r@EHQXm@fAQX}@~A_A`BMVEFo@nA{@|AOVQX[f@Yd@S\\INEFEF}@~AeAnBMROREHGHEFA@GFGHIFGFIDIFIDGDE@UH_@JQHQFQHOLMJA@KLC@MRORQZdAjB\\h@Zj@NPw@pAm@hAs@lAS`@iB`Ds@lA?@Yd@_CbEGNo@jAoBhDAAAACAA??@C?AB?BAB?B?B?B@BQZWf@s@lAYf@Wf@mArB?@w@tAUZy@|Ao@jAa@r@u@rAEHm@dA}CrFeAeBgAkBk@aAYg@Wf@"
            },
            "metrics": {
                "performedShipmentCount": 2,
                "travelDuration": "1194s",
                "waitDuration": "0s",
                "delayDuration": "0s",
                "breakDuration": "0s",
                "visitDuration": "1200s",
                "totalDuration": "2394s",
                "travelDistanceMeters": 6692,
                "maxLoads": {
                    "weight": {
                        "amount": "2"
                    }
                }
            },
            "routeCosts": {
                "model.vehicles.cost_per_kilometer": 6.692
            },
            "routeTotalCost": 6.692
        },
        {
            "vehicleIndex": 1,
            "vehicleLabel": "veh-sprintervan",
            "vehicleStartTime": "2024-07-08T16:00:00Z",
            "vehicleEndTime": "2024-07-08T17:53:47Z",
            "visits": [
                {
                    "shipmentIndex": 7,
                    "startTime": "2024-07-08T16:14:02Z",
                    "detour": "0s",
                    "shipmentLabel": "ev456",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 5,
                    "startTime": "2024-07-08T16:29:52Z",
                    "detour": "1339s",
                    "shipmentLabel": "dt789",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 6,
                    "startTime": "2024-07-08T16:52:52Z",
                    "detour": "2632s",
                    "shipmentLabel": "ev123",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 2,
                    "startTime": "2024-07-08T17:09:24Z",
                    "detour": "3373s",
                    "shipmentLabel": "nv123",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "startTime": "2024-07-08T17:23:54Z",
                    "detour": "4210s",
                    "shipmentLabel": "nv789",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                },
                {
                    "shipmentIndex": 1,
                    "startTime": "2024-07-08T17:43:47Z",
                    "detour": "5187s",
                    "shipmentLabel": "nv987",
                    "loadDemands": {
                        "weight": {
                            "amount": "-1"
                        }
                    }
                }
            ],
            "transitions": [
                {
                    "travelDuration": "842s",
                    "travelDistanceMeters": 5856,
                    "waitDuration": "0s",
                    "totalDuration": "842s",
                    "startTime": "2024-07-08T16:00:00Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "6"
                        }
                    }
                },
                {
                    "travelDuration": "350s",
                    "travelDistanceMeters": 2313,
                    "waitDuration": "0s",
                    "totalDuration": "350s",
                    "startTime": "2024-07-08T16:24:02Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "5"
                        }
                    }
                },
                {
                    "travelDuration": "780s",
                    "travelDistanceMeters": 6366,
                    "waitDuration": "0s",
                    "totalDuration": "780s",
                    "startTime": "2024-07-08T16:39:52Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "4"
                        }
                    }
                },
                {
                    "travelDuration": "392s",
                    "travelDistanceMeters": 5041,
                    "waitDuration": "0s",
                    "totalDuration": "392s",
                    "startTime": "2024-07-08T17:02:52Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "3"
                        }
                    }
                },
                {
                    "travelDuration": "270s",
                    "travelDistanceMeters": 1902,
                    "waitDuration": "0s",
                    "totalDuration": "270s",
                    "startTime": "2024-07-08T17:19:24Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "2"
                        }
                    }
                },
                {
                    "travelDuration": "593s",
                    "travelDistanceMeters": 7110,
                    "waitDuration": "0s",
                    "totalDuration": "593s",
                    "startTime": "2024-07-08T17:33:54Z",
                    "vehicleLoads": {
                        "weight": {
                            "amount": "1"
                        }
                    }
                },
                {
                    "travelDuration": "0s",
                    "waitDuration": "0s",
                    "totalDuration": "0s",
                    "startTime": "2024-07-08T17:53:47Z",
                    "vehicleLoads": {
                        "weight": {}
                    }
                }
            ],
            "routePolyline": {
                "points": "kdxkH|afnVDCDG@K?A?G@GDIBAJ?b@A?eA@eABCBC@C@G@E@E@_B@w@BeD?u@vA@|@?ZBvA@vAB|A@|A?vA?vAD|A@rA@xDBd@?t@@tA?jB?lA@tEBjB@R@fA@f@BRFNBV@h@Bh@B|@@^@vA@vA?r@ERALCPI`@?j@?xD@`@@r@@f@@Z?^@R?T?Z?J@N?V?n@@v@BpABr@?\\?H?PHn@BfA@zADBAX@x@??nAAtB?`D?n@Fh@Ar@?r@C`EAdK?r@CnI?`@?J?f@?fAA^?l@@hAKZ?@?@Af@?vA?tB?jB?jAP@zA@JADA@?bA@zAAP@x@?H@zA@|A?xA@@?vA@zA@rA@VDR@Z@@@DBFBBBNHDBNHTJb@`@f@b@VPVBXD?j@CzA?bAAv@?JA`BAnAAlC?n@AtBC`D?x@CxAAjA?dAAxA?xB?bB?dDClB?zAAbC?lB?h@@hAApA?fA?LCfFAzDAvCAl@CxCEtE?V?RGvFGrFC`EAfBAbCAbCCfCEpEA^APAr@?t@{A?}AAs@@c@@@kAAjAQ?g@Ac@?qCGQCA?g@CkB@KAQ@uAAgAAaACQ?M?y@GQA[Ac@Au@AIAc@Am@CUAo@?eB?aBAsAEkAAq@DU@c@?S?m@@e@@G?I?K?cA?}@B_AB]B]@o@@o@B_BBG@q@@kA@G?e@GsDGeAA_@?C?E?E?QAQ?C?yAEqACYAW?i@Ag@E]?]AWMMEMAIAMAA?OACAKAIAEAEAKESKMGKKKKEEACACIKMWGMEKEOGQCGq@_CKWEOEMCICKGMGMGEMKq@mBIUIUACGQGM?AIQi@wAEIKU_@aAEMGOUi@O_@k@wAMa@Si@M[IQIWGQkAN?IBe@?]@O?O?W@w@DyF?I?K?_@@_@?u@@mB?e@@O@yD@w@?K@i@?Y?{@?i@?e@?kA@k@?Y@gA@oA?o@?I@I@cD?a@?c@@S?_@?q@@]@]?A@[@]B]D[D]D[ZsBD_@D]D_@D]@eB?_@A]@qADmK@mB@w@B{E@mB@mB@w@@sB?u@B}C?G@mB@qB?Y@YD_M?Q?M?K?M?I?IAK?EAI?OCSCSCS?AEQESEQCMESCOAGCOAEAMAGAI?GAI?IAK?G?KAK?K?K?M?QBwD?_ABaC?kC@kC@gC@{B?u@?cB@mBBkDByG?g@?k@@_C@wB?y@@kE@eEByD?U?u@?w@@cB?_@@gA?}@?qG?]?W?[?YuAAA?yA?wA?oA@G?]?}@Ay@Ae@?eACO?gA?S@_ACW?wACA_FA_DA_@Ki@?g@@sG?u@@sG?w@@_K@yD?s@?_@?uE@}@?w@?q@@G@K?iF?kA?aE?w@?y@?mH?_@?sA?cA?oA?G?A?w@?{EAm@DKB_@@eB@kE?qE?mB?e@?U?U@oBAqA?AIi@AcB?}A?Y?i@?Y?i@?K?_@?W?]?W?_@?oA?_A?kB?eE?w@?mB?w@?]?uE?q@?K?mBFg@@_@?E?}@?_A@}A?cB?yA?Y?[AgA@s@?sE?c@Ae@Gi@@qA?qA?]?O?N?\\?pAApAIl@?x@A~A?`BAp@Ol@IVINMP[?i@?oB@mB?}A?k@?O?U?k@?wD@i@?g@?a@@Y?A?_@C]CI?UG[ISIMGMGOGOIOKIGIGKIWSWW[][]QSQAY_@a@i@OSA?QUQUSWQUCEKMSUQUOUQUQSU[OQIMGGOSUYMOQUMOU]QSQUe@m@QUQU_@e@CCOQUUWWe@[ECOGYKUEUEk@Ee@AS?C?u@@e@?a@@_@?E?m@@{@@o@?S@[?W?Q?e@@e@?_@?E?c@@]?C?e@@g@?Y@E?a@?s@@e@?c@@a@?O?_A@eA@k@@{@?{@@u@@_@?{@@m@?W@S?M?U?W@S?cA?cEDI?c@?[@i@?W@_@?[@Q?o@?a@@Q?e@@Y@M@M@YBQBa@FKBKBODMBKD[Hc@Re@Pc@XOJYPQJSLc@XWLOHQFOFIBYJSHMDMBQDSBUBWBk@@[?Y?O?C?K?Y?G?o@AUAS?_BCO?o@AY?k@AW?U?SAi@?]Ai@AW?k@AW?S?UA[[C?EAUCEAOCSCQEMCKEOGSMSUCEACCCCGEIAG?AAAAKAA?EACAM?KAAEMEOi@sBe@eAY]QUMEECQIUQ[K?`@BjA@RFtADv@BhAD`@B\\DTNv@Lj@DNFVFVLd@Nn@FZFT?BDTFf@?p@?ZALAJCTEPITELGLEFEFGHEFABABABAD?De@TMFMFWNKLGLKPEJGPM\\IRQn@Md@CNITA@ETEJAHANAZA^^CR@^B\\@d@B`@?N?L?J@F?|A?pAEtA?`Cm@t@QPE`AU?fD?b@?c@?gDaATQDu@PaCl@uA?qAD}A?G?KAM?O?a@?e@C]A_@CSA_@BIh@El@?X?B?hA?D?X@|@A^?b@@lBC`F?X?JCbKAzAAPEr@?BAr@?@?J?j@?h@?bA?d@Ab@ArBAvA?v@?t@?~@?p@?lECbA?lA?N@HBJ?v@?X?^Av@AfBAbCAjDA`ACtD?nBAhB?ZApAAnB?v@A~@MWKSAA@@JRLV@_A?w@@oB@qA?[@iB?oBBuD@aA@kD@cC@gB@w@?_@?Y?w@BG@E@EBQ?eA?i@BeD?M?aB@mABcDAcB?K?a@@a@QCi@IWICC_@OAAGEQKEE[Wg@q@c@w@Wg@ACIOOYO[q@mAYg@_@q@k@iAi@gASVi@dACF]h@A@CFKLABY^i@p@_@d@i@d@ABABQh@_BjAoA|@aAr@}@n@}@n@{@l@e@\\UNiAx@[TWP{@l@q@f@{@l@w@j@UP}@l@i@^e@\\oA|@YREB]V[Ty@j@c@\\[Ri@`@w@h@IHOLc@^e@f@]`@UZEFYd@QZMT_@x@Sf@ELGNQd@K\\IZGRI`@A@Kh@I`@IZG\\Qz@Kh@Or@Mx@UdAI`@SdAKb@ERGXEPI^GTGTWz@KVQf@KXMZO\\[p@S`@INOXILOTKLMPORILOPQRSVQPQPWTYXA@KHSP]XQNURA@URQNONURQP_@b@Y^SVCBOROVQTEHGL]j@O\\INWh@ABM\\M\\O`@ELITGTGNQl@K`@K`@K`@G^I`@EPCPO~@CTGb@ANCL?@Gj@C`@CPAPEh@A\\ATAZ?^?DA\\?`@@d@?b@@d@?^@L?X@~@BlA@bA@R@v@@jABhA?N?T@^@h@?`@@d@@`@?x@?d@?b@?r@?~@?^?n@AD?v@?l@?d@?zAAvA?F?z@@fA?j@?dA@bBAbA?lAAzA?r@?n@?R?H?h@?@A`A?l@?bA?j@A\\?V?n@?|@?l@AjA?j@?\\ArD?fA?p@?TAfA?hBCnBAlAAnAAz@AdAApAAp@?B?l@?^?fA?h@@X@tC@t@?P?P?H@t@@fA?x@@f@?p@OZA@?BExB?DCtACx@?@?n@A\\?B?X?nA?B@l@?p@?j@?XATA|@Ox@CLCDCDEHQTi@?cCDQPkA@wBCeAAA?iACO?w@?iB?_BCeACA?cBCS?A?wC@U?wAAq@Be@@]AMAIAICGCIEICGEGEIGiCcBuDcCcBkAkAu@gAo@qAy@On@It@Iz@INCH"
            },
            "metrics": {
                "performedShipmentCount": 6,
                "travelDuration": "3227s",
                "waitDuration": "0s",
                "delayDuration": "0s",
                "breakDuration": "0s",
                "visitDuration": "3600s",
                "totalDuration": "6827s",
                "travelDistanceMeters": 28588,
                "maxLoads": {
                    "weight": {
                        "amount": "6"
                    }
                }
            },
            "routeCosts": {
                "model.shipments.costs_per_vehicle": 50,
                "model.vehicles.cost_per_kilometer": 28.588
            },
            "routeTotalCost": 78.588
        }
    ],
    "metrics": {
        "aggregatedRouteMetrics": {
            "performedShipmentCount": 8,
            "travelDuration": "4421s",
            "waitDuration": "0s",
            "delayDuration": "0s",
            "breakDuration": "0s",
            "visitDuration": "4800s",
            "totalDuration": "9221s",
            "travelDistanceMeters": 35280,
            "maxLoads": {
                "weight": {
                    "amount": "6"
                }
            }
        },
        "usedVehicleCount": 2,
        "earliestVehicleStartTime": "2024-07-08T16:00:00Z",
        "latestVehicleEndTime": "2024-07-08T17:53:47Z",
        "totalCost": 85.28,
        "costs": {
            "model.vehicles.cost_per_kilometer": 35.28,
            "model.shipments.costs_per_vehicle": 50
        }
    }
}

Due to the capacity constraint, veh-sprintervan (orange) is assigned to make its first two stops in the downtown area to assist with deliveries that veh-cargobike cannot handle.

Soft constraints allow vehicles from different territories to assist, if needed
Soft constraints allow vehicles from different territories to assist, if needed

Conclusion

So that's how you build a territory optimization system in GMPRO. The same soft constraint approach can be used to preferentially assign bookings to drivers with high ratings (ride share), prioritize high margin deliveries (third party logistics) as well as match jobs to technicians optimally (field services). Please contact [email protected] if you need specific help modelling your business operations with GMPRO.

👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn.

Next: Part 7: GMPRO docs: Solving the VRP with route clustering and soft constraints