r5py 0.1.1.dev2__py3-none-any.whl → 1.0.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.

Potentially problematic release.


This version of r5py might be problematic. Click here for more details.

Files changed (38) hide show
  1. r5py/__init__.py +8 -1
  2. r5py/__main__.py +1 -14
  3. r5py/r5/__init__.py +20 -2
  4. r5py/r5/{base_travel_time_matrix_computer.py → base_travel_time_matrix.py} +28 -8
  5. r5py/r5/{detailed_itineraries_computer.py → detailed_itineraries.py} +82 -20
  6. r5py/r5/direct_leg.py +1 -3
  7. r5py/r5/isochrones.py +351 -0
  8. r5py/r5/regional_task.py +12 -9
  9. r5py/r5/street_layer.py +8 -3
  10. r5py/r5/street_segment.py +41 -0
  11. r5py/r5/transfer_leg.py +2 -6
  12. r5py/r5/transit_layer.py +6 -0
  13. r5py/r5/transit_leg.py +1 -5
  14. r5py/r5/transport_mode.py +5 -3
  15. r5py/r5/transport_network.py +60 -138
  16. r5py/r5/travel_time_matrix.py +209 -0
  17. r5py/r5/trip.py +13 -8
  18. r5py/r5/trip_leg.py +76 -15
  19. r5py/r5/trip_planner.py +109 -54
  20. r5py/util/__init__.py +8 -0
  21. r5py/util/classpath.py +9 -5
  22. r5py/util/config.py +32 -7
  23. r5py/util/environment.py +34 -0
  24. r5py/util/file_digest.py +42 -0
  25. r5py/util/good_enough_equidistant_crs.py +8 -4
  26. r5py/util/memory_footprint.py +3 -5
  27. r5py/util/sample_data_set.py +17 -6
  28. r5py/util/spatially_clustered_geodataframe.py +78 -0
  29. r5py/util/validating_requests_session.py +2 -2
  30. r5py/util/working_copy.py +44 -0
  31. {r5py-0.1.1.dev2.dist-info → r5py-1.0.0.dist-info}/METADATA +34 -33
  32. r5py-1.0.0.dist-info/RECORD +47 -0
  33. {r5py-0.1.1.dev2.dist-info → r5py-1.0.0.dist-info}/WHEEL +1 -1
  34. r5py/r5/travel_time_matrix_computer.py +0 -134
  35. r5py/sampledata/_keep/__init__.py +0 -3
  36. r5py-0.1.1.dev2.dist-info/RECORD +0 -42
  37. {r5py-0.1.1.dev2.dist-info → r5py-1.0.0.dist-info}/LICENSE +0 -0
  38. {r5py-0.1.1.dev2.dist-info → r5py-1.0.0.dist-info}/top_level.txt +0 -0
@@ -5,24 +5,22 @@
5
5
 
6
6
 
7
7
  import functools
8
+ import hashlib
8
9
  import pathlib
9
- import random
10
- import shutil
11
- import time
12
10
  import warnings
13
11
 
14
- import filelock
15
12
  import jpype
16
13
  import jpype.types
17
14
 
18
15
  from .street_layer import StreetLayer
19
16
  from .transit_layer import TransitLayer
20
17
  from .transport_mode import TransportMode
21
- from ..util import Config, contains_gtfs_data, start_jvm
18
+ from ..util import Config, contains_gtfs_data, FileDigest, start_jvm, WorkingCopy
22
19
 
23
20
  import com.conveyal.gtfs
24
21
  import com.conveyal.osmlib
25
22
  import com.conveyal.r5
23
+ import java.io
26
24
 
27
25
 
28
26
  __all__ = ["TransportNetwork"]
@@ -48,106 +46,63 @@ class TransportNetwork:
48
46
  gtfs : str | pathlib.Path | list[str] | list[pathlib.Path]
49
47
  path(s) to public transport schedule information in GTFS format
