Python package · v0.1.0

Arteries

Calculate lowest-cost distances between the world's cities (population > 100,000) over a real infrastructure network, with land kilometers, sea kilometers, and port calls priced to your desired specifications.

2,075 cities1,804 ports236 road networks9,690-node sea graphBlaize Giangiulio

Why

Modeling distance is important. Economists tend to rely on great-circle (as-the-crow-flies) distances because of their numerical simplicity. The caveat is that a majority of goods do not travel in clean arcs to their destinations. They move by road to a port, by sea along shipping lanes and through chokepoints, and by road again. Arteries computes that route for every pair of cities with at least 100,000 people.

You choose what a road kilometer, a sea kilometer, and a port call cost. The package efficiently returns the cheapest route's generalized cost, together with the land and sea kilometers it uses and its number of sea legs.

Results are exact: they equal Dijkstra's algorithm on the full global multimodal graph for any choice of costs. They are also very fast. Routing all 2,075 × 2,075 city pairs takes about 20 seconds on a laptop, a major improvement over previous options.

Two maps of New Jersey. Left: the OpenStreetMap main-road network with 220,868 nodes and 223,458 edges. Right: the same network reduced to 3,234 junction nodes and 5,574 edges, drawn as straight edges between orange junction points.
New Jersey's main roads before and after simplification. The network shrinks from 220,868 nodes to 3,234 junctions (−98.5 %) while every distance between the remaining points is unchanged.

Install

pip install "arteries[fast]"     # includes numba; strongly recommended
pip install arteries             # numpy, scipy, pandas only

The network data (about 21 MB) downloads on first use to ~/.cache/arteries. Set the ARTERIES_DATA environment variable to keep it somewhere else.

Quickstart

import arteries

net = arteries.load()

# Road distances only (km; NaN where no road connects the cities)
net.land_km(["FRA:Paris", "DEU:Berlin"], ["POL:Warszawa (Warsaw)"])

# Cheapest road + sea routes between all cities
res = net.route_costs(land_cost=5.0, sea_cost=1.0, port_cost=1000.0)
df = res.to_frame()
# origin, destination, cost, land_km, sea_km, total_km, sea_legs

# Red Sea closed
closed = net.route_costs(land_cost=5.0, sea_cost=1.0, port_cost=1000.0,
                         closed_regions=["red_sea"])

Cities are identified as ISO3:City (for example CHN:Shanghai). The list is in net.cities, and plain city names are accepted too.

Cost model

A route alternates road legs and sea legs and changes mode only at ports. Its generalized cost is

c = α · kmroad + β · kmsea + τ · (port calls)

Here α is land_cost, β is sea_cost, and τ is port_cost. Each sea leg makes two port calls, one to load and one to unload. Only the ratios between the three parameters affect which route wins, so the sea cost is conveniently set to 1.

The cheapest route between cities i and j is computed as

c*(i,j) = min{ αLij , minp,q [ αLip + Dpq + αLqj ] }
  • L is the precomputed road-only shortest distance between any two cities or ports.
  • S is the shortest sea distance between ports, after removing any closed passages or regions.
  • D is the cheapest port-to-port cost when a route may chain several legs. Each leg is either a road leg costing αLpq or a sea leg costing 2τ + βSpq, and D is computed with Floyd–Warshall over all ports.

Every road leg runs between two cities or ports and every sea leg between two ports, so this formula is exactly the shortest path in the full network. The methods document gives the proof.

Choosing costs

Results depend on the relative prices, especially when a route is disrupted. If road is cheap relative to sea, or port calls are cheap, the model will happily truck goods across continents or around a closed strait. For example, with low costs a Red Sea closure is answered by a Persian Gulf land bridge instead of the Cape of Good Hope. The table shows the route chosen from Shanghai to Rotterdam.

Road cost αPort cost τOpen networkRed Sea closed
2150–5001,225 road + 15,888 sea km, 1 leg2,757 road + 13,862 sea km, 2 legs land bridge
21,0001,225 road + 15,888 sea km, 1 leg11,019 road km all overland
22,00011,019 road km all overland11,019 road km all overland
5–10150–2,00051 road + 19,567 sea km via Suez sea51 road + 25,003 sea km via the Cape sea

Use a road cost of at least about 5 times the sea cost and a port cost in the hundreds to thousands of sea-km equivalents. Report results over a range of costs: the choice between land bridges and long sea detours is exactly what these parameters decide.

Scenarios

Close a whole sea region

Closing a region removes every shipping lane in it, and with them the sea connections of every port inside it. Named regions are red_sea, persian_gulf, black_sea, and baltic_sea. Any box given as (lat_min, lon_min, lat_max, lon_max) also works.

net.route_costs(5, 1, 1000, closed_regions=["red_sea"])
net.route_costs(5, 1, 1000, closed_regions=[(12.45, 32.0, 30.5, 43.6)])   # same box

Close a chokepoint

closed_passages removes only the strait itself. The available passages are suez, babalmandab, panama, malacca, ormuz, gibraltar, bosporus, dardanelles, sunda, bering, chili, south_africa, and northwest. As in searoute, northwest (the Arctic Northwest Passage) is closed by default.

Ports on either side of a closed strait stay open, so ships can still call inside the Red Sea and bridge the straits by road, for example Aden to Mokha and Eilat to Ashkelon. To model a blockade of the Red Sea, close the region.

