Skip to content

Commit

Permalink
add initial commit for routing engine
Browse files Browse the repository at this point in the history
  • Loading branch information
ljwolf committed Nov 18, 2024
1 parent 27eb8ab commit 01f3b51
Show file tree
Hide file tree
Showing 4 changed files with 1,202 additions and 0 deletions.
Empty file added spopt/route/__init__.py
Empty file.
224 changes: 224 additions & 0 deletions spopt/route/engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
try:
import osrm
has_bindings = True
except (ImportError,ModuleNotFoundError) as e:
has_bindings = False
import os
import numpy
import requests
import warnings
import geopandas
import shapely
from sklearn import metrics

Check warning on line 12 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L1-L12

Added lines #L1 - L12 were not covered by tests

# TODO: needs to be configurable by site
_OSRM_DATABASE_FILE = ""

Check warning on line 15 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L15

Added line #L15 was not covered by tests

def build_route_table(demand_sites, candidate_depots, cost='distance', http=not has_bindings, database_path=_OSRM_DATABASE_FILE, port=5000):

Check warning on line 17 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L17

Added line #L17 was not covered by tests
"""
Build a route table using OSRM, either over http or over py-osrm bindings
"""
if isinstance(demand_sites, (geopandas.GeoSeries, geopandas.GeoDataFrame)):
demand_sites = demand_sites.geometry.get_coordinates().values
if isinstance(candidate_depots, (geopandas.GeoSeries, geopandas.GeoDataFrame)):
candidate_depots = candidate_depots.geometry.get_coordinates().values
if cost not in ("distance", "duration", "both"):
raise ValueError(f"cost option '{cost}' not one of the supported options, ('distance', 'duration', 'both')")
if http:
try:
distances, durations = _build_route_table_http(demand_sites, candidate_depots, cost=cost, port=port)
except (requests.ConnectionError, requests.JSONDecodeError):
warnings.warn(

Check warning on line 31 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L21-L31

Added lines #L21 - L31 were not covered by tests
"Failed to connect to routing engine... using haversine distance"
" and (d/500)**.75 for durations"
)
distances = metrics.pairwise_distances(

Check warning on line 35 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L35

Added line #L35 was not covered by tests
numpy.fliplr(numpy.deg2rad(demand_sites)),
numpy.fliplr(numpy.deg2rad(candidate_depots)),
metric="haversine"
) * 6371000
durations = numpy.ceil((distances / 10) ** .75)

Check warning on line 40 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L40

Added line #L40 was not covered by tests
else:
distances, durations = _build_route_table_pyosrm(

Check warning on line 42 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L42

Added line #L42 was not covered by tests
demand_sites, candidate_depots, database_path=database_path
)
for D in (distances, durations):
if D is None:
continue
n_row, n_col = D.shape
assert n_row == len(candidate_depots)
assert n_col == len(demand_sites)
no_route_available = numpy.isnan(D)
D[no_route_available] = D[~no_route_available].sum()
if cost == 'distance':
return distances
elif cost == 'duration':
return durations
elif cost == 'both':
return distances, durations

Check warning on line 58 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L45-L58

Added lines #L45 - L58 were not covered by tests

def build_specific_route(waypoints, port=5000, http=not has_bindings, return_durations=True, database_path=_OSRM_DATABASE_FILE):

Check warning on line 60 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L60

Added line #L60 was not covered by tests
"""
Build a route over the road network from each waypoint to each other waypoint. If the routing engine is not found, this builds straight-line
routes, and measures their duration as a nonlinear function of the
haversine distance between input points.
"""
if isinstance(waypoints, (geopandas.GeoSeries, geopandas.GeoDataFrame)):
waypoints = waypoints.geometry.get_coordinates().values
if http:
try:
out = _build_specific_route_http(waypoints, port=port, return_durations=return_durations)
except (requests.ConnectionError, requests.JSONDecodeError):
warnings.warn(

Check warning on line 72 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L66-L72

Added lines #L66 - L72 were not covered by tests
"Failed to connect to routing engine... constructed routes"
" will be straight lines and may not follow the road network."
)
route = shapely.LineString(waypoints)
prep_points = numpy.fliplr(numpy.deg2rad(waypoints))
durations = [

Check warning on line 78 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L76-L78

Added lines #L76 - L78 were not covered by tests
(metrics.pairwise.haversine_distances([prep_points[i]], [prep_points[i+1]])
* 637000 / 10)**.75
for i in range(len(prep_points)-1)
]
out = (route, durations) if return_durations else route

Check warning on line 83 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L83

Added line #L83 was not covered by tests
else:
route = _build_specific_route_pyosrm(waypoints, database_path=database_path, return_durations=return_durations)
if return_durations:
route, durations = out
return route, durations

Check warning on line 88 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L85-L88

Added lines #L85 - L88 were not covered by tests
else:
route = out
return route

Check warning on line 91 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L90-L91

Added lines #L90 - L91 were not covered by tests

def _build_specific_route_http(waypoints, return_durations=True, port=5000):

Check warning on line 93 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L93

Added line #L93 was not covered by tests

# TODO: needs to be configurable by site
baseurl = f"http://127.0.0.1:{int(port)}/route/v1/driving/"

Check warning on line 96 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L96

Added line #L96 was not covered by tests

point_string = ";".join(

Check warning on line 98 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L98

Added line #L98 was not covered by tests
map(
lambda x: "{},{}".format(*x),
waypoints,
)
)

