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.
Files changed (49) hide show
  1. r5py/__init__.py +27 -0
  2. r5py/__main__.py +3 -0
  3. r5py/r5/__init__.py +39 -0
  4. r5py/r5/access_leg.py +12 -0
  5. r5py/r5/base_travel_time_matrix.py +255 -0
  6. r5py/r5/detailed_itineraries.py +226 -0
  7. r5py/r5/direct_leg.py +38 -0
  8. r5py/r5/egress_leg.py +12 -0
  9. r5py/r5/elevation_cost_function.py +50 -0
  10. r5py/r5/elevation_model.py +89 -0
  11. r5py/r5/file_storage.py +82 -0
  12. r5py/r5/isochrones.py +345 -0
  13. r5py/r5/regional_task.py +600 -0
  14. r5py/r5/scenario.py +36 -0
  15. r5py/r5/street_layer.py +90 -0
  16. r5py/r5/street_segment.py +39 -0
  17. r5py/r5/transfer_leg.py +12 -0
  18. r5py/r5/transit_layer.py +87 -0
  19. r5py/r5/transit_leg.py +12 -0
  20. r5py/r5/transport_mode.py +148 -0
  21. r5py/r5/transport_network.py +299 -0
  22. r5py/r5/travel_time_matrix.py +186 -0
  23. r5py/r5/trip.py +97 -0
  24. r5py/r5/trip_leg.py +204 -0
  25. r5py/r5/trip_planner.py +576 -0
  26. r5py/util/__init__.py +31 -0
  27. r5py/util/camel_to_snake_case.py +25 -0
  28. r5py/util/classpath.py +95 -0
  29. r5py/util/config.py +176 -0
  30. r5py/util/contains_gtfs_data.py +46 -0
  31. r5py/util/data_validation.py +28 -0
  32. r5py/util/environment.py +32 -0
  33. r5py/util/exceptions.py +43 -0
  34. r5py/util/file_digest.py +40 -0
  35. r5py/util/good_enough_equidistant_crs.py +73 -0
  36. r5py/util/jvm.py +138 -0
  37. r5py/util/memory_footprint.py +178 -0
  38. r5py/util/parse_int_date.py +24 -0
  39. r5py/util/sample_data_set.py +76 -0
  40. r5py/util/snake_to_camel_case.py +16 -0
  41. r5py/util/spatially_clustered_geodataframe.py +66 -0
  42. r5py/util/validating_requests_session.py +58 -0
  43. r5py/util/warnings.py +7 -0
  44. r5py/util/working_copy.py +42 -0
  45. r5py-1.1.0.dist-info/METADATA +176 -0
  46. r5py-1.1.0.dist-info/RECORD +49 -0
  47. r5py-1.1.0.dist-info/WHEEL +5 -0
  48. r5py-1.1.0.dist-info/licenses/LICENSE +3 -0
  49. r5py-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Determine a reasonable memory footprint for the Java virtual machine."""
4
+
5
+ import psutil
6
+ import re
7
+ import warnings
8
+
9
+ from .config import Config
10
+ from .warnings import R5pyWarning
11
+
12
+ __all__ = ["MAX_JVM_MEMORY"]
13
+
14
+
15
+ ABSOLUTE_MINIMUM_MEMORY = 200 * 1024**2 # never grant less than 200 MiB to JVM
16
+
17
+
18
+ config = Config()
19
+ config.argparser.add(
20
+ "-m",
21
+ "--max-memory",
22
+ help="""
23
+ Memory limit for the JVM running R5.
24
+
25
+ Use % as a suffix to specify a share of total RAM;
26
+ K, M, G, T to specify KiB, MiB, GiB, or TiB, respectively.
27
+ Values without suffix are interpreted as bytes.
28
+ """,
29
+ default="80%",
30
+ )
31
+
32
+
33
+ def _share_of_ram(share=0.8, leave_at_least=(2 * 1024**3)):
34
+ """
35
+ Calculate a share of total RAM.
36
+
37
+ Arguments
38
+ ---------
39
+ share : float
40
+ Which portion of total RAM to return.
41
+ Default: 0.8
42
+ leave_at_least : float
43
+ How much RAM (in bytes) to leave (for other applications and system) in
44
+ any case. If `total RAM - (total RAM ⨉ share)` is smaller than
45
+ `leave_at_least`, return `total RAM - leave_at_least`, instead.
46
+ Default: 2GiB
47
+
48
+ Returns
49
+ -------
50
+ int
51
+ A value in bytes that is close to `share` portion of total RAM.
52
+ """
53
+ total_ram = psutil.virtual_memory().total
54
+ if total_ram * (1.0 - share) > leave_at_least:
55
+ share_of_ram = round(share * total_ram)
56
+ else:
57
+ share_of_ram = round(total_ram - leave_at_least)
58
+ return share_of_ram
59
+
60
+
61
+ def _parse_value_and_unit(value_and_unit, max_unit_length=1):
62
+ """
63
+ Extract value and unit from a string.
64
+
65
+ The string is allowed to contain a (non-numeric) unit suffix.
66
+ For instance, input values of `'1M'` or `3.732G` would yield return
67
+ values `(1, 'M')` or `(3.732, 'G')`, respectively.
68
+
69
+ Arguments
70
+ ---------
71
+ value_and_unit : str
72
+ Input string (typically passed-through from config
73
+ parameter `--max-memory`).
74
+ max_unit_length : int
75
+ Maximum length in characters of the unit suffix.
76
+ Default: 1
77
+
78
+ Returns
79
+ -------
80
+ tuple: a tuple containing
81
+ - value (float): The value extracted from `value_and_unit`.
82
+ - unit (str): The unit extracted from `value_and_unit`.
83
+ """
84
+ matches = re.match(
85
+ (
86
+ "^(?P<value>[0-9]+(\\.[0-9]+)?)" # value
87
+ f"(?P<unit>[^0-9]){{0,{max_unit_length}}}$" # unit
88
+ ),
89
+ value_and_unit,
90
+ )
91
+ value = float(matches["value"])
92
+ unit = matches["unit"]
93
+
94
+ return value, unit
95
+
96
+
97
+ def _interpret_power_of_two_units(value, unit):
98
+ """
99
+ Convert a value given as value and power-of-two unit into a bytes value.
100
+
101
+ Arguments
102
+ ---------
103
+ value : float
104
+ Input value.
105
+ unit : str
106
+ Unit suffix, as specified in IEC 80000-13, e.g., 'K' for kibibyte.
107
+
108
+ Returns
109
+ -------
110
+ int:
111
+ interpreted value in bytes
112
+ """
113
+ # the position of each suffix in this string is the unit’s exponent
114
+ # over 1024.
115
+ # Compare https://en.wikipedia.org/wiki/
116
+ # ISO%2FIEC_80000#Part_13:_Information_science_and_technology
117
+ SUFFIXES = " KMGTPEZY"
118
+
119
+ if unit is None:
120
+ unit = " "
121
+
122
+ if unit not in SUFFIXES:
123
+ raise ValueError(
124
+ f"Could not interpret unit '{unit}'. "
125
+ "Allowed suffixes are 'K', 'M', 'G', 'T', 'P', 'E', 'Z', and 'Y'."
126
+ )
127
+
128
+ exponent = SUFFIXES.find(unit)
129
+ value *= 1024**exponent
130
+
131
+ return value
132
+
133
+
134
+ def _get_max_memory(max_memory):
135
+ """
136
+ Interpret the config parameter --max-memory.
137
+
138
+ Arguments
139
+ ---------
140
+
141
+ max_memory : str
142
+ Memory limit for the JVM running R5.
143
+
144
+ Use % as a suffix to specify a share of total RAM;
145
+ K, M, G, T suffix specify KiB, MiB, GiB, or TiB, respectively.
146
+ Values without suffix are interpreted as bytes.
147
+
148
+ Returns
149
+ -------
150
+ int
151
+ Maximum amount of memory allocated for R5 in bytes.
152
+ """
153
+ try:
154
+ value, unit = _parse_value_and_unit(max_memory)
155
+ except TypeError:
156
+ raise ValueError(f"Could not interpret `--max-memory` ('{max_memory}').")
157
+
158
+ if unit == "%":
159
+ value = _share_of_ram(share=(value / 100.0))
160
+ else:
161
+ # convert to bytes
162
+ value = _interpret_power_of_two_units(value, unit)
163
+
164
+ max_memory = round(value)
165
+
166
+ if max_memory < ABSOLUTE_MINIMUM_MEMORY:
167
+ max_memory = ABSOLUTE_MINIMUM_MEMORY
168
+ warnings.warn(
169
+ f"Requested maximum JVM heap size is too low for R5, "
170
+ f"setting to minimum value {ABSOLUTE_MINIMUM_MEMORY:d} bytes.",
171
+ R5pyWarning,
172
+ stacklevel=1,
173
+ )
174
+
175
+ return max_memory
176
+
177
+
178
+ MAX_JVM_MEMORY = _get_max_memory(config.arguments.max_memory)
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Parse an integer date value."""
4
+
5
+ import datetime
6
+
7
+ __all__ = ["parse_int_date"]
8
+
9
+
10
+ def parse_int_date(int_date):
11
+ """
12
+ Parse an integer date value.
13
+
14
+ com.conveyal.gtfs.GtfsFeed.services.start_date and .end_date are integer
15
+ values in the format YYYYMMDD, so, e.g., 20220222 for 2 February 2022.
16
+ This function converts such an integer value into the respective
17
+ datetime.datetime (at 0:00 o’clock).
18
+
19
+ """
20
+ year = round(int_date / 10000)
21
+ month = round((int_date % 10000) / 100)
22
+ day = int_date % 100
23
+
24
+ return datetime.datetime(year, month, day)
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ """A remote data set that is downloaded on demand."""
5
+
6
+ import hashlib
7
+ import pathlib
8
+ import warnings
9
+
10
+ from .config import Config
11
+ from .validating_requests_session import ValidatingRequestsSession
12
+
13
+ config = Config()
14
+
15
+
16
+ class SampleDataSet(pathlib.Path):
17
+ """Data set that is downloaded and cached as needed."""
18
+
19
+ # decide which kind of pathlib.Path we are (Windows, Unix, ...)
20
+ # cf. https://stackoverflow.com/a/66613346/463864
21
+ try:
22
+ _flavour = type(pathlib.Path())._flavour
23
+ except AttributeError: # Python>=3.13
24
+ pass
25
+
26
+ _CACHE_DIR = pathlib.Path(config.CACHE_DIR) / "sampledata"
27
+
28
+ def __new__(cls, remote_url, sha256_checksum):
29
+ """Define a data set that is downloaded and cached on demand."""
30
+ # pathlib.Path does everything in __new__, rather than __init__
31
+ cached_path = cls._CACHE_DIR / pathlib.Path(remote_url).name
32
+ return super().__new__(cls, cached_path)
33
+
34
+ def __init__(self, remote_url, sha256_checksum, *args, **kwargs):
35
+ """
36
+ Define a data set that is downloaded and cached on demand.
37
+
38
+ Arguments
39
+ ---------
40
+ remote_url : str
41
+ source URL for this data set
42
+ sha256_checksum : str
43
+ checksum for this data set, using an SHA256 algorithm
44
+ """
45
+ cached_path = self._CACHE_DIR / pathlib.Path(remote_url).name
46
+
47
+ try: # Python>=3.12
48
+ super().__init__(cached_path)
49
+ except TypeError:
50
+ super().__init__()
51
+
52
+ self.remote_url = remote_url
53
+ self.checksum = sha256_checksum
54
+ self.cached_path = cached_path
55
+ self._download_remote_file()
56
+
57
+ def _download_remote_file(self):
58
+ try:
59
+ assert (
60
+ hashlib.sha256(self.cached_path.read_bytes()).hexdigest()
61
+ == self.checksum
62
+ )
63
+ except (AssertionError, FileNotFoundError):
64
+ if config.arguments.verbose:
65
+ warnings.warn(
66
+ f"First access to {pathlib.Path(self.remote_url).name}, "
67
+ "downloading remote file to local cache",
68
+ RuntimeWarning,
69
+ stacklevel=1,
70
+ )
71
+ self.cached_path.parent.mkdir(exist_ok=True)
72
+ with (
73
+ ValidatingRequestsSession() as session,
74
+ session.get(self.remote_url, self.checksum) as response,
75
+ ):
76
+ self.cached_path.write_bytes(response.content)
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Convert a snack_case formated string to a camelCase format."""
4
+
5
+ __all__ = ["snake_to_camel_case"]
6
+
7
+
8
+ def snake_to_camel_case(snake_case):
9
+ """Convert `snake_case` to camelCase spelling."""
10
+ if "_" in snake_case:
11
+ words = snake_case.split("_")
12
+ words = [words[0].lower()] + [word.title() for word in words[1:]]
13
+ camel_case = "".join(words)
14
+ else:
15
+ camel_case = snake_case[0].lower() + snake_case[1:]
16
+ return camel_case
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ """Assign a cluster label column to a point-geometry GeoDataFrame."""
5
+
6
+ import warnings
7
+
8
+ import geopandas
9
+ import numpy
10
+ import shapely
11
+ import sklearn.cluster
12
+
13
+ from .good_enough_equidistant_crs import GoodEnoughEquidistantCrs
14
+
15
+ __all__ = ["SpatiallyClusteredGeoDataFrame"]
16
+
17
+
18
+ class SpatiallyClusteredGeoDataFrame:
19
+ """Assign a cluster label column to a point-geometry GeoDataFrame."""
20
+
21
+ def __new__(cls, data, eps=200.0, min_cluster_size=3):
22
+ """
23
+ Assign a cluster label column to a point-geometry GeoDataFrame.
24
+
25
+ Arguments:
26
+ ----------
27
+ data : geopandas.GeoDataFrame
28
+ input data set
29
+ eps : int | float
30
+ EPS parameter to a DBSCAN cluster algorithm, the maximum
31
+ intra-cluster distance between two points
32
+ min_cluster_size : int
33
+ Do not form clusters with less members
34
+ """
35
+ data = geopandas.GeoDataFrame(data)
36
+
37
+ EQUIDISTANT_CRS = GoodEnoughEquidistantCrs(
38
+ shapely.box(*data.to_crs("EPSG:4326").geometry.total_bounds)
39
+ )
40
+
41
+ # loosely based on:
42
+ # https://github.com/geopandas/scipy2018-geospatial-data/blob/master/08-clustering.ipynb
43
+
44
+ coordinates = numpy.vstack(
45
+ data.to_crs(EQUIDISTANT_CRS)["geometry"]
46
+ .apply(lambda geometry: numpy.hstack(geometry.xy))
47
+ .values
48
+ )
49
+
50
+ with warnings.catch_warnings():
51
+ warnings.filterwarnings(
52
+ "ignore",
53
+ "Could not find the number of physical cores",
54
+ category=UserWarning,
55
+ )
56
+ data["cluster"] = (
57
+ sklearn.cluster.DBSCAN(
58
+ eps=eps,
59
+ min_samples=min_cluster_size,
60
+ n_jobs=-1,
61
+ )
62
+ .fit(coordinates)
63
+ .labels_
64
+ )
65
+
66
+ return data
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Extends requests.Session to enable simple checksum testing."""
4
+
5
+ __all__ = ["ValidatingRequestsSession"]
6
+
7
+ import hashlib
8
+
9
+ import requests
10
+
11
+ from .exceptions import ChecksumFailed
12
+
13
+
14
+ class ValidatingRequestsSession(requests.Session):
15
+ """Download a file and test whether it matches a checksum."""
16
+
17
+ def __init__(self, *args, checksum_algorithm=hashlib.sha256, **kwargs):
18
+ """
19
+ Download a file and test whether it matches a checksum.
20
+
21
+ Arguments
22
+ ---------
23
+ checksum_algorithm : function
24
+ algorithm to use to create checksum of downloaded file,
25
+ default: hashlib.sha256
26
+ *args, **kwargs
27
+ any argument accepted by `requests.Session`
28
+ """
29
+ super().__init__(*args, **kwargs)
30
+ self._algorithm = checksum_algorithm
31
+
32
+ def get(self, url, checksum, **kwargs):
33
+ """Send a GET request, tests checksum."""
34
+ kwargs.setdefault("allow_redirects", True)
35
+ return self.request("GET", url, checksum, **kwargs)
36
+
37
+ def post(self, url, checksum, **kwargs):
38
+ """Send a POST request, tests checksum."""
39
+ return self.request("POST", url, checksum, **kwargs)
40
+
41
+ # delete, put, head don’t return data,
42
+ # testing checksums does not apply
43
+
44
+ def request(self, method, url, checksum, **kwargs):
45
+ """
46
+ Retrieve file from cache or proxy requests.request.
47
+
48
+ Raise `ChecksumFailed` if the file’s digest and `checksum` differ.
49
+ """
50
+ response = super().request(method, url, **kwargs)
51
+ digest = self._algorithm(response.content).hexdigest()
52
+
53
+ if digest != checksum:
54
+ raise ChecksumFailed(
55
+ f"Checksum failed for {url}, expected {checksum}, got {digest}"
56
+ )
57
+
58
+ return response
r5py/util/warnings.py ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """R5py-specific warnings."""
4
+
5
+
6
+ class R5pyWarning(RuntimeWarning):
7
+ """Generic base warning for r5py errors."""
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Create a copy or link of an input file in a cache directory."""
4
+
5
+ import filelock
6
+ import pathlib
7
+ import shutil
8
+
9
+ from .config import Config
10
+
11
+ __all__ = ["WorkingCopy"]
12
+
13
+
14
+ class WorkingCopy(pathlib.Path):
15
+ """Create a copy or link of an input file in a cache directory."""
16
+
17
+ def __new__(cls, path):
18
+ """
19
+ Create a copy or link of an input file in a cache directory.
20
+
21
+ This exists because R5 creates temporary files in the directory of input
22
+ files. This can not only be annoying clutter, but also create problems
23
+ of concurrency, performance, etc., for instance, when the data comes
24
+ from a shared network drive or a read-only file system.
25
+
26
+ Arguments
27
+ ---------
28
+ path : str or pathlib.Path
29
+ The file to create a copy or link of in a cache directory
30
+ """
31
+ # try to first create a symbolic link, if that fails (e.g., on Windows),
32
+ # copy the file to a cache directory
33
+ path = pathlib.Path(path).absolute()
34
+ destination = pathlib.Path(Config().CACHE_DIR / path.name).absolute()
35
+
36
+ with filelock.FileLock(destination.parent / f"{destination.name}.lock"):
37
+ if not destination.exists():
38
+ try:
39
+ destination.symlink_to(path)
40
+ except OSError:
41
+ shutil.copyfile(f"{path}", f"{destination}")
42
+ return destination
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.4
2
+ Name: r5py
3
+ Version: 1.1.0
4
+ Summary: Python wrapper for the R5 routing analysis engine
5
+ Author: Christoph Fink, Willem Klumpenhouwer, Marcus Sairava, Rafael Pereira, Henrikki Tenkanen
6
+ License: GPL-3.0-or-later or MIT
7
+ Project-URL: Documentation, https://r5py.readthedocs.org/
8
+ Project-URL: Repository, https://github.com/r5py/r5py.git
9
+ Project-URL: Change log, https://github.com/r5py/r5py/blob/main/CHANGELOG.md
10
+ Project-URL: Bug tracker, https://github.com/r5py/r5py/issues
11
+ Keywords: accessibility,transport,routing,research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: ConfigArgParse
20
+ Requires-Dist: filelock
21
+ Requires-Dist: geohexgrid
22
+ Requires-Dist: geopandas
23
+ Requires-Dist: joblib
24
+ Requires-Dist: jpype1
25
+ Requires-Dist: numpy
26
+ Requires-Dist: pandas
27
+ Requires-Dist: psutil
28
+ Requires-Dist: pyproj
29
+ Requires-Dist: rasterio
30
+ Requires-Dist: requests
31
+ Requires-Dist: scikit-learn
32
+ Requires-Dist: shapely
33
+ Requires-Dist: simplification
34
+ Requires-Dist: typing_extensions; python_version < "3.13"
35
+ Provides-Extra: dev
36
+ Requires-Dist: black; extra == "dev"
37
+ Requires-Dist: flake8; extra == "dev"
38
+ Requires-Dist: flake8-bugbear; extra == "dev"
39
+ Requires-Dist: flake8-pyproject; extra == "dev"
40
+ Requires-Dist: pydocstyle; extra == "dev"
41
+ Requires-Dist: pylint; extra == "dev"
42
+ Provides-Extra: docs
43
+ Requires-Dist: contextily; extra == "docs"
44
+ Requires-Dist: folium; extra == "docs"
45
+ Requires-Dist: GitPython; extra == "docs"
46
+ Requires-Dist: h3; extra == "docs"
47
+ Requires-Dist: jupyterlab_myst; extra == "docs"
48
+ Requires-Dist: mapclassify; extra == "docs"
49
+ Requires-Dist: matplotlib; extra == "docs"
50
+ Requires-Dist: myst-nb; extra == "docs"
51
+ Requires-Dist: nbsphinx; extra == "docs"
52
+ Requires-Dist: pybtex-apa7-style; extra == "docs"
53
+ Requires-Dist: r5py.sampledata.helsinki; extra == "docs"
54
+ Requires-Dist: r5py.sampledata.sao_paulo; extra == "docs"
55
+ Requires-Dist: shapely; extra == "docs"
56
+ Requires-Dist: sphinx; extra == "docs"
57
+ Requires-Dist: sphinx-book-theme; extra == "docs"
58
+ Requires-Dist: sphinx-design; extra == "docs"
59
+ Requires-Dist: sphinxcontrib-bibtex; extra == "docs"
60
+ Requires-Dist: sphinxcontrib-images; extra == "docs"
61
+ Provides-Extra: tests
62
+ Requires-Dist: pyarrow; extra == "tests"
63
+ Requires-Dist: pytest; extra == "tests"
64
+ Requires-Dist: pytest-cov; extra == "tests"
65
+ Requires-Dist: pytest-lazy-fixtures; extra == "tests"
66
+ Requires-Dist: r5py.sampledata.helsinki; extra == "tests"
67
+ Requires-Dist: r5py.sampledata.sao_paulo; extra == "tests"
68
+ Requires-Dist: typing-extensions; extra == "tests"
69
+ Dynamic: license-file
70
+
71
+ <img class="r5py_logo" align="right" src="https://github.com/r5py/r5py/raw/main/docs/_static/images/r5py_blue.svg" alt="r5py logo" style="width:180px; max-width:30vW;">
72
+
73
+ # r5py: Rapid Realistic Routing with R5 in Python
74
+
75
+ <!-- badges -->
76
+ [![Try r5py with binder][binder-badge]][binder-link]
77
+ [![DOI][doi-badge]][doi-link]
78
+ <br />
79
+ [![stable version][stable-version-badge]][stable-version-link]
80
+ [![downloads (pypi)][downloads-pypi-badge]][downloads-pypi-link]
81
+ [![downloads (conda-forge)][downloads-conda-forge-badge]][downloads-conda-forge-link]
82
+ <br />
83
+ [![Unit tests][test-status-badge]][test-status-link]
84
+ [![Documentation Status][rtd-status-badge]][rtd-status-link]
85
+ [![Coverage][coverage-badge]][coverage-link]
86
+ <br />
87
+
88
+
89
+ **R5py** is a Python library for rapid realistic routing on multimodal transport
90
+ networks (walk, bike, public transport and car). It provides a simple and
91
+ friendly interface to R<sup>5</sup>, the Rapid Realistic Routing on Real-world
92
+ and Reimagined networks, the [routing engine][r5-github] developed by Conveyal.
93
+ **r5py** is inspired by [r5r, a wrapper for R][r5r-vignette], and the library is
94
+ designed to interact with [GeoPandas][geopandas] GeoDataFrames.
95
+
96
+ **R5py** offers a simple way to run R5 locally with Python. It allows users to
97
+ calculate travel time matrices and accessibility by different travel modes. To
98
+ get started, see a detailed demonstration of the **r5py** ‘in action’ from the
99
+ [Usage][rtd-quickstart] section of its documentation. Over time, **r5py** will
100
+ be expanded to incorporate other functionalities from R5.
101
+
102
+ ## Installation
103
+
104
+ **R5py** is available from conda-forge and PyPi. You can use `mamba`, `pip` or
105
+ `conda` to install it. To quickstart your use of **r5py**, we also provide an
106
+ [`environment.yml` file ][env-file], using which you can [quickly set up a
107
+ development environment][conda-create-env-from-yml] and are ready to go.
108
+
109
+ For more details and alternative installation options, read the dedicated
110
+ [installation section][rtd-installation] of the r5py documentation.
111
+
112
+ ## Usage
113
+
114
+ You can find detailed installation instructions, example code, documentation and
115
+ API reference at [r5py.readthedocs.io][rtd-link].
116
+
117
+
118
+ ## Acknowledgements
119
+
120
+ The [R<sup>5</sup> routing engine][r5-github] is developed at
121
+ [Conveyal][conveyal] with contributions from several people.
122
+
123
+ R5py draws a lot of inspiration from [r5r][r5r-github], an interface to R5 from
124
+ the R language that is developed at the Institute for Applied Economic Research
125
+ (Ipea), Brazil.
126
+
127
+
128
+ ## Citation
129
+
130
+ If you use *r5py* for scientific research, please cite it in your publications:
131
+
132
+ Fink, C., Klumpenhouwer, W., Saraiva, M., Pereira, R., &amp; Tenkanen, H., 2022:
133
+ *r5py: Rapid Realistic Routing with R5 in Python*.
134
+ [DOI:10.5281/zenodo.7060437][doi-link]
135
+
136
+
137
+ ## License
138
+
139
+ This work is dual-licensed under GNU General Public License v3.0 or later and
140
+ MIT License. You can choose between the two depending on which license fits
141
+ your project better.
142
+
143
+ `SPDX-License-Identifier: GPL-3.0-or-later OR MIT`
144
+
145
+
146
+ <!-- links used throughout the document -->
147
+
148
+ <!-- (1) badges -->
149
+ [binder-badge]: https://img.shields.io/badge/Try%20r5py%20with-binder-F5A252.svg?logo=data:image/png;base64,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
150
+ [binder-link]: https://mybinder.org/v2/gh/r5py/r5py/stable?urlpath=tree/docs/user-guide/user-manual/quickstart.md
151
+ [coverage-badge]: https://codecov.io/gh/r5py/r5py/branch/main/graph/badge.svg?token=WG8RBMZBK6
152
+ [coverage-link]: https://codecov.io/gh/r5py/r5py
153
+ [doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.7060437.svg
154
+ [doi-link]: https://doi.org/10.5281/zenodo.7060437
155
+ [downloads-conda-forge-badge]: https://img.shields.io/conda/dn/conda-forge/r5py?label=Downloads%20%28conda-forge%29
156
+ [downloads-conda-forge-link]: https://anaconda.org/conda-forge/r5py
157
+ [downloads-pypi-badge]: https://static.pepy.tech/personalized-badge/r5py?period=total&units=international_system&left_color=grey&right_color=orange&left_text=Downloads%20(pypi)
158
+ [downloads-pypi-link]: https://pypi.org/project/r5py/
159
+ [rtd-status-badge]: https://readthedocs.org/projects/r5py/badge/?version=stable
160
+ [rtd-status-link]: https://r5py.readthedocs.io/
161
+ [stable-version-badge]: https://img.shields.io/pypi/v/r5py?label=Stable
162
+ [stable-version-link]: https://github.com/r5py/r5py/releases
163
+ [test-status-badge]: https://github.com/r5py/r5py/actions/workflows/test.yml/badge.svg
164
+ [test-status-link]: https://github.com/r5py/r5py/actions/workflows/test.yml
165
+
166
+ <!-- (2) other links -->
167
+ [conda-create-env-from-yml]: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-from-an-environment-yml-file
168
+ [conveyal]: https://www.conveyal.com/
169
+ [env-file]: https://github.com/r5py/r5py/blob/main/ci/r5py.yaml
170
+ [geopandas]: https://geopandas.org/
171
+ [r5-github]: https://github.com/conveyal/r5/
172
+ [r5r-github]: https://github.com/ipeaGIT/r5r/
173
+ [r5r-vignette]: https://ipeagit.github.io/r5r/
174
+ [rtd-quickstart]: https://r5py.readthedocs.io/stable/user-guide/user-manual/quickstart.html
175
+ [rtd-installation]: https://r5py.readthedocs.io/stable/user-guide/installation/installation.html
176
+ [rtd-link]: https://r5py.readthedocs.io/