River and lake ports

The shipping-lane network has no rivers, so inland ports attach to the nearest lane by a straight connector. That connector is 1,079 km long for Manaus and 752 km for Iquitos. max_port_lane_km=200 turns ports farther than that from a lane into road-only terminals, which is useful as a robustness check.

Reweight individual sea lanes

sea_edge_multiplier takes one factor per sea lane (net.sea.km.size of them). It scales each lane's length, which can model risk premia, ice, or congestion.

API reference

arteries.load(path=None)

Returns a Network. Without a path it downloads the release data once and caches it.

Network.route_costs(...) → RouteResult

land_cost
Cost per road km, α. default 1.0
sea_cost
Cost per sea km, β. default 1.0
port_cost
Cost per port call, τ; each sea leg makes two calls. default 0.0
origins, destinations
Lists of city ids or names. default: all cities
closed_regions
Region names or boxes whose lanes and ports are removed. default none
closed_passages
Straits to remove. default ("northwest",)
max_port_lane_km
Ports farther than this from a lane become road-only. default None
sea_edge_multiplier
Factor per sea lane. default None

RouteResult

cost
Generalized cost matrix (origins × destinations). inf means unreachable.
land_km, sea_km
Kilometers by mode on the cheapest route.
total_km
land_km + sea_km.
sea_legs
Number of sea legs on the cheapest route.
to_frame()
Long-format DataFrame with one row per city pair, ready to merge into a gravity dataset.

Other methods

Network.land_km(o, d)
Road-only distances in km, as a DataFrame.
Network.sea_matrix(...)
Port × port sea distances in km for a scenario.
Network.cities, .ports
Tables of cities and ports, with coordinates, population, how far each was snapped to a road, and its port connection.

Data and coverage

LayerSourceIn the package
roadsOpenStreetMap main road classes, 236 country extracts75.8 M nodes and 5.36 M km of road, simplified to 1.04 M junctions
seaMARNET shipping lanes (via searoute-py)9,690 nodes, 15,947 lanes, 13 named passages
portsIMF PortWatch1,804 ports; 181 offshore oil terminals excluded
citiesUN World Urbanization Prospects and Brinkhoff2,075 cities with at least 100,000 people (latest year ≤ 2026)

Every city attaches to its nearest main road within 25 km, every port within 80 km. The median attachment is 0.3 km for cities and 1.2 km for ports. Six cities have no road within 25 km and are joined to the network by straight links instead: Niamey (132 km), Zinder (109 km), Asmara (65 km), two towns in western Uganda (about 48 km each), and Honiara (3 km).

How it is built

The release is produced by a build pipeline that ships with the package, run as Slurm jobs on Drexel's Picotte cluster.

  1. Ingest. Each country's road segments are read into arrays. Where two roads cross with no shared node (on- and off-ramps are absent from the extracts), a junction is inserted. Segment lengths are measured on the WGS84 ellipsoid.
  2. Merge and repair. Countries join wherever their files share the same coordinates. Gaps of up to 2 km in the data are closed: 5,929 links, 0.17 km on average.
  3. Attach cities and ports to their nearest road.
  4. Simplify exactly. Chains of pass-through nodes are merged into one edge with the summed length, and dead-end spurs are removed. Distances between the remaining points do not change.
  5. Assemble. Cities without a road connection are linked to the network, and ports are attached to the nearest shipping lane.
  6. Road distance matrix. One Dijkstra search per city and port gives road distances between all 3,879 of them.
  7. Package and validate (next section).

Every step, parameter, and proof is in the methods document.

Validation

Each build is checked against independent recomputations. The build fails if any check fails.

CheckResult
Simplified vs. unsimplified road distances (Dijkstra)max rel. diff 5.9 × 10−8
route_costs vs. Dijkstra on the full road + sea graph (3 cost settings)max rel. diff 4.6 × 10−8
Triangle inequality, 200,000 random triples0 violations
Road distance ÷ straight-line distance (5th / 50th / 95th percentile)1.18 / 1.37 / 2.35

Example road distances: Paris–Berlin 1,046 km, Madrid–Warsaw 2,817 km, Beijing–Shanghai 1,176 km, Delhi–Mumbai 1,356 km, Sydney–Melbourne 857 km, Johannesburg–Cape Town 1,395 km.

Limitations

  • A few countries and territories are missing from the road data, including Niger, Eritrea, Nauru, Tuvalu, Puerto Rico, Hong Kong, Macau, and the French overseas departments. Cities there use the straight links listed above.
  • On- and off-ramps are absent from the extracts, so junctions are inserted wherever roads cross. Where a road passes over another without an interchange, this can shorten a route slightly.
  • Costs are per kilometer. Road class, speed, tolls, and border delays are not modeled.
  • Ferries are represented as sea legs between ports, and rivers are not in the sea network (see max_port_lane_km).

Citation and licenses

Giangiulio, B. (2026). Arteries: exact multimodal transport distances
between world cities. Python package, version 0.1.0.
  • Code: MIT.
  • Road data: © OpenStreetMap contributors, ODbL 1.0. The distance data is a derivative database and is shared under the ODbL.
  • Shipping lanes: searoute-py (Apache-2.0), derived from Eurostat SeaRoute and ORNL's global shipping lanes.
  • Ports: IMF PortWatch.
  • Cities: UN World Urbanization Prospects and citypopulation.de (Brinkhoff).