Skip to content

Commit

Permalink
[transform_breaks] Add avoidUTurns support for breaks at location.
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrasej committed Sep 11, 2024
1 parent 1eb891d commit 58f2b48
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 25 deletions.
83 changes: 58 additions & 25 deletions python/gmpro/json/transforms_breaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ class BreakTransformRule:
"depot", the break is at the start waypoint of the vehicle; otherwise, the
value must be a valid Waypoint JSON object, and it will be used as the
location of the break. The break request itself is removed.
avoid_u_turns: When True, the visit request created for the break has
`avoidUTurns` set to True. Can be True only when `break_at_waypoint` is
not None.
virtual_shipment_label: WHen the break request is transformed into a virtual
shipment, this string is used as a base of the label of this shipment.
"""
Expand All @@ -172,6 +175,7 @@ class BreakTransformRule:
actions: Sequence[BreakTransformAction]
new_break_request: bool
break_at_waypoint: cfr_json.Waypoint | str | None
avoid_u_turns: bool
virtual_shipment_label: str

def applies_to(
Expand Down Expand Up @@ -581,6 +585,7 @@ def compile_rules(rules: str) -> Sequence[BreakTransformRule]:
actions: list[BreakTransformAction] = []
new_break_request = False
break_at_waypoint = None
avoid_u_turns = False
virtual_shipment_label = "break"

for component in _tokenize(rules):
Expand All @@ -590,15 +595,21 @@ def compile_rules(rules: str) -> Sequence[BreakTransformRule]:
or selectors
or context_selectors
or break_at_waypoint
or avoid_u_turns
or new_break_request
):
if avoid_u_turns and break_at_waypoint is None:
raise ValueError(
"`avoidUTurns` can be used only together with `location`"
)
compiled_rules.append(
BreakTransformRule(
selectors=selectors,
context_selectors=context_selectors,
actions=actions,
new_break_request=new_break_request,
break_at_waypoint=break_at_waypoint,
avoid_u_turns=avoid_u_turns,
virtual_shipment_label=virtual_shipment_label,
)
)
Expand All @@ -607,6 +618,7 @@ def compile_rules(rules: str) -> Sequence[BreakTransformRule]:
actions = []
new_break_request = False
break_at_waypoint = None
avoid_u_turns = False
virtual_shipment_label = "break"
continue

Expand Down Expand Up @@ -663,6 +675,12 @@ def compile_rules(rules: str) -> Sequence[BreakTransformRule]:
f"Only '=' is allowed for `location`, found {str(component)!r}"
)
break_at_waypoint = component.value
case "avoidUTurns":
if component.operator is not None:
raise ValueError(
f"avoidUTurns does not accept operands, found {str(component)!r}"
)
avoid_u_turns = True
case "virtualShipmentLabel":
if component.operator != "=":
raise ValueError(
Expand Down Expand Up @@ -707,6 +725,14 @@ def compile_rules(rules: str) -> Sequence[BreakTransformRule]:
return compiled_rules


@dataclasses.dataclass(frozen=True)
class _BreakAtWaypoint:
waypoint: cfr_json.Waypoint | str
break_request: cfr_json.BreakRequest
label: str
avoid_u_turns: bool


def transform_breaks_for_vehicle(
compiled_rules: Sequence[BreakTransformRule],
model: cfr_json.ShipmentModel,
Expand All @@ -719,9 +745,7 @@ def transform_breaks_for_vehicle(
if (break_rule := vehicle.get("breakRule")) is not None:
if (old_break_requests := break_rule.get("breakRequests")) is not None:
break_requests = old_break_requests
breaks_at_waypoint: list[
tuple[cfr_json.Waypoint, cfr_json.BreakRequest, str]
] = []
breaks_at_waypoint: list[_BreakAtWaypoint] = []

logging.debug("Processing vehicle_index=%d", vehicle_index)
for transform in compiled_rules:
Expand Down Expand Up @@ -758,11 +782,14 @@ def transform_breaks_for_vehicle(
rule_new_requests = transform.apply_to(model, vehicle, request)
if transform.break_at_waypoint:
for new_request in rule_new_requests:
breaks_at_waypoint.append((
transform.break_at_waypoint,
new_request,
transform.virtual_shipment_label,
))
breaks_at_waypoint.append(
_BreakAtWaypoint(
waypoint=transform.break_at_waypoint,
break_request=new_request,
label=transform.virtual_shipment_label,
avoid_u_turns=transform.avoid_u_turns,
)
)
else:
new_requests.extend(rule_new_requests)

Expand All @@ -789,11 +816,14 @@ def transform_breaks_for_vehicle(
)
if transform.break_at_waypoint:
for new_request in rule_new_requests:
breaks_at_waypoint.append((
transform.break_at_waypoint,
new_request,
transform.virtual_shipment_label,
))
breaks_at_waypoint.append(
_BreakAtWaypoint(
waypoint=transform.break_at_waypoint,
break_request=new_request,
label=transform.virtual_shipment_label,
avoid_u_turns=transform.avoid_u_turns,
)
)
else:
new_requests.extend(rule_new_requests)

Expand All @@ -811,27 +841,30 @@ def transform_breaks_for_vehicle(
if shipments is None:
shipments = []
model["shipments"] = shipments
for src_waypoint, break_request, shipment_label_base in breaks_at_waypoint:
match src_waypoint:
for break_at_waypoint in breaks_at_waypoint:
match break_at_waypoint.waypoint:
case "depot":
# TODO(ondrasej): Also support `startLocation`.
waypoint = vehicle["startWaypoint"]
case value if isinstance(value, dict):
waypoint = cast(cfr_json.Waypoint, src_waypoint)
waypoint = cast(cfr_json.Waypoint, break_at_waypoint.waypoint)
case _:
raise ValueError("Unexpected waypoint value {waypoint!r}")
shipment_label = f"{shipment_label_base}, {vehicle_index=}"
shipment_label = f"{break_at_waypoint.label}, {vehicle_index=}"
if vehicle_label := vehicle.get("label"):
shipment_label += f", {vehicle_label=}"
shipment: cfr_json.Shipment = {
"deliveries": [{
"arrivalWaypoint": waypoint,
"duration": break_request["minDuration"],
"timeWindows": [{
"startTime": break_request["earliestStartTime"],
"endTime": break_request["latestStartTime"],
}],
delivery: cfr_json.VisitRequest = {
"arrivalWaypoint": waypoint,
"duration": break_at_waypoint.break_request["minDuration"],
"timeWindows": [{
"startTime": break_at_waypoint.break_request["earliestStartTime"],
"endTime": break_at_waypoint.break_request["latestStartTime"],
}],
}
if break_at_waypoint.avoid_u_turns:
delivery["avoidUTurns"] = True
shipment: cfr_json.Shipment = {
"deliveries": [delivery],
"label": shipment_label,
"allowedVehicleIndices": [vehicle_index],
}
Expand Down
70 changes: 70 additions & 0 deletions python/gmpro/json/transforms_breaks_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,70 @@ def test_break_at_location(self):
expected_model,
)

def test_avoid_u_turns_at_location(self):
model: cfr_json.ShipmentModel = {
"globalStartTime": "2024-02-09T08:00:00Z",
"globalEndTime": "2024-02-09T18:00:00Z",
"vehicles": [{
"startWaypoint": {"placeId": "foobar"},
"breakRule": {
"breakRequests": [
{
"earliestStartTime": "2024-02-09T11:30:00Z",
"latestStartTime": "2024-02-09T12:30:00Z",
"minDuration": "3600s",
},
{
"earliestStartTime": "2024-02-09T14:00:00Z",
"latestStartTime": "2024-02-09T16:00:00Z",
"minDuration": "3600s",
},
]
},
}],
}
expected_model: cfr_json.ShipmentModel = {
"globalStartTime": "2024-02-09T08:00:00Z",
"globalEndTime": "2024-02-09T18:00:00Z",
"vehicles": [{
"startWaypoint": {"placeId": "foobar"},
"breakRule": {
"breakRequests": [
{
"earliestStartTime": "2024-02-09T11:30:00Z",
"latestStartTime": "2024-02-09T12:30:00Z",
"minDuration": "3600s",
},
]
},
}],
"shipments": [{
"allowedVehicleIndices": [0],
"label": "This is a break, vehicle_index=0",
"deliveries": [{
"arrivalWaypoint": {"placeId": "barbaz", "sideOfRoad": True},
"timeWindows": [{
"startTime": "2024-02-09T14:00:00Z",
"endTime": "2024-02-09T16:00:00Z",
}],
"duration": "3600s",
"avoidUTurns": True,
}],
}],
}
self.assertEqual(
self.run_transform_breaks(
model,
"""
@time=14:00:00
location={"placeId": "barbaz", "sideOfRoad": true}
virtualShipmentLabel="This is a break"
avoidUTurns
""",
),
expected_model,
)

def test_all_return_to_depot(self):
model: cfr_json.ShipmentModel = {
"globalStartTime": "2024-02-09T08:00:00Z",
Expand Down Expand Up @@ -1093,6 +1157,12 @@ def test_invalid_name(self):
with self.assertRaisesRegex(ValueError, "Unexpected name .foo."):
transforms_breaks.compile_rules("""foo=bar""")

def test_avoid_u_turns_without_location(self):
with self.assertRaisesRegex(
ValueError, "`avoidUTurns` can be used only together with `location`"
):
transforms_breaks.compile_rules("""avoidUTurns""")


class ParseTimeTest(unittest.TestCase):
"""Tests for _parse_time."""
Expand Down

0 comments on commit 58f2b48

Please sign in to comment.