Metadata-Version: 2.4
Name: routematrix
Version: 0.1.0
Summary: Large origin-destination travel-time and distance matrices from OSRM, Valhalla, GraphHopper and openrouteservice, with tiling, caching, resume and accessibility tools.
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/YusufEminoglu/routematrix_sdk
Project-URL: Repository, https://github.com/YusufEminoglu/routematrix_sdk
Project-URL: Issues, https://github.com/YusufEminoglu/routematrix_sdk/issues
Project-URL: Changelog, https://github.com/YusufEminoglu/routematrix_sdk/blob/main/CHANGELOG.md
Keywords: routing,travel-time,od-matrix,distance-matrix,accessibility,osrm,valhalla,graphhopper,openrouteservice,isochrone,logistics,transport-planning,gis
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.20.0
Provides-Extra: pandas
Requires-Dist: pandas>=1.3.0; extra == "pandas"
Provides-Extra: geo
Requires-Dist: pandas>=1.3.0; extra == "geo"
Requires-Dist: geopandas>=0.12.0; extra == "geo"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: pandas>=1.3.0; extra == "dev"
Dynamic: license-file

# routematrix

Large origin–destination (OD) travel-time and distance matrices from open routing
engines, for accessibility, logistics and planning analysis.

- One client for **OSRM**, **Valhalla**, **GraphHopper** and **openrouteservice**, plus a
  clearly labelled great-circle **estimate** fallback.
- **Automatic tiling** of N × M jobs within each engine's request limits, stitched back
  into dense numpy arrays: `durations` (s) and `distances` (m), `NaN` = unreachable.
- **Polite parallelism**: bounded concurrency, rate limiting for hosted APIs, retries
  with exponential backoff (honours `Retry-After`), timeouts.
- **Persistent SQLite cache and resume**: every finished tile is stored at once; rerun
  an interrupted job and only the missing pairs are requested.
- **Snapping diagnostics**: distance between each input point and where the engine
  snapped it, with flags for points that are probably off the network.
- **Analysis**: nearest-k destinations, cumulative opportunities, gravity accessibility,
  symmetry checks and engine-to-engine comparison.
- Dependencies: the standard library (`urllib`, `sqlite3`) and numpy. pandas and
  geopandas are optional extras.

```bash
pip install routematrix            # core
pip install "routematrix[pandas]"  # DataFrame export / input
pip install "routematrix[geo]"     # GeoDataFrame input, isochrones as GeoDataFrame
```

## Quick start

```python
from routematrix import RouteMatrix, cumulative_opportunities

with RouteMatrix("osrm", url="http://localhost:5000", profile="driving",
                 cache="od_cache.sqlite", progress=True) as rm:
    res = rm.compute("homes.csv", "clinics.csv")   # CSV, (N, 2) lon/lat array,
                                                   # DataFrame or GeoDataFrame
res.durations        # (N, M) float seconds, NaN = unreachable
res.distances        # (N, M) float metres
res.origin_snap_m    # distance from each origin to its snapped location
res.snap_flags(500)  # (origin_flags, destination_flags) for snaps > 500 m
res.to_dataframe()   # long format: origin_id, destination_id, duration_s, distance_m, ...
res.to_csv("matrix.csv")              # long CSV
res.to_csv("wide.csv", wide=True)     # one row per origin

# Clinics reachable within 15 minutes, per home
counts = cumulative_opportunities(res, threshold=15 * 60)
```

Coordinates are always **longitude, latitude** (x, y) in EPSG:4326, like GeoJSON. CSV
columns are detected from common names (`lon`/`lng`/`longitude`/`x`,
`lat`/`latitude`/`y`, and `id`/`name` for labels).

### Other engines

```python
RouteMatrix("valhalla", url="http://localhost:8002", profile="walking")   # -> pedestrian
RouteMatrix("ors", url="http://localhost:8080/ors", profile="cycling")    # -> cycling-regular
RouteMatrix("ors", profile="driving")          # hosted API, key from ORS_API_KEY
RouteMatrix("graphhopper", profile="car")      # hosted API, key from GRAPHHOPPER_API_KEY
RouteMatrix("estimate", profile="walking", detour_factor=1.3)   # NOT routed: an estimate
```

`driving` / `walking` / `cycling` are mapped to each engine's own profile names; any
other value is passed through unchanged (e.g. `truck`, `foot-hiking`, `motor_scooter`).

### Valhalla isochrones

```python
from routematrix import isochrones_to_geodataframe

rm = RouteMatrix("valhalla", url="http://localhost:8002", profile="pedestrian")
fc = rm.isochrone((28.97, 41.01), minutes=[5, 10, 15])   # GeoJSON FeatureCollection
gdf = isochrones_to_geodataframe(fc)                     # needs the geo extra
```

### Accessibility and diagnostics

```python
from routematrix import (nearest_k, cumulative_opportunities, gravity_accessibility,
                         asymmetry, compare_engines)

idx, secs = nearest_k(res, k=3)                       # 3 closest destinations per origin
jobs_30 = cumulative_opportunities(res, 1800, opportunities=jobs)
hansen = gravity_accessibility(res, jobs, function="exponential", beta=0.1, scale=60)
asymmetry(square_result)            # one-way streets vs. data problems
compare_engines(origins, dests, RouteMatrix("osrm", url=A), RouteMatrix("valhalla", url=B))
```