50
48
  """
51
- osm_pbf = self._working_copy(pathlib.Path(osm_pbf)).absolute()
49
+ osm_pbf = WorkingCopy(osm_pbf)
52
50
  if isinstance(gtfs, (str, pathlib.Path)):
53
51
  gtfs = [gtfs]
54
- gtfs = [str(self._working_copy(path).absolute()) for path in gtfs]
52
+ gtfs = [WorkingCopy(path) for path in gtfs]
55
53
 
56
- transport_network = com.conveyal.r5.transit.TransportNetwork()
57
- transport_network.scenarioId = PACKAGE
58
-
59
- osm_mapdb = pathlib.Path(f"{osm_pbf}.mapdb")
60
- osm_file = com.conveyal.osmlib.OSM(f"{osm_mapdb}")
61
- osm_file.intersectionDetection = True
62
- osm_file.readFromFile(f"{osm_pbf}")
63
-
64
- self.osm_file = osm_file # keep the mapdb open, close in destructor
65
-
66
- transport_network.streetLayer = com.conveyal.r5.streets.StreetLayer()
67
- transport_network.streetLayer.loadFromOsm(osm_file)
68
- transport_network.streetLayer.parentNetwork = transport_network
69
- transport_network.streetLayer.indexStreets()
70
-
71
- transport_network.transitLayer = com.conveyal.r5.transit.TransitLayer()
72
- for gtfs_file in gtfs:
73
- gtfs_feed = com.conveyal.gtfs.GTFSFeed.readOnlyTempFileFromGtfs(gtfs_file)
74
- transport_network.transitLayer.loadFromGtfs(gtfs_feed)
75
- gtfs_feed.close()
76
- transport_network.transitLayer.parentNetwork = transport_network
54
+ # a hash representing all input files
55
+ digest = hashlib.sha256(
56
+ "".join([FileDigest(osm_pbf)] + [FileDigest(path) for path in gtfs]).encode(
57
+ "utf-8"
58
+ )
59
+ ).hexdigest()
77
60
 
78
- transport_network.streetLayer.associateStops(transport_network.transitLayer)
79
- transport_network.streetLayer.buildEdgeLists()
61
+ try:
62
+ transport_network = self._load_pickled_transport_network(
63
+ Config().CACHE_DIR / f"{digest}.transport_network"
64
+ )
65
+ except FileNotFoundError:
66
+ transport_network = com.conveyal.r5.transit.TransportNetwork()
67
+ transport_network.scenarioId = PACKAGE
68
+
69
+ osm_mapdb = Config().CACHE_DIR / f"{digest}.mapdb"
70
+ osm_file = com.conveyal.osmlib.OSM(f"{osm_mapdb}")
71
+ osm_file.intersectionDetection = True
72
+ osm_file.readFromFile(f"{osm_pbf}")
73
+
74
+ transport_network.streetLayer = com.conveyal.r5.streets.StreetLayer()
75
+ transport_network.streetLayer.parentNetwork = transport_network
76
+ transport_network.streetLayer.loadFromOsm(osm_file)
77
+ transport_network.streetLayer.indexStreets()
78
+
79
+ transport_network.transitLayer = com.conveyal.r5.transit.TransitLayer()
80
+ transport_network.transitLayer.parentNetwork = transport_network
81
+ for gtfs_file in gtfs:
82
+ gtfs_feed = com.conveyal.gtfs.GTFSFeed.readOnlyTempFileFromGtfs(
83
+ f"{gtfs_file}"
84
+ )
85
+ transport_network.transitLayer.loadFromGtfs(gtfs_feed)
86
+ gtfs_feed.close()
80
87
 
81
- transport_network.transitLayer.rebuildTransientIndexes()
88
+ transport_network.streetLayer.associateStops(transport_network.transitLayer)
89
+ transport_network.streetLayer.buildEdgeLists()
82
90
 
83
- transfer_finder = com.conveyal.r5.transit.TransferFinder(transport_network)
84
- transfer_finder.findTransfers()
85
- transfer_finder.findParkRideTransfer()
91
+ transport_network.transitLayer.rebuildTransientIndexes()
86
92
 
87
- transport_network.transitLayer.buildDistanceTables(None)
93
+ transfer_finder = com.conveyal.r5.transit.TransferFinder(transport_network)
94
+ transfer_finder.findTransfers()
95
+ transfer_finder.findParkRideTransfer()
88
96
 
89
- self._transport_network = transport_network
97
+ transport_network.transitLayer.buildDistanceTables(None)
90
98
 
91
- def __del__(self):
92
- """
93
- Delete all temporary files upon destruction.
94
- """
95
- MAX_TRIES = 10
99
+ osm_file.close() # not needed after here?
96
100
 
97
- # first, close the open osm_file,
98
- # delete Java objects, and
99
- # trigger Java garbage collection
100
- try:
101
- self.osm_file.close()
102
- except jpype.JVMNotRunning:
103
- # JVM was stopped already, file should be closed
104
- pass
105
- try:
106
- del self.street_layer
107
- except AttributeError: # might not have been accessed a single time
108
- pass
109
- try:
110
- del self.transit_layer
111
- except AttributeError:
112
- pass
113
- del self._transport_network
114
-
115
- time.sleep(0.5)
116
- jpype.java.lang.System.gc()
117
-
118
- # then, try to delete all files in cache directory
119
- temporary_files = [child for child in self._cache_directory.iterdir()]
120
- for _ in range(MAX_TRIES):
121
- for temporary_file in temporary_files:
122
- try:
123
- temporary_file.unlink()
124
- temporary_files.remove(temporary_file)
125
- except (FileNotFoundError, IOError, OSError):
126
- print(
127
- f"could not delete {temporary_file}, keeping in {temporary_files}"
128
- )
129
- pass
130
-
131
- if not temporary_files: # empty
132
- break
133
-
134
- # there are still files open, let’s wait a moment and try again
135
- time.sleep(0.1)
136
- else:
137
- remaining_files = ", ".join(
138
- [f"{temporary_file}" for temporary_file in temporary_files]
139
- )
140
- warnings.warn(
141
- f"Failed to clean cache directory ‘{self._cache_directory}’. "
142
- f"Remaining file(s): {remaining_files}",
143
- RuntimeWarning,
101
+ self._save_pickled_transport_network(
102
+ transport_network, Config().CACHE_DIR / f"{digest}.transport_network"
144
103
  )
145
104
 
146
- # finally, try to delete the cache directory itself
147
- try:
148
- self._cache_directory.rmdir()
149
- except OSError: # not empty
150
- pass # the JVM destructor is going to take care of this
105
+ self._transport_network = transport_network
151
106
 
152
107
  @classmethod
153
108
  def from_directory(cls, path):
@@ -206,60 +161,27 @@ class TransportNetwork:
206
161
 
207
162
  @property
208
163
  def extent(self):
164
+ """The geographic area covered, as a `shapely.box`."""
209
165
  # TODO: figure out how to get the extent of the GTFS schedule,
210
166
  # then find the smaller extent of the two (or the larger one?)
211
167
  return self.street_layer.extent
212
168
 
213
- @functools.cached_property
214
- def _cache_directory(self):
215
- cache_dir = (
216
- pathlib.Path(Config().TEMP_DIR)
217
- / f"{self.__class__.__name__:s}_{id(self):x}_{random.randrange(16**5):07x}"
218
- )
219
- cache_dir.mkdir(exist_ok=True)
220
- return cache_dir
221
-
222
- def _working_copy(self, input_file):
223
- """Create a copy or link of an input file in a cache directory.
224
-
225
- This method exists because R5 creates temporary files in the
226
- directory of input files. This can not only be annoying clutter,
227
- but also create problems of concurrency, performance, etc., for
228
- instance, when the data comes from a shared network drive or a
229
- read-only file system.
230
-
231
- Arguments
232
- ---------
233
- input_file : str or pathlib.Path
234
- The file to create a copy or link of in a cache directory
235
-
236
- Returns
237
- -------
238
- pathlib.Path
239
- The path to the copy or link created
240
- """
241
- # try to first create a symbolic link, if that fails (e.g., on Windows),
242
- # copy the file to a cache directory
243
- input_file = pathlib.Path(input_file).absolute()
244
- destination_file = pathlib.Path(
245
- self._cache_directory / input_file.name
246
- ).absolute()
247
-
248
- with filelock.FileLock(
249
- destination_file.parent / f"{destination_file.name}.lock"
250
- ):
251
- if not destination_file.exists():
252
- try:
253
- destination_file.symlink_to(input_file)
254
- except OSError:
255
- shutil.copyfile(str(input_file), str(destination_file))
256
- return destination_file
257
-
258
169
  @property
259
170
  def linkage_cache(self):
260
171
  """Expose the `TransportNetwork`’s `linkageCache` to Python."""
261
172
  return self._transport_network.linkageCache
262
173
 
174
+ def _load_pickled_transport_network(self, path):
175
+ try:
176
+ input_file = java.io.File(f"{path}")
177
+ return com.conveyal.r5.kryo.KryoNetworkSerializer.read(input_file)
178
+ except java.io.FileNotFoundException:
179
+ raise FileNotFoundError
180
+
181
+ def _save_pickled_transport_network(self, transport_network, path):
182
+ output_file = java.io.File(f"{path}")
183
+ com.conveyal.r5.kryo.KryoNetworkSerializer.write(transport_network, output_file)
184
+
263
185
  def snap_to_network(
264
186
  self,
265
187
  points,
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Calculate travel times between many origins and destinations."""
4
+
5
+ import copy
6
+
7
+ try:
8
+ from warnings import deprecated
9
+ except ImportError: # Python<=3.12
10
+ from typing_extensions import deprecated
11
+
12
+ import pandas
13
+
14
+ from .base_travel_time_matrix import BaseTravelTimeMatrix
15
+ from ..util import start_jvm
16
+
17
+ import com.conveyal.r5
18
+
19
+
20
+ __all__ = ["TravelTimeMatrix", "TravelTimeMatrixComputer"]
21
+
22
+
23
+ start_jvm()
24
+
25
+
26
+ class TravelTimeMatrix(BaseTravelTimeMatrix):
27
+ """Compute travel times between many origins and destinations."""
28
+
29
+ def __init__(
30
+ self,
31
+ transport_network,
32
+ origins=None,
33
+ destinations=None,
34
+ snap_to_network=False,
35
+ **kwargs,
36
+ ):
37
+ """
38
+ Compute travel times between many origins and destinations.
39
+
40
+ ``r5py.TravelTimeMatrix`` are child classes of ``pandas.DataFrame`` and
41
+ support all of their methods and properties,
42
+ see https://pandas.pydata.org/docs/
43
+
44
+ Arguments
45
+ ---------
46
+ transport_network : r5py.TransportNetwork | tuple(str, list(str), dict)
47
+ The transport network to route on. This can either be a readily
48
+ initialised r5py.TransportNetwork or a tuple of the parameters
49
+ passed to ``TransportNetwork.__init__()``: the path to an OpenStreetMap
50
+ extract in PBF format, a list of zero of more paths to GTFS transport
51
+ schedule files, and a dict with ``build_config`` options.
52
+ origins : geopandas.GeoDataFrame
53
+ Places to find a route _from_
54
+ Has to have a point geometry, and at least an `id` column
55
+ destinations : geopandas.GeoDataFrame (optional)
56
+ Places to find a route _to_
57
+ Has to have a point geometry, and at least an `id` column
58
+ If omitted, use same data set as for origins
59
+ snap_to_network : bool or int, default False
60
+ Should origin an destination points be snapped to the street network
61
+ before routing? If `True`, the default search radius (defined in
62
+ `com.conveyal.r5.streets.StreetLayer.LINK_RADIUS_METERS`) is used,
63
+ if `int`, use `snap_to_network` meters as the search radius.
64
+ **kwargs : mixed
65
+ Any arguments than can be passed to r5py.RegionalTask:
66
+ ``departure``, ``departure_time_window``, ``percentiles``, ``transport_modes``,
67
+ ``access_modes``, ``egress_modes``, ``max_time``, ``max_time_walking``,
68
+ ``max_time_cycling``, ``max_time_driving``, ``speed_cycling``, ``speed_walking``,
69
+ ``max_public_transport_rides``, ``max_bicycle_traffic_stress``
70
+ """
71
+ super().__init__(
72
+ transport_network,
73
+ origins,
74
+ destinations,
75
+ snap_to_network,
76
+ **kwargs,
77
+ )
78
+ data = self._compute()
79
+ for column in data.columns:
80
+ self[column] = data[column]
81
+ del self.transport_network
82
+
83
+ def _compute(self):
84
+ """
85
+ Compute travel times from all origins to all destinations.
86
+
87
+ Returns
88
+ -------
89
+ pandas.DataFrame
90
+ A data frame containing the columns ``from_id``, ``to_id``, and
91
+ ``travel_time``, where ``travel_time`` is the median calculated
92
+ travel time between ``from_id`` and ``to_id`` or ``numpy.nan``
93
+ if no connection with the given parameters was found.
94
+ If non-default ``percentiles`` were requested: one or more columns
95
+ ``travel_time_p{:02d}`` representing the particular percentile of
96
+ travel time.
97
+ """
98
+ self._prepare_origins_destinations()
99
+ self.request.destinations = self.destinations
100
+
101
+ od_matrix = pandas.concat(
102
+ [self._travel_times_per_origin(from_id) for from_id in self.origins.id],
103
+ ignore_index=True,
104
+ )
105
+
106
+ try:
107
+ od_matrix = od_matrix.to_crs(self._origins_crs)
108
+ except AttributeError: # (not a GeoDataFrame)
109
+ pass
110
+ return od_matrix
111
+
112
+ def _parse_results(self, from_id, results):
113
+ """
114
+ Parse the results of an R5 TravelTimeMatrix.
115
+
116
+ Parse data as returned from `com.conveyal.r5.analyst.TravelTimeComputer.computeTravelTimes()`,
117
+ cast data to Python types, and return as a `pandas.Dataframe`. Because of the way r5py
118
+ and R5 interact, this parses the results of routing from one origin to many (all) destinations.
119
+
120
+ Arguments
121
+ ---------
122
+ from_id : mixed
123
+ The value of the ID column of the origin record to report on.
124
+ results : `com.conveyal.r5.OneOriginResult` (Java object)
125
+
126
+ Returns
127
+ -------
128
+ pandas.DataFrame
129
+ A data frame containing the columns ``from_id``, ``to_id``, and
130
+ ``travel_time``, where ``travel_time`` is the median calculated
131
+ travel time between ``from_id`` and ``to_id`` or ``numpy.nan``
132
+ if no connection with the given parameters was found.
133
+ If non-default ``percentiles`` were requested: one or more columns
134
+ ``travel_time_p{:02d}`` representing the particular percentile of
135
+ travel time.
136
+ """
137
+ # First, create an empty DataFrame (this forces column types)
138
+ od_matrix = pandas.DataFrame(
139
+ {
140
+ "from_id": pandas.Series(dtype=str),
141
+ "to_id": pandas.Series(dtype=str),
142
+ }
143
+ | {
144
+ f"travel_time_p{percentile:d}": pandas.Series(dtype=float)
145
+ for percentile in self.request.percentiles
146
+ }
147
+ )
148
+
149
+ # first assign columns with correct length (`to_id`),
150
+ # only then fill `from_id` (it’s a scalar)
151
+ od_matrix["to_id"] = self.destinations.id
152
+ od_matrix["from_id"] = from_id
153
+
154
+ for p, percentile in enumerate(self.request.percentiles):
155
+ travel_times = results.travelTimes.getValues()[p]
156
+ od_matrix[f"travel_time_p{percentile:d}"] = travel_times
157
+
158
+ # rename percentile column if only median requested (the default)
159
+ if self.request.percentiles == [50]:
160
+ od_matrix = od_matrix.rename(columns={"travel_time_p50": "travel_time"})
161
+
162
+ # R5’s NULL value is MAX_INT32
163
+ od_matrix = self._fill_nulls(od_matrix)
164
+
165
+ # re-index (and don’t keep the old index as a new column)
166
+ od_matrix = od_matrix.reset_index(drop=True)
167
+
168
+ return od_matrix
169
+
170
+ def _travel_times_per_origin(self, from_id):
171
+ request = copy.copy(self.request)
172
+ request.origin = self.origins[self.origins.id == from_id].geometry.item()
173
+
174
+ travel_time_computer = com.conveyal.r5.analyst.TravelTimeComputer(
175
+ request, self.transport_network
176
+ )
177
+ results = travel_time_computer.computeTravelTimes()
178
+
179
+ od_matrix = self._parse_results(from_id, results)
180
+
181
+ return od_matrix
182
+
183
+
184
+ @deprecated(
185
+ "Use `TravelTimeMatrix` instead, `TravelTimeMatrixComputer will be deprecated in a future release."
186
+ )
187
+ class TravelTimeMatrixComputer:
188
+ """Compute travel times between many origins and destinations."""
189
+
190
+ def __init__(self, *args, **kwargs):
191
+ """Compute travel times between many origins and destinations."""
192
+ self._ttm = TravelTimeMatrix(*args, **kwargs)
193
+
194
+ def compute_travel_times(self):
195
+ """
196
+ Compute travel times from all origins to all destinations.
197
+
198
+ Returns
199
+ -------
200
+ pandas.DataFrame
201
+ A data frame containing the columns ``from_id``, ``to_id``, and
202
+ ``travel_time``, where ``travel_time`` is the median calculated
203
+ travel time between ``from_id`` and ``to_id`` or ``numpy.nan``
204
+ if no connection with the given parameters was found.
205
+ If non-default ``percentiles`` were requested: one or more columns
206
+ ``travel_time_p{:02d}`` representing the particular percentile of
207
+ travel time.
208
+ """
209
+ return self._ttm
r5py/r5/trip.py CHANGED
@@ -15,9 +15,7 @@ __all__ = ["Trip"]
15
15
 
