Skip to content

Commit

Permalink
Add visit start times to the injected first solution in global refine…
Browse files Browse the repository at this point in the history
…ment steps.

This should help bootstrap the solver in global refinement requests, and will
be needed to terminate the refinement with a local request.

For now, we only added the code, but it is protected by a flag that is
turned off by default, i.e. there is no change in behavior.
  • Loading branch information
ondrasej committed Jan 12, 2024
1 parent c222fb8 commit 1b85e97
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -567,82 +567,98 @@
{
"shipmentIndex": 0,
"shipmentLabel": "p:0 S0021",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T08:13:38Z"
},
{
"shipmentIndex": 1,
"shipmentLabel": "p:1 S0014,S0015",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T08:34:51Z"
},
{
"shipmentIndex": 2,
"shipmentLabel": "p:2 S0022",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T09:06:00Z"
},
{
"shipmentIndex": 3,
"shipmentLabel": "p:3 S0020",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T09:30:53Z"
},
{
"shipmentIndex": 4,
"shipmentLabel": "p:4 S0012",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T10:01:34Z"
},
{
"shipmentIndex": 5,
"shipmentLabel": "p:5 S0013",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T10:53:16Z"
},
{
"shipmentIndex": 6,
"shipmentLabel": "p:6 S0009",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T11:23:40Z"
},
{
"shipmentIndex": 7,
"shipmentLabel": "s:4 S0005",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T11:58:15Z"
},
{
"shipmentIndex": 8,
"shipmentLabel": "p:7 S0018,S0019",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T13:09:24Z"
},
{
"shipmentIndex": 9,
"shipmentLabel": "s:2 S0003",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T13:54:05Z"
},
{
"shipmentIndex": 10,
"shipmentLabel": "s:3 S0004",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T14:06:07Z"
},
{
"shipmentIndex": 11,
"shipmentLabel": "p:8 S0011",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T14:23:31Z"
},
{
"shipmentIndex": 12,
"shipmentLabel": "p:9 S0007,S0008",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T14:53:16Z"
},
{
"shipmentIndex": 13,
"shipmentLabel": "s:1 S0002",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T15:49:31Z"
},
{
"shipmentIndex": 14,
"shipmentLabel": "p:10 S0016,S0017",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T15:54:33Z"
},
{
"shipmentIndex": 15,
"shipmentLabel": "p:11 S0006",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T17:15:20Z"
}
],
"breaks": [
Expand All @@ -666,12 +682,14 @@
{
"shipmentIndex": 16,
"shipmentLabel": "p:12 S0010",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T08:01:45Z"
},
{
"shipmentIndex": 17,
"shipmentLabel": "s:0 S0001",
"isPickup": false
"isPickup": false,
"startTime": "2024-01-04T08:29:43Z"
}
],
"breaks": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@
{
"vehicleIndex": 0,
"visits": [
{ "isPickup": false, "shipmentIndex": 0, "shipmentLabel": "s:8 S009" },
{
"isPickup": false,
"shipmentIndex": 0,
"shipmentLabel": "s:8 S009",
"startTime": "2023-08-11T08:07:01Z"
},
{
"isPickup": false,
"shipmentIndex": 1,
"shipmentLabel": "p:0 S001,S004,S003,S002"
"shipmentLabel": "p:0 S001,S004,S003,S002",
"startTime": "2023-08-11T13:51:14Z"
},
{ "isPickup": false, "shipmentIndex": 2, "shipmentLabel": "p:1 S007" }
{
"isPickup": false,
"shipmentIndex": 2,
"shipmentLabel": "p:1 S007",
"startTime": "2023-08-11T14:40:52Z"
}
]
},
{
Expand All @@ -20,7 +31,8 @@
{
"isPickup": false,
"shipmentIndex": 3,
"shipmentLabel": "p:2 S006,S008,S005"
"shipmentLabel": "p:2 S006,S008,S005",
"startTime": "2023-08-11T08:10:53Z"
}
]
}
Expand Down
69 changes: 55 additions & 14 deletions python/cfr/two_step_routing/two_step_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,7 @@ def integrate_local_refinement(
global_request: cfr_json.OptimizeToursRequest,
global_response: cfr_json.OptimizeToursResponse,
refinement_response: cfr_json.OptimizeToursResponse,
inject_visit_times_to_global_request: bool = False,
) -> tuple[
cfr_json.OptimizeToursRequest,
cfr_json.OptimizeToursResponse,
Expand Down Expand Up @@ -850,6 +851,9 @@ def integrate_local_refinement(
global_response: A solution for `global_request`.
refinement_response: A solution for a request created by
`self.make_local_refinement_request(local_response, global_response)`.
inject_visit_times_to_global_request: When True, the injected first
solution in the integrated global request will contain visit times based
on visit times in `global_response`.
Returns:
A triple `(integrated_local_request, integrated_local_response,
Expand All @@ -868,6 +872,7 @@ def integrate_local_refinement(
global_request=global_request,
global_response=global_response,
refinement_response=refinement_response,
inject_visit_times_to_global_request=inject_visit_times_to_global_request,
)
return integration.integrate()

Expand Down Expand Up @@ -1453,6 +1458,7 @@ def __init__(
global_request: cfr_json.OptimizeToursRequest,
global_response: cfr_json.OptimizeToursResponse,
refinement_response: cfr_json.OptimizeToursResponse,
inject_visit_times_to_global_request: bool,
):
"""Initializes the integration algorithm.
Expand All @@ -1467,6 +1473,9 @@ def __init__(
global_response: A solution for `global_request`.
refinement_response: A solution for a request created by
`self.make_local_refinement_request(local_response, global_response)`.
inject_visit_times_to_global_request: When True, the injected first
solution in the integrated global request will contain visit times based
on visit times in `global_response`.
"""
self._options = options
self._parking_locations = parking_locations
Expand All @@ -1487,6 +1496,10 @@ def __init__(
self._global_response = global_response
self._global_routes = cfr_json.get_routes(global_response)

self._inject_visit_times_to_global_request = (
inject_visit_times_to_global_request
)

refinement_routes = cfr_json.get_routes(refinement_response)

# Shipments in the integrated local model are the same as in the base local
Expand Down Expand Up @@ -1636,14 +1649,19 @@ def _integrate_global_skipped_shipments(self) -> None:
# integration, so we can reuse it in the integrate model without any
# changes.
self._add_integrated_global_shipment(
global_shipment, add_to_visits=None
global_shipment,
add_to_visits=None,
visit_start_time=None,
)
case "p":
# The visit is for a round of deliveries from a parking location. It
# was skipped in the base global model, and so it could not have been
# part of a refinement.
self._integrate_unmodified_local_route(
global_shipment, local_route_index=index, add_to_visits=None
global_shipment,
local_route_index=index,
add_to_visits=None,
visit_start_time=None,
)
case _:
raise ValueError("Unexpected global visit type: {visit_type!r}")
Expand Down Expand Up @@ -1707,16 +1725,20 @@ def _integrate_global_routes(self) -> None:
else:
# This global visit was not part of the refinement, we just need to
# carry over the shipment or local delivery round from the base model.
visit_start_time = global_visit["startTime"]
match visit_type:
case "s":
self._add_integrated_global_shipment(
global_shipment, add_to_visits=integrated_global_visits
global_shipment,
add_to_visits=integrated_global_visits,
visit_start_time=visit_start_time,
)
case "p":
self._integrate_unmodified_local_route(
global_shipment,
local_route_index=index,
add_to_visits=integrated_global_visits,
visit_start_time=visit_start_time,
)
case _:
raise ValueError(f"Unexpected global visit type: {visit_type!r}")
Expand All @@ -1725,7 +1747,7 @@ def _integrate_global_routes(self) -> None:
def _integrate_refined_local_route(
self,
refined_local_route: cfr_json.ShipmentRoute,
add_to_visits: list[cfr_json.Visit] | None,
add_to_visits: list[cfr_json.Visit],
) -> None:
"""Integrates a refined local route to the refined models and solutions.
Expand All @@ -1736,9 +1758,8 @@ def _integrate_refined_local_route(
Args:
refined_local_route: The route from the refinement local model that
represents the new delivery rounds for the parking location.
add_to_visits: When not None, visits for the shipments that represent the
new delivery rounds in the integrated global model are added to this
list.
add_to_visits: Visits for the shipments that represent the new delivery
rounds in the integrated global model are added to this list.
"""
refined_route_splits = _split_refined_local_route(refined_local_route)
num_refined_route_splits = len(refined_route_splits)
Expand Down Expand Up @@ -1810,14 +1831,19 @@ def _integrate_refined_local_route(
transition_attributes=self._transition_attributes,
)
self._add_integrated_global_shipment(
integrated_global_shipment, add_to_visits
integrated_global_shipment,
add_to_visits,
# In the integrated local model, the vehicle start/end times are
# exactly the start/end times of the local delivery rounds.
visit_start_time=integrated_local_route["vehicleStartTime"],
)

def _integrate_unmodified_local_route(
self,
global_shipment: cfr_json.Shipment,
local_route_index: int,
add_to_visits: list[cfr_json.Visit] | None,
visit_start_time: cfr_json.TimeString | None,
) -> None:
"""Integrates a local route into the refined models and solutions.
Expand All @@ -1829,8 +1855,12 @@ def _integrate_unmodified_local_route(
global_shipment: The shipment in the global model that represents the
local route.
local_route_index: The index of the local route in the base local model.
add_to_visits: When not none, a visit for the shipment that represents the
local route in the refined global model is added to this list.
add_to_visits: When not none, a delivery visit for the shipment that
represents the local route in the refined global model is added to this
list. Must be None when `visit_start_time` is None.
visit_start_time: The start time of the delivery visit for the new
shipment in the integrated global model. Must be None when
`add_to_visits` is None.
"""
# Copy the original local vehicle and route to the integrated local request
# and response. We preserve the indices of shipments in the local model, so
Expand All @@ -1857,20 +1887,25 @@ def _integrate_unmodified_local_route(
f"p:{integrated_local_vehicle_index} {original_shipment_label}"
)
self._add_integrated_global_shipment(
integrated_global_shipment, add_to_visits
integrated_global_shipment, add_to_visits, visit_start_time
)

def _add_integrated_global_shipment(
self,
shipment: cfr_json.Shipment,
add_to_visits: list[cfr_json.Visit] | None,
visit_start_time: cfr_json.TimeString | None,
) -> int:
"""Adds `shipment` to the integrated request and returns its index.
Args:
shipment: The shipment to be added.
add_to_visits: When not None, the method adds a delivery visit to the
newly added shipment to this list.
newly added shipment to this list. Must be None when `visit_start_time`
is None.
visit_start_time: The start time of the delivery visit for the new
shipment in the integrated global model. Must be None when
`add_to_visits` is None.
Returns:
The index of the newly added integrated shipment.
Expand All @@ -1879,15 +1914,21 @@ def _add_integrated_global_shipment(
# delivery option and no pickups.
assert len(shipment["deliveries"]) == 1
assert not shipment.get("pickups")
# Visit start time must be provided when creating a visit for the integrated
# global shipment, even if it is not included in the initial solution.
assert (visit_start_time is None) == (add_to_visits is None)

shipment_index = len(self._integrated_global_shipments)
self._integrated_global_shipments.append(shipment)
if add_to_visits is not None:
add_to_visits.append({
visit: cfr_json.Visit = {
"shipmentIndex": shipment_index,
"shipmentLabel": shipment.get("label", ""),
"isPickup": False,
})
}
if self._inject_visit_times_to_global_request:
visit["startTime"] = visit_start_time
add_to_visits.append(visit)
return shipment_index


Expand Down
Loading

0 comments on commit 1b85e97

Please sign in to comment.