Unexpected behaviour of preparation_time

Hi,

We found a behaviour of the route optimization API and we’re not sure if it’s expected or not. We were expecting that preparation_time would be incurred when visiting a “unique” location but we were expecting this “identity” to come from the location_id given in the address. It appears instead that it doesn’t use location_id and instead internally generates a key based on the latitude & longitude.

E.g. in this request:

{
  "configuration": {
    "routing": {
      "calc_points": true,
      "consider_traffic": false,
      "return_snapped_waypoints": true,
      "network_data_provider": "openstreetmap",
      "snap_preventions": [
        "motorway",
        "tunnel",
        "ferry"
      ],
      "fail_fast": false
    },
    "optimization": {
      "free_insertion": false
    }
  },
  "vehicle_types": [
    {
      "type_id": "vhc_wENrMxxkSabf99cynXcX",
      "profile": "car_delivery",
      "capacity": [
        5000
      ],
      "speed_factor": 1,
      "service_time_factor": 1
    }
  ],
  "vehicles": [
    {
      "type_id": "vhc_wENrMxxkSabf99cynXcX",
      "vehicle_id": "wENrMxxkSabf99cynXcX",
      "start_address": {
        "location_id": "start_address_resolved_from_planDriver_ChIJ2Ul6wooxGQ0RkLIc_0fucmI",
        "lat": 38.77761,
        "lon": -9.099499999999999,
        "street_hint": "Moscavide",
        "curbside": "any",
        "name": "Moscavide"
      },
      "end_address": {
        "location_id": "start_address_resolved_from_planDriver_ChIJ2Ul6wooxGQ0RkLIc_0fucmI_end_from_roundtrip_start",
        "lat": 38.77761,
        "lon": -9.099499999999999,
        "street_hint": "Moscavide",
        "curbside": "any",
        "name": "Moscavide"
      },
      "return_to_depot": true,
      "earliest_start": 1778482800,
      "min_jobs": 1
    }
  ],
  "objectives": [
    {
      "type": "min",
      "value": "completion_time"
    }
  ],
  "services": [],
  "shipments": [
    {
      "id": "Jo89FemllIhggFowUbEf",
      "name": "R. Fernando Caldeira 18",
      "allowed_vehicles": [
        "wENrMxxkSabf99cynXcX"
      ],
      "size": [
        0
      ],
      "priority": 2,
      "pickup": {
        "address": {
          "location_id": "yJWfJBtrhD8cG8XswIxi",
          "lat": 38.7429855,
          "lon": -9.138266999999999,
          "street_hint": "Avenida de Roma",
          "curbside": "any",
          "name": "Av. de Roma 10"
        },
        "preparation_time": 60,
        "duration": 0,
        "time_windows": []
      },
      "delivery": {
        "address": {
          "location_id": "Jo89FemllIhggFowUbEf",
          "lat": 38.7529169,
          "lon": -9.1471198,
          "street_hint": "Rua Fernando Caldeira",
          "curbside": "any",
          "name": "R. Fernando Caldeira 18"
        },
        "duration": 60,
        "time_windows": []
      }
    },
    {
      "id": "L1sgwA1fi08h06Mi3oRd",
      "name": "R. Saraiva de Carvalho 2",
      "allowed_vehicles": [
        "wENrMxxkSabf99cynXcX"
      ],
      "size": [
        4000
      ],
      "priority": 2,
      "pickup": {
        "address": {
          "location_id": "s3Zh3T6ehowBU6X5LlcK",
          "lat": 38.7054431,
          "lon": -9.1676617,
          "street_hint": "Avenida Infante Santo",
          "curbside": "any",
          "name": "Av. Infante Santo 10"
        },
        "preparation_time": 60,
        "duration": 0,
        "time_windows": []
      },
      "delivery": {
        "address": {
          "location_id": "L1sgwA1fi08h06Mi3oRd",
          "lat": 38.7141643,
          "lon": -9.1654159,
          "street_hint": "Rua Saraiva de Carvalho",
          "curbside": "any",
          "name": "R. Saraiva de Carvalho 2"
        },
        "duration": 60,
        "time_windows": []
      }
    },
    {
      "id": "XLYW1EamTORGCV9jI6yu",
      "name": "R. Alfredo Cortês 10",
      "allowed_vehicles": [
        "wENrMxxkSabf99cynXcX"
      ],
      "size": [
        0
      ],
      "priority": 2,
      "pickup": {
        "address": {
          "location_id": "yJWfJBtrhD8cG8XswIxi",
          "lat": 38.7429855,
          "lon": -9.138266999999999,
          "street_hint": "Avenida de Roma",
          "curbside": "any",
          "name": "Av. de Roma 10"
        },
        "preparation_time": 60,
        "duration": 0,
        "time_windows": []
      },
      "delivery": {
        "address": {
          "location_id": "XLYW1EamTORGCV9jI6yu",
          "lat": 38.7460594,
          "lon": -9.1451633,
          "street_hint": "Rua Alfredo Cortês",
          "curbside": "any",
          "name": "R. Alfredo Cortês 10"
        },
        "duration": 60,
        "time_windows": []
      }
    },
    {
      "id": "XY0djSsR2c0XlkSc1NnV",
      "name": "R. Cel. Marques Leitão 18",
      "allowed_vehicles": [
        "wENrMxxkSabf99cynXcX"
      ],
      "size": [
        0
      ],
      "priority": 2,
      "pickup": {
        "address": {
          "location_id": "yJWfJBtrhD8cG8XswIxi",
          "lat": 38.7429855,
          "lon": -9.138266999999999,
          "street_hint": "Avenida de Roma",
          "curbside": "any",
          "name": "Av. de Roma 10"
        },
        "preparation_time": 60,
        "duration": 0,
        "time_windows": []
      },
      "delivery": {
        "address": {
          "location_id": "XY0djSsR2c0XlkSc1NnV",
          "lat": 38.7574635,
          "lon": -9.1392615,
          "street_hint": "Rua Coronel Marques Leitão",
          "curbside": "any",
          "name": "R. Cel. Marques Leitão 18"
        },
        "duration": 60,
        "time_windows": []
      }
    },
    {
      "id": "f6w5nmQ2XDrZjpQG8Geo",
      "name": "R. Augusta 14",
      "allowed_vehicles": [
        "wENrMxxkSabf99cynXcX"
      ],
      "size": [
        0
      ],
      "priority": 2,
      "pickup": {
        "address": {
          "location_id": "yJWfJBtrhD8cG8XswIxi",
          "lat": 38.7429855,
          "lon": -9.138266999999999,
          "street_hint": "Avenida de Roma",
          "curbside": "any",
          "name": "Av. de Roma 10"
        },
        "preparation_time": 60,
        "duration": 0,
        "time_windows": []
      },
      "delivery": {
        "address": {
          "location_id": "f6w5nmQ2XDrZjpQG8Geo",
          "lat": 38.7087273,
          "lon": -9.1368209,
          "street_hint": "Rua Augusta",
          "curbside": "any",
          "name": "R. Augusta 14"
        },
        "duration": 60,
        "time_windows": []
      }
    },
    {
      "id": "gu7TdBgwPjb6M5KAe3eE",
      "name": "R. Ferreira Borges 16",
      "allowed_vehicles": [
        "wENrMxxkSabf99cynXcX"
      ],
      "size": [
        0
      ],
      "priority": 2,
      "pickup": {
        "address": {
          "location_id": "s3Zh3T6ehowBU6X5LlcK",
          "lat": 38.7054431,
          "lon": -9.1676617,
          "street_hint": "Avenida Infante Santo",
          "curbside": "any",
          "name": "Av. Infante Santo 10"
        },
        "preparation_time": 60,
        "duration": 0,
        "time_windows": []
      },
      "delivery": {
        "address": {
          "location_id": "gu7TdBgwPjb6M5KAe3eE",
          "lat": 38.7161458,
          "lon": -9.1637826,
          "street_hint": "Rua Ferreira Borges",
          "curbside": "any",
          "name": "R. Ferreira Borges 16"
        },
        "duration": 60,
        "time_windows": []
      }
    },
    {
      "id": "hrWrCxA4aQ3uXaksUSqA",
      "name": "R. Coelho da Rocha 20",
      "allowed_vehicles": [
        "wENrMxxkSabf99cynXcX"
      ],
      "size": [
        0
      ],
      "priority": 2,
      "pickup": {
        "address": {
          "location_id": "s3Zh3T6ehowBU6X5LlcK",
          "lat": 38.7054431,
          "lon": -9.1676617,
          "street_hint": "Avenida Infante Santo",
          "curbside": "any",
          "name": "Av. Infante Santo 10"
        },
        "preparation_time": 60,
        "duration": 0,
        "time_windows": []
      },
      "delivery": {
        "address": {
          "location_id": "hrWrCxA4aQ3uXaksUSqA",
          "lat": 38.7166946,
          "lon": -9.1627154,
          "street_hint": "Rua Coelho da Rocha",
          "curbside": "any",
          "name": "R. Coelho da Rocha 20"
        },
        "duration": 60,
        "time_windows": []
      }
    },
    {
      "id": "lrZEeJrc1FymYGarkgOr",
      "name": "R. Tomás da Anunciação 10",
      "allowed_vehicles": [
        "wENrMxxkSabf99cynXcX"
      ],
      "size": [
        4000
      ],
      "priority": 2,
      "pickup": {
        "address": {
          "location_id": "depot",
          "lat": 38.7574635,
          "lon": -9.1392615,
          "street_hint": "Rua Coronel Marques Leitão",
          "curbside": "any",
          "name": "Rua Coronel Marques Leitão 18"
        },
        "preparation_time": 3600,
        "duration": 0,
        "time_windows": []
      },
      "delivery": {
        "address": {
          "location_id": "lrZEeJrc1FymYGarkgOr",
          "lat": 38.7960832,
          "lon": -9.1812246,
          "street_hint": "Rua Tomás da Anunciação",
          "curbside": "any",
          "name": "R. Tomás da Anunciação 10"
        },
        "duration": 60,
        "time_windows": []
      }
    }
  ],
  "relations": []
}

