bikelts
Bicycle Level of Traffic Stress (LTS 1-4) for Python. bikelts classifies street
segments and intersections for cycling from OpenStreetMap tags or from your own
tabular attributes, records why each segment got its score and which inputs were
assumed, and analyses the connectivity of the resulting low-stress network.
It is written for active-transportation planners and researchers who need a transparent, reproducible LTS map rather than a black box.
- Three selectable, documented rulesets: Mekuria, Furth & Nixon (2012), Furth (2017) v2.0, and the Bike Ottawa OSM tag rules.
- Directional results (
lts_forward,lts_backward) with contraflow handling. - Per-segment reasons and an assumption log, e.g.
maxspeed missing -> assumed 50 km/h from highway=residential urban default for country DE. - Intersection criteria: right-turn-lane approaches and unsignalized crossings.
- Network analysis in pure Python: LTS shares, low-stress islands, connectivity metrics, Dijkstra with an LTS cap, origin-destination reachability and detour ratios.
- Offline readers for OSM XML, Overpass JSON and GeoJSON; optional Overpass download.
- No required dependencies; optional
geopandasandnetworkxconverters.
Installation
pip install bikelts # core, no dependencies
pip install "bikelts[geo]" # + geopandas/shapely (to_geodataframe)
pip install "bikelts[graph]" # + networkx (to_networkx)
pip install "bikelts[all]"
Python 3.9 or newer.
Quick start
Command line
# Optional: fetch highways for a bounding box (south west north east) from Overpass
bikelts download --bbox 41.000 29.000 41.010 29.015 -o area.json --timeout 60
# Classify (OSM XML .osm, Overpass .json or GeoJSON) and write GeoJSON with LTS + reasons
bikelts classify area.osm -o lts.geojson --method furth2017 --country DE --intersections
# Low-stress islands, LTS shares and OD reachability
bikelts islands lts.geojson --max-lts 2 -o islands.geojson
bikelts stats lts.geojson --max-lts 2
bikelts reach lts.geojson --max-lts 2 --pairs 500 --max-detour 1.25
# Tabular input: CSV in, CSV out (columns listed below)
bikelts classify segments.csv -o segments_lts.csv --method mekuria2012
bikelts methods # list rulesets with their sources
Every analysis command accepts --json for machine-readable output.
Python
from bikelts import LTSParameters, classify_tags
params = LTSParameters(country="DE")
r = classify_tags(
{"highway": "secondary", "maxspeed": "50", "cycleway:right": "lane", "parking:right": "lane"},
method="furth2017",
params=params,
)
r.lts_forward, r.lts_backward # (2, 3)
r.reasons
# ['forward: bike lane alongside parking: 1 lane per direction, reach 12-14 ft (reach 13 ft), 30 mph -> LTS 2',
# 'backward: mixed traffic, 1 thru lane per direction, ADT 3000+, 30 mph -> LTS 3']
r.assumptions
# ['ADT missing -> assumed 9000 veh/day from highway=secondary default',
# 'lanes missing -> assumed 2 from highway=secondary two-way default', ...]
A whole extract and its network:
from bikelts import (LTSNetwork, LTSParameters, classify_osm, connectivity_metrics,
detect_crossings, low_stress_islands, od_reachability,
random_od_pairs, read_osm, to_geojson, write_geojson)
params = LTSParameters(country="DE")
data = read_osm("area.osm") # .osm, Overpass .json or .geojson
ways = classify_osm(data, "furth2017", params)
crossings = detect_crossings(data, ways, params)
write_geojson("lts.geojson", to_geojson(ways, crossings, data))
net = LTSNetwork.from_classified(ways, crossings)
islands = low_stress_islands(net, max_lts=2)
metrics = connectivity_metrics(net, max_lts=2)
results, summary = od_reachability(net, random_od_pairs(net, 500, seed=1), max_lts=2)
summary["low_stress_connected_share"] # share of OD pairs connected at LTS <= 2
Tabular records (dicts or CSV rows) use classify_record / classify_records. See
examples/ for a runnable script, a synthetic OSM extract and a sample CSV.
Rulesets
Select with method= (Python) or --method (CLI).
mekuria2012 |
furth2017 (default) |
bikeottawa |
|
|---|---|---|---|
| Source | Mekuria, Furth & Nixon (2012), MTI Report 11-19 | Furth (2017), LTS Criteria for Road Segments v2.0 | Bike Ottawa stressmodel.js v1.0.1 |
| Mixed traffic | total lanes (2-3 / 4-5 / 6+) x speed (<=25 / 30 / 35+ mph); lower value when no centreline and ADT <= 3000 | thru lanes per direction (unlaned / 1 / 2 / 3+) x ADT x speed (<=20 ... 50+ mph); effective ADT = 1.5 x ADT on one-way roads | maxspeed (km/h), total lanes, highway=residential, service types |
| Bike lane, no parking | weakest link over lanes/median, width (6 ft), speed, blockage | lanes x width (6+ / 4-5 ft) x speed; < 4 ft does not qualify | lanes, explicit width, speed, residential |
| Bike lane with parking | weakest link over lanes, reach (15 / 14 / 13.5 ft), speed, blockage | lanes x reach (15+ / 12-14 ft) x speed (<= 35 mph); reach < 12 ft does not qualify | lanes, explicit width, speed, residential |
| Separated paths, cycle tracks | LTS 1 | LTS 1 | LTS 1 (also highway=footway) |
| Blocked lanes | blockage row (LTS 3) | rated as mixed traffic | not used |
| ADT, centreline | ADT <= 3000 + no centreline footnote | central | not used |
| Direction | per direction | per direction | one score per way |
| Intersections | Table 5 and 6 (below) | Table 5 and 6 (below) | Table 6 if requested |
Table numbers for the 2012 criteria follow Furth's republished LTS Criteria Tables: Table 1 segments by bikeway type, Table 2 bike lanes alongside a parking lane, Table 3 bike lanes not alongside a parking lane, Table 4 mixed traffic, Table 5 intersection approaches with a right-turn lane, Table 6a/6b unsignalized crossings without/with a crossing island. The 2017 v2.0 document has three unnumbered tables (mixed traffic; bike lanes and shoulders not adjacent to a parking lane; bike lanes alongside a parking lane). The test suite checks every cell of all of these tables.
Intersections
- Right-turn lanes (Table 5):
right_turn_config(length_ft, bike_lane_position, turning_speed_mph, rt_lanes, option_lane)returns one ofshort_straight(LTS >= 2),long_straight(>= 3),shift_left(>= 3) orother(4). A table row can carryrt_lane_config; the floor is applied to bike lanes and mixed traffic, not to separated facilities. OSM does not describe these geometries, so this is table input only. - Unsignalized crossings (Table 6):
unsignalized_crossing_lts(lanes, speed_mph, island). In OSM data,detect_crossingsfinds nodes where a dominant street (the hardest to cross, plus ways sharing itsname/ref) passes through and a less major way meets it. Signals (highway=traffic_signals,crossing=traffic_signals,crossing:signals=yes) give LTS 1; islands come fromcrossing:island=yes,crossing=islandortraffic_calming=island. Furth's 2017 update revised segments only, so the 2012 intersection tables are used with every ruleset.
In network analysis, a crossing with LTS above the cap blocks movements between two non-dominant approaches (going straight across), while turning onto the dominant street is governed by that street's own segment LTS.
Inputs, defaults and assumptions
OSM tags understood
| Topic | Tags |
|---|---|
| Access | highway, bicycle (no, use_sidepath, dismount close the way), access, vehicle, motor_vehicle |
| Direction | oneway (yes/-1/reversible), oneway:bicycle, junction=roundabout, cycleway=opposite*, cycleway:<side>:oneway |
| Bikeways | cycleway, cycleway:left/right/both = lane, track, shared_lane, share_busway, opposite_lane, opposite_track, opposite, separate, no; subkeys :lane (advisory, pictogram), :width, :buffer, :separation (kerb, bollard, flex_post... make a lane a protected lane), sidewalk[:side]:bicycle=designated, shoulder[:side] + shoulder:access:bicycle |
| Speed | maxspeed (50, 30 mph, DE:urban, FR:zone30, GB:nsl_single, none, walk), maxspeed:forward/backward, zone:maxspeed, source:maxspeed, bicycle_road/cyclestreet |
| Lanes | lanes, lanes:forward/backward, lanes:bus/psv (excluded), lane_markings, dual_carriageway |
| Parking | new scheme parking:<side>=lane/street_side/on_kerb/half_on_kerb/no/separate, old scheme parking:lane:<side>=parallel/diagonal/perpendicular/no_parking/no_stopping, parking:<side>:width |
| Other | surface (paved / cobbled / unpaved classes), adt/aadt if you add counts |
Sides are mapped to travel directions using the driving side of the country (left-hand
traffic for GB, IE, JP, AU, NZ, IN, ZA and others).
Defaults (all logged when used)
| Missing input | Default |
|---|---|
maxspeed |
national urban/rural/motorway/living-street limit for 35 countries (LTSParameters(country=..., context=...)), generic 50/90/120 km/h otherwise; service/track capped at 30 km/h |
lanes |
by class: trunk 4, primary...residential 2 (one-way: half) |
| centreline | none on residential, living_street, service, track; marked elsewhere |
| ADT | planning-level guess by class: residential 600, unclassified 1500, tertiary 4000, secondary 9000, primary 15000, trunk 25000 veh/day (adt_overrides to change) |
| bike lane width | 1.5 m (about 5 ft); buffer yes = 0.5 m; shoulder 1.2 m |
| parking next to a bike lane | absent (assume_parking=True to flip); parking lane 2.4 m |
| blockage | rare (default_blockage="frequent") |
| speed columns | metric speeds are rounded to the nearest 5 mph before the mph tables (50 km/h -> 30 mph); speed_binning="exact" disables this |
| widths | rounded to the nearest 0.5 ft before the ft tables |
Tabular columns
id, highway, facility (mixed | bike_lane | shoulder | separated | no_motor_traffic), speed_mph or speed_kmh, lanes or lanes_per_direction, oneway, contraflow, centerline, adt, median, residential, blockage (rare | frequent), bike_lane_width_ft|_m, buffer_ft|_m, parking, parking_width_ft|_m, reach_ft|_m, rt_lane_config, permitted, surface. All optional; missing
values are assumed and logged.
Network analysis
lts_share(net)- length and share by LTS (worst permitted direction).low_stress_islands(net, max_lts=2)- components of the network of edges with LTS <= k in at least one direction, broken at crossings whose LTS exceeds k.connectivity_metrics(net, max_lts)- low-stress length and share, number of islands, largest-island shares, concentration index sum(L_i^2)/(sum L_i)^2, alpha/beta/gamma indices.shortest_path,distances_from- Dijkstra with direction, an LTS cap and crossing restrictions (states are node + incoming edge at crossing nodes).od_reachability(net, pairs, max_lts=2, max_detour=1.25)- for each OD pair the unrestricted and the low-stress shortest distance and their ratio; the summary gives the share reachable at low stress and the share connected within the detour allowance (25 % by default, following Mekuria et al. 2012).to_networkx(net, max_lts)andto_geodataframe(ways)with the optional extras.
Limitations
- LTS is only as good as the tags. OSM rarely records ADT, bike lane width, parking, medians
or right-turn lane geometry; check the
assumptionsfield and supply better data (tags,adt_overrides, table input) where it matters. - The speed used is the posted limit, not the prevailing (85th percentile) speed the criteria prefer; pass measured speeds through table input when you have them.
- Divided roads mapped as two one-way ways are counted per carriageway; use
dual_carriageway=yesorLTSParameters(infer_dual_carriageway=True)for the 2012 street width. - Crossing detection is a heuristic (dominant street by Table 6 LTS, lanes and speed); left turns, roundabouts and the draft roundabout criteria are not modelled.
- The
bikeottawaport follows the reference rules but takes speeds and lanes from the bikelts parser (country defaults, units) and uses widths only when tagged; see the module docstring. - Surfaces are reported but not scored unless you opt in with
unpaved_min_lts, which is an extension and not part of any published criteria. - Default speed tables reflect general national limits; regional and local rules differ.
References
- Mekuria, M. C., Furth, P. G., & Nixon, H. (2012). Low-Stress Bicycling and Network Connectivity (MTI Report 11-19). Mineta Transportation Institute, San Jose State University.
- Furth, P. G. LTS Criteria Tables (republication of the 2012 criteria, Tables 1-6), Northeastern University.
- Furth, P. G. (2017). Level of Traffic Stress Criteria for Road Segments, version 2.0, June 2017. Northeastern University.
- Bike Ottawa. stressmodel (version 1.0.1), https://github.com/BikeOttawa/stressmodel
- OpenStreetMap Wiki. Key:cycleway, Key:parking, Default speed limits.
- Kansky, K. J. (1963). Structure of Transportation Networks. University of Chicago.
If you use bikelts in research, please cite the method papers above and this software (see
CITATION.cff).
Development
pip install -e ".[dev]"
pytest --cov=bikelts
ruff check . && ruff format --check src tests && mypy src
License
MIT, see LICENSE. OSM data is (c) OpenStreetMap contributors under the ODbL; the
sample extract in examples/ is synthetic.
Metadata
Release files for bikelts 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| bikelts-0.1.0.tar.gz | 79.9 kB | With provenance | Details for bikelts-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| bikelts-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for bikelts-0.1.0-py3-none-any.whl |
Total release size: 139.5 kB
Release files / bikelts-0.1.0.tar.gz
| Download URL | bikelts-0.1.0.tar.gz |
|---|---|
| Size | 79.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f593d8b6e1d409f1901fbf364c861ec73851c0cf48931abaf6de60ae20352a12
|
|
BLAKE2b-256 checksum How to use checksums |
a96a1898dd8c57a78428ff01884985e36aff98838c6d290ca52517c52edf5565
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / bikelts-0.1.0-py3-none-any.whl
| Download URL | bikelts-0.1.0-py3-none-any.whl |
|---|---|
| Size | 59.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a4d688dbed38a646d5e2d79e8682801cee4a6bb0c8d174b3555397a09155219c
|
|
BLAKE2b-256 checksum How to use checksums |
4def247477eeda99e7ae769cd2c81a826478f648ba67d963a6b24d29d55bab71
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for bikelts 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for bikelts-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.