diff --git a/python/cfr/analysis/__init__.py b/python/cfr/analysis/__init__.py new file mode 100644 index 00000000..5dc74f7f --- /dev/null +++ b/python/cfr/analysis/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023 Google LLC. All Rights Reserved. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE file or at https://opensource.org/licenses/MIT. diff --git a/python/cfr/analysis/analysis.py b/python/cfr/analysis/analysis.py new file mode 100644 index 00000000..c70f64c5 --- /dev/null +++ b/python/cfr/analysis/analysis.py @@ -0,0 +1,257 @@ +"""Contains helper functions and classes for CFR reuqest/response analysis.""" + +import collections +from collections.abc import Mapping, Sequence, Set +import dataclasses +import functools +from typing import Any + +from ..json import cfr_json +from ..two_step_routing import two_step_routing + + +@dataclasses.dataclass(frozen=True) +class ParkingLocationData: + """Contains aggregated data about parking locations. + + Attributes: + all_parking_tags: The set of all parking location tags that are visited in + the solution. Note that parking locations that are not visited by any + vehicle do not appear in the solution and by consequence, do not appear in + this set. + vehicles_by_parking: For each parking tag, contains a mapping from vehicle + indices to the list of indices of the visits made by this vehicle. + consecutive_visits: The per-vehicle list of consecutive visits to a parking + location. The key of the mapping is the vehicle index, values are lists of + visits to a parking location. Each element of this list is a pair + (parking_tag, visit_index) such that + `shipments_by_parking[parking_tag][visit_index]` is the visit that + generated the entry. + non_consecutive_visits: The per-vehicle list of non-consecutive visits to a + parking location. The format is the same as for consecutive_visits. + shipments_by_parking: The list of parking location visits, indexed by the + parking tag. The value is a list of lists of shipment indices. Each + element of the outer list corresponds to one visit to the parking location + and the elements of the inner list are the shipments delivered during this + visit. + """ + + all_parking_tags: Set[str] + vehicles_by_parking: Mapping[str, Mapping[int, Sequence[int]]] + consecutive_visits: Mapping[int, Sequence[tuple[str, int]]] + non_consecutive_visits: Mapping[int, Sequence[tuple[str, int]]] + shipments_by_parking: Mapping[str, Sequence[Sequence[int]]] + + +@dataclasses.dataclass +class Scenario: + """Holds data from a single scenario. + + Attributes: + name: A unique name of the scenario. Used as unique key for the scenario, + and as an index in the data frames used throughout the notebook. + scenario: The JSON data of the scenario. + solution: The JSON data of the solution. + parking_json: The parking location data in the JSON format. + parking_locations: The list of parking locations indexed by the parking + location tags. + parking_for_shipment: The assignment of shipments to parking locations. The + keys are shipment indices; the values are the parking location tags for + the shipments. + parking_location_data: Contains aggregated data about parking locations for + the scenario. + """ + + name: str + scenario: cfr_json.OptimizeToursRequest + solution: cfr_json.OptimizeToursResponse + parking_json: dataclasses.InitVar[Any] = None + parking_locations: Mapping[str, two_step_routing.ParkingLocation] | None = ( + None + ) + parking_for_shipment: Mapping[int, str] | None = None + parking_location_data: ParkingLocationData = dataclasses.field(init=False) + + def __post_init__(self, parking_json: Any | None) -> None: + super().__init__() + self.parking_location_data = get_parking_location_aggregate_data(self) + if parking_json is not None: + if ( + self.parking_locations is not None + or self.parking_for_shipment is not None + ): + raise ValueError( + "Either only parking_json or only parking_locations and" + " parking_for_shipment can not be None." + ) + parking_locations, self.parking_for_shipment = ( + two_step_routing.load_parking_from_json(parking_json) + ) + self.parking_locations = {} + for parking_location in parking_locations: + if parking_location.tag in self.parking_locations: + raise ValueError( + f"Duplicate parking location tag: {parking_location.tag}" + ) + self.parking_locations[parking_location.tag] = parking_location + if (self.parking_locations is not None) != ( + self.parking_for_shipment is not None + ): + raise ValueError( + "parking_locations and parking_for_shipment must either both be None" + " or both be not None." + ) + if self.parking_locations is None: + # Create empty parking data structures, so that we do not need to do too + # much branching in the code below. + self.parking_locations = {} + self.parking_for_shipment = {} + + @property + def model(self) -> cfr_json.ShipmentModel: + """Returns model of the scenario.""" + return self.scenario["model"] + + @property + def shipments(self) -> Sequence[cfr_json.Shipment]: + """Returns the list of shipments in the scenario.""" + return self.model.get("shipments", ()) + + @property + def vehicles(self) -> Sequence[cfr_json.Vehicle]: + """Returns the list of vehicles in the scenario.""" + return self.model.get("vehicles", ()) + + @property + def routes(self) -> Sequence[cfr_json.ShipmentRoute]: + """Returns the list of routes in the scenario.""" + return self.solution.get("routes", ()) + + @property + def skipped_shipments(self) -> Sequence[cfr_json.SkippedShipment]: + """Returns the list of skipped shipments in the scenario.""" + return self.solution.get("skippedShipments", ()) + + @functools.cached_property + def vehicle_for_shipment(self) -> Mapping[int, int]: + """Returns a mapping from a shipment to the vehicle that serves it. + + Skipped shipments are not included. + + Returns: + A mapping where the key is a shipment index and the value is a vehicle + index. + """ + vehicle_for_shipment = {} + for vehicle_index, route in enumerate(self.routes): + for visit in route.get("visits", ()): + shipment_index = visit.get("shipmentIndex", 0) + vehicle_for_shipment[shipment_index] = vehicle_index + return vehicle_for_shipment + + def vehicle_label(self, vehicle_index: int) -> str: + """Returns the label of a vehicle.""" + return self.vehicles[vehicle_index].get("label", "") + + +def get_parking_location_aggregate_data( + scenario: Scenario, +) -> ParkingLocationData: + """Collects aggregated parking location data from a scenario.""" + + routes = scenario.routes + all_parking_tags = set() + # The number of visits to each parking location by each vehicle. + visits_by_vehicle = collections.defaultdict( + functools.partial(collections.defaultdict, int) + ) + # The set of vehicles that are used to serve the given parking. + vehicles_by_parking = collections.defaultdict( + functools.partial(collections.defaultdict, list) + ) + + vehicle_consecutive_visits = collections.defaultdict(list) + vehicle_non_consecutive_visits = collections.defaultdict(list) + + shipments_by_parking = collections.defaultdict(list) + + for vehicle_index, route in enumerate(routes): + visits = route.get("visits", ()) + vehicle_label = route.get("vehicleLabel", f"vehicle {vehicle_index}") + current_parking_tag = None + parking_tag_left_in_previous_visit = None + for visit in visits: + label = visit.get("shipmentLabel") + shipment_index = visit.get("shipmentIndex", 0) + departure_tag = consume_suffix(label, " departure") + if departure_tag is not None: + if current_parking_tag != departure_tag: + raise ValueError( + "Parking tag mismatch for a departure. Expected" + f" {current_parking_tag!r}, found {departure_tag!r}." + ) + all_parking_tags.add(departure_tag) + parking_tag_left_in_previous_visit = departure_tag + current_parking_tag = None + continue + arrival_tag = consume_suffix(label, " arrival") + if arrival_tag is not None: + if current_parking_tag is not None: + raise ValueError( + f"Unexpected arrival to parking {arrival_tag!r}, currently in" + f" parking {current_parking_tag!r}" + ) + current_parking_tag = arrival_tag + parking_visit_index = len(shipments_by_parking[arrival_tag]) + parking_visit_tuple = (arrival_tag, parking_visit_index) + + parking_vehicles = vehicles_by_parking[arrival_tag] + if parking_tag_left_in_previous_visit == arrival_tag: + # This is a consecutive visit to the parking location. + vehicle_consecutive_visits[vehicle_index].append(parking_visit_tuple) + elif vehicle_index in parking_vehicles: + # parking_tag_left_in_previous_visit != arrival_tag holds because of + # the previous if statement. This is a non-consecutive visit to this + # parking by this vehicle. + vehicle_non_consecutive_visits[vehicle_index].append( + parking_visit_tuple + ) + + visits_by_vehicle[vehicle_label][arrival_tag] += 1 + parking_vehicles[vehicle_index].append(parking_visit_index) + shipments_by_parking[arrival_tag].append([]) + + if ( + arrival_tag is None + and departure_tag is None + and current_parking_tag is not None + ): + # This is a shipment served from a parking location. + shipments_by_parking[current_parking_tag][-1].append(shipment_index) + pass + + parking_tag_left_in_previous_visit = None + + return ParkingLocationData( + all_parking_tags=all_parking_tags, + vehicles_by_parking=vehicles_by_parking, + consecutive_visits=vehicle_consecutive_visits, + non_consecutive_visits=vehicle_non_consecutive_visits, + shipments_by_parking=shipments_by_parking, + ) + + +def consume_suffix(text: str, suffix: str) -> str | None: + """Consumes the suffix of a text. + + Args: + text: The text from which the suffix is consumed. + suffix: The suffix to be consumed. + + Returns: + When `text` ends with `suffix`, returns `text` with `suffix` removed from + the end. Otherwise, returns `None`. + """ + if not text.endswith(suffix): + return None + return text[: -len(suffix)] diff --git a/python/cfr/analysis/cfr-json-analysis.ipynb b/python/cfr/analysis/cfr-json-analysis.ipynb index 254febfe..d5d1a48a 100644 --- a/python/cfr/analysis/cfr-json-analysis.ipynb +++ b/python/cfr/analysis/cfr-json-analysis.ipynb @@ -122,9 +122,11 @@ "import ipywidgets\n", "import pandas as pd\n", "\n", + "# Clone the CFR library from GitHub, and import from there.\n", "!git clone https://github.com/google/cfr\n", - "from python.cfr.json import cfr_json\n", - "from python.cfr.two_step_routing import two_step_routing\n" + "from cfr.python.cfr.analysis import analysis\n", + "from cfr.python.cfr.json import cfr_json\n", + "from cfr.python.cfr.two_step_routing import two_step_routing\n" ] }, { @@ -155,172 +157,6 @@ "_DATETIME_MAX_UTC = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)\n", "\n", "\n", - "@dataclasses.dataclass(frozen=True)\n", - "class ParkingLocationData:\n", - " \"\"\"Contains aggregated data about parking locations.\n", - "\n", - " Attributes:\n", - " all_parking_tags: The set of all parking location tags that are visited in\n", - " the solution. Note that parking locations that are not visited by any\n", - " vehicle do not appear in the solution and by consequence, do not appear in\n", - " this set.\n", - " vehicles_by_parking: For each parking tag, contains a mapping from vehicle\n", - " indices to the list of indices of the visits made by this vehicle.\n", - " consecutive_visits: The per-vehicle list of consecutive visits to a parking\n", - " location. The key of the mapping is the vehicle index, values are lists of\n", - " visits to a parking location. Each element of this list is a pair\n", - " (parking_tag, visit_index) such that\n", - " `shipments_by_parking[parking_tag][visit_index]` is the visit that\n", - " generated the entry.\n", - " non_consecutive_visits: The per-vehicle list of non-consecutive visits to a\n", - " parking location. The format is the same as for consecutive_visits.\n", - " shipments_by_parking: The list of parking location visits, indexed by the\n", - " parking tag. The value is a list of lists of shipment indices. Each\n", - " element of the outer list corresponds to one visit to the parking location\n", - " and the elements of the inner list are the shipments delivered during this\n", - " visit.\n", - " \"\"\"\n", - "\n", - " all_parking_tags: Set[str]\n", - " vehicles_by_parking: Mapping[str, Mapping[int, Sequence[int]]]\n", - " consecutive_visits: Mapping[int, Sequence[tuple[str, int]]]\n", - " non_consecutive_visits: Mapping[int, Sequence[tuple[str, int]]]\n", - " shipments_by_parking: Mapping[str, Sequence[Sequence[int]]]\n", - "\n", - "\n", - "@dataclasses.dataclass\n", - "class Scenario:\n", - " \"\"\"Holds data from a single scenario.\n", - "\n", - " Attributes:\n", - " name: A unique name of the scenario. Used as unique key for the scenario,\n", - " and as an index in the data frames used throughout the notebook.\n", - " scenario: The JSON data of the scenario.\n", - " scenario_file: The file name of the scenario.\n", - " solution: The JSON data of the solution.\n", - " solution_file: The file name of the solution.\n", - " parking_json: The parking location data in the JSON format.\n", - " parking_file: The file name of the parking JSON file.\n", - " parking_locations: The list of parking locations indexed by the parking\n", - " location tags.\n", - " parking_for_shipment: The assignment of shipments to parking locations. The\n", - " keys are shipment indices; the values are the parking location tags for\n", - " the shipments.\n", - " parking_location_data: Contains aggregated data about parking locations for\n", - " the scenario.\n", - " \"\"\"\n", - "\n", - " name: str\n", - " scenario: cfr_json.OptimizeToursRequest\n", - " scenario_file: str\n", - " solution: cfr_json.OptimizeToursResponse\n", - " solution_file: str\n", - " parking_json: dataclasses.InitVar[Any] = None\n", - " parking_file: str | None = None\n", - " parking_locations: Mapping[str, two_step_routing.ParkingLocation] = None\n", - " parking_for_shipment: Mapping[int, str] = None\n", - " parking_location_data: ParkingLocationData = dataclasses.field(init=False)\n", - "\n", - " def __post_init__(self, parking_json: Any | None) -> None:\n", - " super().__init__()\n", - " self.parking_location_data = get_parking_location_aggregate_data(self)\n", - " if parking_json is not None:\n", - " if (\n", - " self.parking_locations is not None\n", - " or self.parking_for_shipment is not None\n", - " ):\n", - " raise ValueError(\n", - " \"Either only parking_json or only parking_locations and\"\n", - " \" parking_for_shipment can not be None.\"\n", - " )\n", - " self.parking_locations = {}\n", - " for parking_location_args in parking_json.get(\"parking_locations\", ()):\n", - " parking_location = two_step_routing.ParkingLocation(\n", - " **parking_location_args\n", - " )\n", - " if parking_location.tag in self.parking_locations:\n", - " print(parking_location)\n", - " raise ValueError(\n", - " f\"Duplicate parking location tag: {parking_location.tag}\"\n", - " )\n", - " self.parking_locations[parking_location.tag] = parking_location\n", - " self.parking_for_shipment = {\n", - " int(shipment_index): parking_tag\n", - " for shipment_index, parking_tag in parking_json.get(\n", - " \"parking_for_shipment\", {}\n", - " ).items()\n", - " }\n", - " if (self.parking_locations is not None) != (\n", - " self.parking_for_shipment is not None\n", - " ):\n", - " raise ValueError(\n", - " \"parking_locations and parking_for_shipment must either both be None\"\n", - " \" or both be not None.\"\n", - " )\n", - " if self.parking_locations is None:\n", - " # Create empty parking data structures, so that we do not need to do too\n", - " # much branching in the code below.\n", - " self.parking_locations = {}\n", - " self.parking_for_shipment = {}\n", - "\n", - " @property\n", - " def model(self) -> cfr_json.ShipmentModel:\n", - " \"\"\"Returns model of the scenario.\"\"\"\n", - " return self.scenario[\"model\"]\n", - "\n", - " @property\n", - " def shipments(self) -> Sequence[cfr_json.Shipment]:\n", - " \"\"\"Returns the list of shipments in the scenario.\"\"\"\n", - " return self.model.get(\"shipments\", ())\n", - "\n", - " @property\n", - " def vehicles(self) -> Sequence[cfr_json.Vehicle]:\n", - " \"\"\"Returns the list of vehicles in the scenario.\"\"\"\n", - " return self.model.get(\"vehicles\", ())\n", - "\n", - " @property\n", - " def routes(self) -> Sequence[cfr_json.ShipmentRoute]:\n", - " \"\"\"Returns the list of routes in the scenario.\"\"\"\n", - " return self.solution.get(\"routes\", ())\n", - "\n", - " @property\n", - " def skipped_shipments(self) -> Sequence[cfr_json.SkippedShipment]:\n", - " \"\"\"Returns the list of skipped shipments in the scenario.\"\"\"\n", - " return self.solution.get(\"skippedShipments\", ())\n", - "\n", - " @functools.cached_property\n", - " def vehicle_for_shipment(self) -> Mapping[int, int]:\n", - " \"\"\"Returns a mapping from a shipment to the vehicle that serves it.\n", - "\n", - " Skipped shipments are not included.\n", - "\n", - " Returns:\n", - " A mapping where the key is a shipment index and the value is a vehicle\n", - " index.\n", - " \"\"\"\n", - " vehicle_for_shipment = {}\n", - " for vehicle_index, route in enumerate(self.routes):\n", - " for visit in route.get(\"visits\", ()):\n", - " shipment_index = visit.get(\"shipmentIndex\", 0)\n", - " vehicle_for_shipment[shipment_index] = vehicle_index\n", - " return vehicle_for_shipment\n", - "\n", - " def vehicle_label(self, vehicle_index: int) -> str:\n", - " \"\"\"Returns the label of a vehicle.\"\"\"\n", - " return self.vehicles[vehicle_index].get(\"label\", \"\")\n", - "\n", - "\n", - "def consume_suffix(text, suffix):\n", - " \"\"\"Consumes the suffix of a text.\n", - "\n", - " When `text` ends with `suffix`, returns `text` without the suffix. Otherwise,\n", - " returns None.\n", - " \"\"\"\n", - " if not text.endswith(suffix):\n", - " return None\n", - " return text[: -len(suffix)]\n", - "\n", - "\n", "# The scenario data used for the analyses in the notebook.\n", "_scenarios = {}\n", "\n", @@ -328,27 +164,20 @@ "def add_scenario_and_solution_from_bytes(\n", " name: str,\n", " scenario_bytes: bytes,\n", - " scenario_file: str,\n", " solution_bytes: bytes,\n", - " solution_file: str,\n", " parking_bytes: bytes | None,\n", - " parking_file: str | None,\n", - ") -> Scenario:\n", + ") -> analysis.Scenario:\n", " \"\"\"Adds a scenario-solution pair to the collection of analyzed scenarios.\n", "\n", " Loads the scenario and solution data by treating `scenario_bytes` and\n", - " `solution_bytes` as encoded JSON data. `scenario_file` and `solution_file` are\n", - " taken only as metadata preserved with the scenario/solution pair.\n", + " `solution_bytes` as encoded JSON data.\n", "\n", " Args:\n", " name: The name of the new scenario. If a scenario of this name already\n", " exists, it is overwritten.\n", " scenario_bytes: The serialized JSON data of the scenario.\n", - " scenario_file: The file name of the scenario. Used only for metadata.\n", " solution_bytes: The serialized JSON data of the solution.\n", - " solution_file: The file name of the solution. Used only for metadata.\n", " parking_bytes: The serialized JSON data for the parking location.\n", - " parking_file: The file name of the parking data. Used only for metadata.\n", "\n", " Returns:\n", " The added scenario.\n", @@ -362,14 +191,11 @@ " else None\n", " )\n", "\n", - " scenario = Scenario(\n", + " scenario = analysis.Scenario(\n", " name=name,\n", - " scenario_file=scenario_file,\n", " scenario=scenario_json,\n", - " solution_file=solution_file,\n", " solution=solution_json,\n", " parking_json=parking_json,\n", - " parking_file=parking_file,\n", " )\n", " _scenarios[name] = scenario\n", " return scenario\n", @@ -377,7 +203,7 @@ "\n", "def add_scenario_and_solution_from_file(\n", " name: str, scenario_file: str, solution_file: str, parking_file: str | None\n", - ") -> Scenario:\n", + ") -> analysis.Scenario:\n", " \"\"\"Adds a scenario-solution pair to the collection of analyzed scenarios.\n", "\n", " Assumes that `scenario_file` and `solution_file` are two JSON files that are\n", @@ -404,13 +230,10 @@ " parking_json = json.load(f)\n", " else:\n", " parking_file = None\n", - " scenario = Scenario(\n", + " scenario = analysis.Scenario(\n", " name=name,\n", - " scenario_file=scenario_file,\n", " scenario=scenario_json,\n", - " solution_file=solution_file,\n", " solution=solution_json,\n", - " parking_file=parking_file,\n", " parking_json=parking_json,\n", " )\n", " _scenarios[name] = scenario\n", @@ -423,7 +246,9 @@ "\n", "\n", "def dataframe_from_all_scenarios(\n", - " row_callback: Callable[[Scenario], dict[str, Any] | list[dict[str, Any]]]\n", + " row_callback: Callable[\n", + " [analysis.Scenario], dict[str, Any] | list[dict[str, Any]]\n", + " ]\n", "):\n", " \"\"\"Creates a data frame by calling `row_callback` for each scenario.\n", "\n", @@ -450,7 +275,9 @@ "\n", "\n", "def show_table_from_all_scenarios(\n", - " row_callback: Callable[[Scenario], dict[str, Any] | list[dict[str, Any]]]\n", + " row_callback: Callable[\n", + " [analysis.Scenario], dict[str, Any] | list[dict[str, Any]]\n", + " ]\n", "):\n", " display.display(\n", " data_table.DataTable(dataframe_from_all_scenarios(row_callback))\n", @@ -497,93 +324,6 @@ "test_get_glob_substitutions()\n", "\n", "\n", - "def get_parking_location_aggregate_data(\n", - " scenario: Scenario,\n", - ") -> ParkingLocationData:\n", - " \"\"\"Collects aggregated parking location data from a scenario.\"\"\"\n", - "\n", - " routes = scenario.routes\n", - " all_parking_tags = set()\n", - " # The number of visits to each parking location by each vehicle.\n", - " visits_by_vehicle = collections.defaultdict(\n", - " functools.partial(collections.defaultdict, int)\n", - " )\n", - " # The set of vehicles that are used to serve the given parking.\n", - " vehicles_by_parking = collections.defaultdict(\n", - " functools.partial(collections.defaultdict, list)\n", - " )\n", - "\n", - " vehicle_consecutive_visits = collections.defaultdict(list)\n", - " vehicle_non_consecutive_visits = collections.defaultdict(list)\n", - "\n", - " shipments_by_parking = collections.defaultdict(list)\n", - "\n", - " for vehicle_index, route in enumerate(routes):\n", - " visits = route.get(\"visits\", ())\n", - " vehicle_label = route.get(\"vehicleLabel\", f\"vehicle {vehicle_index}\")\n", - " current_parking_tag = None\n", - " parking_tag_left_in_previous_visit = None\n", - " for visit in visits:\n", - " label = visit.get(\"shipmentLabel\")\n", - " shipment_index = visit.get(\"shipmentIndex\", 0)\n", - " departure_tag = consume_suffix(label, \" departure\")\n", - " if departure_tag is not None:\n", - " if current_parking_tag != departure_tag:\n", - " raise ValueError(\n", - " \"Parking tag mismatch for a departure. Expected\"\n", - " f\" {current_parking_tag!r}, found {departure_tag!r}.\"\n", - " )\n", - " all_parking_tags.add(departure_tag)\n", - " parking_tag_left_in_previous_visit = departure_tag\n", - " current_parking_tag = None\n", - " continue\n", - " arrival_tag = consume_suffix(label, \" arrival\")\n", - " if arrival_tag is not None:\n", - " if current_parking_tag is not None:\n", - " raise ValueError(\n", - " f\"Unexpected arrival to parking {arrival_tag!r}, currently in\"\n", - " f\" parking {current_parking_tag!r}\"\n", - " )\n", - " current_parking_tag = arrival_tag\n", - " parking_visit_index = len(shipments_by_parking[arrival_tag])\n", - " parking_visit_tuple = (arrival_tag, parking_visit_index)\n", - "\n", - " parking_vehicles = vehicles_by_parking[arrival_tag]\n", - " if parking_tag_left_in_previous_visit == arrival_tag:\n", - " # This is a consecutive visit to the parking location.\n", - " vehicle_consecutive_visits[vehicle_index].append(parking_visit_tuple)\n", - " elif vehicle_index in parking_vehicles:\n", - " # parking_tag_left_in_previous_visit != arrival_tag holds because of\n", - " # the previous if statement. This is a non-consecutive visit to this\n", - " # parking by this vehicle.\n", - " vehicle_non_consecutive_visits[vehicle_index].append(\n", - " parking_visit_tuple\n", - " )\n", - "\n", - " visits_by_vehicle[vehicle_label][arrival_tag] += 1\n", - " parking_vehicles[vehicle_index].append(parking_visit_index)\n", - " shipments_by_parking[arrival_tag].append([])\n", - "\n", - " if (\n", - " arrival_tag is None\n", - " and departure_tag is None\n", - " and current_parking_tag is not None\n", - " ):\n", - " # This is a shipment served from a parking location.\n", - " shipments_by_parking[current_parking_tag][-1].append(shipment_index)\n", - " pass\n", - "\n", - " parking_tag_left_in_previous_visit = None\n", - "\n", - " return ParkingLocationData(\n", - " all_parking_tags=all_parking_tags,\n", - " vehicles_by_parking=vehicles_by_parking,\n", - " consecutive_visits=vehicle_consecutive_visits,\n", - " non_consecutive_visits=vehicle_non_consecutive_visits,\n", - " shipments_by_parking=shipments_by_parking,\n", - " )\n", - "\n", - "\n", "def reformat_timestring_or_default(\n", " value: cfr_json.TimeString | None, default: str\n", ") -> str:\n", @@ -711,15 +451,14 @@ " name,\n", " scenario_bytes=scenario_bytes,\n", " solution_bytes=solution_bytes,\n", - " scenario_file=scenario_file,\n", - " solution_file=solution_file,\n", " parking_bytes=parking_bytes,\n", - " parking_file=parking_file,\n", " )\n", " print(\n", " f\"Added scenario: {scenario.name!r} from {scenario_file} and\"\n", " f\" {solution_file}\"\n", " )\n", + " if parking_file is not None:\n", + " print(f\"Loaded parking data from {parking_file}.\")\n", " return\n", "\n", " # Otherwise, go through all ZIP files and try to treat them as the fleet\n", @@ -738,10 +477,8 @@ " scenario_bytes = zipped_file.read(\"scenario.json\")\n", " solution_bytes = zipped_file.read(\"solution.json\")\n", " parking_bytes = None\n", - " parking_file = None\n", " try:\n", " parking_bytes = zipped_file.read(\"parking.json\")\n", - " parking_file = filename + \"/parking.json\"\n", " except KeyError:\n", " pass\n", " scenario_name = f\"{name}/{filename}\" if name else filename\n", @@ -750,9 +487,6 @@ " scenario_bytes=scenario_bytes,\n", " solution_bytes=solution_bytes,\n", " parking_bytes=parking_bytes,\n", - " scenario_file=filename + \"/scenario.json\",\n", - " solution_file=filename + \"/solution.json\",\n", - " parking_file=parking_file,\n", " )\n", " print(f\"Added scenario {scenario_name!r} from zip {filename}\")\n", "\n", @@ -845,7 +579,7 @@ "\n", "def _apply_substitutions(pattern, substitutions):\n", " num_wildcards = pattern.count(\"*\")\n", - " if num_wildcards \u003e 0:\n", + " if num_wildcards > 0:\n", " if num_wildcards != len(substitutions):\n", " raise ValueError(\n", " \"The number of substitutions in the pattern does not match the number\"\n", @@ -1439,7 +1173,7 @@ " return [{\n", " \"# distinct visited parkings\": len(parking_data.all_parking_tags),\n", " \"# parkings served by multiple vehicles\": sum(\n", - " int(len(parking_vehicles) \u003e 1)\n", + " int(len(parking_vehicles) > 1)\n", " for parking_vehicles in parking_data.vehicles_by_parking.values()\n", " ),\n", " \"# ping-pongs\": sum(\n",