request_url = (

Check warning on line 105 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L105

Added line #L105 was not covered by tests
baseurl
+ point_string
+ "?"
+ "steps=true"
+ "&"
+ f"geometries=geojson"
+ "&"
+ "annotations=true"
)
routes = requests.get(request_url).json()['routes']
assert len(routes) == 1
route = routes[0]

Check warning on line 117 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L115-L117

Added lines #L115 - L117 were not covered by tests
#sub_coordinates = numpy.empty(shape=(0,2))
route_shape = shapely.geometry.shape(route['geometry'])
leg_durations = numpy.array([leg['duration'] for leg in route['legs']])
"""

Check warning on line 121 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L119-L121

Added lines #L119 - L121 were not covered by tests
for leg_i, leg in enumerate(route['legs']):
durations[i] = leg['duration']
for steps in leg['steps']:
assert steps['geometry']['type'] == "LineString"
sub_coordinates = numpy.row_stack((sub_coordinates,
numpy.asarray(steps['geometry']['coordinates'])[:-1]
))
"""
#route_shape = shapely.LineString(sub_coordinates)
numpy.testing.assert_array_equal(

Check warning on line 131 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L131

Added line #L131 was not covered by tests
shapely.get_num_geometries(route_shape),
numpy.ones((len(waypoints),))
)
if return_durations:
return route_shape, leg_durations

Check warning on line 136 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L135-L136

Added lines #L135 - L136 were not covered by tests
else:
return route_shape

Check warning on line 138 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L138

Added line #L138 was not covered by tests

def _build_specific_route_pyosrm(waypoints, database_path=_OSRM_DATABASE_FILE, return_durations=False):

Check warning on line 140 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L140

Added line #L140 was not covered by tests
raise NotImplementedError()

def _build_route_table_http(demand_sites, candidate_depots, cost='distance', port=5000):

Check warning on line 143 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L143

Added line #L143 was not covered by tests
"""
Build a route table using the http interface to the OSRM engine
"""
request_url = _create_route_request(demand_sites, candidate_depots, cost=cost, port=port)
request = requests.get(request_url)
content = request.json()
if cost == 'distance':
D = numpy.asarray(content["distances"]).astype(float)
output = (D,None)
elif cost == 'duration':
D = numpy.asarray(content["durations"]).astype(float)
output = (None,D)
elif cost == 'both':
distances = numpy.asarray(content["distances"]).astype(float)
durations = numpy.asarray(content["durations"]).astype(float)
output = (distances, durations)

Check warning on line 159 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L147-L159

Added lines #L147 - L159 were not covered by tests
else:
raise ValueError(f"cost option '{cost}' not one of the supported options, ('distance', 'duration', 'both')")
return output

Check warning on line 162 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L161-L162

Added lines #L161 - L162 were not covered by tests


def _create_route_request(demand_sites, candidate_depots, cost='distance', port=5000):
point_string = ";".join(

Check warning on line 166 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L165-L166

Added lines #L165 - L166 were not covered by tests
map(
lambda x: "{},{}".format(*x),
numpy.row_stack((candidate_depots, demand_sites)),
)
)
n_demands = len(demand_sites)
n_supplys = len(candidate_depots)
source_string = "sources=" + ";".join(numpy.arange(n_supplys).astype(str))
destination_string = "destinations=" + ";".join(

Check warning on line 175 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L172-L175

Added lines #L172 - L175 were not covered by tests
numpy.arange(n_supplys, n_demands + n_supplys).astype(str)
)
# TODO: needs to be configurable by site
baseurl = f"http://127.0.0.1:{int(port)}/table/v1/driving/"
if cost=='distance':
annotation = "&annotations=distance"
elif cost=='duration':
annotation = "&annotations=duration"
elif cost=='both':
annotation = "&annotations=duration,distance"

Check warning on line 185 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L179-L185

Added lines #L179 - L185 were not covered by tests
else:
annotation = ""

Check warning on line 187 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L187

Added line #L187 was not covered by tests

request_url = (

Check warning on line 189 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L189

Added line #L189 was not covered by tests
baseurl
+ point_string
+ "?"
+ source_string
+ "&"
+ destination_string
+ annotation
+ "&exclude=ferry"
)
return request_url

Check warning on line 199 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L199

Added line #L199 was not covered by tests


def _build_route_table_pyosrm(demand_sites, candidate_depots, database_path=_OSRM_DATABASE_FILE):

Check warning on line 202 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L202

Added line #L202 was not covered by tests
"""
build a route table using py-osrm
https://github.com/gis-ops/py-osrm
"""
engine = osrm.OSRM(

Check warning on line 207 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L207

Added line #L207 was not covered by tests
storage_config=database_path,
use_shared_memory=False
)
n_demands = len(demand_sites)
n_supplys = len(candidate_depots)
query_params = osrm.TableParameters( # noqa: F821

Check warning on line 213 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L211-L213

Added lines #L211 - L213 were not covered by tests
coordinates=[
(float(lon), float(lat))
for (lon, lat)
in numpy.row_stack((demand_sites, candidate_depots))
],
sources=list(numpy.arange(n_demands)),
destinations=list(numpy.arange(n_demands, n_demands + n_supplys)),
annotations=["distance"],
)
res = engine.Table(query_params)
return numpy.asarray(res["distances"]).astype(float).T

Check warning on line 224 in spopt/route/engine.py

View check run for this annotation

Codecov / codecov/patch

spopt/route/engine.py#L223-L224

Added lines #L223 - L224 were not covered by tests
Loading

0 comments on commit 01f3b51

Please sign in to comment.