Skip to content

Commit

Permalink
Added a function that recomputes transition start times and durations…
Browse files Browse the repository at this point in the history
… from visit times.
  • Loading branch information
ondrasej committed Jan 17, 2024
1 parent b7739a7 commit 7c4b20c
Show file tree
Hide file tree
Showing 6 changed files with 131,506 additions and 0 deletions.
77 changes: 77 additions & 0 deletions python/cfr/json/cfr_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,83 @@ def recompute_route_metrics(
}


def _recompute_one_transition_start_and_durations(
transition: Transition,
start_time: datetime.datetime,
end_time: datetime.datetime,
) -> None:
"""Updates `startTime` and `totalDuration` of one transition.
Updates the transition to fit between `start_time` and `end_time`. Updates
`startTime`, `waitDuration` and `totalDuration` of the transition so that the
transition starts at `start_time` and the next visit starts at `end_time`.
Other durations of the transition are preserved.
Args:
transition: The transition to be updated.
start_time: The requested start time of the transition.
end_time: The requested end time of the transition.
Raises:
ValueError: When sum of the other durations of the transition is greater
than the duration between `start_time` and `end_time`.
"""
non_wait_duration = (
parse_duration_string(transition.get("travelDuration", "0s"))
+ parse_duration_string(transition.get("delayDuration", "0s"))
+ parse_duration_string(transition.get("breakDuration", "0s"))
)
total_duration = end_time - start_time
if non_wait_duration > total_duration:
raise ValueError(
"The minimal duration of the transition is greater than the available"
" time slot."
)
wait_duration = total_duration - non_wait_duration
transition["startTime"] = as_time_string(start_time)
transition["totalDuration"] = as_duration_string(total_duration)
transition["waitDuration"] = as_duration_string(wait_duration)


def recompute_transition_starts_and_durations(
model: ShipmentModel, route: ShipmentRoute
) -> None:
"""Recomputes transition start times and wait durations based on visit times.
Assumes that the list of visits of `route` is complete and that the visit
start times are correct. Updates transition start times so that they match the
visit start times, and adds wait time as needed for padding.
Args:
model: The model in which the transition times are recomputed.
route: The route, for which the transition times are recomputed. Modified in
place.
"""
visits = get_visits(route)
if not visits:
# Unused vehicle.
return

transitions = get_transitions(route)

previous_visit_end_time = parse_time_string(route["vehicleStartTime"])
for visit_index, visit in enumerate(visits):
current_visit_start_time = parse_time_string(visit["startTime"])
transition_in = transitions[visit_index]
_recompute_one_transition_start_and_durations(
transition_in, previous_visit_end_time, current_visit_start_time
)

visit_duration = get_visit_request_duration(get_visit_request(model, visit))
previous_visit_end_time = current_visit_start_time + visit_duration

_recompute_one_transition_start_and_durations(
transitions[-1],
previous_visit_end_time,
parse_time_string(route["vehicleEndTime"]),
)


def get_num_decreasing_visit_times(
model: ShipmentModel,
route: ShipmentRoute,
Expand Down
71 changes: 71 additions & 0 deletions python/cfr/json/cfr_json_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import unittest

from . import cfr_json
from ..testdata import testdata


class MakeShipmentTest(unittest.TestCase):
Expand Down Expand Up @@ -1795,6 +1796,76 @@ def test_non_empty_route(self):
self.assertEqual(route["metrics"], self._EXPECTED_METRICS)


class RecomputeTransitionStartsAndDurations(unittest.TestCase):
"""Tests for recompute_transition_starts_and_durations."""

maxDiff = None

def recompute_existing_solution(
self,
request: cfr_json.OptimizeToursRequest,
response: cfr_json.OptimizeToursResponse,
) -> None:
"""Tests the function by restoring start time and durations on a route.
Takes all routes from a response, removes start time and durations from them
and uses `recompute_transition_starts_and_durations` to restore them. Checks
that they have all been restored to the original state (which is the only
possible).
"""
model = request["model"]
expected_routes = cfr_json.get_routes(response)

# Get routes from the response, but remove transition start times and some
# durations.
routes = copy.deepcopy(cfr_json.get_routes(response))
for route, expected_route in zip(routes, expected_routes):
transitions = cfr_json.get_transitions(route)
for transition in transitions:
transition.pop("startTime", None)
transition.pop("totalDuration", None)
transition.pop("waitDuration", None)

if transitions:
self.assertNotEqual(route, expected_route)
cfr_json.recompute_transition_starts_and_durations(model, route)
self.assertEqual(route, expected_route)

def test_moderate_local(self):
self.recompute_existing_solution(
testdata.json("moderate/scenario.local_request.json"),
testdata.json("moderate/scenario.local_response.60s.json"),
)

def test_moderate_global(self):
self.recompute_existing_solution(
testdata.json("moderate/scenario.global_request.60s.json"),
testdata.json("moderate/scenario.global_response.60s.180s.json"),
)

def test_insufficient_time(self):
model: cfr_json.ShipmentModel = {
"shipments": [{"deliveries": [{"duration": "120s"}]}],
}
route: cfr_json.ShipmentRoute = {
"vehicleIndex": 0,
"vehicleStartTime": "2024-01-15T10:00:00Z",
"vehicleEndTime": "2024-01-15T11:00:00z",
"visits": [{
"shipmentIndex": 0,
"visitRequestIndex": 0,
"isPickup": False,
"startTime": "2024-01-15T10:10:00Z",
}],
"transitions": [
{},
{"breakDuration": "1800s", "travelDuration": "1200s"},
],
}
with self.assertRaisesRegex(ValueError, "minimal duration"):
cfr_json.recompute_transition_starts_and_durations(model, route)


class UpdateTimeStringTest(unittest.TestCase):
"""Tests of update_time_string."""

Expand Down
Loading

0 comments on commit 7c4b20c

Please sign in to comment.