Velocipede is a Python-based implementation of a bike route choice model adapted from SANDAG's ABM. The bike route choice model is a multinomial logit discrete choice model that estimates the probabilities of an individual's choosing among multiple alternative routes between a given origin and destination. This discrete choice model forms the basis of both the estimation of the level of service between OD pairs used by the demand models and the estimation of the number of cyclists assigned to network links. The level of service is the expected maximum utility, or logsum, from the model, and network link assignments are made with individual route probabilities.
This project uses UV as its package manager. To install UV, follow the instructions at https://docs.astral.sh/uv/getting-started/installation/ or run:
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | shOnce UV is installed, create the virtual environment and install dependencies:
uv sync --lockedThe bike model is launched from bike_route_choice.py and accepts a single optional command-line argument specifying the path to a settings YAML file. The default value is bike_route_choice_settings.yaml.
# Using the default settings file
uv run python bike_route_choice.py
# Specifying a settings file
uv run python bike_route_choice.py spec/or_metro/bike_route_choice_settings_taz.yamlExample settings files are provided in spec/ for both the Oregon Metro (spec/or_metro/) and SANDAG (spec/sandag/) networks.
A settings YAML file defines all model inputs, parameters, and outputs. Below is a minimal example:
# Network inputs
node_file: nodes.csv
link_file: links.csv
traversal_file: turns.csv
data_dir: path/to/network/data
# Utility specifications (relative to spec/ directory)
edge_util_file: or_metro/bike_edge_utils.csv
traversal_util_file: or_metro/bike_traversal_utils.csv
# Zone configuration
zone_level: taz # "taz" or "maz"
max_dijkstra_utility: 28 # threshold utility for early termination
# Path sampling
number_of_iterations: 5 # number of Dijkstra iterations per OD pair
# Runtime
number_of_batches: 10
number_of_processors: 3
# Output
output_path: output/bike/taz
bike_speed: 3.5Network
data_dir: path to the directory containing network input filesnode_file: input file containing bike network nodes (shapefile or CSV with WKT geometry)link_file: input file containing bike network links (shapefile or CSV with WKT geometry)traversal_file: input file containing turn/traversal records (optional ifgenerate_traversals: True)connector_file: input file containing centroid connector links (optional ifread_centroid_connectors: True)zone_level: zone level on which path choice should be performed. Allowed values:taz,mazzone_subset: subset of zones to use for model testing (integer count or list of zone IDs)bidirectionalize_links: whether to create reverse-direction copies of all linksgenerate_traversals: whether to derive traversal records from the link topologytrim_dead_ends: whether to remove dead-end links from the network
Utilities
edge_util_file: file containing the utility specification for link coststraversal_util_file: file containing the utility specification for traversal costspath_util_file: file containing the utility specification for path-level costs (optional)path_attributor_file: Python module with functions to calculate path attributes (optional)random_scale_coef: scaling coefficient for randomized utility coefficients (default: 0.1)random_scale_link: scaling coefficient for randomized edge cost multiplier (default: 0.2)
Model Hyperparameters
number_of_iterations: number of Dijkstra iterations (paths sampled) per OD pairnumber_of_batches: number of batches into which origin centroids are divided for sequential processingnumber_of_processors: number of processors to use for parallel processing within each batchmax_dijkstra_utility: cutoff threshold utility (positive) for early termination of shortest-path searchmin_iterations: minimum number of paths required; OD pairs with fewer paths are discarded
Demand Assignment
demand_matrix_file: path to an OpenMatrix (.omx) file containing OD demanddemand_segments: dictionary of demand segment names to matrix mappingsassign_demand: whether to assign demand to paths based on calculated utilitiesdemand_assignment_method:"proportional"or"monte_carlo"
Output and Caching
output_path: directory in which model outputs are writtenlogsum_format: output format for logsums ("csv"or"omx")save_bike_net: whether to write the derived network to CSV cacheread_cached_bike_net: whether to read a cached network instead of re-derivinguse_cached_paths: whether to skip Dijkstra and load pre-computed paths from a previous runbike_speed: speed (in distance units per hour) used to calculate bike travel times
Tracing
trace_bike_utilities: whether to trace edge and traversal utility calculationstrace_origins: list of origin zone IDs whose paths will be tracedtrace_destinations: list of destination zone IDs corresponding to the traced originsgenerate_shapefile: whether to output traced paths as a shapefilecrs: coordinate reference system for output shapefiles (e.g.,epsg:2913)
The variable specifications used in the utility function for edges and traversals
are stored in CSV files (e.g., bike_edge_utils.csv, bike_traversal_utils.csv,
and optionally bike_path_utils.csv), including the expressions and their associated
coefficients. Utilities can be specified at three levels:
- Edge utilities: additive costs evaluated per-link (distance, facility type, traffic exposure)
- Traversal utilities: costs associated with transitions between links (turns, signal delay, unsignalized crossings)
- Path utilities (optional): non-additive costs evaluated over the full path (share of distance on facility types, slope, log-distance)
To account for the correlation in the random utilities of overlapping routes, the utility function in the model includes a "path size" measure, described in the following section.
The size of path alternative
where
Given an origin, paths are sampled to all relevant destinations in the network by repeatedly applying Dijkstra's algorithm to search for the paths that minimize a stochastic link generalized cost with a mean given by the additive inverse of the path utility. For each path sampling iteration, a random coefficient vector is first sampled from a non-negative multivariate uniform distribution with zero covariance and mean equal to the link generalized cost coefficients corresponding to the path choice utility function. As the path search extends outward from the origin, the random cost coefficients do not vary over links, but only over iterations of path sampling. In the path search, the cost of each subsequent link is calculated by summing the product of the random coefficients with their respective link attributes, and then multiplying the result by a non-negative discrete random edge cost multiplier. Path sampling is repeated until both a minimum count of paths and a preset target for the total of all path sizes in each alternative list is reached. If the total path size does not reach its target after a given maximum number of sampling iterations, sampling terminates to prevent excessively long computation time.
When a demand matrix is provided (via demand_matrix_file and demand_segments), the model
can assign OD demand to network links using one of two methods:
- Proportional: demand is distributed across paths in proportion to their multinomial logit probabilities.
- Monte Carlo: for each OD pair, individual trips are randomly assigned to paths by drawing from the multinomial probability distribution.
Edge-level demand is written to CSV files in the output directory (one per demand segment).
To improve runtime, the Dijkstra's algorithm implementation allows for early termination once a predetermined utility threshold has been reached in its search, with no paths found for zone pairs whose utility is beyond the threshold. However, the utility of paths is not necessarily the most user-friendly metric — it is reasonable to aim to define the cutoff by distance rather than utility. Because the underlying implementation of Dijkstra's algorithm is only aware of utilities, not distances, this is not directly feasible, as utility and distance do not correspond directly.
To address this, the bike_threshold_calculator.py script provides a mechanism
to search for a utility threshold which approximates a desired distance cutoff. Given a valid
configuration YAML file, the script iteratively performs a binary search,
using the YAML file's max_dijkstra_utility setting as a starting threshold utility and modifying
its value on subsequent iterations until the target distance is within the allowed margin of error
(or the maximum number of bike model iterations has been completed).
Usage:
uv run python bike_threshold_calculator.py <settings filepath> <target distance> [target_margin [percentile [max_iterations]]]
parameters:
settings filepath: path to YAML file containing bike model settings
target distance: the distance for which the search should aim (in miles)
target margin: the margin of error (< 1) allowed before termination (optional, default: 0.1)
percentile: the percentile of distance to compare against the target (optional, default: 0.99)
max iterations: the most bike model iterations that can be performed in the search (optional, default: 20)
examples:
python bike_threshold_calculator.py bike_route_choice_settings_taz.yaml 20
# the resulting 99th %ile distance must be w/in 10% of the 20-mile target distance
# equivalent to:
python bike_threshold_calculator.py bike_route_choice_settings_taz.yaml 20 0.1 0.99 20
python bike_threshold_calculator.py bike_route_choice_settings_maz.yaml 3 0.05
# the resulting 99th %ile distance must be w/in 5% of the three-mile target distance
With the default parameters, the script will search until either 20 iterations have elapsed or
the 99th percentile of distances found is within 10% of the specified target distance. At termination,
a CSV named threshold_results.csv will be written to the output directory with the input utility
thresholds, the iteration runtime, and the chosen percentile of distance.