## Command line

```bash
routematrix compute origins.csv destinations.csv --engine osrm \
    --url http://localhost:5000 --profile driving -o matrix.csv --cache od.sqlite
routematrix compute origins.csv --engine estimate --profile walking --wide -o walk.csv
routematrix nearest homes.csv clinics.csv --engine valhalla --url http://localhost:8002 \
    --profile auto -k 3 -o nearest.csv
routematrix engines            # default tile limits per engine
```

Exit codes: `0` success, `2` error, `3` finished with failed tiles (`--on-error nan`).
Progress and a summary (reachability, cache hits, snap flags) go to stderr.

## Engines

| Engine | Endpoint | Default tile | Snapping info | Units returned | Key |
|---|---|---|---|---|---|
| OSRM | `GET /table/v1/{profile}/…` | 100 × 100 (`osrm-routed --max-table-size` default 100) | yes | s, m | – |
| Valhalla | `POST /sources_to_targets` | 50 × 50 = 2 500 pairs (`max_matrix_location_pairs`) | no | s, km → m | – |
| GraphHopper | `POST /matrix` | 25 × 25 (plan dependent) | no | s, m | `GRAPHHOPPER_API_KEY` (hosted) |
| openrouteservice | `POST /v2/matrix/{profile}` | 50 × 50 = 2 500 routes (`maximum_routes`) | yes | s, m | `ORS_API_KEY` (hosted) |
| estimate | none (local) | 2 000 × 2 000 | no | s, m (estimated) | – |

If your server allows larger requests (e.g. `osrm-routed --max-table-size 1000`), pass
`max_sources=`, `max_destinations=` and `max_elements=` (CLI: `--max-sources` …). The
open-source GraphHopper server has no `/matrix` endpoint; "self-hosted" there means a
server exposing the same API. Valhalla also enforces `max_matrix_distance` (400 km for
`auto` by default); such requests fail with an engine error.

Defaults: concurrency 4 for your own servers, 1 for hosted APIs and demo servers;
hosted openrouteservice is spaced 1.6 s per request (free plan: 40 matrix requests/min),
hosted GraphHopper 0.5 s.

## Run an engine locally (Docker)

OSRM (example extract: Monaco):

```bash
wget https://download.geofabrik.de/europe/monaco-latest.osm.pbf
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-extract -p /opt/car.lua /data/monaco-latest.osm.pbf
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-partition /data/monaco-latest.osrm
docker run -t -v "${PWD}:/data" ghcr.io/project-osrm/osrm-backend osrm-customize /data/monaco-latest.osrm
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
```

Valhalla:

```bash
mkdir -p custom_files
wget -O custom_files/monaco-latest.osm.pbf https://download.geofabrik.de/europe/monaco-latest.osm.pbf
docker run -dt --name valhalla -p 8002:8002 -v "${PWD}/custom_files:/custom_files" ghcr.io/valhalla/valhalla-scripted:latest
```

Then use `--url http://localhost:5000` (OSRM) or `--url http://localhost:8002` (Valhalla).

## Responsible use

- **No public server by default.** OSRM and Valhalla always need an explicit `url`.
  The public demo servers (`router.project-osrm.org`, `valhalla1.openstreetmap.de`) are
  run by volunteers for testing; routematrix accepts them only for tiny jobs
  (at most 10 points and 25 pairs), forces one request at a time with a 1 s gap, and
  warns. Anything larger raises `DemoServerLimitError`: run your own server.
- **Hosted APIs** (openrouteservice, GraphHopper) have quotas and terms of use. Keys are
  read from environment variables, never written to the cache, and redacted from
  error messages.
- Every request carries an identifying `User-Agent`; set your own contact via
  `RouteMatrix(..., user_agent="myproject/1.0 (+mailto:me@example.org)")`.
- Respect the OpenStreetMap ODbL: credit "© OpenStreetMap contributors" when you publish
  results derived from OSM-based routing.
- The `estimate` engine is not routing. Results carry `is_estimate=True`, a warning is
  emitted, and the CLI prints `ESTIMATE`. Use it for sanity checks or as a clearly
  labelled fallback only.
- Check snapping: points snapped hundreds of metres away (parks, water, private
  ground, outside the extract) produce misleading times. Use `snap_flags()`.

## Caching and resume

The cache key is `(engine + server URL, profile, origin, destination)` with
coordinates rounded to 1e-6 degrees. Unreachable pairs are cached as known-unreachable.
Rerunning a job after a crash, a `Ctrl+C` or with `on_error="nan"` requests only the
origin rows that still have gaps. Cached data does not expire: delete the file (or use
`MatrixCache(path).clear()`) after updating the road network.

## Development

```bash
pip install -e ".[dev]"
pytest            # uses local mock servers only, never the network
ruff check . && ruff format --check src tests && mypy src
```

## License

MIT © Yusuf Eminoğlu
