Skip to content

Commit

Permalink
Updated the two_step_routing library and the Colab notebook.
Browse files Browse the repository at this point in the history
Changes include:
- added a new two_step_routing.Planner option to include travelModel and
  travelDurationMultiple in the transitions in the merged solution. This
  option is off by default.
- added code to compute route metrics in the merged plan. For now, this
  is used only for individual routes, the aggregated metrics for the
  whole plan are not updated (yet).
- Added instructions how to run the Colab notebook with a local runtime
  (via Docker) to the notebook.
- Added time travel stats to the notebook + cleaned up their computation.
- Refactored parking location issue detection in the notebook (no
  functional changed),
- More refactorings and new code in preparation for a third+fourth solve
  phases (refinement of the local and global model). For now, these
  changes are there in preparation, and they do not change the behavior
  of the solver.
  • Loading branch information
ondrasej committed Oct 25, 2023
1 parent e1f7754 commit e97825e
Show file tree
Hide file tree
Showing 6 changed files with 2,566 additions and 266 deletions.
214 changes: 128 additions & 86 deletions examples/two_step_routing/cfr-json-analysis.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -11,54 +11,75 @@
},
{
"cell_type": "markdown",
"metadata": {
"id": "RrvDGML6NlWW"
},
"source": [
"## License\n",
"\n",
"Copyright 2023 Google LLC. All Rights Reserved.\n",
"\n",
"Use of this source code is governed by an MIT-style license that can be found\n",
"in the LICENSE file or at https://opensource.org/licenses/MIT."
],
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "RrvDGML6NlWW"
}
"id": "qZIL_uwHMAsg"
},
"source": [
"## Using the notebook"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "HvdsDlH_R6TH"
},
"source": [
"## Prerequisities\n",
"\n",
"* If you're not familiar with Colab notebooks, check out the\n",
" [Welcome to Colaboratory](https://colab.research.google.com/notebooks/intro.ipynb)\n",
" notebook first for a tutorial.\n",
"* We have tested the notebook with the public Colab runtime. It may also work\n",
" with a\n",
" [local runtime](https://research.google.com/colaboratory/local-runtimes.html),\n",
" but this has not been tested yet.\n",
"\n",
"## How to use the colab\n",
"### Prerequisities\n",
"\n",
"If you're not familiar with Colab notebooks, check out the\n",
"[Welcome to Colaboratory](https://colab.research.google.com/notebooks/intro.ipynb)\n",
"notebook first for a tutorial.\n",
"\n",
"To run the cells in the notebook, you need a runtime:\n",
"\n",
"* We regularly test the notebook with the public hosted runtime. This should\n",
" be sufficient for experiments and to analyze smaller scenarios.\n",
"* If the hosted runtime is too slow or you need to process large amount of\n",
" data,\n",
" [running a local runtime](https://research.google.com/colaboratory/local-runtimes.html)\n",
" might be a better option. To use a local runtime with this notebook, start\n",
" the local runtime with `sudo docker run -p 127.0.0.1:9000:8080 -e\n",
" COLAB_KERNEL_MANAGER_PROXY_PORT=9000\n",
" us-docker.pkg.dev/colab-images/public/runtime` and then follow the rest of\n",
" the instructions from the\n",
" [local runtime guide](https://research.google.com/colaboratory/local-runtimes.html).\n",
" As of 2023-10-23, using the `COLAB_KERNEL_MANAGER_PROXY_PORT` option is\n",
" needed to make file upload work correctly.\n",
"\n",
"### How to use the colab\n",
"\n",
"1. Once you're connected to a runtime, the first setp is to run the cells in\n",
" the \"Imports, helper functions\" section to initialize the notebook. You can\n",
" re-run the cell \"Import everything, ...\" at any time to quickly remove all\n",
" loaded scenarios from the notebook.\n",
" re-run the cell \"Define helper functions, ...\" at any time to quickly remove\n",
" all loaded scenarios from the notebook.\n",
"\n",
"2. Once the notebook is initialized, you will be able to add CFR scenarios and\n",
" solutions to the notebook to analyze them. The easiest way is to upload\n",
" either ZIP files from the fleet routing app or the scenario/solution JSON\n",
" file pairs through the form in the section\n",
" [Upload scenarios and solutions](#scrollTo=G6mXfeDxgA4M&line=1&uniqifier=1).\n",
" [Upload scenarios and solutions](#scrollTo=G6mXfeDxgA4M\u0026line=1\u0026uniqifier=1).\n",
"\n",
"3. Now you have data to analyze. Run any other cell in the notebook to walk\n",
" through the data.\n",
"\n",
"## Don't panic!\n",
"### Don't panic!\n",
"\n",
"If you have any questions or run into issues with using the colab, contact\n",
"ondrasej at google dot com."
"ondrasej at google dot com.\n"
]
},
{
Expand All @@ -70,30 +91,16 @@
"## Imports, helper functions (run these first)"
]
},
{
"cell_type": "code",
"source": [
"# @title Pull the CFR libraries from GitHub (run this once)\n",
"\n",
"!git clone https://github.com/google/cfr"
],
"metadata": {
"cellView": "form",
"id": "vG20dqFKAPpE"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"cellView": "form",
"id": "ukVuR1MgMp4y"
"id": "vG20dqFKAPpE"
},
"outputs": [],
"source": [
"# @title Import everything, define helper functions, reset data structures\n",
"# @title Import everything (run this once)\n",
"\n",
"import collections\n",
"from collections.abc import Callable, Mapping, Sequence, Set\n",
Expand All @@ -115,8 +122,21 @@
"import ipywidgets\n",
"import pandas as pd\n",
"\n",
"!git clone https://github.com/google/cfr\n",
"from cfr.examples.two_step_routing import cfr_json\n",
"from cfr.examples.two_step_routing import two_step_routing\n",
"from cfr.examples.two_step_routing import two_step_routing\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"cellView": "form",
"id": "ukVuR1MgMp4y"
},
"outputs": [],
"source": [
"# @title Define helper functions, reset data structures\n",
"\n",
"\n",
"# Increase the default limit on the number of rows in a DataTable. The default\n",
Expand Down Expand Up @@ -144,26 +164,27 @@
" 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: The set of vehicles that visit a particular parking\n",
" location. The key of the mapping is the parking location tag, the value is\n",
" the set of vehicle indices.\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",
" parking location tags of parkings that are visited right after another\n",
" visit to the same parking location. Note that if a certain parking\n",
" location is visited more than twice in a row, it will appear in the list\n",
" multiple times.\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 set of shipments that visit a parking location.\n",
" Note that this mapping contains only shipments that are performed in the\n",
" plan.\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, Set[int]]\n",
" consecutive_visits: Mapping[int, Sequence[str]]\n",
" non_consecutive_visits: Mapping[int, Sequence[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",
Expand Down Expand Up @@ -488,7 +509,9 @@
" 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(set)\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",
Expand Down Expand Up @@ -522,19 +545,23 @@
" 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 (\n",
" vehicle_index in parking_vehicles\n",
" and parking_tag_left_in_previous_visit != arrival_tag\n",
" ):\n",
" # This is a non-consecutive visit to this parking by this vehicle.\n",
" vehicle_non_consecutive_visits[vehicle_index].append(arrival_tag)\n",
" elif parking_tag_left_in_previous_visit == arrival_tag:\n",
" vehicle_consecutive_visits[vehicle_index].append(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.add(vehicle_index)\n",
" parking_vehicles[vehicle_index].append(parking_visit_index)\n",
" shipments_by_parking[arrival_tag].append([])\n",
"\n",
" if (\n",
Expand All @@ -561,7 +588,7 @@
" value: cfr_json.TimeString | None, default: str\n",
") -> str:\n",
" \"\"\"Returns a formatted timestamp or `default`, if `value` is `None`.\"\"\"\n",
" # TODO(ondrasej): If the global span of the scenario is <= 24 hours, do not\n",
" # TODO(ondrasej): If the global span of the scenario is \u003c= 24 hours, do not\n",
" # show the date. Also, normalize all timestamps to the same timezone and do\n",
" # not show the timezone suffix.\n",
" if value is not None:\n",
Expand Down Expand Up @@ -598,13 +625,22 @@
" )"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1UyAZd59TLc2"
},
"source": [
"## Upload scenarios and solutions"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "G6mXfeDxgA4M"
},
"source": [
"### Upload scenarios and solutions"
"### Upload via browser"
]
},
{
Expand Down Expand Up @@ -809,7 +845,7 @@
"\n",
"def _apply_substitutions(pattern, substitutions):\n",
" num_wildcards = pattern.count(\"*\")\n",
" if num_wildcards > 0:\n",
" if num_wildcards \u003e 0:\n",
" if num_wildcards != len(substitutions):\n",
" raise ValueError(\n",
" \"The number of substitutions in the pattern does not match the number\"\n",
Expand Down Expand Up @@ -988,11 +1024,19 @@
" max_working_hours = datetime.timedelta()\n",
" actual_working_hours = datetime.timedelta()\n",
"\n",
" num_time_travel = 0\n",
" num_hard_time_travel = 0\n",
" for vehicle, route in zip(vehicles, routes, strict=True):\n",
" max_working_hours += cfr_json.get_vehicle_max_working_hours(\n",
" scenario.model, vehicle\n",
" )\n",
" actual_working_hours += cfr_json.get_vehicle_actual_working_hours(route)\n",
" num_time_travel += cfr_json.get_num_decreasing_visit_times(\n",
" scenario.model, route, consider_visit_duration=True\n",
" )\n",
" num_hard_time_travel += cfr_json.get_num_decreasing_visit_times(\n",
" scenario.model, route, consider_visit_duration=False\n",
" )\n",
"\n",
" return {\n",
" \"# vehicles\": len(vehicles),\n",
Expand All @@ -1001,6 +1045,8 @@
" \"actual working time %\": (\n",
" f\"{100 * actual_working_hours / max_working_hours:.2f} %\"\n",
" ),\n",
" \"# soft time travel\": str(num_time_travel),\n",
" \"# time travel\": str(num_hard_time_travel),\n",
" }\n",
"\n",
"\n",
Expand Down Expand Up @@ -1102,7 +1148,8 @@
" \"actual working time\": \"\",\n",
" \"# consecutive visits\": \"\",\n",
" \"# non-consecutive visits\": \"\",\n",
" \"num time travel\": \"\",\n",
" \"# time travel\": \"\",\n",
" \"# soft time travel\": \"\",\n",
" }\n",
" visits = route.get(\"visits\", ())\n",
" if not visits:\n",
Expand All @@ -1111,23 +1158,12 @@
" data.append(row)\n",
" continue\n",
"\n",
" last_visit_end = _DATETIME_MAX_UTC\n",
" num_time_travel = 0\n",
" for visit in visits:\n",
" shipment_index = visit.get(\"shipmentIndex\", 0)\n",
" shipment = shipments[shipment_index]\n",
" is_pickup = visit.get(\"isPickup\", False)\n",
" visit_request_index = visit.get(\"visitRequestIndex\", 0)\n",
" visit_type = \"pickups\" if is_pickup else \"deliveries\"\n",
" visit_request = shipment[visit_type][visit_request_index]\n",
" visit_duration = cfr_json.parse_duration_string(\n",
" visit_request.get(\"duration\", \"0s\")\n",
" )\n",
"\n",
" visit_time = cfr_json.parse_time_string(visit[\"startTime\"])\n",
" if visit_time < last_visit_end:\n",
" num_time_travel += 1\n",
" last_visit_end = visit_time + visit_duration\n",
" num_time_travel = cfr_json.get_num_decreasing_visit_times(\n",
" scenario.model, route, False\n",
" )\n",
" num_soft_time_travel = cfr_json.get_num_decreasing_visit_times(\n",
" scenario.model, route, True\n",
" )\n",
"\n",
" start_time = cfr_json.parse_time_string(route[\"vehicleStartTime\"])\n",
" end_time = cfr_json.parse_time_string(route[\"vehicleEndTime\"])\n",
Expand Down Expand Up @@ -1167,7 +1203,8 @@
" if non_consecutive_visits is not None\n",
" else \"\"\n",
" )\n",
" row[\"num time travel\"] = str(num_time_travel)\n",
" row[\"# time travel\"] = str(num_time_travel)\n",
" row[\"# soft time travel\"] = str(num_soft_time_travel)\n",
" data.append(row)\n",
" return data\n",
"\n",
Expand Down Expand Up @@ -1307,7 +1344,7 @@
"\n",
" def add_breaks_before_timestamp(timestamp):\n",
" nonlocal next_break_start\n",
" while next_break_start < timestamp:\n",
" while next_break_start \u003c timestamp:\n",
" current_break = breaks.pop(0)\n",
" data.append({\n",
" \"shipment\": \"\",\n",
Expand Down Expand Up @@ -1402,7 +1439,7 @@
" return [{\n",
" \"# distinct visited parkings\": len(parking_data.all_parking_tags),\n",
" \"# parkings served by multiple vehicles\": sum(\n",
" int(len(parking_vehicles) > 1)\n",
" int(len(parking_vehicles) \u003e 1)\n",
" for parking_vehicles in parking_data.vehicles_by_parking.values()\n",
" ),\n",
" \"# ping-pongs\": sum(\n",
Expand Down Expand Up @@ -1546,11 +1583,16 @@
],
"metadata": {
"colab": {
"private_outputs": true,
"provenance": [],
"collapsed_sections": [
"q7PVQWUsf3iF"
]
],
"last_runtime": {
"build_target": "",
"kind": "local"
},
"name": "CFR JSON request/response analysis",
"private_outputs": true,
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
Expand Down
Loading

0 comments on commit e97825e

Please sign in to comment.