From 5c5492ca30bc5d1d33c9ed7f9aab11db155a8b0f Mon Sep 17 00:00:00 2001 From: Ondrej Sykora Date: Mon, 6 Nov 2023 20:19:25 +0000 Subject: [PATCH] Add detours to the merged response. For shipments picked up/delivered directly, the detours are taken from the global model and they have the usual meaning. For shipments delivered from a parking location, the detour is computed as the sum of the detour in the global model (the extra time spent globally on the way to the parking) + the detour in the local model (the extra time spent locally in the parking). For the virtual visits, the detours are defined as: - for the arrival, it is the time difference compared to driving straight to the parking (the detour of the parking visit in the global model). - for the departure, it is the time spent at the parking (the duration of the local route). --- python/cfr/json/cfr_json.py | 10 ++- .../small/expected_merged_response.json | 62 +++++++++++++------ ...erged_response_with_skipped_shipments.json | 45 +++++++++----- .../cfr/two_step_routing/two_step_routing.py | 57 ++++++++++++----- 4 files changed, 121 insertions(+), 53 deletions(-) diff --git a/python/cfr/json/cfr_json.py b/python/cfr/json/cfr_json.py index 6404d933..0c99e5eb 100644 --- a/python/cfr/json/cfr_json.py +++ b/python/cfr/json/cfr_json.py @@ -253,8 +253,9 @@ class ShipmentRoute(TypedDict, total=False): vehicleIndex: int vehicleLabel: str - vehicleStartTime: str - vehicleEndTime: str + vehicleStartTime: TimeString + vehicleEndTime: TimeString + vehicleDetour: DurationString visits: list[Visit] transitions: list[Transition] @@ -466,6 +467,11 @@ def get_visit_request_duration( return parse_duration_string(visit_request.get("duration")) +def get_visit_detour(visit: Visit) -> datetime.timedelta: + """Returns the detour of a visit on a route.""" + return parse_duration_string(visit.get("detour", "0s")) + + def get_global_start_time(model: ShipmentModel) -> datetime.datetime: """Returns the global start time of `model`.""" global_start_time = model.get("globalStartTime") diff --git a/python/cfr/two_step_routing/testdata/small/expected_merged_response.json b/python/cfr/two_step_routing/testdata/small/expected_merged_response.json index 8e8b9edc..1db37349 100644 --- a/python/cfr/two_step_routing/testdata/small/expected_merged_response.json +++ b/python/cfr/two_step_routing/testdata/small/expected_merged_response.json @@ -5,6 +5,7 @@ "vehicleLabel": "V001", "vehicleStartTime": "2023-08-11T08:00:00Z", "vehicleEndTime": "2023-08-11T16:00:00Z", + "vehicleDetour": "28800s", "visits": [ { "startTime": "2023-08-11T08:07:01Z", @@ -37,67 +38,80 @@ { "shipmentIndex": 9, "shipmentLabel": "P001 arrival", - "startTime": "2023-08-11T13:51:14Z" + "startTime": "2023-08-11T13:51:14Z", + "detour": "20574s" }, { "shipmentIndex": 3, "shipmentLabel": "S004", - "startTime": "2023-08-11T14:00:00Z" + "startTime": "2023-08-11T14:00:00Z", + "detour": "20574s" }, { "shipmentIndex": 10, "shipmentLabel": "P001 departure", - "startTime": "2023-08-11T14:05:37Z" + "startTime": "2023-08-11T14:05:37Z", + "detour": "863s" }, { "shipmentIndex": 11, "shipmentLabel": "P001 arrival", - "startTime": "2023-08-11T14:05:37Z" + "startTime": "2023-08-11T14:05:37Z", + "detour": "21437s" }, { "shipmentIndex": 0, "shipmentLabel": "S001", - "startTime": "2023-08-11T14:08:11Z" + "startTime": "2023-08-11T14:08:11Z", + "detour": "21437s" }, { "shipmentIndex": 1, "shipmentLabel": "S002", - "startTime": "2023-08-11T14:16:22Z" + "startTime": "2023-08-11T14:16:22Z", + "detour": "21556s" }, { "shipmentIndex": 12, "shipmentLabel": "P001 departure", - "startTime": "2023-08-11T14:23:29Z" + "startTime": "2023-08-11T14:23:29Z", + "detour": "1072s" }, { "shipmentIndex": 13, "shipmentLabel": "P001 arrival", - "startTime": "2023-08-11T14:23:29Z" + "startTime": "2023-08-11T14:23:29Z", + "detour": "22509s" }, { "shipmentIndex": 2, "shipmentLabel": "S003", - "startTime": "2023-08-11T14:32:15Z" + "startTime": "2023-08-11T14:32:15Z", + "detour": "22509s" }, { "shipmentIndex": 14, "shipmentLabel": "P001 departure", - "startTime": "2023-08-11T14:37:52Z" + "startTime": "2023-08-11T14:37:52Z", + "detour": "863s" }, { "shipmentIndex": 15, "shipmentLabel": "P002 arrival", - "startTime": "2023-08-11T14:40:52Z" + "startTime": "2023-08-11T14:40:52Z", + "detour": "23372s" }, { "shipmentIndex": 6, "shipmentLabel": "S007", - "startTime": "2023-08-11T14:43:22Z" + "startTime": "2023-08-11T14:43:22Z", + "detour": "23372s" }, { "shipmentIndex": 16, "shipmentLabel": "P002 departure", - "startTime": "2023-08-11T14:48:21Z" + "startTime": "2023-08-11T14:48:21Z", + "detour": "449s" } ], "transitions": [ @@ -339,41 +353,49 @@ "vehicleLabel": "V002", "vehicleStartTime": "2023-08-11T08:00:00Z", "vehicleEndTime": "2023-08-11T20:00:00Z", + "vehicleDetour": "43200s", "visits": [ { "shipmentIndex": 17, "shipmentLabel": "P002 arrival", - "startTime": "2023-08-11T08:10:53Z" + "startTime": "2023-08-11T08:10:53Z", + "detour": "0s" }, { "shipmentIndex": 7, "shipmentLabel": "S008", - "startTime": "2023-08-11T08:13:23Z" + "startTime": "2023-08-11T08:13:23Z", + "detour": "0s" }, { "shipmentIndex": 18, "shipmentLabel": "P002 departure", - "startTime": "2023-08-11T08:18:22Z" + "startTime": "2023-08-11T08:18:22Z", + "detour": "449s" }, { "shipmentIndex": 19, "shipmentLabel": "P002 arrival", - "startTime": "2023-08-11T08:19:22Z" + "startTime": "2023-08-11T08:19:22Z", + "detour": "482s" }, { "shipmentIndex": 5, "shipmentLabel": "S006", - "startTime": "2023-08-11T08:21:52Z" + "startTime": "2023-08-11T08:21:52Z", + "detour": "482s" }, { "shipmentIndex": 4, "shipmentLabel": "S005", - "startTime": "2023-08-11T08:24:23Z" + "startTime": "2023-08-11T08:24:23Z", + "detour": "633s" }, { "shipmentIndex": 20, "shipmentLabel": "P002 departure", - "startTime": "2023-08-11T08:29:22Z" + "startTime": "2023-08-11T08:29:22Z", + "detour": "600s" } ], "transitions": [ diff --git a/python/cfr/two_step_routing/testdata/small/expected_merged_response_with_skipped_shipments.json b/python/cfr/two_step_routing/testdata/small/expected_merged_response_with_skipped_shipments.json index f7892e6b..84ced69e 100644 --- a/python/cfr/two_step_routing/testdata/small/expected_merged_response_with_skipped_shipments.json +++ b/python/cfr/two_step_routing/testdata/small/expected_merged_response_with_skipped_shipments.json @@ -202,77 +202,92 @@ { "shipmentIndex": 9, "shipmentLabel": "P002 arrival", - "startTime": "2023-08-11T15:05:44Z" + "startTime": "2023-08-11T15:05:44Z", + "detour": "0s" }, { "shipmentIndex": 7, "shipmentLabel": "S008", - "startTime": "2023-08-11T15:07:59Z" + "startTime": "2023-08-11T15:07:59Z", + "detour": "0s" }, { "shipmentIndex": 6, "shipmentLabel": "S007", - "startTime": "2023-08-11T15:10:29Z" + "startTime": "2023-08-11T15:10:29Z", + "detour": "0s" }, { "shipmentIndex": 4, "shipmentLabel": "S005", - "startTime": "2023-08-11T15:12:59Z" + "startTime": "2023-08-11T15:12:59Z", + "detour": "0s" }, { "shipmentIndex": 10, "shipmentLabel": "P002 departure", - "startTime": "2023-08-11T15:17:44Z" + "startTime": "2023-08-11T15:17:44Z", + "detour": "720s" }, { "shipmentIndex": 11, "shipmentLabel": "P001 arrival", - "startTime": "2023-08-11T15:17:44Z" + "startTime": "2023-08-11T15:17:44Z", + "detour": "0s" }, { "shipmentIndex": 3, "shipmentLabel": "S004", - "startTime": "2023-08-11T15:26:39Z" + "startTime": "2023-08-11T15:26:39Z", + "detour": "0s" }, { "shipmentIndex": 12, "shipmentLabel": "P001 departure", - "startTime": "2023-08-11T15:29:47Z" + "startTime": "2023-08-11T15:29:47Z", + "detour": "723s" }, { "shipmentIndex": 13, "shipmentLabel": "P001 arrival", - "startTime": "2023-08-11T15:29:47Z" + "startTime": "2023-08-11T15:29:47Z", + "detour": "0s" }, { "shipmentIndex": 0, "shipmentLabel": "S001", - "startTime": "2023-08-11T15:32:23Z" + "startTime": "2023-08-11T15:32:23Z", + "detour": "0s" }, { "shipmentIndex": 1, "shipmentLabel": "S002", - "startTime": "2023-08-11T15:40:40Z" + "startTime": "2023-08-11T15:40:40Z", + "detour": "0s" }, { "shipmentIndex": 14, "shipmentLabel": "P001 departure", - "startTime": "2023-08-11T15:45:18Z" + "startTime": "2023-08-11T15:45:18Z", + "detour": "931s" }, { "shipmentIndex": 15, "shipmentLabel": "P001 arrival", - "startTime": "2023-08-11T15:45:18Z" + "startTime": "2023-08-11T15:45:18Z", + "detour": "0s" }, { "shipmentIndex": 2, "shipmentLabel": "S003", - "startTime": "2023-08-11T15:54:13Z" + "startTime": "2023-08-11T15:54:13Z", + "detour": "0s" }, { "shipmentIndex": 16, "shipmentLabel": "P001 departure", - "startTime": "2023-08-11T15:57:21Z" + "startTime": "2023-08-11T15:57:21Z", + "detour": "723s" } ], "metrics": { diff --git a/python/cfr/two_step_routing/two_step_routing.py b/python/cfr/two_step_routing/two_step_routing.py index 31240331..5245c44f 100644 --- a/python/cfr/two_step_routing/two_step_routing.py +++ b/python/cfr/two_step_routing/two_step_routing.py @@ -876,24 +876,27 @@ def add_merged_transition( merged_visits: list[cfr_json.Visit] = [] merged_transitions: list[cfr_json.Transition] = [] merged_travel_steps: list[cfr_json.TravelStep] = [] - merged_routes.append( - { - "routeTotalCost": global_route["routeTotalCost"], - "transitions": merged_transitions, - "travelSteps": merged_travel_steps, - "vehicleEndTime": global_route["vehicleEndTime"], - "vehicleIndex": global_route.get("vehicleIndex", 0), - "vehicleLabel": global_route["vehicleLabel"], - "vehicleStartTime": global_route["vehicleStartTime"], - "visits": merged_visits, - # TODO(ondrasej): metrics, detailed costs, ... - } - ) + merged_route: cfr_json.ShipmentRoute = { + "routeTotalCost": global_route["routeTotalCost"], + "transitions": merged_transitions, + "travelSteps": merged_travel_steps, + "vehicleEndTime": global_route["vehicleEndTime"], + "vehicleIndex": global_route.get("vehicleIndex", 0), + "vehicleLabel": global_route["vehicleLabel"], + "vehicleStartTime": global_route["vehicleStartTime"], + "visits": merged_visits, + # TODO(ondrasej): metrics, detailed costs, ... + } # Copy breaks from the global route, if present. - global_breaks = global_route.get("breaks") - if global_breaks is not None: - merged_routes[-1]["breaks"] = global_breaks + if (global_breaks := global_route.get("breaks")) is not None: + merged_route["breaks"] = global_breaks + + # Copy vehicle detour from the global route, if present. + if (global_detour := global_route.get("vehicleDetour")) is not None: + merged_route["vehicleDetour"] = global_detour + + merged_routes.append(merged_route) def add_parking_location_shipment( parking: ParkingLocation, arrival: bool @@ -922,6 +925,7 @@ def add_parking_location_shipment( vehicle=global_vehicle, ) global_visit_label = global_visit["shipmentLabel"] + global_visit_detour = cfr_json.get_visit_detour(global_visit) visit_type, index = _parse_global_shipment_label(global_visit_label) match visit_type: case "s": @@ -953,6 +957,10 @@ def add_parking_location_shipment( "shipmentIndex": arrival_shipment_index, "shipmentLabel": arrival_shipment["label"], "startTime": global_visit["startTime"], + # NOTE(ondrasej): The detour of the parking arrival visit is the + # difference from a plan where the vehicle drives directly to + # this parking location. + "detour": cfr_json.as_duration_string(global_visit_detour), }) # Transfer all visits and transitions from the local route. Update @@ -976,12 +984,22 @@ def add_parking_location_shipment( shipment_index = _get_shipment_index_from_local_route_visit( local_visit ) + local_visit_detour = cfr_json.get_visit_detour(local_visit) merged_visit: cfr_json.Visit = { "shipmentIndex": shipment_index, "shipmentLabel": self._shipments[shipment_index]["label"], "startTime": cfr_json.update_time_string( local_visit["startTime"], local_to_global_delta ), + # NOTE(ondrasej): The computation of the detour works with the + # assumption that all visits on the local route are for + # delivery-only shipments. The sum of the local and global + # detours is equivalent to the detour from a route where the + # vehicle drivers straight to the current parking location and + # where the driver then goes directly to this visit. + "detour": cfr_json.as_duration_string( + global_visit_detour + local_visit_detour + ), } merged_visits.append(merged_visit) @@ -1002,12 +1020,19 @@ def add_parking_location_shipment( departure_shipment_index, departure_shipment = ( add_parking_location_shipment(parking, arrival=False) ) + local_route_duration = cfr_json.parse_duration_string( + local_route["metrics"]["totalDuration"] + ) merged_visits.append({ "shipmentIndex": departure_shipment_index, "shipmentLabel": departure_shipment["label"], "startTime": cfr_json.update_time_string( local_route["vehicleEndTime"], local_to_global_delta ), + # NOTE(ondrasej): The detour of the parking departure visit is + # the time spent in the parking (the delta between the arrival + # to the parking and the departure from the parking). + "detour": cfr_json.as_duration_string(local_route_duration), }) case _: raise ValueError(f"Unexpected visit type: '{visit_type}'")