Home
Softono
everystreet

everystreet

Open source MIT Jupyter Notebook
129
Stars
26
Forks
11
Issues
8
Watchers
1 year
Last Commit

About everystreet

everystreet is an algorithm and web application that finds an optimal route covering every street in a given city or neighborhood. Inspired by the everystreet challenge, which involves running or cycling along every single street of an area, the project tackles this as the Chinese Postman Problem, also known as the Route Inspection Problem, seeking the shortest closed path that visits every edge of a street network graph. The software uses Open Street Map data via the OSMnx library to retrieve street networks, applying custom filters to exclude highways, pedestrian paths, and restricted roads. It converts directed graphs to undirected ones, finds nodes with odd degree, computes shortest distances between them, creates a complete weighted graph, finds a minimum weight matching, and finally generates an Eulerian circuit using Hierholzer algorithm, as proposed by Edmonds and Johnson. Key features include map data retrieval through OSMnx, graph simplification by ignoring one-way street direction, and an included

Platforms

Web Self-hosted

Languages

Jupyter Notebook

#everystreet algorithm

Over the COVID-19 lockdown many runners and cyclist found themselves captured within their cities. Some of them ended up running or spinning endless hours on virtual races such as Zwift, but some as e.g. Mark Beaumont [1] decided to join #everystreet challenge. Every street is a challenge originated by Rickey Gates [2, 3] who run every single street in city of San Francisco in fall 2018 which took him 46 days and run 1,303 miles.

Inspired by Mark Beaumont who did this challenge over the lockdown in Edinburgh, place where I spend the lockdown, and literally everyone else who managed to accomplish this challenge (I would have never had the patience and motivation to run that much in the city) I said to myself that I am a mathematician and software engineer more then a runner or cyclist. So, I ask myself, what is the optimal route? Is there any algorithm which can generate such route?

Every street challenge

Rules of every street challenge are run or cycle every single street of given (metropolitan) area which is usually a city or a neighborhood. Till this point the rules are simple and clear, but the problem is the street definition. How do we define a street? Do we include pedestrian paths, parks or motorways? For simplicity, we consider a street network of edges and nodes (interception of two or more edges and dead-end roads) accessible by car*. Assuming that runner can run oneway roads in both direction, we do not consider road direction. In order to find such network we used Open Street Map API [4].

Chinese postman problem

Finding a route for #everystreet challenge is basically a well-known problem of the chinese postman, called after Chinese mathematician Kuan Mei-Ko. (Also known as Postman Tour or Route Inspection Problem) The problem is to find the shortest closed path (or circuit) such that visits every edge of a (closed and undirected) graph.

from libs.tools import *
from libs.graph_route import plot_graph_route

import networkx as nx
import osmnx as ox
import matplotlib.pyplot as plt

from network import Network
from network.algorithms import hierholzer

We used OSMnx as a source for geographical data. As an example, we chose an Edinburgh neighborhood of the Grange. In order to avoid heavy traffic we specify OSMnx query and limit the highway selection.

CUSTOM_FILTER = (
    '["highway"]["area"!~"yes"]["highway"!~"bridleway|bus_guideway|bus_stop|construction|'
    'cycleway|elevator|footway|motorway|motorway_junction|motorway_link|escalator|proposed|'
    'construction|platform|raceway|rest_area|path|service"]["access"!~"customers|no|private"]'
    '["public_transport"!~"platform"]["fee"!~"yes"]["foot"!~"no"]["service"!~"drive-through|'
    'driveway|parking_aisle"]["toll"!~"yes"]'
)

location = "The Grange, Edinburgh, Scotland"
org_graph = ox.graph_from_place(location, custom_filter=CUSTOM_FILTER)

""" Simplifying the original directed multi-graph to undirected, so we can go both 
    ways in one way streets """
graph = ox.convert.to_undirected(org_graph)
fig, ax = ox.plot_graph(graph, node_zorder=2, node_color="k", bgcolor="w")

Algorithm

In this work, we used algorithm proposed by Edmonds, J. and Johnson [5], which states as follow:

  1. Find all nodes with odd degree
  2. Calculate the sortest distance between all odd-degree nodes
  3. Create a complete weighted graph of all odd-degree nodes, as weights we use distances from step 2.
  4. Find minimal matching in the complete weighted graph
  5. Add matched pairs into original graph
  6. Find the Eulerian circuit using Hierholzer [10] algorithm

👉 For more details see the theoretical summary.

