Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add initial commit for routing engine #465

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading