OpenStreetMap Example#

In this example, we download a road network from OSM using the OSMNx package, and then process the result, resulting in a RouteE Compass network dataset.

requirements#

To download an open street maps dataset, we'll need some extra dependnecies which are included with the conda distribution of this pacakge:

conda create -n routee-compass -c conda-forge python=3.11 nrel.routee.compass
import osmnx as ox

import json

from nrel.routee.compass import CompassApp
from nrel.routee.compass.io import generate_compass_dataset, results_to_geopandas
from nrel.routee.compass.plot import plot_route_folium, plot_routes_folium

Building RouteE Compass Dataset#

Get OSM graph#

First, we need to get an OSM graph that we can use to convert into the format RouteE Compass expects.

In this example we will load in a road network that covers Golden, Colorado as a small example, but this workflow will work with any osmnx graph (osmnx provides many graph download operations).

g = ox.graph_from_place("Denver, Colorado, USA", network_type="drive")

Convert Graph to Compass Dataset#

Now, we call the generate_compass_dataset function which will convert the osmnx graph into files that are compatible with RouteE Compass.

Note

In order to get the most accurate energy results from the routee-powertrain vehicle models, it's important to include road grade information since it plays a large factor in vehicle energy consumption (add_grade=True) That being said, adding grade can be a big lift computationally. In our case, we pull digital elevation model (DEM) raster files from USGS and then use osmnx to append elevation and grade to the graph. If the graph is large, this can take a while to download and could take up a lot of disk space. So, we recommend that you include grade information in your graph but want to be clear about the requirements for doing so.

generate_compass_dataset(g, output_directory="denver_co", add_grade=True)
processing graph topology and speeds
adding grade information
processing vertices
processing edges
writing vertex files
writing edge files
writing edge attribute files
copying default configuration TOML files
downloading the default RouteE Powertrain models

This will parse the OSM graph and write the RouteE Compass files into a new folder "denver_co/". If you take a look in this directory, you'll notice some .toml files like: osm_default_energy.toml. These are configurations for the compass application. Take a look here for more information about this file.

Running#

Load Application#

Now we can load the application from one of our config files. We'll pick osm_default_energy.toml for computing energy optimal routes.

app = CompassApp.from_config_file("denver_co/osm_default_energy.toml")

Queries#

With our application loaded we can start computing routes by passing queries to the app. To demonstrate, we'll route between two locations in Denver, CO utilzing the grid search input plugin to run three separate searches.

The model_name is the vehicle we want to use for the route. If you look in the folder denver_co/models you'll see a collection of routee-powertrain models that can be used to compute the energy for your query.

The vehicle_state_variable_rates section defines rates to be applied to each component of the cost function. In this case we use the following costs:

  • 0.655 dollars per mile

  • 20 dollars per hour (or 0.333 dollars per minute)

  • 3 dollars per gallon of gas