# Finds the odd degree nodes and minimal matching
odd_degree_nodes = get_odd_degree_nodes(graph)
pair_weights = get_shortest_distance_for_odd_degrees(graph, odd_degree_nodes)
matched_edges_with_weights = min_matching(pair_weights)

Result of minimal matching plotted into original graph (red edges).

fig, ax = plt.subplots(figsize=(8, 8), facecolor='black', frameon=False)
for v, u, w in matched_edges_with_weights:
    x = graph.nodes[v]["x"], graph.nodes[u]["x"]
    y = graph.nodes[v]["y"], graph.nodes[u]["y"]
    ax.plot(x, y, c='red', alpha=0.3)
    ax.scatter(x, y, c='red', edgecolor="none")

fig, ax = ox.plot_graph(graph, node_zorder=2, node_color='g', bgcolor='k', ax=ax)

Counting the final_path with Hierholzer algorithm and plotting on map. As we can see all edges were visited.

# List all edges of the extended graph including original edges and edges from minimal matching
single_edges = [(u, v) for u, v, k in graph.edges]
added_edges = get_shortest_paths(graph, matched_edges_with_weights)
edges = map_osmnx_edges2integers(graph, single_edges + added_edges)

# Finds the Eulerian path
network = Network(len(graph.nodes), edges, weighted=True)
eulerian_path = hierholzer(network)
converted_eulerian_path = convert_integer_path2osmnx_nodes(eulerian_path, graph.nodes())
double_edge_heap = get_double_edge_heap(org_graph)

# Finds the final path with edge IDs
final_path = convert_path(graph, converted_eulerian_path, double_edge_heap)

fig, ax = plot_graph_route(org_graph, final_path, route_linewidth=6, node_size=0, bgcolor="w", route_alpha=0.2, route_color="w")```

In order to see how the runner should accomplish the route on the map, we created a simple GIF.

for i, e in enumerate(final_path, start=1):
    fig, ax = plot_graph_route(org_graph, final_path[:i], route_linewidth=6, node_size=0, bgcolor="w", route_alpha=0.2)
    ax.set_title(location)
    fig.savefig(f"/img_{i}.png", dpi=120, bbox_inches="tight")
The Grange route

If you would like to generate GPX file with the route, follow Issue #6 (comment)

Usage

In order to run it locally you need to install it on Python +3.8:

pip install -r requirements.txt  # Using pip
poetry install  # or by using Poetry

Feel more that free to use, modify and copy the code, just follow the licence and cite it:

@misc{Kerekrety2020,
  author = {Kerekrety, M},
  title = {#everystreet algorithm},
  year = {2020},
  publisher = {GitHub},
  journal = {GitHub repository},
  howpublished = {\url{https://github.com/matejker/everystreet}}
}

Related readings

References

[1] Beaumont M. (2020), Strava Profile, https://www.strava.com/athletes/8288853
[2] Gates R. (2019), Every Single Street with Rickey Gates, https://www.everysinglestreet.com/why
[3] Turner K. (2019), Every Single Street, Strava stories, https://blog.strava.com/every-single-street-17484/
[4] Open Street Map (2020), http://openstreetmap.org
[5] Edmonds, J. and Johnson, E.L (1973), Matching, Euler tours and the Chinese postman. Mathematical Programming 5, 88124 https://doi.org/10.1007/BF01580113
[6] Bondy J. A. and Murty U. S. R. (1976), Graph theory with applications, ISBN 0333177916
[7] Diestel. R, (2005), Graph Theory Graduate Texts in Mathematics, Springer
[8] Erben, M., (1652), Knigsberg 1651, https://en.wikipedia.org/wiki/Knigsberg#/media/File:Image- Koenigsberg, Map by Merian-Erben 1652.jpg
[9] Euler L. (1741), Commentarii academiae scientiarum Petropolitanae, Vol- ume 8, pp. 128-140. https://scholarlycommons.pacific.edu/euler-works/53/
[10] Fleischner H. (2016), Algorithms in Graph Theory, https://www.dbai.tuwien.ac.at/staff/kronegger/misc/ AlgorithmsInGraphTheory Script.pdf
[11] Cormen, T. H. (2001), Introduction to algorithms, Cambridge, Mass: MIT Press.
[12] Galil Z. (1986), Efficient algorithms for finding maximum matching in graphs, https://www.semanticscholar.org/paper/Efficient-algorithms-for- finding-maximum-matching-Galil/ef1b31b4728615a52e3b8084379a4897 b8e526ea
[13] Edmonds J. (2008), Weighted maximum matching in general graphs, http://jorisvr.nl/files/graphmatching/20130407/mwmatching.py