It errors out with:

{
  "message": "JSON input could be read, but there has been an error to create the problem e47a634c-1a78-4a50-b19a-070f27b06152",
  "hints": [
    {
      "message": "Location ACTIVITY_[x=-9.139262][y=38.757464]_any has been assigned two different preparation times (3600.0 and 0.0), but must have only one preparation time. Note that if no preparation time value has been assigned, the default value is 0.0.",
      "details": "class java.lang.IllegalArgumentException"
    }
  ],
  "status": "finished"
}

We have 2 pickup locations that have the same coordinates but different location_id and were under the impression that this would be okay. Is it intentional that it ignores location_id as part of this comparison?

The preparation_time is bound to the physical location (coordinates), not the location_id. Therefore it is only incurred once per physical location arrival (e.g., parking, finding the entrance). This is by design.

The error you’re seeing is because the same coordinates appear as both a delivery (with no preparation_time, defaulting to 0) and a pickup (with preparation_time: 3600), which creates a conflict.

Would it help if we took the maximum of all preparation times at a unique location instead of returning this error?

Would it help if we took the maximum of all preparation times at a unique location instead of returning this error?

I don’t think this would help in our case. Essentially we’re modelling the deliveries as individual dropoffs that take a couple minutes each, but a pickup is a “batch” operation where it could take 1 hour to load everything at the depot. That’s why we’re using preparation_time over duration for pickups, but only duration for deliveries. It is possible that there is an address that needs pickups and deliveries, but we’d still like to maintain the same model. I.e. delivering something to the depot mid-route shouldn’t take 1 hour, but loading all the pickups at the depot should take 1 hour. So I don’t think taking the maximum preparation_time would work here.

I guess as a workaround we could “wiggle” the coordinates a little bit to make them not equal (it seems you’re rounding to 6 decimal places so there should be enough room without losing too much precision). Though ideally I think it would be great if there was either an option or a new field or something that we could provide that could be included as part of the “key” that is used to determine when the preparation_time cost is incurred.

Ah, understand. And yes, changing coordinates is a workaround. Let me think this through and I will come up with a more elegant solution.