16
16
 
17
17
  class Trip:
18
- """
19
- Represent one trip, consisting of one or more `TripLeg`s.
20
- """
18
+ """Represent one trip, consisting of one or more `r5py.r5.TripLeg`."""
21
19
 
22
20
  COLUMNS = [
23
21
  "segment",
@@ -25,7 +23,7 @@ class Trip:
25
23
 
26
24
  def __init__(self, legs=[]):
27
25
  """
28
- Represent one trip, consisting of one of more `TripLeg`s.
26
+ Represent one trip, consisting of one of more `r5py.r5.TripLeg`.
29
27
 
30
28
  Arguments
31
29
  =========
@@ -34,7 +32,13 @@ class Trip:
34
32
  """
35
33
  self.legs = legs
36
34
 
35
+ def __eq__(self, other):
36
+ """Check whether `self` and `other` are equal."""
37
+ if isinstance(other, self.__class__):
38
+ return self.legs == other.legs
39
+
37
40
  def __repr__(self):
41
+ """Return a string representation of `self`."""
38
42
  legs = ", ".join([str(leg) for leg in self.legs])
39
43
  return (
40
44
  f"<{self.__class__.__name__}: "
@@ -50,8 +54,9 @@ class Trip:
50
54
  Returns
51
55
  =======
52
56
  list : detailed information about this trip and its legs (segments):
53
- ``segment``, ``transport_mode``, ``departure_time``, ``distance``,
54
- ``travel_time``, ``wait_time``, ``route``, ``geometry``
57
+ ``segment``, ``transport_mode``, ``departure_time``, ``distance``,
58
+ ``travel_time``, ``wait_time``, ``feed``, ``agency_id``, ``route_id``,
59
+ ``start_stop_id``, ``end_stop_id``, ``geometry``
55
60
  """
56
61
  return [[segment] + leg.as_table_row() for segment, leg in enumerate(self.legs)]
57
62
 
@@ -72,9 +77,9 @@ class Trip:
72
77
  )
73
78
 
74
79
  @property
75
- def routes(self):
80
+ def route_ids(self):
76
81
  """The public transport route(s) used on this trip."""
77
- return [leg.route for leg in self.legs]
82
+ return [leg.route_id for leg in self.legs]
78
83
 
79
84
  @property
80
85
  def transport_modes(self):