routematrix 0.1.0__tar.gz
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.
- routematrix-0.1.0/CHANGELOG.md +28 -0
- routematrix-0.1.0/CITATION.cff +19 -0
- routematrix-0.1.0/LICENSE +21 -0
- routematrix-0.1.0/MANIFEST.in +8 -0
- routematrix-0.1.0/PKG-INFO +222 -0
- routematrix-0.1.0/README.md +182 -0
- routematrix-0.1.0/pyproject.toml +122 -0
- routematrix-0.1.0/setup.cfg +4 -0
- routematrix-0.1.0/src/routematrix/__init__.py +84 -0
- routematrix-0.1.0/src/routematrix/_version.py +1 -0
- routematrix-0.1.0/src/routematrix/analysis.py +217 -0
- routematrix-0.1.0/src/routematrix/cache.py +154 -0
- routematrix-0.1.0/src/routematrix/cli.py +218 -0
- routematrix-0.1.0/src/routematrix/client.py +368 -0
- routematrix-0.1.0/src/routematrix/engines.py +506 -0
- routematrix-0.1.0/src/routematrix/errors.py +50 -0
- routematrix-0.1.0/src/routematrix/geo.py +28 -0
- routematrix-0.1.0/src/routematrix/http.py +189 -0
- routematrix-0.1.0/src/routematrix/points.py +143 -0
- routematrix-0.1.0/src/routematrix/py.typed +0 -0
- routematrix-0.1.0/src/routematrix/result.py +217 -0
- routematrix-0.1.0/src/routematrix/tiling.py +55 -0
- routematrix-0.1.0/src/routematrix.egg-info/PKG-INFO +222 -0
- routematrix-0.1.0/src/routematrix.egg-info/SOURCES.txt +32 -0
- routematrix-0.1.0/src/routematrix.egg-info/dependency_links.txt +1 -0
- routematrix-0.1.0/src/routematrix.egg-info/entry_points.txt +2 -0
- routematrix-0.1.0/src/routematrix.egg-info/requires.txt +15 -0
- routematrix-0.1.0/src/routematrix.egg-info/top_level.txt +1 -0
- routematrix-0.1.0/tests/conftest.py +44 -0
- routematrix-0.1.0/tests/mock_engines.py +328 -0
- routematrix-0.1.0/tests/test_analysis_points.py +197 -0
- routematrix-0.1.0/tests/test_cache_http.py +253 -0
- routematrix-0.1.0/tests/test_cli.py +161 -0
- routematrix-0.1.0/tests/test_engines.py +268 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-09
|
|
8
|
+
### Added
|
|
9
|
+
- `RouteMatrix` client with one interface for OSRM (`/table/v1`), Valhalla
|
|
10
|
+
(`/sources_to_targets`, `/isochrone`), GraphHopper (`/matrix`, self-hosted or hosted
|
|
11
|
+
with `GRAPHHOPPER_API_KEY`) and openrouteservice (`/v2/matrix`, `ORS_API_KEY`).
|
|
12
|
+
- `estimate` engine: great-circle distance x detour factor / speed, always flagged as an
|
|
13
|
+
estimate (`MatrixResult.is_estimate`, a warning, and CLI output).
|
|
14
|
+
- Automatic tiling of N x M jobs within each engine's source, destination and element
|
|
15
|
+
limits; tiles are stitched back into dense numpy arrays (seconds, metres, `NaN` for
|
|
16
|
+
unreachable pairs).
|
|
17
|
+
- Bounded parallel requests, polite rate limiting for hosted services, retries with
|
|
18
|
+
exponential backoff and `Retry-After`, per-request timeouts, API keys redacted from errors.
|
|
19
|
+
- Persistent SQLite pair cache keyed by engine + server, profile, origin and destination;
|
|
20
|
+
interrupted jobs resume and only request missing rows.
|
|
21
|
+
- Snapping distances (OSRM, openrouteservice) with `snap_flags()`; long-format records,
|
|
22
|
+
pandas DataFrame and CSV export (long or wide).
|
|
23
|
+
- Analysis: `nearest_k`, `cumulative_opportunities`, `gravity_accessibility` (exponential,
|
|
24
|
+
power, Gaussian, linear decay), `asymmetry`, `compare_matrices`, `compare_engines`.
|
|
25
|
+
- Public demo servers (`router.project-osrm.org`, `valhalla1.openstreetmap.de`) are
|
|
26
|
+
accepted only for tiny jobs (10 points, 25 pairs) with a warning; OSRM and Valhalla
|
|
27
|
+
always need an explicit base URL.
|
|
28
|
+
- CLI: `routematrix compute`, `routematrix nearest`, `routematrix engines`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
authors:
|
|
4
|
+
- family-names: "Eminoğlu"
|
|
5
|
+
given-names: "Yusuf"
|
|
6
|
+
email: "yusufeminoglu@gmail.com"
|
|
7
|
+
orcid: "https://orcid.org/0009-0005-6000-2934"
|
|
8
|
+
title: "routematrix"
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
date-released: "2026-11-09"
|
|
11
|
+
license: "MIT"
|
|
12
|
+
repository-code: "https://github.com/YusufEminoglu/routematrix_sdk"
|
|
13
|
+
url: "https://pypi.org/project/routematrix/"
|
|
14
|
+
abstract: "Large origin-destination travel-time and distance matrices from open routing engines (OSRM, Valhalla, GraphHopper, openrouteservice) with automatic tiling, polite parallel requests, a persistent SQLite cache with resume, snapping diagnostics and accessibility measures."
|
|
15
|
+
keywords:
|
|
16
|
+
- routing
|
|
17
|
+
- accessibility
|
|
18
|
+
- origin-destination matrix
|
|
19
|
+
- travel time
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuf Eminoğlu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: routematrix
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Large origin-destination travel-time and distance matrices from OSRM, Valhalla, GraphHopper and openrouteservice, with tiling, caching, resume and accessibility tools.
|
|
5
|
+
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YusufEminoglu/routematrix_sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/YusufEminoglu/routematrix_sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/YusufEminoglu/routematrix_sdk/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/YusufEminoglu/routematrix_sdk/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: routing,travel-time,od-matrix,distance-matrix,accessibility,osrm,valhalla,graphhopper,openrouteservice,isochrone,logistics,transport-planning,gis
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
21
|
+
Classifier: Intended Audience :: Science/Research
|
|
22
|
+
Classifier: Intended Audience :: Developers
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.20.0
|
|
28
|
+
Provides-Extra: pandas
|
|
29
|
+
Requires-Dist: pandas>=1.3.0; extra == "pandas"
|
|
30
|
+
Provides-Extra: geo
|
|
31
|
+
Requires-Dist: pandas>=1.3.0; extra == "geo"
|
|
32
|
+
Requires-Dist: geopandas>=0.12.0; extra == "geo"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
37
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pandas>=1.3.0; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# routematrix
|
|
42
|
+
|
|
43
|
+
Large origin–destination (OD) travel-time and distance matrices from open routing
|
|
44
|
+
engines, for accessibility, logistics and planning analysis.
|
|
45
|
+
|
|
46
|
+
- One client for **OSRM**, **Valhalla**, **GraphHopper** and **openrouteservice**, plus a
|
|
47
|
+
clearly labelled great-circle **estimate** fallback.
|
|
48
|
+
- **Automatic tiling** of N × M jobs within each engine's request limits, stitched back
|
|
49
|
+
into dense numpy arrays: `durations` (s) and `distances` (m), `NaN` = unreachable.
|
|
50
|
+
- **Polite parallelism**: bounded concurrency, rate limiting for hosted APIs, retries
|
|
51
|
+
with exponential backoff (honours `Retry-After`), timeouts.
|
|
52
|
+
- **Persistent SQLite cache and resume**: every finished tile is stored at once; rerun
|
|
53
|
+
an interrupted job and only the missing pairs are requested.
|
|
54
|
+
- **Snapping diagnostics**: distance between each input point and where the engine
|
|
55
|
+
snapped it, with flags for points that are probably off the network.
|
|
56
|
+
- **Analysis**: nearest-k destinations, cumulative opportunities, gravity accessibility,
|
|
57
|
+
symmetry checks and engine-to-engine comparison.
|
|
58
|
+
- Dependencies: the standard library (`urllib`, `sqlite3`) and numpy. pandas and
|
|
59
|
+
geopandas are optional extras.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install routematrix # core
|
|
63
|
+
pip install "routematrix[pandas]" # DataFrame export / input
|
|
64
|
+
pip install "routematrix[geo]" # GeoDataFrame input, isochrones as GeoDataFrame
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from routematrix import RouteMatrix, cumulative_opportunities
|
|
71
|
+
|
|
72
|
+
with RouteMatrix("osrm", url="http://localhost:5000", profile="driving",
|
|
73
|
+
cache="od_cache.sqlite", progress=True) as rm:
|
|
74
|
+
res = rm.compute("homes.csv", "clinics.csv") # CSV, (N, 2) lon/lat array,
|
|
75
|
+
# DataFrame or GeoDataFrame
|
|
76
|
+
res.durations # (N, M) float seconds, NaN = unreachable
|
|
77
|
+
res.distances # (N, M) float metres
|
|
78
|
+
res.origin_snap_m # distance from each origin to its snapped location
|
|
79
|
+
res.snap_flags(500) # (origin_flags, destination_flags) for snaps > 500 m
|
|
80
|
+
res.to_dataframe() # long format: origin_id, destination_id, duration_s, distance_m, ...
|
|
81
|
+
res.to_csv("matrix.csv") # long CSV
|
|
82
|
+
res.to_csv("wide.csv", wide=True) # one row per origin
|
|
83
|
+
|
|
84
|
+
# Clinics reachable within 15 minutes, per home
|
|
85
|
+
counts = cumulative_opportunities(res, threshold=15 * 60)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Coordinates are always **longitude, latitude** (x, y) in EPSG:4326, like GeoJSON. CSV
|
|
89
|
+
columns are detected from common names (`lon`/`lng`/`longitude`/`x`,
|
|
90
|
+
`lat`/`latitude`/`y`, and `id`/`name` for labels).
|
|
91
|
+
|
|
92
|
+
### Other engines
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
RouteMatrix("valhalla", url="http://localhost:8002", profile="walking") # -> pedestrian
|
|
96
|
+
RouteMatrix("ors", url="http://localhost:8080/ors", profile="cycling") # -> cycling-regular
|
|
97
|
+
RouteMatrix("ors", profile="driving") # hosted API, key from ORS_API_KEY
|
|
98
|
+
RouteMatrix("graphhopper", profile="car") # hosted API, key from GRAPHHOPPER_API_KEY
|
|
99
|
+
RouteMatrix("estimate", profile="walking", detour_factor=1.3) # NOT routed: an estimate
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`driving` / `walking` / `cycling` are mapped to each engine's own profile names; any
|
|
103
|
+
other value is passed through unchanged (e.g. `truck`, `foot-hiking`, `motor_scooter`).
|
|
104
|
+
|
|
105
|
+
### Valhalla isochrones
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from routematrix import isochrones_to_geodataframe
|
|
109
|
+
|
|
110
|
+
rm = RouteMatrix("valhalla", url="http://localhost:8002", profile="pedestrian")
|
|
111
|
+
fc = rm.isochrone((28.97, 41.01), minutes=[5, 10, 15]) # GeoJSON FeatureCollection
|
|
112
|
+
gdf = isochrones_to_geodataframe(fc) # needs the geo extra
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Accessibility and diagnostics
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from routematrix import (nearest_k, cumulative_opportunities, gravity_accessibility,
|
|
119
|
+
asymmetry, compare_engines)
|
|
120
|
+
|
|
121
|
+
idx, secs = nearest_k(res, k=3) # 3 closest destinations per origin
|
|
122
|
+
jobs_30 = cumulative_opportunities(res, 1800, opportunities=jobs)
|
|
123
|
+
hansen = gravity_accessibility(res, jobs, function="exponential", beta=0.1, scale=60)
|
|
124
|
+
asymmetry(square_result) # one-way streets vs. data problems
|
|
125
|
+
compare_engines(origins, dests, RouteMatrix("osrm", url=A), RouteMatrix("valhalla", url=B))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Command line
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
routematrix compute origins.csv destinations.csv --engine osrm \
|
|
132
|
+
--url http://localhost:5000 --profile driving -o matrix.csv --cache od.sqlite
|
|
133
|
+
routematrix compute origins.csv --engine estimate --profile walking --wide -o walk.csv
|
|
134
|
+
routematrix nearest homes.csv clinics.csv --engine valhalla --url http://localhost:8002 \
|
|
135
|
+
--profile auto -k 3 -o nearest.csv
|
|
136
|
+
routematrix engines # default tile limits per engine
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Exit codes: `0` success, `2` error, `3` finished with failed tiles (`--on-error nan`).
|
|
140
|
+
Progress and a summary (reachability, cache hits, snap flags) go to stderr.
|
|
141
|
+
|
|
142
|
+
## Engines
|
|
143
|
+
|
|
144
|
+
| Engine | Endpoint | Default tile | Snapping info | Units returned | Key |
|
|
145
|
+
|---|---|---|---|---|---|
|
|
146
|
+
| OSRM | `GET /table/v1/{profile}/…` | 100 × 100 (`osrm-routed --max-table-size` default 100) | yes | s, m | – |
|
|
147
|
+
| Valhalla | `POST /sources_to_targets` | 50 × 50 = 2 500 pairs (`max_matrix_location_pairs`) | no | s, km → m | – |
|
|
148
|
+
| GraphHopper | `POST /matrix` | 25 × 25 (plan dependent) | no | s, m | `GRAPHHOPPER_API_KEY` (hosted) |
|
|
149
|
+
| openrouteservice | `POST /v2/matrix/{profile}` | 50 × 50 = 2 500 routes (`maximum_routes`) | yes | s, m | `ORS_API_KEY` (hosted) |
|
|
150
|
+
| estimate | none (local) | 2 000 × 2 000 | no | s, m (estimated) | – |
|
|
151
|
+
|
|
152
|
+
If your server allows larger requests (e.g. `osrm-routed --max-table-size 1000`), pass
|
|
153
|
+
`max_sources=`, `max_destinations=` and `max_elements=` (CLI: `--max-sources` …). The
|
|
154
|
+
open-source GraphHopper server has no `/matrix` endpoint; "self-hosted" there means a
|
|
155
|
+
server exposing the same API. Valhalla also enforces `max_matrix_distance` (400 km for
|
|
156
|
+
`auto` by default); such requests fail with an engine error.
|
|
157
|
+
|
|
158
|
+
Defaults: concurrency 4 for your own servers, 1 for hosted APIs and demo servers;
|
|
159
|
+
hosted openrouteservice is spaced 1.6 s per request (free plan: 40 matrix requests/min),
|
|
160
|
+
hosted GraphHopper 0.5 s.
|
|
161
|
+
|
|
162
|
+
## Run an engine locally (Docker)
|
|
163
|
+
|
|
164
|
+
OSRM (example extract: Monaco):
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
wget https://download.geofabrik.de/europe/monaco-latest.osm.pbf
|
|
168
|
+
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-extract -p /opt/car.lua /data/monaco-latest.osm.pbf
|
|
169
|
+
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-partition /data/monaco-latest.osrm
|
|
170
|
+
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-customize /data/monaco-latest.osrm
|
|
171
|
+
docker run -t -p 5000:5000 -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-routed --algorithm mld --max-table-size 1000 /data/monaco-latest.osrm
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Valhalla:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
mkdir -p custom_files
|
|
178
|
+
wget -O custom_files/monaco-latest.osm.pbf https://download.geofabrik.de/europe/monaco-latest.osm.pbf
|
|
179
|
+
docker run -dt --name valhalla -p 8002:8002 -v "${PWD}/custom_files:/custom_files" ghcr.io/valhalla/valhalla-scripted:latest
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Then use `--url http://localhost:5000` (OSRM) or `--url http://localhost:8002` (Valhalla).
|
|
183
|
+
|
|
184
|
+
## Responsible use
|
|
185
|
+
|
|
186
|
+
- **No public server by default.** OSRM and Valhalla always need an explicit `url`.
|
|
187
|
+
The public demo servers (`router.project-osrm.org`, `valhalla1.openstreetmap.de`) are
|
|
188
|
+
run by volunteers for testing; routematrix accepts them only for tiny jobs
|
|
189
|
+
(at most 10 points and 25 pairs), forces one request at a time with a 1 s gap, and
|
|
190
|
+
warns. Anything larger raises `DemoServerLimitError`: run your own server.
|
|
191
|
+
- **Hosted APIs** (openrouteservice, GraphHopper) have quotas and terms of use. Keys are
|
|
192
|
+
read from environment variables, never written to the cache, and redacted from
|
|
193
|
+
error messages.
|
|
194
|
+
- Every request carries an identifying `User-Agent`; set your own contact via
|
|
195
|
+
`RouteMatrix(..., user_agent="myproject/1.0 (+mailto:me@example.org)")`.
|
|
196
|
+
- Respect the OpenStreetMap ODbL: credit "© OpenStreetMap contributors" when you publish
|
|
197
|
+
results derived from OSM-based routing.
|
|
198
|
+
- The `estimate` engine is not routing. Results carry `is_estimate=True`, a warning is
|
|
199
|
+
emitted, and the CLI prints `ESTIMATE`. Use it for sanity checks or as a clearly
|
|
200
|
+
labelled fallback only.
|
|
201
|
+
- Check snapping: points snapped hundreds of metres away (parks, water, private
|
|
202
|
+
ground, outside the extract) produce misleading times. Use `snap_flags()`.
|
|
203
|
+
|
|
204
|
+
## Caching and resume
|
|
205
|
+
|
|
206
|
+
The cache key is `(engine + server URL, profile, origin, destination)` with
|
|
207
|
+
coordinates rounded to 1e-6 degrees. Unreachable pairs are cached as known-unreachable.
|
|
208
|
+
Rerunning a job after a crash, a `Ctrl+C` or with `on_error="nan"` requests only the
|
|
209
|
+
origin rows that still have gaps. Cached data does not expire: delete the file (or use
|
|
210
|
+
`MatrixCache(path).clear()`) after updating the road network.
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pip install -e ".[dev]"
|
|
216
|
+
pytest # uses local mock servers only, never the network
|
|
217
|
+
ruff check . && ruff format --check src tests && mypy src
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT © Yusuf Eminoğlu
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# routematrix
|
|
2
|
+
|
|
3
|
+
Large origin–destination (OD) travel-time and distance matrices from open routing
|
|
4
|
+
engines, for accessibility, logistics and planning analysis.
|
|
5
|
+
|
|
6
|
+
- One client for **OSRM**, **Valhalla**, **GraphHopper** and **openrouteservice**, plus a
|
|
7
|
+
clearly labelled great-circle **estimate** fallback.
|
|
8
|
+
- **Automatic tiling** of N × M jobs within each engine's request limits, stitched back
|
|
9
|
+
into dense numpy arrays: `durations` (s) and `distances` (m), `NaN` = unreachable.
|
|
10
|
+
- **Polite parallelism**: bounded concurrency, rate limiting for hosted APIs, retries
|
|
11
|
+
with exponential backoff (honours `Retry-After`), timeouts.
|
|
12
|
+
- **Persistent SQLite cache and resume**: every finished tile is stored at once; rerun
|
|
13
|
+
an interrupted job and only the missing pairs are requested.
|
|
14
|
+
- **Snapping diagnostics**: distance between each input point and where the engine
|
|
15
|
+
snapped it, with flags for points that are probably off the network.
|
|
16
|
+
- **Analysis**: nearest-k destinations, cumulative opportunities, gravity accessibility,
|
|
17
|
+
symmetry checks and engine-to-engine comparison.
|
|
18
|
+
- Dependencies: the standard library (`urllib`, `sqlite3`) and numpy. pandas and
|
|
19
|
+
geopandas are optional extras.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install routematrix # core
|
|
23
|
+
pip install "routematrix[pandas]" # DataFrame export / input
|
|
24
|
+
pip install "routematrix[geo]" # GeoDataFrame input, isochrones as GeoDataFrame
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from routematrix import RouteMatrix, cumulative_opportunities
|
|
31
|
+
|
|
32
|
+
with RouteMatrix("osrm", url="http://localhost:5000", profile="driving",
|
|
33
|
+
cache="od_cache.sqlite", progress=True) as rm:
|
|
34
|
+
res = rm.compute("homes.csv", "clinics.csv") # CSV, (N, 2) lon/lat array,
|
|
35
|
+
# DataFrame or GeoDataFrame
|
|
36
|
+
res.durations # (N, M) float seconds, NaN = unreachable
|
|
37
|
+
res.distances # (N, M) float metres
|
|
38
|
+
res.origin_snap_m # distance from each origin to its snapped location
|
|
39
|
+
res.snap_flags(500) # (origin_flags, destination_flags) for snaps > 500 m
|
|
40
|
+
res.to_dataframe() # long format: origin_id, destination_id, duration_s, distance_m, ...
|
|
41
|
+
res.to_csv("matrix.csv") # long CSV
|
|
42
|
+
res.to_csv("wide.csv", wide=True) # one row per origin
|
|
43
|
+
|
|
44
|
+
# Clinics reachable within 15 minutes, per home
|
|
45
|
+
counts = cumulative_opportunities(res, threshold=15 * 60)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Coordinates are always **longitude, latitude** (x, y) in EPSG:4326, like GeoJSON. CSV
|
|
49
|
+
columns are detected from common names (`lon`/`lng`/`longitude`/`x`,
|
|
50
|
+
`lat`/`latitude`/`y`, and `id`/`name` for labels).
|
|
51
|
+
|
|
52
|
+
### Other engines
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
RouteMatrix("valhalla", url="http://localhost:8002", profile="walking") # -> pedestrian
|
|
56
|
+
RouteMatrix("ors", url="http://localhost:8080/ors", profile="cycling") # -> cycling-regular
|
|
57
|
+
RouteMatrix("ors", profile="driving") # hosted API, key from ORS_API_KEY
|
|
58
|
+
RouteMatrix("graphhopper", profile="car") # hosted API, key from GRAPHHOPPER_API_KEY
|
|
59
|
+
RouteMatrix("estimate", profile="walking", detour_factor=1.3) # NOT routed: an estimate
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`driving` / `walking` / `cycling` are mapped to each engine's own profile names; any
|
|
63
|
+
other value is passed through unchanged (e.g. `truck`, `foot-hiking`, `motor_scooter`).
|
|
64
|
+
|
|
65
|
+
### Valhalla isochrones
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from routematrix import isochrones_to_geodataframe
|
|
69
|
+
|
|
70
|
+
rm = RouteMatrix("valhalla", url="http://localhost:8002", profile="pedestrian")
|
|
71
|
+
fc = rm.isochrone((28.97, 41.01), minutes=[5, 10, 15]) # GeoJSON FeatureCollection
|
|
72
|
+
gdf = isochrones_to_geodataframe(fc) # needs the geo extra
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Accessibility and diagnostics
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from routematrix import (nearest_k, cumulative_opportunities, gravity_accessibility,
|
|
79
|
+
asymmetry, compare_engines)
|
|
80
|
+
|
|
81
|
+
idx, secs = nearest_k(res, k=3) # 3 closest destinations per origin
|
|
82
|
+
jobs_30 = cumulative_opportunities(res, 1800, opportunities=jobs)
|
|
83
|
+
hansen = gravity_accessibility(res, jobs, function="exponential", beta=0.1, scale=60)
|
|
84
|
+
asymmetry(square_result) # one-way streets vs. data problems
|
|
85
|
+
compare_engines(origins, dests, RouteMatrix("osrm", url=A), RouteMatrix("valhalla", url=B))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Command line
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
routematrix compute origins.csv destinations.csv --engine osrm \
|
|
92
|
+
--url http://localhost:5000 --profile driving -o matrix.csv --cache od.sqlite
|
|
93
|
+
routematrix compute origins.csv --engine estimate --profile walking --wide -o walk.csv
|
|
94
|
+
routematrix nearest homes.csv clinics.csv --engine valhalla --url http://localhost:8002 \
|
|
95
|
+
--profile auto -k 3 -o nearest.csv
|
|
96
|
+
routematrix engines # default tile limits per engine
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Exit codes: `0` success, `2` error, `3` finished with failed tiles (`--on-error nan`).
|
|
100
|
+
Progress and a summary (reachability, cache hits, snap flags) go to stderr.
|
|
101
|
+
|
|
102
|
+
## Engines
|
|
103
|
+
|
|
104
|
+
| Engine | Endpoint | Default tile | Snapping info | Units returned | Key |
|
|
105
|
+
|---|---|---|---|---|---|
|
|
106
|
+
| OSRM | `GET /table/v1/{profile}/…` | 100 × 100 (`osrm-routed --max-table-size` default 100) | yes | s, m | – |
|
|
107
|
+
| Valhalla | `POST /sources_to_targets` | 50 × 50 = 2 500 pairs (`max_matrix_location_pairs`) | no | s, km → m | – |
|
|
108
|
+
| GraphHopper | `POST /matrix` | 25 × 25 (plan dependent) | no | s, m | `GRAPHHOPPER_API_KEY` (hosted) |
|
|
109
|
+
| openrouteservice | `POST /v2/matrix/{profile}` | 50 × 50 = 2 500 routes (`maximum_routes`) | yes | s, m | `ORS_API_KEY` (hosted) |
|
|
110
|
+
| estimate | none (local) | 2 000 × 2 000 | no | s, m (estimated) | – |
|
|
111
|
+
|
|
112
|
+
If your server allows larger requests (e.g. `osrm-routed --max-table-size 1000`), pass
|
|
113
|
+
`max_sources=`, `max_destinations=` and `max_elements=` (CLI: `--max-sources` …). The
|
|
114
|
+
open-source GraphHopper server has no `/matrix` endpoint; "self-hosted" there means a
|
|
115
|
+
server exposing the same API. Valhalla also enforces `max_matrix_distance` (400 km for
|
|
116
|
+
`auto` by default); such requests fail with an engine error.
|
|
117
|
+
|
|
118
|
+
Defaults: concurrency 4 for your own servers, 1 for hosted APIs and demo servers;
|
|
119
|
+
hosted openrouteservice is spaced 1.6 s per request (free plan: 40 matrix requests/min),
|
|
120
|
+
hosted GraphHopper 0.5 s.
|
|
121
|
+
|
|
122
|
+
## Run an engine locally (Docker)
|
|
123
|
+
|
|
124
|
+
OSRM (example extract: Monaco):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
wget https://download.geofabrik.de/europe/monaco-latest.osm.pbf
|
|
128
|
+
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-extract -p /opt/car.lua /data/monaco-latest.osm.pbf
|
|
129
|
+
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-partition /data/monaco-latest.osrm
|
|
130
|
+
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-customize /data/monaco-latest.osrm
|
|
131
|
+
docker run -t -p 5000:5000 -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-routed --algorithm mld --max-table-size 1000 /data/monaco-latest.osrm
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Valhalla:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
mkdir -p custom_files
|
|
138
|
+
wget -O custom_files/monaco-latest.osm.pbf https://download.geofabrik.de/europe/monaco-latest.osm.pbf
|
|
139
|
+
docker run -dt --name valhalla -p 8002:8002 -v "${PWD}/custom_files:/custom_files" ghcr.io/valhalla/valhalla-scripted:latest
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Then use `--url http://localhost:5000` (OSRM) or `--url http://localhost:8002` (Valhalla).
|
|
143
|
+
|
|
144
|
+
## Responsible use
|
|
145
|
+
|
|
146
|
+
- **No public server by default.** OSRM and Valhalla always need an explicit `url`.
|
|
147
|
+
The public demo servers (`router.project-osrm.org`, `valhalla1.openstreetmap.de`) are
|
|
148
|
+
run by volunteers for testing; routematrix accepts them only for tiny jobs
|
|
149
|
+
(at most 10 points and 25 pairs), forces one request at a time with a 1 s gap, and
|
|
150
|
+
warns. Anything larger raises `DemoServerLimitError`: run your own server.
|
|
151
|
+
- **Hosted APIs** (openrouteservice, GraphHopper) have quotas and terms of use. Keys are
|
|
152
|
+
read from environment variables, never written to the cache, and redacted from
|
|
153
|
+
error messages.
|
|
154
|
+
- Every request carries an identifying `User-Agent`; set your own contact via
|
|
155
|
+
`RouteMatrix(..., user_agent="myproject/1.0 (+mailto:me@example.org)")`.
|
|
156
|
+
- Respect the OpenStreetMap ODbL: credit "© OpenStreetMap contributors" when you publish
|
|
157
|
+
results derived from OSM-based routing.
|
|
158
|
+
- The `estimate` engine is not routing. Results carry `is_estimate=True`, a warning is
|
|
159
|
+
emitted, and the CLI prints `ESTIMATE`. Use it for sanity checks or as a clearly
|
|
160
|
+
labelled fallback only.
|
|
161
|
+
- Check snapping: points snapped hundreds of metres away (parks, water, private
|
|
162
|
+
ground, outside the extract) produce misleading times. Use `snap_flags()`.
|
|
163
|
+
|
|
164
|
+
## Caching and resume
|
|
165
|
+
|
|
166
|
+
The cache key is `(engine + server URL, profile, origin, destination)` with
|
|
167
|
+
coordinates rounded to 1e-6 degrees. Unreachable pairs are cached as known-unreachable.
|
|
168
|
+
Rerunning a job after a crash, a `Ctrl+C` or with `on_error="nan"` requests only the
|
|
169
|
+
origin rows that still have gaps. Cached data does not expire: delete the file (or use
|
|
170
|
+
`MatrixCache(path).clear()`) after updating the road network.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install -e ".[dev]"
|
|
176
|
+
pytest # uses local mock servers only, never the network
|
|
177
|
+
ruff check . && ruff format --check src tests && mypy src
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT © Yusuf Eminoğlu
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "routematrix"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Yusuf Eminoğlu", email = "yusufeminoglu@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
description = "Large origin-destination travel-time and distance matrices from OSRM, Valhalla, GraphHopper and openrouteservice, with tiling, caching, resume and accessibility tools."
|
|
12
|
+
keywords = [
|
|
13
|
+
"routing",
|
|
14
|
+
"travel-time",
|
|
15
|
+
"od-matrix",
|
|
16
|
+
"distance-matrix",
|
|
17
|
+
"accessibility",
|
|
18
|
+
"osrm",
|
|
19
|
+
"valhalla",
|
|
20
|
+
"graphhopper",
|
|
21
|
+
"openrouteservice",
|
|
22
|
+
"isochrone",
|
|
23
|
+
"logistics",
|
|
24
|
+
"transport-planning",
|
|
25
|
+
"gis",
|
|
26
|
+
]
|
|
27
|
+
readme = "README.md"
|
|
28
|
+
license = "MIT"
|
|
29
|
+
license-files = ["LICENSE"]
|
|
30
|
+
requires-python = ">=3.9"
|
|
31
|
+
classifiers = [
|
|
32
|
+
"Development Status :: 4 - Beta",
|
|
33
|
+
"Programming Language :: Python :: 3",
|
|
34
|
+
"Programming Language :: Python :: 3.9",
|
|
35
|
+
"Programming Language :: Python :: 3.10",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Programming Language :: Python :: 3.13",
|
|
39
|
+
"Operating System :: OS Independent",
|
|
40
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
41
|
+
"Intended Audience :: Science/Research",
|
|
42
|
+
"Intended Audience :: Developers",
|
|
43
|
+
"Typing :: Typed",
|
|
44
|
+
]
|
|
45
|
+
dependencies = [
|
|
46
|
+
"numpy>=1.20.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
pandas = [
|
|
51
|
+
"pandas>=1.3.0",
|
|
52
|
+
]
|
|
53
|
+
geo = [
|
|
54
|
+
"pandas>=1.3.0",
|
|
55
|
+
"geopandas>=0.12.0",
|
|
56
|
+
]
|
|
57
|
+
dev = [
|
|
58
|
+
"pytest>=7.0.0",
|
|
59
|
+
"pytest-cov>=4.0.0",
|
|
60
|
+
"ruff>=0.1.0",
|
|
61
|
+
"mypy>=1.0.0",
|
|
62
|
+
"pandas>=1.3.0",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[project.scripts]
|
|
66
|
+
routematrix = "routematrix.cli:main"
|
|
67
|
+
|
|
68
|
+
[project.urls]
|
|
69
|
+
Homepage = "https://github.com/YusufEminoglu/routematrix_sdk"
|
|
70
|
+
Repository = "https://github.com/YusufEminoglu/routematrix_sdk"
|
|
71
|
+
Issues = "https://github.com/YusufEminoglu/routematrix_sdk/issues"
|
|
72
|
+
Changelog = "https://github.com/YusufEminoglu/routematrix_sdk/blob/main/CHANGELOG.md"
|
|
73
|
+
|
|
74
|
+
[tool.setuptools]
|
|
75
|
+
package-dir = {"" = "src"}
|
|
76
|
+
|
|
77
|
+
[tool.setuptools.packages.find]
|
|
78
|
+
where = ["src"]
|
|
79
|
+
|
|
80
|
+
[tool.setuptools.package-data]
|
|
81
|
+
routematrix = ["py.typed"]
|
|
82
|
+
|
|
83
|
+
[tool.ruff]
|
|
84
|
+
line-length = 100
|
|
85
|
+
target-version = "py39"
|
|
86
|
+
exclude = [
|
|
87
|
+
"scratch",
|
|
88
|
+
".venv",
|
|
89
|
+
"build",
|
|
90
|
+
"dist",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
[tool.ruff.lint]
|
|
94
|
+
select = ["E", "F", "W", "I", "B", "C4"]
|
|
95
|
+
ignore = ["E501"]
|
|
96
|
+
|
|
97
|
+
[tool.mypy]
|
|
98
|
+
files = ["src"]
|
|
99
|
+
|
|
100
|
+
[[tool.mypy.overrides]]
|
|
101
|
+
module = [
|
|
102
|
+
"pandas.*",
|
|
103
|
+
"geopandas.*",
|
|
104
|
+
]
|
|
105
|
+
ignore_missing_imports = true
|
|
106
|
+
|
|
107
|
+
[tool.pytest.ini_options]
|
|
108
|
+
testpaths = ["tests"]
|
|
109
|
+
# Lets `pytest` run straight from a clone, without an editable install first.
|
|
110
|
+
pythonpath = ["src"]
|
|
111
|
+
filterwarnings = [
|
|
112
|
+
"ignore::DeprecationWarning",
|
|
113
|
+
"ignore::PendingDeprecationWarning",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[tool.coverage.run]
|
|
117
|
+
source = ["routematrix"]
|
|
118
|
+
branch = true
|
|
119
|
+
|
|
120
|
+
[tool.coverage.report]
|
|
121
|
+
show_missing = true
|
|
122
|
+
fail_under = 85
|