The grid_search section defines our test cases. Here, we have three cases: [least_time, least_energy, least_cost]. In the least_time and least_energy cases, we zero out all other variable contributions using the state_variable_coefficients which always get applied to each cost componenet. In the least_cost case, we allow each cost component to contribute equally and the algorithm will minimize the resulting cost from all components being added together (after getting multiplied by the appropriate vehicle_state_variable_rate.

query = [
    {
        "origin_x": -104.969307,
        "origin_y": 39.779021,
        "destination_x": -104.975360,
        "destination_y": 39.693005,
        "model_name": "2016_TOYOTA_Camry_4cyl_2WD",
        "vehicle_rates": {
            "distance": {"type": "factor", "factor": 0.655},
            "time": {"type": "factor", "factor": 0.33},
            "energy_liquid": {"type": "factor", "factor": 3.0},
        },
        "grid_search": {
            "test_cases": [
                {
                    "name": "least_time",
                    "weights": {"distance": 0, "time": 1, "energy_liquid": 0},
                },
                {
                    "name": "least_energy",
                    "weights": {"distance": 0, "time": 0, "energy_liquid": 1},
                },
                {
                    "name": "least_cost",
                    "weights": {"distance": 1, "time": 1, "energy_liquid": 1},
                },
            ]
        },
    },
]

Now, let's pass the query to the application.

Note

A query can be a single object, or, a list of objects. If the input is a list of objects, the application will run these queries in parallel over the number of threads defined in the config file under the paralellism key (defaults to 2).

results = app.run(query)
input plugins: 100%|██████████| 1/1 [00:00<00:00, 2323.24it/s]


search: 100%|██████████| 3/3 [00:00<00:00, 108.38it/s]

Analysis#

The application returns the results as a list of python dictionaries. Since we used the grid search to specify three separate searches, we should get three results back:

for r in results:
    error = r.get("error")
    if error is not None:
        print(f"request had error: {error}")

assert len(results) == 3, f"expected 3 results, found {len(results)}"

Traversal and Cost Summaries#

Since we have the traversal output plugin activated by default, we can take a look at the summary for each result under the traversal_summary key.

def pretty_print(dict):
    print(json.dumps(dict, indent=4))


shortest_time_result = next(
    filter(lambda r: r["request"]["name"] == "least_time", results)
)
least_energy_result = next(
    filter(lambda r: r["request"]["name"] == "least_energy", results)
)
least_cost_result = next(
    filter(lambda r: r["request"]["name"] == "least_cost", results)
)




shortest_time_result["route"]["path"]["features"][-1]
{'type': 'Feature',
 'geometry': {'type': 'LineString',
  'coordinates': [[-104.97888946533203, 39.69377136230469],
   [-104.97833251953125, 39.693267822265625],
   [-104.9778823852539, 39.6928596496582],
   [-104.97766876220703, 39.692665100097656],
   [-104.97666931152344, 39.69175720214844],
   [-104.97602081298828, 39.691261291503906],
   [-104.97518920898438, 39.690696716308594]]},
 'properties': {'edge_id': 1,
  'access_cost': 0.0,
  'traversal_cost': 0.0,
  'result_state': [8.968190648210127,
   10.197468943043088,
   0.30224151217983125]},
 'id': 1}

Summary of route result for distance, time, and energy:

pretty_print(shortest_time_result["route"]["traversal_summary"])
{
    "distance": 8.968190648210127,
    "time": 10.197468943043088,
    "energy_liquid": 0.30224151217983125
}

And, if we need to know the units and/or the initial conditions for the search, we can look at the state model

pretty_print(shortest_time_result["route"]["state_model"])
{
    "distance": {
        "distance_unit": "miles",
        "initial": 0.0,
        "index": 0,
        "name": "distance"
    },
    "time": {
        "time_unit": "minutes",
        "initial": 0.0,
        "index": 1,
        "name": "time"
    },
    "energy_liquid": {
        "energy_unit": "gallons_gasoline",
        "initial": 0.0,
        "index": 2,
        "name": "energy_liquid"
    }
}

The cost section shows the costs per unit assigned to the trip, in dollars.

This is based on the user assumptions assigned in the configuration which can be overriden in the route request query.

pretty_print(shortest_time_result["route"]["cost"])
{
    "distance": 5.874164874577634,
    "time": 3.3651647512042193,
    "total_cost": 10.146054162321347,
    "energy_liquid": 0.9067245365394938
}

The cost_model section includes details for how these costs were calculated.

The user can set different state variable coefficients in the query that are weighted against the vehicle state variable rates.

The algorithm will rely on the weighted costs while the cost summary will show the final costs without weight coefficients applied.

pretty_print(shortest_time_result["route"]["cost_model"])
{
    "distance": {
        "feature": "distance",
        "weight": 0.0,
        "vehicle_rate": {
            "type": "factor",
            "factor": 0.655
        },
        "network_rate": {
            "type": "zero"
        }
    },
    "time": {
        "feature": "time",
        "weight": 1.0,
        "vehicle_rate": {
            "type": "factor",
            "factor": 0.33
        },
        "network_rate": {
            "type": "zero"
        }
    },
    "energy_liquid": {
        "feature": "energy_liquid",
        "weight": 0.0,
        "vehicle_rate": {
            "type": "factor",
            "factor": 3.0
        },
        "network_rate": {
            "type": "zero"
        }
    },
    "cost_aggregation": "sum"
}

Each response object contains this information. The least energy traversal and cost summary are below.

pretty_print(least_energy_result["route"]["traversal_summary"])
pretty_print(least_energy_result["route"]["cost"])
{
    "distance": 7.1767805317576805,
    "time": 13.061726766693392,
    "energy_liquid": 0.2667693229264744
}
{
    "energy_liquid": 0.8003079687794231,
    "distance": 4.700791248301281,
    "time": 4.31036983300882,
    "total_cost": 9.811469050089524
}

What becomes interesting is if we can compare our choices. Here's a quick comparison of the shortest time and least energy routes:

dist_diff = (
    shortest_time_result["route"]["traversal_summary"]["distance"]
    - least_energy_result["route"]["traversal_summary"]["distance"]
)
time_diff = (
    shortest_time_result["route"]["traversal_summary"]["time"]
    - least_energy_result["route"]["traversal_summary"]["time"]
)
enrg_diff = (
    shortest_time_result["route"]["traversal_summary"]["energy_liquid"]
    - least_energy_result["route"]["traversal_summary"]["energy_liquid"]
)
cost_diff = (
    shortest_time_result["route"]["cost"]["total_cost"]
    - least_energy_result["route"]["cost"]["total_cost"]
)
dist_unit = shortest_time_result["route"]["state_model"]["distance"]["distance_unit"]
time_unit = shortest_time_result["route"]["state_model"]["time"]["time_unit"]
enrg_unit = shortest_time_result["route"]["state_model"]["energy_liquid"]["energy_unit"]
print(f" - distance: {dist_diff:.2f} {dist_unit} further with time-optimal")
print(f" - time: {-time_diff:.2f} {time_unit} longer with energy-optimal")
print(f" - energy: {enrg_diff:.2f} {enrg_unit} more with time-optimal")
print(f" - cost: ${cost_diff:.2f} more with time-optimal")
 - distance: 1.79 miles further with time-optimal
 - time: 2.86 minutes longer with energy-optimal
 - energy: 0.04 gallons_gasoline more with time-optimal
 - cost: $0.33 more with time-optimal

In addition to the summary, the result also contains much more information. Here's a list of all the different sections that get returned:

def print_keys(d, indent=0):
    for k in sorted(d.keys()):
        print(f"{' '*indent} - {k}")
        if isinstance(d[k], dict):
            print_keys(d[k], indent + 2)


print_keys(least_energy_result)
 - iterations
 - output_plugin_executed_time
 - request
   - destination_edge
   - destination_x
   - destination_y
   - model_name
   - name
   - origin_edge
   - origin_x
   - origin_y
   - query_weight_estimate
   - vehicle_rates
     - distance
       - factor
       - type
     - energy_liquid
       - factor
       - type
     - time
       - factor
       - type
   - weights
     - distance
     - energy_liquid
     - time
 - route
   - cost
     - distance
     - energy_liquid
     - time
     - total_cost
   - cost_model
     - cost_aggregation
     - distance
       - feature
       - network_rate
         - type
       - vehicle_rate
         - factor
         - type
       - weight
     - energy_liquid
       - feature
       - network_rate
         - type
       - vehicle_rate
         - factor
         - type
       - weight
     - time
       - feature
       - network_rate
         - type
       - vehicle_rate
         - factor
         - type
       - weight
   - path
     - features
     - type
   - state_model
     - distance
       - distance_unit
       - index
       - initial
       - name
     - energy_liquid
       - energy_unit
       - index
       - initial
       - name
     - time
       - index
       - initial
       - name
       - time_unit
   - traversal_summary
     - distance
     - energy_liquid
     - time
 - route_edges
 - search_executed_time
 - search_result_size_mib
 - search_runtime
 - tree_size_count

We can also convert the results into a geodataframe:

gdf = results_to_geopandas(least_energy_result)
gdf.head()
output_plugin_executed_time search_executed_time search_runtime route_edges tree_size_count search_result_size_mib iterations request.origin_x request.origin_y request.destination_x ... route.cost_model.distance.weight route.cost_model.distance.vehicle_rate.type route.cost_model.distance.vehicle_rate.factor route.cost_model.distance.network_rate.type route.cost_model.cost_aggregation route.cost.energy_liquid route.cost.distance route.cost.time route.cost.total_cost geometry
route_id
0 2025-01-02T20:42:34.067132603+00:00 2025-01-02T20:42:34.057360881+00:00 0:00:00.009 80 3607 0.08828 3431 -104.969307 39.779021 -104.97536 ... 0.0 factor 0.655 zero sum 0.800308 4.700791 4.31037 9.811469 LINESTRING (-104.96917 39.77903, -104.9693 39....

1 rows × 61 columns

Plotting#

We can plot the results to see the difference between the two routes.

We can use the plot_route_folium function to plot single routes, passing in the line_kwargs parameter to customize the folium linestring:

m = plot_route_folium(
    shortest_time_result, line_kwargs={"color": "red", "tooltip": "Shortest Time"}
)
m = plot_route_folium(
    least_energy_result,
    line_kwargs={"color": "green", "tooltip": "Least Energy"},
    folium_map=m,
)
m = plot_route_folium(
    least_cost_result,
    line_kwargs={"color": "blue", "tooltip": "Least Cost"},
    folium_map=m,
)
m
Make this Notebook Trusted to load map: File -> Trust Notebook

We can also use the plot_routes_folium function and pass in multiple results. The function will color the routes based on the value_fn which takes a single result as an argument. For example, we can tell it to color the routes based on the total energy usage.

folium_map = plot_routes_folium(
    results,
    value_fn=lambda r: r["route"]["traversal_summary"]["energy_liquid"],
    color_map="plasma",
)
folium_map
Make this Notebook Trusted to load map: File -> Trust Notebook

And the plot_routes_folium can also accept an existing folium_map parameter. Let's query our application with different origin and destination places:

query[0] = {
    **query[0],
    "origin_x": -105.081406,
    "origin_y": 39.667736,
    "destination_x": -104.95414,
    "destination_y": 39.65316,
}
new_results = app.run(query)


folium_map = plot_routes_folium(
    new_results,
    value_fn=lambda r: r["route"]["traversal_summary"]["energy_liquid"],
    color_map="plasma",
    folium_map=folium_map,
)
folium_map
input plugins: 100%|██████████| 1/1 [00:00<00:00, 10817.59it/s]


search: 100%|██████████| 3/3 [00:00<00:00, 97.17it/s]
Make this Notebook Trusted to load map: File -> Trust Notebook