r5py 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- r5py/__init__.py +27 -0
- r5py/__main__.py +3 -0
- r5py/r5/__init__.py +39 -0
- r5py/r5/access_leg.py +12 -0
- r5py/r5/base_travel_time_matrix.py +255 -0
- r5py/r5/detailed_itineraries.py +226 -0
- r5py/r5/direct_leg.py +38 -0
- r5py/r5/egress_leg.py +12 -0
- r5py/r5/elevation_cost_function.py +50 -0
- r5py/r5/elevation_model.py +89 -0
- r5py/r5/file_storage.py +82 -0
- r5py/r5/isochrones.py +345 -0
- r5py/r5/regional_task.py +600 -0
- r5py/r5/scenario.py +36 -0
- r5py/r5/street_layer.py +90 -0
- r5py/r5/street_segment.py +39 -0
- r5py/r5/transfer_leg.py +12 -0
- r5py/r5/transit_layer.py +87 -0
- r5py/r5/transit_leg.py +12 -0
- r5py/r5/transport_mode.py +148 -0
- r5py/r5/transport_network.py +299 -0
- r5py/r5/travel_time_matrix.py +186 -0
- r5py/r5/trip.py +97 -0
- r5py/r5/trip_leg.py +204 -0
- r5py/r5/trip_planner.py +576 -0
- r5py/util/__init__.py +31 -0
- r5py/util/camel_to_snake_case.py +25 -0
- r5py/util/classpath.py +95 -0
- r5py/util/config.py +176 -0
- r5py/util/contains_gtfs_data.py +46 -0
- r5py/util/data_validation.py +28 -0
- r5py/util/environment.py +32 -0
- r5py/util/exceptions.py +43 -0
- r5py/util/file_digest.py +40 -0
- r5py/util/good_enough_equidistant_crs.py +73 -0
- r5py/util/jvm.py +138 -0
- r5py/util/memory_footprint.py +178 -0
- r5py/util/parse_int_date.py +24 -0
- r5py/util/sample_data_set.py +76 -0
- r5py/util/snake_to_camel_case.py +16 -0
- r5py/util/spatially_clustered_geodataframe.py +66 -0
- r5py/util/validating_requests_session.py +58 -0
- r5py/util/warnings.py +7 -0
- r5py/util/working_copy.py +42 -0
- r5py-1.1.0.dist-info/METADATA +176 -0
- r5py-1.1.0.dist-info/RECORD +49 -0
- r5py-1.1.0.dist-info/WHEEL +5 -0
- r5py-1.1.0.dist-info/licenses/LICENSE +3 -0
- r5py-1.1.0.dist-info/top_level.txt +1 -0
r5py/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Python wrapper for the R5 routing analysis engine."""
|
|
4
|
+
|
|
5
|
+
__version__ = "1.1.0"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from .r5 import (
|
|
9
|
+
DetailedItineraries,
|
|
10
|
+
ElevationCostFunction,
|
|
11
|
+
Isochrones,
|
|
12
|
+
RegionalTask,
|
|
13
|
+
TransportMode,
|
|
14
|
+
TransportNetwork,
|
|
15
|
+
TravelTimeMatrix,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DetailedItineraries",
|
|
20
|
+
"ElevationCostFunction",
|
|
21
|
+
"Isochrones",
|
|
22
|
+
"RegionalTask",
|
|
23
|
+
"TransportMode",
|
|
24
|
+
"TransportNetwork",
|
|
25
|
+
"TravelTimeMatrix",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
r5py/__main__.py
ADDED
r5py/r5/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""R5 classes."""
|
|
4
|
+
|
|
5
|
+
from .access_leg import AccessLeg
|
|
6
|
+
from .detailed_itineraries import DetailedItineraries
|
|
7
|
+
from .direct_leg import DirectLeg
|
|
8
|
+
from .egress_leg import EgressLeg
|
|
9
|
+
from .elevation_cost_function import ElevationCostFunction
|
|
10
|
+
from .isochrones import Isochrones
|
|
11
|
+
from .regional_task import RegionalTask
|
|
12
|
+
from .scenario import Scenario
|
|
13
|
+
from .street_layer import StreetLayer
|
|
14
|
+
from .transfer_leg import TransferLeg
|
|
15
|
+
from .transit_leg import TransitLeg
|
|
16
|
+
from .transport_mode import TransportMode
|
|
17
|
+
from .transport_network import TransportNetwork
|
|
18
|
+
from .travel_time_matrix import TravelTimeMatrix
|
|
19
|
+
from .trip import Trip
|
|
20
|
+
from .trip_planner import TripPlanner
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AccessLeg",
|
|
24
|
+
"DetailedItineraries",
|
|
25
|
+
"DirectLeg",
|
|
26
|
+
"EgressLeg",
|
|
27
|
+
"ElevationCostFunction",
|
|
28
|
+
"Isochrones",
|
|
29
|
+
"RegionalTask",
|
|
30
|
+
"Scenario",
|
|
31
|
+
"StreetLayer",
|
|
32
|
+
"TransferLeg",
|
|
33
|
+
"TransitLeg",
|
|
34
|
+
"TransportMode",
|
|
35
|
+
"TransportNetwork",
|
|
36
|
+
"TravelTimeMatrix",
|
|
37
|
+
"Trip",
|
|
38
|
+
"TripPlanner",
|
|
39
|
+
]
|
r5py/r5/access_leg.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Represent one leg of a trip, specifically access to a public transport stop."""
|
|
5
|
+
|
|
6
|
+
from .transfer_leg import TransferLeg
|
|
7
|
+
|
|
8
|
+
__all__ = ["AccessLeg"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AccessLeg(TransferLeg):
|
|
12
|
+
"""Represent one leg of a trip, specifically access to a public transport stop."""
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Calculate travel times between many origins and destinations."""
|
|
4
|
+
|
|
5
|
+
import collections.abc
|
|
6
|
+
import inspect
|
|
7
|
+
import math
|
|
8
|
+
import multiprocessing
|
|
9
|
+
import pathlib
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
import geopandas
|
|
13
|
+
import numpy
|
|
14
|
+
import pandas
|
|
15
|
+
import shapely
|
|
16
|
+
|
|
17
|
+
from ..util import Config, check_od_data_set
|
|
18
|
+
from .regional_task import RegionalTask
|
|
19
|
+
from .transport_network import TransportNetwork
|
|
20
|
+
|
|
21
|
+
__all__ = ["BaseTravelTimeMatrix"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# R5 fills cut-off (NULL) values with MAX_INT32
|
|
25
|
+
MAX_INT32 = (2**31) - 1
|
|
26
|
+
|
|
27
|
+
# how many (Python) threads to start
|
|
28
|
+
# (they still run many Java threads, so be careful what you wish for ;) )
|
|
29
|
+
# TODO: benchmark the optimal number of threads
|
|
30
|
+
NUM_THREADS = math.ceil(multiprocessing.cpu_count() * 0.5)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BaseTravelTimeMatrix(geopandas.GeoDataFrame):
|
|
34
|
+
"""Base class for travel time computers between many origins and destinations."""
|
|
35
|
+
|
|
36
|
+
MAX_INT32 = MAX_INT32
|
|
37
|
+
|
|
38
|
+
NUM_THREADS = NUM_THREADS
|
|
39
|
+
|
|
40
|
+
_r5py_attributes = [
|
|
41
|
+
"_destinations",
|
|
42
|
+
"_destinations_crs",
|
|
43
|
+
"_origins",
|
|
44
|
+
"_origins_crs",
|
|
45
|
+
"destinations",
|
|
46
|
+
"origins",
|
|
47
|
+
"request",
|
|
48
|
+
"snap_to_network",
|
|
49
|
+
"transport_network",
|
|
50
|
+
"verbose",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
_constructor = geopandas.GeoDataFrame
|
|
54
|
+
|
|
55
|
+
_constructor_sliced = pandas.Series
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def _geodataframe_constructor_with_fallback(
|
|
59
|
+
cls,
|
|
60
|
+
*args,
|
|
61
|
+
**kwargs,
|
|
62
|
+
): # pragma: no cover
|
|
63
|
+
"""
|
|
64
|
+
A flexible constructor for r5py frames.
|
|
65
|
+
|
|
66
|
+
Checks whether or not arguments of the child class are used.
|
|
67
|
+
"""
|
|
68
|
+
# `transport_network` is the only required argument across all r5py
|
|
69
|
+
# data classes, it can be passed either as an r5py.TransportNetwork
|
|
70
|
+
# or a tuple (as described in the signature of __init__(), below)
|
|
71
|
+
if (
|
|
72
|
+
"transport_network" in kwargs
|
|
73
|
+
or isinstance(args[0], TransportNetwork)
|
|
74
|
+
or (
|
|
75
|
+
isinstance(args[0], tuple)
|
|
76
|
+
and isinstance(args[0][0], (pathlib.Path, str))
|
|
77
|
+
and isinstance(args[0][1], collections.abc.Iterable)
|
|
78
|
+
and all(isinstance(arg, (pathlib.Path, str)) for arg in args[0][1])
|
|
79
|
+
)
|
|
80
|
+
):
|
|
81
|
+
df = cls(*args, **kwargs)
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
df = geopandas.GeoDataFrame(*args, **kwargs)
|
|
85
|
+
geometry_cols_mask = df.dtypes == "geometry"
|
|
86
|
+
if len(geometry_cols_mask) == 0 or geometry_cols_mask.sum() == 0:
|
|
87
|
+
df = pandas.DataFrame(df)
|
|
88
|
+
|
|
89
|
+
return df
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
transport_network,
|
|
94
|
+
origins=None,
|
|
95
|
+
destinations=None,
|
|
96
|
+
snap_to_network=False,
|
|
97
|
+
**kwargs,
|
|
98
|
+
):
|
|
99
|
+
"""
|
|
100
|
+
Compute travel times between many origins and destinations.
|
|
101
|
+
|
|
102
|
+
Arguments
|
|
103
|
+
---------
|
|
104
|
+
transport_network : r5py.TransportNetwork | tuple(
|
|
105
|
+
pathlib.Paths | str, list(pathlib.Path | str))
|
|
106
|
+
The transport network to route on. This can either be a readily
|
|
107
|
+
initialised r5py.TransportNetwork or a tuple of the parameters
|
|
108
|
+
passed to ``TransportNetwork.__init__()``: the path to an
|
|
109
|
+
OpenStreetMap extract in PBF format, and a list of zero of more
|
|
110
|
+
paths to GTFS transport schedule files.
|
|
111
|
+
origins : geopandas.GeoDataFrame
|
|
112
|
+
Places to find a route _from_
|
|
113
|
+
Has to have a point geometry, and at least an `id` column
|
|
114
|
+
destinations : geopandas.GeoDataFrame (optional)
|
|
115
|
+
Places to find a route _to_
|
|
116
|
+
Has to have a point geometry, and at least an `id` column
|
|
117
|
+
If omitted, use same data set as for origins
|
|
118
|
+
snap_to_network : bool or int, default False
|
|
119
|
+
Should origin an destination points be snapped to the street network
|
|
120
|
+
before routing? If `True`, the default search radius (defined in
|
|
121
|
+
`com.conveyal.r5.streets.StreetLayer.LINK_RADIUS_METERS`) is used,
|
|
122
|
+
if `int`, use `snap_to_network` meters as the search radius.
|
|
123
|
+
**kwargs : mixed
|
|
124
|
+
Any arguments than can be passed to r5py.RegionalTask:
|
|
125
|
+
``departure``, ``departure_time_window``, ``percentiles``,
|
|
126
|
+
``transport_modes``, ``access_modes``, ``egress_modes``,
|
|
127
|
+
``max_time``, ``max_time_walking``, ``max_time_cycling``,
|
|
128
|
+
``max_time_driving``, ``speed_cycling``, ``speed_walking``,
|
|
129
|
+
``max_public_transport_rides``, ``max_bicycle_traffic_stress``
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
super_parameters = inspect.signature(geopandas.GeoDataFrame).parameters
|
|
133
|
+
super_kwargs = {
|
|
134
|
+
key: value for key, value in kwargs.items() if key in super_parameters
|
|
135
|
+
}
|
|
136
|
+
kwargs = {
|
|
137
|
+
key: value for key, value in kwargs.items() if key not in super_parameters
|
|
138
|
+
}
|
|
139
|
+
super().__init__(**super_kwargs)
|
|
140
|
+
|
|
141
|
+
if not isinstance(transport_network, TransportNetwork):
|
|
142
|
+
transport_network = TransportNetwork(*transport_network)
|
|
143
|
+
self.transport_network = transport_network
|
|
144
|
+
|
|
145
|
+
self.snap_to_network = snap_to_network
|
|
146
|
+
|
|
147
|
+
self.origins = origins
|
|
148
|
+
self.destinations = destinations
|
|
149
|
+
|
|
150
|
+
self.request = RegionalTask(
|
|
151
|
+
transport_network,
|
|
152
|
+
origin=None,
|
|
153
|
+
destinations=None,
|
|
154
|
+
**kwargs,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
self.verbose = Config().arguments.verbose
|
|
158
|
+
|
|
159
|
+
def __repr__(self):
|
|
160
|
+
"""Return a string representation of `self`."""
|
|
161
|
+
return f"<{self.__class__.__name__}>"
|
|
162
|
+
|
|
163
|
+
def __setattr__(self, attr, val):
|
|
164
|
+
"""Catch our own attributes here so we don’t mess with (geo)pandas columns."""
|
|
165
|
+
if attr in self._r5py_attributes:
|
|
166
|
+
object.__setattr__(self, attr, val)
|
|
167
|
+
else:
|
|
168
|
+
super().__setattr__(attr, val)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def destinations(self):
|
|
172
|
+
"""The destinations of this travel time matrix (`geopandas.GeoDataFrame`)."""
|
|
173
|
+
return self._destinations
|
|
174
|
+
|
|
175
|
+
@destinations.setter
|
|
176
|
+
def destinations(self, destinations):
|
|
177
|
+
if destinations is not None:
|
|
178
|
+
check_od_data_set(destinations)
|
|
179
|
+
self._destinations_crs = destinations.crs
|
|
180
|
+
self._destinations = destinations.to_crs("EPSG:4326").copy()
|
|
181
|
+
|
|
182
|
+
def _fill_nulls(self, data_set):
|
|
183
|
+
"""
|
|
184
|
+
Fill NULL values in a data set returned from R5.
|
|
185
|
+
|
|
186
|
+
R5 uses `MAX_INT32` as a marker for NULL values, this function
|
|
187
|
+
replaces those values in `data_set` with `numpy.nan`
|
|
188
|
+
|
|
189
|
+
Arguments
|
|
190
|
+
---------
|
|
191
|
+
data_set : pandas.DataFrame
|
|
192
|
+
Data frame in which NULLs are represented by MAX_INT32
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
pandas.DataFrame
|
|
197
|
+
Data frame in which all MAX_INT32 have been replaced by `numpy.nan`.
|
|
198
|
+
"""
|
|
199
|
+
return data_set.map(lambda x: numpy.nan if x == MAX_INT32 else x)
|
|
200
|
+
|
|
201
|
+
def _prepare_origins_destinations(self):
|
|
202
|
+
"""Make sure we received enough information."""
|
|
203
|
+
try:
|
|
204
|
+
self.origins
|
|
205
|
+
except AttributeError as exception:
|
|
206
|
+
raise ValueError("No routing origins defined") from exception
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
self.destinations
|
|
210
|
+
assert self.destinations is not None
|
|
211
|
+
except (AssertionError, AttributeError):
|
|
212
|
+
self.destinations = self.origins.copy()
|
|
213
|
+
if self.verbose:
|
|
214
|
+
warnings.warn(
|
|
215
|
+
"No routing destinations defined, "
|
|
216
|
+
"using origins as destinations, too.",
|
|
217
|
+
RuntimeWarning,
|
|
218
|
+
stacklevel=1,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if self.snap_to_network:
|
|
222
|
+
for which_end in ("origins", "destinations"):
|
|
223
|
+
points = getattr(self, f"_{which_end}")
|
|
224
|
+
points.geometry = self.transport_network.snap_to_network(
|
|
225
|
+
points.geometry
|
|
226
|
+
)
|
|
227
|
+
if len(points[points.geometry == shapely.Point()]):
|
|
228
|
+
# if there are origins/destinations for which
|
|
229
|
+
# no snapped point could be found
|
|
230
|
+
points = points[points.geometry != shapely.Point()]
|
|
231
|
+
warnings.warn(
|
|
232
|
+
f"Some {which_end[:-1]} points could not be "
|
|
233
|
+
"snapped to the street network",
|
|
234
|
+
RuntimeWarning,
|
|
235
|
+
stacklevel=1,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if points.empty:
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"After snapping, no valid {which_end[:-1]} points remain"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
setattr(self, f"_{which_end}", points.copy())
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def origins(self):
|
|
247
|
+
"""The origins of this travel time matrix (`geopandas.GeoDataFrame`)."""
|
|
248
|
+
return self._origins
|
|
249
|
+
|
|
250
|
+
@origins.setter
|
|
251
|
+
def origins(self, origins):
|
|
252
|
+
if origins is not None:
|
|
253
|
+
check_od_data_set(origins)
|
|
254
|
+
self._origins_crs = origins.crs
|
|
255
|
+
self._origins = origins.to_crs("EPSG:4326").copy()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Calculate detailed itineraries between many origins and destinations."""
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
import geopandas
|
|
10
|
+
import joblib
|
|
11
|
+
import pandas
|
|
12
|
+
|
|
13
|
+
from .base_travel_time_matrix import BaseTravelTimeMatrix
|
|
14
|
+
from .trip import Trip
|
|
15
|
+
from .trip_planner import TripPlanner
|
|
16
|
+
|
|
17
|
+
__all__ = ["DetailedItineraries"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DetailedItineraries(BaseTravelTimeMatrix):
|
|
21
|
+
"""Compute detailed itineraries between many origins and destinations."""
|
|
22
|
+
|
|
23
|
+
COLUMNS = ["from_id", "to_id", "option"] + Trip.COLUMNS
|
|
24
|
+
|
|
25
|
+
_r5py_attributes = BaseTravelTimeMatrix._r5py_attributes + [
|
|
26
|
+
"all_to_all",
|
|
27
|
+
"od_pairs",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
transport_network,
|
|
33
|
+
origins=None,
|
|
34
|
+
destinations=None,
|
|
35
|
+
snap_to_network=False,
|
|
36
|
+
force_all_to_all=False,
|
|
37
|
+
**kwargs,
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Compute travel times between many origins and destinations.
|
|
41
|
+
|
|
42
|
+
``r5py.DetailedItineraries`` are child classes of
|
|
43
|
+
``geopandas.GeoDataFrame`` and support all of their methods and
|
|
44
|
+
properties, see https://geopandas.org/en/stable/docs.html
|
|
45
|
+
|
|
46
|
+
Arguments
|
|
47
|
+
---------
|
|
48
|
+
transport_network : r5py.TransportNetwork | tuple(str, list(str), dict)
|
|
49
|
+
The transport network to route on. This can either be a readily
|
|
50
|
+
initialised r5py.TransportNetwork or a tuple of the parameters
|
|
51
|
+
passed to ``TransportNetwork.__init__()``: the path to an OpenStreetMap
|
|
52
|
+
extract in PBF format, a list of zero of more paths to GTFS transport
|
|
53
|
+
schedule files, and a dict with ``build_config`` options.
|
|
54
|
+
origins : geopandas.GeoDataFrame
|
|
55
|
+
Places to find a route _from_
|
|
56
|
+
Has to have a point geometry, and at least an `id` column
|
|
57
|
+
destinations : geopandas.GeoDataFrame (optional)
|
|
58
|
+
Places to find a route _to_
|
|
59
|
+
Has to have a point geometry, and at least an `id` column
|
|
60
|
+
If omitted, use same data set as for origins
|
|
61
|
+
snap_to_network : bool or int, default False
|
|
62
|
+
Should origin an destination points be snapped to the street network
|
|
63
|
+
before routing? If `True`, the default search radius (defined in
|
|
64
|
+
`com.conveyal.r5.streets.StreetLayer.LINK_RADIUS_METERS`) is used,
|
|
65
|
+
if `int`, use `snap_to_network` meters as the search radius.
|
|
66
|
+
force_all_to_all : bool, default False
|
|
67
|
+
If ``origins`` and ``destinations`` have the same length, by
|
|
68
|
+
default, ``DetailedItineraries`` finds routes between pairs
|
|
69
|
+
of origins and destinations, i.e., it routes from origin #1 to
|
|
70
|
+
destination #1, origin #2 to destination #2, ... .
|
|
71
|
+
Set ``force_all_to_all=True`` to route from each origin to all
|
|
72
|
+
destinations (this is the default, if ``origins`` and ``destinations``
|
|
73
|
+
have different lengths, or if ``destinations`` is omitted)
|
|
74
|
+
**kwargs : mixed
|
|
75
|
+
Any arguments than can be passed to r5py.RegionalTask:
|
|
76
|
+
``departure``, ``departure_time_window``, ``percentiles``,
|
|
77
|
+
``transport_modes``, ``access_modes``, ``egress_modes``,
|
|
78
|
+
``max_time``, ``max_time_walking``, ``max_time_cycling``,
|
|
79
|
+
``max_time_driving``, ``speed_cycling``, ``speed_walking``,
|
|
80
|
+
``max_public_transport_rides``, ``max_bicycle_traffic_stress`` Note
|
|
81
|
+
that not all arguments might make sense in this context, and the
|
|
82
|
+
underlying R5 engine might ignore some of them.
|
|
83
|
+
"""
|
|
84
|
+
super().__init__(
|
|
85
|
+
transport_network,
|
|
86
|
+
origins,
|
|
87
|
+
destinations,
|
|
88
|
+
snap_to_network,
|
|
89
|
+
**kwargs,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if destinations is None:
|
|
93
|
+
self.all_to_all = True
|
|
94
|
+
if self.verbose:
|
|
95
|
+
warnings.warn(
|
|
96
|
+
"No destinations specified, computing an all-to-all matrix",
|
|
97
|
+
RuntimeWarning,
|
|
98
|
+
stacklevel=1,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
elif len(origins) != len(destinations):
|
|
102
|
+
self.all_to_all = True
|
|
103
|
+
if self.verbose:
|
|
104
|
+
warnings.warn(
|
|
105
|
+
"Origins and destinations are of different length, "
|
|
106
|
+
"computing an all-to-all matrix",
|
|
107
|
+
RuntimeWarning,
|
|
108
|
+
stacklevel=1,
|
|
109
|
+
)
|
|
110
|
+
elif origins.equals(destinations):
|
|
111
|
+
self.all_to_all = True
|
|
112
|
+
if self.verbose:
|
|
113
|
+
warnings.warn(
|
|
114
|
+
"Origins and destinations are identical, "
|
|
115
|
+
"computing an all-to-all matrix",
|
|
116
|
+
RuntimeWarning,
|
|
117
|
+
stacklevel=1,
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
self.all_to_all = force_all_to_all
|
|
121
|
+
|
|
122
|
+
self.od_pairs = None
|
|
123
|
+
|
|
124
|
+
data = self._compute()
|
|
125
|
+
with warnings.catch_warnings():
|
|
126
|
+
warnings.filterwarnings(
|
|
127
|
+
"ignore",
|
|
128
|
+
message=(
|
|
129
|
+
"You are adding a column named 'geometry' to a GeoDataFrame "
|
|
130
|
+
"constructed without an active geometry column"
|
|
131
|
+
),
|
|
132
|
+
category=FutureWarning,
|
|
133
|
+
)
|
|
134
|
+
for column in data.columns:
|
|
135
|
+
self[column] = data[column]
|
|
136
|
+
self.set_geometry("geometry")
|
|
137
|
+
|
|
138
|
+
del self.transport_network
|
|
139
|
+
|
|
140
|
+
def _compute(self):
|
|
141
|
+
"""
|
|
142
|
+
Compute travel times from all origins to all destinations.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
geopandas.GeoDataFrame
|
|
147
|
+
The resulting detailed routes. For each origin/destination pair,
|
|
148
|
+
multiple route alternatives (‘options’) might be reported that each
|
|
149
|
+
consist of one or more segments. Each segment represents one row.
|
|
150
|
+
multiple route alternatives (‘options’) might be reported that each
|
|
151
|
+
consist of one or more segments. Each segment represents one row.
|
|
152
|
+
|
|
153
|
+
The data frame comprises of the following columns: `from_id`,
|
|
154
|
+
`to_id`, `option` (`int`), `segment` (`int`), `transport_mode`
|
|
155
|
+
(`r5py.TransportMode`), `departure_time` (`datetime.datetime`),
|
|
156
|
+
`distance` (`float`, metres), `travel_time` (`datetime.timedelta`),
|
|
157
|
+
`wait_time` (`datetime.timedelta`), `feed` (`str`, the feed name
|
|
158
|
+
used), `agency_id` (`str` the public transport agency identifier),
|
|
159
|
+
`route_id` (`str`, public transport route ID), `start_stop_id`
|
|
160
|
+
(`str`, the GTFS stop_id for boarding), `end_stop_id` (`str`, the
|
|
161
|
+
GTFS stop_id for alighting), `geometry` (`shapely.LineString`)
|
|
162
|
+
"""
|
|
163
|
+
self._prepare_origins_destinations()
|
|
164
|
+
|
|
165
|
+
# loop over all origin/destination pairs, modify the request, and
|
|
166
|
+
# compute times, distance, and other details for each trip
|
|
167
|
+
with joblib.Parallel(
|
|
168
|
+
prefer="threads",
|
|
169
|
+
verbose=(10 * self.verbose), # joblib has a funny verbosity scale
|
|
170
|
+
n_jobs=self.NUM_THREADS,
|
|
171
|
+
) as parallel:
|
|
172
|
+
matrices = parallel(
|
|
173
|
+
joblib.delayed(self._travel_details_per_od_pair)(from_id, to_id)
|
|
174
|
+
for _, (from_id, to_id) in self.od_pairs.iterrows()
|
|
175
|
+
)
|
|
176
|
+
od_matrix = pandas.concat(
|
|
177
|
+
[matrix.astype(matrices[0].dtypes) for matrix in matrices],
|
|
178
|
+
ignore_index=True,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
od_matrix = geopandas.GeoDataFrame(od_matrix, crs=self._origins_crs)
|
|
182
|
+
return od_matrix
|
|
183
|
+
|
|
184
|
+
def _prepare_origins_destinations(self):
|
|
185
|
+
"""Make sure we received enough information."""
|
|
186
|
+
super()._prepare_origins_destinations()
|
|
187
|
+
|
|
188
|
+
if self.all_to_all:
|
|
189
|
+
# manually create a list of all all-to-all permutations
|
|
190
|
+
self.od_pairs = self.origins[["id"]].join(
|
|
191
|
+
self.destinations[["id"]],
|
|
192
|
+
how="cross",
|
|
193
|
+
lsuffix="_origin",
|
|
194
|
+
rsuffix="_destination",
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
# origins and destinations are same length, run one-to-one routing
|
|
198
|
+
self.od_pairs = pandas.DataFrame(
|
|
199
|
+
{
|
|
200
|
+
"id_origin": self.origins.id.values,
|
|
201
|
+
"id_destination": self.destinations.id.values,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _travel_details_per_od_pair(self, from_id, to_id):
|
|
206
|
+
origin = self.origins[self.origins.id == from_id]
|
|
207
|
+
destination = self.destinations[self.destinations.id == to_id]
|
|
208
|
+
|
|
209
|
+
request = copy.copy(self.request)
|
|
210
|
+
request._regional_task.fromLat = origin.geometry.item().y
|
|
211
|
+
request._regional_task.fromLon = origin.geometry.item().x
|
|
212
|
+
request._regional_task.toLat = destination.geometry.item().y
|
|
213
|
+
request._regional_task.toLon = destination.geometry.item().x
|
|
214
|
+
|
|
215
|
+
trip_planner = TripPlanner(self.transport_network, request)
|
|
216
|
+
trips = trip_planner.trips
|
|
217
|
+
|
|
218
|
+
# fmt: off
|
|
219
|
+
trips = [
|
|
220
|
+
[from_id, to_id, option] + segment
|
|
221
|
+
for option, trip in enumerate(trips)
|
|
222
|
+
for segment in trip.as_table()
|
|
223
|
+
]
|
|
224
|
+
# fmt: on
|
|
225
|
+
|
|
226
|
+
return pandas.DataFrame(trips, columns=self.COLUMNS)
|
r5py/r5/direct_leg.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Represent one leg of a direct mode (walk, cycle, car) trip."""
|
|
5
|
+
|
|
6
|
+
import datetime
|
|
7
|
+
|
|
8
|
+
import shapely
|
|
9
|
+
|
|
10
|
+
from .trip_leg import TripLeg
|
|
11
|
+
|
|
12
|
+
__all__ = ["DirectLeg"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DirectLeg(TripLeg):
|
|
16
|
+
"""Represent one leg of a public transport trip."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, transport_mode, street_segment):
|
|
19
|
+
"""
|
|
20
|
+
Represent one leg of a public transport trip.
|
|
21
|
+
|
|
22
|
+
Arguments
|
|
23
|
+
=========
|
|
24
|
+
transport_mode : r5py.TransportMode
|
|
25
|
+
mode of transport this trip leg was travelled
|
|
26
|
+
street_segment : com.conveyal.r5.profile.StreetSegment
|
|
27
|
+
the leg’s data as output by R5’s `StreetRouter`
|
|
28
|
+
"""
|
|
29
|
+
distance = street_segment.distance / 1000.0 # millimetres!
|
|
30
|
+
travel_time = datetime.timedelta(seconds=street_segment.duration)
|
|
31
|
+
geometry = shapely.from_wkt(str(street_segment.geometry))
|
|
32
|
+
|
|
33
|
+
super().__init__(
|
|
34
|
+
transport_mode=transport_mode,
|
|
35
|
+
distance=distance,
|
|
36
|
+
travel_time=travel_time,
|
|
37
|
+
geometry=geometry,
|
|
38
|
+
)
|
r5py/r5/egress_leg.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Represent one leg of a trip, specifically egress from a public transport stop."""
|
|
5
|
+
|
|
6
|
+
from .transfer_leg import TransferLeg
|
|
7
|
+
|
|
8
|
+
__all__ = ["EgressLeg"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EgressLeg(TransferLeg):
|
|
12
|
+
"""Represent one leg of a trip, specifically egress from a public transport stop."""
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""The elevation cost functions supported by R5 (Tobler, Minetti)."""
|
|
5
|
+
|
|
6
|
+
import enum
|
|
7
|
+
|
|
8
|
+
import jpype
|
|
9
|
+
|
|
10
|
+
from ..util import start_jvm
|
|
11
|
+
|
|
12
|
+
import com.conveyal.r5
|
|
13
|
+
|
|
14
|
+
__all__ = ["ElevationCostFunction"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
start_jvm()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ElevationCostFunction(enum.Enum):
|
|
21
|
+
"""
|
|
22
|
+
Elevation cost functions.
|
|
23
|
+
|
|
24
|
+
TOBLER: Waldo Tobler’s hiking function,
|
|
25
|
+
cf. https://en.wikipedia.org/wiki/Tobler%27s_hiking_function
|
|
26
|
+
MINETTI: Minetti et al.’s perceived effort/energy consumption,
|
|
27
|
+
cf. https://doi.org/10.1152/japplphysiol.01177.2001
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def _missing_(cls, value):
|
|
33
|
+
value = str(value).upper()
|
|
34
|
+
for member in cls:
|
|
35
|
+
if value == member.value:
|
|
36
|
+
return member
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
TOBLER = "TOBLER"
|
|
40
|
+
MINETTI = "MINETTI"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@jpype._jcustomizer.JConversion(
|
|
44
|
+
"com.conveyal.r5.analyst.scenario.RasterCost.CostFunction",
|
|
45
|
+
exact=ElevationCostFunction,
|
|
46
|
+
)
|
|
47
|
+
def _cast_LegMode(java_class, object_):
|
|
48
|
+
return com.conveyal.r5.analyst.scenario.RasterCost.CostFunction.valueOf(
|
|
49
|
+
object_.name
|
|
50
|
+
)
|