| Title: | Transport Modeling: Network Processing, Route Enumeration, and Traffic Assignment |
|---|---|
| Description: | High-performance tools for transport modeling - network processing, route enumeration, and traffic assignment in R. The package implements the Path-Sized Logit model for traffic assignment - Ben-Akiva and Bierlaire (1999) <doi:10.1007/978-1-4615-5203-1_2> - an efficient route enumeration algorithm, and provides powerful utility functions for (multimodal) network generation, consolidation/contraction, and/or simplification. The user is expected to provide a transport network (either a graph or collection of linestrings) and an origin-destination (OD) matrix of trade/traffic flows. Maintained by transport consultants at CPCS (cpcs.ca). |
| Authors: | Sebastian Krantz [aut, cre], Kamol Roy [ctb] |
| Maintainer: | Sebastian Krantz <[email protected]> |
| License: | GPL-3 |
| Version: | 0.3.0 |
| Built: | 2026-05-29 16:48:28 UTC |
| Source: | https://github.com/sebkrantz/flownet |
flownet provides efficient tools for transportation modeling in R, supporting network processing, route enumeration, and traffic assignment tasks. It implements the path-sized logit (PSL) model for traffic assignment and provides powerful utilities for network processing/preparation.
Network Processing
linestrings_to_graph() — Convert LINESTRING geometries to graphcreate_undirected_graph() — Convert directed graph to undirectedconsolidate_graph() — Consolidate graph, removing redundant nodes/edgessimplify_network() — Spatially simplify network graph
Traffic Assignment
run_assignment() — Run traffic assignment using path-sized logit model
Graph Utilities
normalize_graph() — Normalize node IDs to consecutive integersnodes_from_graph() — Extract unique nodes from graphlinestrings_from_graph() — Convert graph to LINESTRING geometriesdistances_from_graph() — Compute distance matrix from graphcritical_link_analysis() — Compute edge detour costs and vulnerability ratios
OD Matrix Utilities
melt_od_matrix() — Convert origin-destination matrix to long format
Data
africa_trade — Average BACI HS96 2012-22 trade flows by section between 47 continental African countriesafrica_cities_ports — The 453 largest (port-)cities in continental Africa within a 70km radius - from Krantz (2024), doi:10.1596/1813-9450-10893africa_network — African continental road network + extensions to optimally connect the 453 cities - from Krantz (2024), doi:10.1596/1813-9450-10893africa_segments — Primary segments derived from OpenStreetMap routes between the 453 cities - from Krantz (2024), doi:10.1596/1813-9450-10893
Replication materials: https://github.com/SebKrantz/OptimalAfricanRoads
The package uses efficient C implementations for critical path operations and leverages:
collapse - Fast data transformations
geodist - Fast geodesic distance computations
igraph - Graph operations and shortest path algorithms
leaderCluster - Fast spatial clustering for network simplification
Sebastian Krantz [email protected] and Kamol Roy [email protected]
Ben-Akiva, M., & Bierlaire, M. (1999). Discrete choice methods and their applications to short term travel decisions. In R. W. Hall (Ed.), Handbook of Transportation Science (pp. 5-33). Springer US. doi:10.1007/978-1-4615-5203-1_2
Cascetta, E. (2001). Transportation systems engineering: Theory and methods. Springer.
Ben-Akiva, M., & Lerman, S. R. (1985). Discrete choice analysis: Theory and application to travel demand. The MIT Press.
Ramming, M. S. (2002). Network knowledge and route choice (Doctoral dissertation). Massachusetts Institute of Technology.
Prato, C. G. (2009). Route choice modeling: Past, present and future research directions. Journal of Choice Modelling, 2(1), 65-100. doi:10.1016/S1755-5345(13)70005-8
AequilibiaE Python Documentation: https://www.aequilibrae.com/develop/python/route_choice/path_size_logit.html
A spatial dataset containing 453 major African cities (population > 100,000) and international ports. Cities are deduplicated within 50-100km radii, with populations aggregated from nearby settlements. Port cities include cargo flow data from the World Bank Global Ports dataset.
data(africa_cities_ports)data(africa_cities_ports)
A Simple feature collection (sf object, also inheriting from data.table) with 453 POINT features and 12 fields:
Character. Unique city-country identifier (e.g., "Cairo - Egypt", "Lagos - Nigeria").
Character. City name.
Character. Country name.
Character. ISO 3166-1 alpha-2 country code.
Character. ISO 3166-1 alpha-3 country code.
Character. Administrative region or province name.
Character. Capital status: "" (none), "admin" (administrative), "minor", or "primary" (national capital).
Numeric. City population including nearby settlements within 30km.
Character. UN/LOCODE port identifier (empty string for non-port cities).
Character. Official port name (empty string for non-port cities).
Character. Port status code (empty string for non-port cities).
Numeric. Outflows in TEU in Q1 of 2020 (NA for non-port cities). 51 cities have port outflow data.
POINT. Spatial geometry in WGS 84 (EPSG:4326) coordinate reference system.
The dataset was constructed by:
Selecting cities with population > 50,000 from Simplemaps World Cities database
Weighting by administrative importance (capital status)
Deduplicating within 50-100km radii, keeping largest weighted city
Aggregating populations from settlements within 30km
Matching with World Bank international ports within 30km
The bounding box spans from approximately 34S to 37N latitude and 17W to 49E longitude, covering continental Africa.
City data from Simplemaps World Cities Database (https://simplemaps.com/data/world-cities). Port data from World Bank Global International Ports dataset (https://datacatalog.worldbank.org/search/dataset/0038118/global-international-ports).
Dataset constructed for: Krantz, S. (2024). Optimal Investments in Africa's Road Network. Policy Research Working Paper 10893. World Bank. doi:10.1596/1813-9450-10893. Replication materials: https://github.com/SebKrantz/OptimalAfricanRoads.
africa_network, africa_trade, flownet-package
library(sf) data(africa_cities_ports) head(africa_cities_ports) # View largest cities largest <- africa_cities_ports[order(-africa_cities_ports$population), ] largest[1:10, c("city", "country", "population")] # Filter port cities ports <- africa_cities_ports[!is.na(africa_cities_ports$port_locode), ] nrow(ports) # 51 ports plot(africa_cities_ports["population"])library(sf) data(africa_cities_ports) head(africa_cities_ports) # View largest cities largest <- africa_cities_ports[order(-africa_cities_ports$population), ] largest[1:10, c("city", "country", "population")] # Filter port cities ports <- africa_cities_ports[!is.na(africa_cities_ports$port_locode), ] nrow(ports) # 51 ports plot(africa_cities_ports["population"])
A spatial dataset representing a discretized road transport network connecting major African cities and ports. The network combines existing road infrastructure (2,344 edges) with proposed new links (481 edges) identified through network efficiency analysis. Each edge contains distance, travel time, border crossing costs, terrain characteristics, and road upgrade cost estimates.
data(africa_network)data(africa_network)
A Simple feature collection (sf object) with 2,825 LINESTRING features and 28 fields:
Integer. Origin node index (1 to 1,377).
Integer. Destination node index (2 to 1,379).
Character. Origin country ISO3 code (49 countries).
Character. Destination country ISO3 code (49 countries).
Numeric. Origin node longitude.
Numeric. Origin node latitude.
Numeric. Destination node longitude.
Numeric. Destination node latitude.
Numeric. Spherical (great-circle) distance in meters.
Numeric. Road distance in meters from OSRM routing.
Numeric. Travel duration in minutes from OSRM routing (NA for proposed links).
Numeric. Average speed in km/h (distance/duration) (NA for proposed links).
Numeric. Number of optimal inter-city routes passing through this edge (NA for proposed links).
Numeric. Sum of population gravity weights from routes using this edge (NA for proposed links).
Numeric. Sum of road-distance-weighted gravity from routes (NA for proposed links).
Numeric. Additional distance for border crossing in meters (0 for domestic links).
Numeric. Total distance including border crossing penalty in meters.
Numeric. Additional time for border crossing in minutes.
Numeric. Total travel time including border crossing in minutes.
Numeric. Hypothetical travel time at 100 km/h in minutes.
Numeric. Hypothetical total time at 100 km/h including border penalties.
Numeric. Terrain ruggedness index along the edge.
Numeric. Population within corridor (WorldPop data).
Numeric. Population density per km2 along corridor.
Numeric. Estimated road construction/maintenance cost per km in USD.
Character. Road upgrade category: "Nothing", "Asphalt Mix Resurfacing", "Mixed Works", "Upgrade", or NA.
Numeric. Upgrade cost per km in USD.
Logical. TRUE for proposed new links, FALSE for existing road network edges.
LINESTRING. Spatial geometry in WGS 84 (EPSG:4326) coordinate reference system.
The network was constructed through the following process:
Computing optimal routes between all city pairs within 2,000km using OSRM
Filtering routes using network efficiency criteria (alpha = 45 degrees, EU-grade efficiency)
Intersecting and aggregating overlapping route segments
Contracting the network to reduce complexity while preserving connectivity
Identifying proposed new links that would improve network route efficiency
Adding border crossing costs based on country pairs
Computing terrain, population, and road cost attributes
The gravity and gravity_rd fields measure edge importance based on the population
gravity model: routes between larger, closer cities contribute more weight to edges they traverse.
The bounding box spans continental Africa from approximately 34S to 37N latitude and 17W to 49E longitude.
Road network derived from OpenStreetMap via OSRM routing. Border crossing data from World Bank estimates. Terrain data from SRTM elevation models. Population data from WorldPop.
Dataset constructed for: Krantz, S. (2024). Optimal Investments in Africa's Road Network. Policy Research Working Paper 10893. World Bank. doi:10.1596/1813-9450-10893. Replication materials: https://github.com/SebKrantz/OptimalAfricanRoads.
africa_cities_ports, africa_segments,
africa_trade, flownet-package
library(sf) data(africa_network) head(africa_network) # Existing vs proposed links table(africa_network$add) # Cross-border links cross_border <- africa_network[africa_network$from_ctry != africa_network$to_ctry, ] nrow(cross_border) # Upgrade categories table(africa_network$upgrade_cat, useNA = "ifany") # Plot by gravity plot(africa_network["gravity_rd"]) # Highlight proposed new links plot(africa_network[africa_network$add, "geometry"], col = "red", add = TRUE)library(sf) data(africa_network) head(africa_network) # Existing vs proposed links table(africa_network$add) # Cross-border links cross_border <- africa_network[africa_network$from_ctry != africa_network$to_ctry, ] nrow(cross_border) # Upgrade categories table(africa_network$upgrade_cat, useNA = "ifany") # Plot by gravity plot(africa_network["gravity_rd"]) # Highlight proposed new links plot(africa_network[africa_network$add, "geometry"], col = "red", add = TRUE)
A dataset containing 14,358 raw network segments representing intersected road routes
between African cities. Each segment is defined by start and end coordinates with
aggregate importance metrics. This dataset is provided to demonstrate how package
functions like consolidate_graph() and
simplify_network() can process messy segment data
into clean analytical networks like africa_network.
data(africa_segments)data(africa_segments)
A data frame with 14,358 rows and 7 columns:
Numeric. Start point longitude (range: -17.4 to 49.2).
Numeric. Start point latitude (range: -34.2 to 37.2).
Numeric. End point longitude (range: -17.0 to 49.1).
Numeric. End point latitude (range: -34.2 to 37.2).
Integer. Number of optimal inter-city routes passing through this segment. Range: 1 to 1,615, median: 46.
Numeric. Sum of population gravity weights from routes using this segment. Computed as sum of (pop_origin * pop_destination / spherical_distance_km) / 1e9.
Numeric. Sum of road-distance-weighted gravity from routes. Computed as sum of (pop_origin * pop_destination / road_distance_m) / 1e9.
This dataset represents an intermediate stage in network construction, after routes have been
intersected but before network simplification. The segments have been simplified using
linestrings_from_graph() to retain only start and end coordinates.
The segments can be used to demonstrate the flownet network processing workflow:
Convert segments to an sf LINESTRING object using linestrings_from_graph()
Apply consolidate_graph() to merge nearby nodes
Apply simplify_network() to remove intermediate nodes
The passes field indicates how many optimal city-to-city routes use each segment,
serving as a measure of segment importance in the network. Higher values indicate
segments that are critical for efficient inter-city connectivity.
Derived from OpenStreetMap routing data via OSRM, processed through route intersection and aggregation.
Dataset constructed for: Krantz, S. (2024). Optimal Investments in Africa's Road Network. Policy Research Working Paper 10893. World Bank. doi:10.1596/1813-9450-10893. Replication materials: https://github.com/SebKrantz/OptimalAfricanRoads.
africa_network, consolidate_graph(),
simplify_network(), linestrings_from_graph(),
flownet-package
data(africa_segments) head(africa_segments) # Summary statistics summary(africa_segments[, c("passes", "gravity", "gravity_rd")]) # Segments with highest traffic africa_segments[order(-africa_segments$passes), ][1:10, ] # Convert to sf and plot library(sf) segments_sf <- linestrings_from_graph(africa_segments) plot(segments_sf["passes"])data(africa_segments) head(africa_segments) # Summary statistics summary(africa_segments[, c("passes", "gravity", "gravity_rd")]) # Segments with highest traffic africa_segments[order(-africa_segments$passes), ][1:10, ] # Convert to sf and plot library(sf) segments_sf <- linestrings_from_graph(africa_segments) plot(segments_sf["passes"])
A dataset containing bilateral trade flows between 47 African countries, aggregated by HS (Harmonized System) section. Values represent annual averages over 2012-2022 from the CEPII BACI database (HS96 nomenclature).
data(africa_trade)data(africa_trade)
A data.table with 27,721 rows and 8 columns:
Factor. Exporter (origin) country ISO 3166-1 alpha-3 code (47 countries).
Factor. Importer (destination) country ISO 3166-1 alpha-3 code (47 countries).
Integer. HS section code (1 to 21).
Factor. HS section description (21 categories, e.g., "Live animals and animal products", "Mineral products", "Machinery and mechanical appliances...").
Factor. Comma-separated HS 2-digit codes within the section (e.g., "84, 85" for machinery).
Numeric. Trade value in thousands of USD (current prices).
Numeric. Trade value in thousands of constant 2015 USD.
Numeric. Trade quantity in metric tons.
The dataset provides bilateral trade flows aggregated from HS 6-digit product codes (via HS 2-digit) to 21 HS sections. Trade values and quantities are annual averages computed over the 2012-2022 period.
HS sections cover broad product categories:
Sections 1-5: Animal and vegetable products
Sections 6-7: Chemical and plastic products
Sections 8-14: Raw materials and manufactured goods
Sections 15-16: Base metals and machinery
Sections 17-21: Transport, instruments, and miscellaneous
Note: Some country pairs may have sparse trade relationships. Very small values indicate limited trade below typical reporting thresholds.
CEPII BACI Database (HS96 nomenclature), Version 202401b, released 2024-04-09. Available at https://www.cepii.fr/DATA_DOWNLOAD/baci/doc/baci_webpage.html.
Reference: Gaulier, G. and Zignago, S. (2010). BACI: International Trade Database at the Product-Level. The 1994-2007 Version. CEPII Working Paper, N 2010-23.
africa_cities_ports, africa_network, flownet-package
data(africa_trade) head(africa_trade) # Number of trading pairs length(unique(paste(africa_trade$iso3_o, africa_trade$iso3_d))) # Total trade by section aggregate(value ~ section_name, data = africa_trade, FUN = sum) # Largest bilateral flows africa_trade[order(-africa_trade$value), ][1:10, ] # Trade between specific countries subset(africa_trade, iso3_o == "ZAF" & iso3_d == "NGA")data(africa_trade) head(africa_trade) # Number of trading pairs length(unique(paste(africa_trade$iso3_o, africa_trade$iso3_d))) # Total trade by section aggregate(value ~ section_name, data = africa_trade, FUN = sum) # Largest bilateral flows africa_trade[order(-africa_trade$value), ][1:10, ] # Trade between specific countries subset(africa_trade, iso3_o == "ZAF" & iso3_d == "NGA")
Consolidate a graph by contracting/removing intermediate nodes (nodes that occur exactly twice) and dropping loop, duplicate, and singleton edges (leading to dead ends). This simplifies the network topology while preserving connectivity.
consolidate_graph( graph_df, directed = FALSE, drop.edges = c("loop", "duplicate", "single"), contract = TRUE, by = NULL, keep.nodes = NULL, ..., recursive = "full", verbose = TRUE )consolidate_graph( graph_df, directed = FALSE, drop.edges = c("loop", "duplicate", "single"), contract = TRUE, by = NULL, keep.nodes = NULL, ..., recursive = "full", verbose = TRUE )
graph_df |
A data frame representing a graph with columns:
|
directed |
Logical (default: FALSE). Whether the graph is directed. |
drop.edges |
Character vector (default: |
contract |
Logical (default: TRUE). If TRUE, contracts the graph by removing
intermediate nodes (nodes that occur exactly twice) and merging connecting edges.
If FALSE, only drops edges as specified in |
by |
Link characteristics to preserve/not contract across, passed as a one-sided formula or character vector of column names. Typically this includes attributes like mode, type, or capacity to ensure that only edges with the same characteristics are contracted. |
keep.nodes |
Numeric vector (optional). Node IDs to preserve during consolidation, even if they occur exactly twice. Also used to preserve nodes when dropping singleton edges. |
... |
Arguments passed to |
recursive |
One of |
verbose |
Logical (default: TRUE). Whether to print messages about dropped edges and consolidation progress. |
This function consolidates/simplifies a graph by:
Dropping edges: Optionally removes self-loops, duplicate edges, and edges leading to singleton nodes (nodes that appear only once in the graph)
Contracting nodes: Removes intermediate nodes (nodes that occur exactly twice) by merging the two edges connected through them into a single longer edge
Aggregating attributes: When edges are merged, attributes/columns are aggregated
using collap(). The default aggregation is mean for numeric columns and mode for categorical columns.
Recursive consolidation: If recursive = TRUE, the function continues
consolidating until no more nodes can be dropped or contracted, ensuring complete consolidation
Consolidation is useful for simplifying network topology while preserving connectivity.
For example, if node B connects A->B and B->C, it will be removed and replaced with A->C.
With recursive = TRUE, long chains (A->B->C->D) are fully contracted to A->D in
a single call.
For undirected graphs, the algorithm also handles cases where a node appears twice as either origin or destination (circular cases).
If coordinate columns (FX, FY, TX, TY) are present in the input,
they are preserved and updated based on the consolidated node coordinates from the original graph.
A data frame representing the consolidated graph with:
All columns from graph_df (aggregated if consolidation occurred),
excluding from, to, and optionally edge and FX, FY, TX, TY
(which are re-added if present in original)
from, to, edge - Node/edge IDs (updated after consolidation)
Coordinate columns (FX, FY, TX, TY) if present in original
Attribute "keep.edges" - Indices of original edges that were kept (before aggregation)
Attribute "group.id" - Integer vector aligned with "keep.edges", mapping each kept edge to its row in the result (after aggregation)
"keep.edges" and "group.id" are attached only for
recursive = "none" or "partial". With the default
recursive = "full" they are omitted, because consolidation runs over
several passes and a single-pass mapping back to the original edges would be
ambiguous. Use recursive = "partial" (repeatedly, if needed) when you
need to trace edges through consolidation.
create_undirected_graph simplify_network flownet-package
library(flownet) library(sf) # Convert segments to undirected graph graph <- africa_segments |> linestrings_from_graph() |> linestrings_to_graph() |> create_undirected_graph(FUN = "fsum") # Get nodes to preserve (city/port locations) nodes <- nodes_from_graph(graph, sf = TRUE) nearest_nodes <- nodes$node[st_nearest_feature(africa_cities_ports, nodes)] # Consolidate graph, preserving city nodes graph_cons <- consolidate_graph(graph, keep = nearest_nodes, w = ~ passes) # Consolidated graph has fewer edges c(original = nrow(graph), consolidated = nrow(graph_cons))library(flownet) library(sf) # Convert segments to undirected graph graph <- africa_segments |> linestrings_from_graph() |> linestrings_to_graph() |> create_undirected_graph(FUN = "fsum") # Get nodes to preserve (city/port locations) nodes <- nodes_from_graph(graph, sf = TRUE) nearest_nodes <- nodes$node[st_nearest_feature(africa_cities_ports, nodes)] # Consolidate graph, preserving city nodes graph_cons <- consolidate_graph(graph, keep = nearest_nodes, w = ~ passes) # Consolidated graph has fewer edges c(original = nrow(graph), consolidated = nrow(graph_cons))
Convert a directed graph to an undirected graph by normalizing edges and aggregating duplicate connections.
create_undirected_graph(graph_df, by = NULL, ...)create_undirected_graph(graph_df, by = NULL, ...)
graph_df |
A data frame representing a directed graph including columns:
|
by |
Link characteristics to preserve/not aggregate across, passed as a one-sided formula or character vector of column names. Typically this includes attributes like mode, type, or capacity to ensure that only edges with the same characteristics are aggregated. |
... |
Arguments passed to |
This function converts a directed graph to an undirected graph by:
Normalizing edge directions so that from < to for all edges
Collapsing duplicate edges (same from and to nodes)
For spatial/identifier columns (edge, FX, FY, TX, TY),
taking the first value from duplicates
For aggregation columns, collap() will be applied.
A data frame representing an undirected graph with:
edge - Edge identifier (first value from duplicates)
from - Starting node ID (normalized to be < to)
to - Ending node ID (normalized to be > from)
FX - Starting node X-coordinate (first value from duplicates)
FY - Starting node Y-coordinate (first value from duplicates)
TX - Ending node X-coordinate (first value from duplicates)
TY - Ending node Y-coordinate (first value from duplicates)
Aggregated columns
The result also carries grouping attributes describing how the input (directed) edges map onto the undirected output edges:
Attribute "group.id" - Integer vector (length nrow(graph_df)) mapping each input edge to its row in the result
Attribute "group.starts" - Index of the first input edge of each output group
Attribute "group.sizes" - Number of input edges aggregated into each output edge
library(flownet) # Convert segments to graph and make undirected graph <- africa_segments |> linestrings_from_graph() |> linestrings_to_graph() graph_undir <- create_undirected_graph(graph, FUN = "fsum") # Fewer edges after removing directional duplicates c(directed = nrow(graph), undirected = nrow(graph_undir))library(flownet) # Convert segments to graph and make undirected graph <- africa_segments |> linestrings_from_graph() |> linestrings_to_graph() graph_undir <- create_undirected_graph(graph, FUN = "fsum") # Fewer edges after removing directional duplicates c(directed = nrow(graph), undirected = nrow(graph_undir))
Compute edge-level detour costs and vulnerability ratios for a graph.
critical_link_analysis( graph_df, directed = FALSE, cost.column = "cost", nthreads = 1L )critical_link_analysis( graph_df, directed = FALSE, cost.column = "cost", nthreads = 1L )
graph_df |
A data.frame with columns |
directed |
Logical (default: |
cost.column |
Character string (default: |
nthreads |
Integer (default: |
The detour cost for edge e = (u, v) is the shortest path cost from u
to v after removing that physical edge row. Parallel edges are kept as distinct
alternatives. If no detour exists, detour_cost and detour_ratio are Inf.
The implementation uses a compiled multi-label Dijkstra routine, run once per unique
from node. For each target node, it tracks the two best distances whose first
physical edge differs, which yields the best endpoint detour for every outgoing edge of
that source without repeated graph deletion. Per-source Dijkstras are independent and
are parallelized across nthreads OpenMP threads.
A data.frame with one row per input edge and columns:
edge - Edge identifier (index of the edge in graph_df)
from - Original from-node ID
to - Original to-node ID
edge_cost - Cost of traversing the edge
detour_cost - Shortest path cost between the edge endpoints after removing the edge
detour_ratio - detour_cost / edge_cost
distances_from_graph run_assignment flownet-package
library(flownet) graph <- data.frame( from = c(1, 2, 1), to = c(2, 3, 3), cost = c(1, 1, 3) ) critical_link_analysis(graph, cost.column = "cost")library(flownet) graph <- data.frame( from = c(1, 2, 1), to = c(2, 3, 3), cost = c(1, 1, 3) ) critical_link_analysis(graph, cost.column = "cost")
Compute a distance matrix for all node pairs in a graph using igraph.
distances_from_graph(graph_df, directed = FALSE, cost.column = "cost", ...)distances_from_graph(graph_df, directed = FALSE, cost.column = "cost", ...)
graph_df |
A data frame representing a graph with columns:
|
directed |
Logical (default: FALSE). If TRUE, treats the graph as directed; if FALSE, treats it as undirected. |
cost.column |
Character string (optional). Name of the cost column in |
... |
Additional arguments passed to |
This function:
Converts the graph data frame to a igraph graph object
Contracts the graph for efficient distance computation
Computes the distance matrix for all node pairs using the specified algorithm
A matrix of distances between all node pairs, where rows and columns
correspond to node IDs. The matrix contains the shortest path distances
(based on the cost column) between all pairs of nodes.
library(flownet) # Create a simple graph graph <- data.frame( from = c(1, 2, 2, 3), to = c(2, 3, 4, 4), cost = c(1, 2, 3, 1) ) # Compute distance matrix dmat <- distances_from_graph(graph, cost.column = "cost") dmatlibrary(flownet) # Create a simple graph graph <- data.frame( from = c(1, 2, 2, 3), to = c(2, 3, 4, 4), cost = c(1, 2, 3, 1) ) # Compute distance matrix dmat <- distances_from_graph(graph, cost.column = "cost") dmat
Convert a graph data frame with node coordinates to an sf object with LINESTRING geometries.
linestrings_from_graph(graph_df, crs = 4326)linestrings_from_graph(graph_df, crs = 4326)
graph_df |
A data frame representing a graph with columns:
|
crs |
Numeric or character (default: 4326). Coordinate reference system to assign to the output sf object. |
This function is the inverse operation of linestrings_to_graph. It:
Creates LINESTRING geometries from node coordinates (FX, FY, TX, TY)
Removes the coordinate columns from the output
Preserves all other columns from the input graph data frame
Returns an sf object suitable for spatial operations and visualization
An sf data frame with LINESTRING geometry, containing all columns from
graph_df except FX, FY, TX, and TY. Each row
represents an edge as a LINESTRING connecting the from-node (FX, FY)
to the to-node (TX, TY).
linestrings_to_graph flownet-package
library(flownet) library(sf) # Convert segments data frame to sf LINESTRING object segments_sf <- linestrings_from_graph(africa_segments) class(segments_sf) head(segments_sf) # Plot segments colored by route importance plot(segments_sf["passes"])library(flownet) library(sf) # Convert segments data frame to sf LINESTRING object segments_sf <- linestrings_from_graph(africa_segments) class(segments_sf) head(segments_sf) # Plot segments colored by route importance plot(segments_sf["passes"])
Convert Linestring to Graph
linestrings_to_graph( lines, digits = 6, keep.cols = is.atomic, compute.length = TRUE )linestrings_to_graph( lines, digits = 6, keep.cols = is.atomic, compute.length = TRUE )
lines |
An sf data frame of LINESTRING geometries. |
digits |
Numeric rounding applied to coordinates (to ensure that matching points across different linestrings is not impaired by numeric precision issues). Set to |
keep.cols |
Character vector of column names to keep from the input data frame. |
compute.length |
Applies |
A data.frame representing the graph with columns:
edge - Edge identifier
from - Starting node ID
FX - Starting node X-coordinate (longitude)
FY - Starting node Y-coordinate (latitude)
to - Ending node ID
TX - Ending node X-coordinate (longitude)
TY - Ending node Y-coordinate (latitude)
simplify_network flownet-package
library(flownet) library(sf) # Load existing network edges (exclude proposed new links) africa_net <- africa_network[!africa_network$add, ] # Convert network LINESTRING geometries to graph graph <- linestrings_to_graph(africa_net) head(graph) # Graph contains edge, from/to nodes, and coordinates names(graph)library(flownet) library(sf) # Load existing network edges (exclude proposed new links) africa_net <- africa_network[!africa_network$add, ] # Convert network LINESTRING geometries to graph graph <- linestrings_to_graph(africa_net) head(graph) # Graph contains edge, from/to nodes, and coordinates names(graph)
Convert an origin-destination (OD) matrix to a long-format data frame
with columns from, to, and flow.
melt_od_matrix(od_matrix, nodes = NULL, sort = TRUE)melt_od_matrix(od_matrix, nodes = NULL, sort = TRUE)
od_matrix |
A numeric matrix with origin-destination flows. Rows represent origins, columns represent destinations. The matrix should be square (same number of rows and columns). |
nodes |
(Optional) Numeric or integer vector of node IDs in the graph matching the rows
and columns of the matrix. If provided, must have length equal to both |
sort |
Sort long OD-matrix in ascending order of from and to columns. This can have computational benefits, e.g., when multithreading with |
This function converts a square OD matrix to long format, which is required by
run_assignment(). The behavior depends on whether nodes
is provided:
When nodes is provided:
The nodes vector is used directly as node IDs for both origins and destinations
Row and column names are ignored (but must match if both are present)
This is the recommended approach when working with zone-based OD matrices that need to be mapped to graph nodes, as it ensures the node IDs match those in the graph
When nodes is omitted:
Row and column names are extracted from the matrix (if available)
Names are coerced to integer if possible; if coercion fails or names are missing, sequential integers are used
This approach works well when the matrix row/column names already correspond to graph node IDs
In both cases, the function:
Creates a long-format data frame with all origin-destination pairs
Filters out non-finite and zero flow values
The function is useful for converting OD matrices to the long format required by
run_assignment().
A data frame with columns:
from - Origin node ID (integer)
to - Destination node ID (integer)
flow - Flow value (numeric)
Only rows with finite, positive flow values are included.
africa_cities_ports, africa_network,
nodes_from_graph(),
run_assignment(), flownet-package
library(flownet) library(sf) # Load existing network and convert to graph africa_net <- africa_network[!africa_network$add, ] graph <- linestrings_to_graph(africa_net) nodes <- nodes_from_graph(graph, sf = TRUE) # Map cities/ports to nearest network nodes nearest_nodes <- nodes$node[st_nearest_feature(africa_cities_ports, nodes)] # Example 1: Simple gravity-based OD matrix od_mat <- outer(africa_cities_ports$population, africa_cities_ports$population) / 1e12 dimnames(od_mat) <- list(nearest_nodes, nearest_nodes) od_long <- melt_od_matrix(od_mat) head(od_long) # Example 2: Using nodes argument (when matrix has zone IDs, not node IDs) # Here zones are 1:n_cities, nodes argument maps them to graph nodes dimnames(od_mat) <- NULL od_long2 <- melt_od_matrix(od_mat, nodes = nearest_nodes) head(od_long2)library(flownet) library(sf) # Load existing network and convert to graph africa_net <- africa_network[!africa_network$add, ] graph <- linestrings_to_graph(africa_net) nodes <- nodes_from_graph(graph, sf = TRUE) # Map cities/ports to nearest network nodes nearest_nodes <- nodes$node[st_nearest_feature(africa_cities_ports, nodes)] # Example 1: Simple gravity-based OD matrix od_mat <- outer(africa_cities_ports$population, africa_cities_ports$population) / 1e12 dimnames(od_mat) <- list(nearest_nodes, nearest_nodes) od_long <- melt_od_matrix(od_mat) head(od_long) # Example 2: Using nodes argument (when matrix has zone IDs, not node IDs) # Here zones are 1:n_cities, nodes argument maps them to graph nodes dimnames(od_mat) <- NULL od_long2 <- melt_od_matrix(od_mat, nodes = nearest_nodes) head(od_long2)
Extract unique nodes with their coordinates from a graph data frame.
nodes_from_graph(graph_df, sf = FALSE, crs = 4326)nodes_from_graph(graph_df, sf = FALSE, crs = 4326)
graph_df |
A data frame representing a graph with columns:
|
sf |
Logical. If TRUE, returns result as an |
crs |
Coordinate reference system for sf output; default is 4326. |
This function extracts all unique nodes from both the from and to
columns of the graph, along with their corresponding coordinates. Duplicate nodes
are removed, keeping only unique node IDs with their coordinates.
A data frame (or sf object if sf = TRUE) with unique nodes and coordinates:
node - Node ID
X - Node X-coordinate (typically longitude)
Y - Node Y-coordinate (typically latitude)
Result is sorted by node ID.
library(flownet) library(sf) # Load existing network edges and convert to graph africa_net <- africa_network[!africa_network$add, ] graph <- linestrings_to_graph(africa_net) # Extract nodes from graph nodes <- nodes_from_graph(graph) head(nodes) # Get nodes as sf POINT object for spatial operations nodes_sf <- nodes_from_graph(graph, sf = TRUE) class(nodes_sf) # Find nearest network nodes to cities/ports nearest_nodes <- nodes_sf$node[st_nearest_feature(africa_cities_ports, nodes_sf)] head(nearest_nodes)library(flownet) library(sf) # Load existing network edges and convert to graph africa_net <- africa_network[!africa_network$add, ] graph <- linestrings_to_graph(africa_net) # Extract nodes from graph nodes <- nodes_from_graph(graph) head(nodes) # Get nodes as sf POINT object for spatial operations nodes_sf <- nodes_from_graph(graph, sf = TRUE) class(nodes_sf) # Find nearest network nodes to cities/ports nearest_nodes <- nodes_sf$node[st_nearest_feature(africa_cities_ports, nodes_sf)] head(nearest_nodes)
Normalize node IDs in a graph to be consecutive integers starting from 1. This is useful for ensuring compatibility with graph algorithms that require sequential node IDs.
normalize_graph(graph_df)normalize_graph(graph_df)
graph_df |
A data frame representing a graph with columns:
|
This function:
Extracts all unique node IDs from both from and to columns
Sorts them in ascending order
Remaps the original node IDs to sequential integers (1, 2, 3, ...)
Updates both from and to columns with the normalized IDs
Normalization is useful when:
Node IDs are non-consecutive (e.g., 1, 5, 10, 20)
Node IDs are non-numeric or contain gaps
Graph algorithms require sequential integer node IDs starting from 1
Note: This function only normalizes the node IDs; it does not modify the graph structure or any other attributes. The mapping preserves the relative ordering of nodes.
A data frame with the same structure as graph_df, but with from
and to columns remapped to consecutive integer IDs starting from 1.
All other columns are preserved unchanged.
nodes_from_graph flownet-package
library(flownet) # Create graph with non-consecutive node IDs graph <- data.frame( from = c(10, 20, 20), to = c(20, 30, 40), cost = c(1, 2, 3) ) # Normalize to consecutive integers (1, 2, 3, 4) graph_norm <- normalize_graph(graph) graph_normlibrary(flownet) # Create graph with non-consecutive node IDs graph <- data.frame( from = c(10, 20, 20), to = c(20, 30, 40), cost = c(1, 2, 3) ) # Normalize to consecutive integers (1, 2, 3, 4) graph_norm <- normalize_graph(graph) graph_norm
Assign traffic flows to network edges using either Path-Sized Logit (PSL) or All-or-Nothing (AoN) assignment methods.
run_assignment( graph_df, od_matrix_long, directed = FALSE, cost.column = "cost", method = c("PSL", "AoN"), beta = 1, ..., detour.max = 1.5, angle.max = 90, unique.cost = TRUE, npaths.max = Inf, dmat.max.size = 10000^2, return.extra = NULL, verbose = TRUE, nthreads = 1L ) ## S3 method for class 'flownet' print(x, digits = 2, ...)run_assignment( graph_df, od_matrix_long, directed = FALSE, cost.column = "cost", method = c("PSL", "AoN"), beta = 1, ..., detour.max = 1.5, angle.max = 90, unique.cost = TRUE, npaths.max = Inf, dmat.max.size = 10000^2, return.extra = NULL, verbose = TRUE, nthreads = 1L ) ## S3 method for class 'flownet' print(x, digits = 2, ...)
graph_df |
A data.frame with columns |
||||||||||||||||||||||||||||||||||||
od_matrix_long |
A data.frame with columns |
||||||||||||||||||||||||||||||||||||
directed |
Logical (default: FALSE). Whether the graph is directed. |
||||||||||||||||||||||||||||||||||||
cost.column |
Character string (default: "cost") or numeric vector. Name of the cost column
in |
||||||||||||||||||||||||||||||||||||
method |
Character string (default: "PSL"). Assignment method:
|
||||||||||||||||||||||||||||||||||||
beta |
Numeric (default: 1). Path-sized logit parameter (beta_PSL). Only used for PSL method. |
||||||||||||||||||||||||||||||||||||
... |
Additional arguments (currently ignored). |
||||||||||||||||||||||||||||||||||||
detour.max |
Numeric (default: 1.5). Maximum detour factor for alternative routes (applied to shortest paths cost). Only used for PSL method. This is a key parameter controlling the execution time of the algorithm: considering more routes (higher |
||||||||||||||||||||||||||||||||||||
angle.max |
Numeric (default: 90). Maximum detour angle (in degrees, two sided). Only used for PSL method. I.e., nodes not within this angle measured against a straight line from origin to destination node will not be considered for detours. |
||||||||||||||||||||||||||||||||||||
unique.cost |
Logical (default: TRUE). Deduplicates paths based on the total cost prior to generating them. Only used for PSL method. Since multiple 'intermediate nodes' may be on the same path, there is likely a significant number of duplicate paths which this option removes. |
||||||||||||||||||||||||||||||||||||
npaths.max |
Integer (default: Inf). Maximum number of paths to compute per OD-pair. Only used for PSL method. If the number of paths exceeds this number, a random sample will be taken from all but the shortest path. |
||||||||||||||||||||||||||||||||||||
dmat.max.size |
Integer (default: 1e4^2). Maximum size of distance matrices (both shortest paths and geodesic) to precompute. If smaller than |
||||||||||||||||||||||||||||||||||||
return.extra |
Character vector specifying additional results to return.
Use
|
||||||||||||||||||||||||||||||||||||
verbose |
Logical (default: TRUE). Show progress bar and intermediate steps completion status? |
||||||||||||||||||||||||||||||||||||
nthreads |
Integer (default: 1L). Number of threads (daemons) to use for parallel processing with |
||||||||||||||||||||||||||||||||||||
x |
An object of class |
||||||||||||||||||||||||||||||||||||
digits |
Number of digits for summarizing final flows. Passed to |
This function performs traffic assignment using one of two methods: All-or-Nothing (AoN) is fast but assigns all flow to a single shortest path; Path-Sized Logit (PSL) considers multiple routes with overlap correction for more realistic flow distribution.
A simple assignment method that assigns all flow from each OD pair to the single shortest path.
This is much faster than PSL but does not consider route alternatives or overlaps.
Parameters detour.max, angle.max, unique.cost, npaths.max,
beta, and dmat.max.size are ignored for AoN.
A more sophisticated assignment method that considers multiple alternative routes and
accounts for route overlap when assigning flows. The PSL model adjusts choice probabilities
based on how much each route overlaps with other alternatives, preventing overestimation
of flow on shared segments. The beta parameter controls the sensitivity to overlap.
The probability of choosing route from the set of alternatives is:
where the utility is defined as:
Here is the generalized cost of route , is the
path-size parameter (the beta argument), and is the path-size factor.
The path-size factor quantifies route uniqueness:
where is the set of edges in path , is the cost of
edge , and is the number of alternative routes using edge .
If a path is unique ( for all edges), then and the
model reduces to standard MNL. For overlapping routes, and
, so a positive beta penalizes overlap. Higher beta
values strengthen penalization; beta = 0 gives standard MNL behavior.
For more information about the PSL model consult some of the references below.
For each origin-destination pair, the algorithm identifies alternative routes as follows:
Compute the shortest path cost from origin to destination. If sqrt(dmat.max.size) < N.nodes, the entire shortest-path-distance matrix is precomputed.
For each potential intermediate node, calculate the total cost of going origin -> intermediate -> destination (also using the distance matrix).
Keep only routes where total cost is within detour.max times the
shortest path cost.
If angle.max is specified, filter to intermediate nodes that lie
roughly in the direction of the destination (within the specified angle - see further details below). If sqrt(dmat.max.size) < N.nodes a geodesic-distance-matrix is precomputed for speedy calculations using the triangle equation.
If unique.cost = TRUE, remove duplicate routes based on total cost -
as multiple intermediate nodes may yield exactly the same route.
(Optionally) use npaths.max to sample the remaining routes if still too many.
Compute the actual paths and filter out those with duplicate edges (where the intermediate node is approached and departed via the same edge). In directed graphs, edges with matching "to-from" and "from-to" nodes are considered the same edge for this step.
This pre-selection using distance matrices speeds up route enumeration considerably by avoiding the computation of implausible paths.
When angle.max is specified and graph_df contains coordinate columns
(FX, FY, TX, TY), the function uses geographic distance
calculations to restrict detours. Only intermediate nodes that are (a) closer to the
origin than the destination is, and (b) within the specified angle from the
origin-destination line are considered. This improves both computational efficiency
and route realism by excluding geographically implausible detours.
A list of class "flownet" containing:
call - The function call
final_flows - Numeric vector of assigned flows for each edge (same length as nrow(graph_df))
od_pairs_used - Indices of OD pairs with valid flows
Additional elements as specified in return.extra:
graph - The igraph graph object
paths - For PSL: list of lists of edge indices (multiple routes per OD pair); for AoN: list of edge index vectors (one shortest path per OD pair)
path_costs - For PSL: list of path costs per OD pair; for AoN: numeric vector of shortest path costs
path_size_factors - List of path-size factors per OD pair (PSL only)
path_weights - List of path weights (probabilities) for each OD pair (PSL only)
edges - List of edge indices used for each OD pair (PSL only)
edge_counts - For PSL: list of edge visit counts per OD pair; for AoN: integer vector of global edge traversal counts
edge_weights - List of edge weight vectors (summed path probabilities per edge, PSL only)
Ben-Akiva, M., & Bierlaire, M. (1999). Discrete choice methods and their applications to short term travel decisions. In R. W. Hall (Ed.), Handbook of Transportation Science (pp. 5-33). Springer US. doi:10.1007/978-1-4615-5203-1_2
Cascetta, E. (2001). Transportation systems engineering: Theory and methods. Springer.
Ben-Akiva, M., & Lerman, S. R. (1985). Discrete choice analysis: Theory and application to travel demand. The MIT Press.
Ramming, M. S. (2002). Network knowledge and route choice (Doctoral dissertation). Massachusetts Institute of Technology.
Prato, C. G. (2009). Route choice modeling: Past, present and future research directions. Journal of Choice Modelling, 2(1), 65-100. doi:10.1016/S1755-5345(13)70005-8
AequilibiaE Python Documentation: https://www.aequilibrae.com/develop/python/route_choice/path_size_logit.html
library(flownet) library(collapse) library(sf) # Load existing network edges (exclude proposed new links) africa_net <- africa_network[!africa_network$add, ] # Convert to graph (use atomic_elem to drop sf geometry, qDF for data.frame) graph <- atomic_elem(africa_net) |> qDF() nodes <- nodes_from_graph(graph, sf = TRUE) # Map cities/ports to nearest network nodes nearest_nodes <- nodes$node[st_nearest_feature(africa_cities_ports, nodes)] # Simple gravity-based OD matrix od_mat <- outer(africa_cities_ports$population, africa_cities_ports$population) / 1e12 dimnames(od_mat) <- list(nearest_nodes, nearest_nodes) od_matrix_long <- melt_od_matrix(od_mat) # Run Traffic Assignment (All-or-Nothing method) result_aon <- run_assignment(graph, od_matrix_long, cost.column = "duration", method = "AoN", return.extra = "all") print(result_aon) # Run Traffic Assignment (Path-Sized Logit method) # Note: PSL is slower but produces more realistic flow distribution result_psl <- run_assignment(graph, od_matrix_long, cost.column = "duration", method = "PSL", nthreads = 1L, return.extra = c("edges", "counts", "costs", "weights")) print(result_psl) # Visualize AoN Results africa_net$final_flows_log10 <- log10(result_psl$final_flows + 1) plot(africa_net["final_flows_log10"], main = "PSL Assignment") # --- Trade Flow Disaggregation Example --- # Disaggregate country-level trade to city-level using population shares # Compute each city's share of its country's population city_pop <- africa_cities_ports |> atomic_elem() |> qDF() |> fcompute(node = nearest_nodes, city = qF(city_country), pop_share = fsum(population, iso3, TRA = "/"), keep = "iso3") # Aggregate trade to country-country level and disaggregate to cities trade_agg <- africa_trade |> collap(quantity ~ iso3_o + iso3_d, fsum) od_matrix_trade <- trade_agg |> join(city_pop |> add_stub("_o", FALSE), multiple = TRUE) |> join(city_pop |> add_stub("_d", FALSE), multiple = TRUE) |> fmutate(flow = quantity * pop_share_o * pop_share_d) |> frename(from = node_o, to = node_d) |> fsubset(flow > 0 & from != to) # Run AoN assignment with trade flows result_trade_aon <- run_assignment(graph, od_matrix_trade, cost.column = "duration", method = "AoN", return.extra = "all") print(result_trade_aon) # Visualize trade flow results africa_net$trade_flows_log10 <- log10(result_trade_aon$final_flows + 1) plot(africa_net["trade_flows_log10"], main = "Trade Flow Assignment (AoN)") # Run PSL assignment with trade flows (nthreads can be increased for speed) result_trade_psl <- run_assignment(graph, od_matrix_trade, cost.column = "duration", method = "PSL", nthreads = 1L, return.extra = c("edges", "counts", "costs", "weights")) print(result_trade_psl) # Compare PSL vs AoN: PSL typically shows more distributed flows africa_net$trade_flows_psl_log10 <- log10(result_trade_psl$final_flows + 1) plot(africa_net["trade_flows_psl_log10"], main = "Trade Flow Assignment (PSL)")library(flownet) library(collapse) library(sf) # Load existing network edges (exclude proposed new links) africa_net <- africa_network[!africa_network$add, ] # Convert to graph (use atomic_elem to drop sf geometry, qDF for data.frame) graph <- atomic_elem(africa_net) |> qDF() nodes <- nodes_from_graph(graph, sf = TRUE) # Map cities/ports to nearest network nodes nearest_nodes <- nodes$node[st_nearest_feature(africa_cities_ports, nodes)] # Simple gravity-based OD matrix od_mat <- outer(africa_cities_ports$population, africa_cities_ports$population) / 1e12 dimnames(od_mat) <- list(nearest_nodes, nearest_nodes) od_matrix_long <- melt_od_matrix(od_mat) # Run Traffic Assignment (All-or-Nothing method) result_aon <- run_assignment(graph, od_matrix_long, cost.column = "duration", method = "AoN", return.extra = "all") print(result_aon) # Run Traffic Assignment (Path-Sized Logit method) # Note: PSL is slower but produces more realistic flow distribution result_psl <- run_assignment(graph, od_matrix_long, cost.column = "duration", method = "PSL", nthreads = 1L, return.extra = c("edges", "counts", "costs", "weights")) print(result_psl) # Visualize AoN Results africa_net$final_flows_log10 <- log10(result_psl$final_flows + 1) plot(africa_net["final_flows_log10"], main = "PSL Assignment") # --- Trade Flow Disaggregation Example --- # Disaggregate country-level trade to city-level using population shares # Compute each city's share of its country's population city_pop <- africa_cities_ports |> atomic_elem() |> qDF() |> fcompute(node = nearest_nodes, city = qF(city_country), pop_share = fsum(population, iso3, TRA = "/"), keep = "iso3") # Aggregate trade to country-country level and disaggregate to cities trade_agg <- africa_trade |> collap(quantity ~ iso3_o + iso3_d, fsum) od_matrix_trade <- trade_agg |> join(city_pop |> add_stub("_o", FALSE), multiple = TRUE) |> join(city_pop |> add_stub("_d", FALSE), multiple = TRUE) |> fmutate(flow = quantity * pop_share_o * pop_share_d) |> frename(from = node_o, to = node_d) |> fsubset(flow > 0 & from != to) # Run AoN assignment with trade flows result_trade_aon <- run_assignment(graph, od_matrix_trade, cost.column = "duration", method = "AoN", return.extra = "all") print(result_trade_aon) # Visualize trade flow results africa_net$trade_flows_log10 <- log10(result_trade_aon$final_flows + 1) plot(africa_net["trade_flows_log10"], main = "Trade Flow Assignment (AoN)") # Run PSL assignment with trade flows (nthreads can be increased for speed) result_trade_psl <- run_assignment(graph, od_matrix_trade, cost.column = "duration", method = "PSL", nthreads = 1L, return.extra = c("edges", "counts", "costs", "weights")) print(result_trade_psl) # Compare PSL vs AoN: PSL typically shows more distributed flows africa_net$trade_flows_psl_log10 <- log10(result_trade_psl$final_flows + 1) plot(africa_net["trade_flows_psl_log10"], main = "Trade Flow Assignment (PSL)")
Spatially simplify a network graph using shortest paths or node clustering methods. This further simplifies the network topology but does not preserve full connectivity. It should ideally be called after consolidate_graph() if the network is still too large/complex.
simplify_network( graph_df, nodes = NULL, method = c("shortest-paths", "cluster"), directed = FALSE, cost.column = "cost", by = NULL, radius_km = list(nodes = 7, cluster = 20), verbose = TRUE, nthreads = 1L, nodes.algo.rann = FALSE, cluster.algo.dbscan = FALSE, ... )simplify_network( graph_df, nodes = NULL, method = c("shortest-paths", "cluster"), directed = FALSE, cost.column = "cost", by = NULL, radius_km = list(nodes = 7, cluster = 20), verbose = TRUE, nthreads = 1L, nodes.algo.rann = FALSE, cluster.algo.dbscan = FALSE, ... )
graph_df |
A data.frame with columns |
nodes |
For |
method |
Character string (default: "shortest-paths"). Method to use for simplification:
|
directed |
Logical (default: FALSE). Whether the graph is directed.
For |
cost.column |
Character string (default: "cost"). Name of the cost column in |
by |
Link characteristics to preserve/not simplify across, passed as a one-sided
formula or character vector of column names. Typically includes attributes like
mode, type, or capacity.
For |
radius_km |
Named list with elements |
verbose |
Logical (default: TRUE). Whether to print progress messages/bars. |
nthreads |
Integer (default: 1). Number of threads used with |
nodes.algo.rann |
Logical (default: FALSE). If TRUE, use RANN for fast 3D Cartesian nearest-neighbor preselection with exact geodesic validation when assigning nodes close to preserved nodes. This requires the RANN package. |
cluster.algo.dbscan |
Logical (default: FALSE). If TRUE, use |
... |
For |
simplify_network() provides two methods to simplify large transport networks:
Method: "shortest-paths"
Validates that all origin and destination nodes exist in the network
Computes shortest paths from each origin to all destinations using igraph
Marks all edges that are traversed by at least one shortest path
Returns only the subset of edges that were traversed
If nodes is a data frame with from and to columns, paths are computed
from each unique origin to its specified destinations
Method: "cluster"
Requires the graph to have spatial coordinates (FX, FY, TX, TY)
If nodes is provided, these nodes are preserved as cluster centroids
Nearby nodes (within radius_km$nodes km) are assigned to the nearest preserved node
using exact geodesic distances after optional RANN-based preselection when
nodes.algo.rann = TRUE
Remaining nodes are clustered using leaderCluster by default,
or dbscan when cluster.algo.dbscan = TRUE, with
radius_km$cluster as the clustering radius
For each cluster, the node closest to the cluster centroid is selected as representative
The graph is contracted by mapping all nodes to their cluster representatives
Self-loops (edges where both endpoints map to the same cluster) are dropped
This is a spatial simplification method and does not preserve intra-cluster topology
For undirected graphs (directed = FALSE), edges are normalized so from < to,
merging opposite-direction edges; for directed graphs, A->B and B->A remain separate
Edge attributes are aggregated using collap (default: mean for
numeric, mode for categorical); customize via ...
A data.frame containing the simplified graph with:
For method = "shortest-paths":
All columns from the input graph_df (for edges that were kept)
Attribute "keep.edges": integer vector of edge indices from the original graph
Attribute "edge.counts": integer vector indicating how many times each retained edge was traversed
For method = "cluster":
edge - New edge identifier
from, to - Cluster centroid node IDs
FX, FY, TX, TY - Coordinates of cluster centroid nodes
Aggregated edge attributes from the original graph
Attribute "keep.edges": integer vector of edge indices from the original graph that were kept
Attribute "group.id": integer vector aligned with "keep.edges", mapping each kept edge to its row in the result
Attribute "group.starts": index of the first kept edge of each output group
Attribute "group.sizes": number of original edges aggregated into each output edge
library(flownet) library(sf) # Convert segments to undirected graph graph <- africa_segments |> linestrings_from_graph() |> linestrings_to_graph() |> create_undirected_graph(FUN = "fsum") # Get city/port nodes to preserve nodes_df <- nodes_from_graph(graph, sf = TRUE) nearest_nodes <- nodes_df$node[st_nearest_feature(africa_cities_ports, nodes_df)] # Initial consolidation graph <- consolidate_graph(graph, keep = nearest_nodes, w = ~ passes) # Method 1: Shortest-paths simplification (keeps only traversed edges) graph_simple <- simplify_network(graph, nearest_nodes, method = "shortest-paths", cost.column = ".length") nrow(graph_simple) # Reduced number of edges # Method 2: Cluster-based simplification (contracts graph spatially) # Compute node weights for clustering node_weights <- collapse::rowbind( collapse::fselect(graph, node = from, gravity_rd), collapse::fselect(graph, to, gravity_rd), use.names = FALSE) |> collapse::collap(~ node, "fsum") graph_cluster <- simplify_network(graph, nearest_nodes, method = "cluster", cost.column = node_weights$gravity_rd, radius_km = list(nodes = 30, cluster = 27), w = ~ passes) nrow(graph_cluster)library(flownet) library(sf) # Convert segments to undirected graph graph <- africa_segments |> linestrings_from_graph() |> linestrings_to_graph() |> create_undirected_graph(FUN = "fsum") # Get city/port nodes to preserve nodes_df <- nodes_from_graph(graph, sf = TRUE) nearest_nodes <- nodes_df$node[st_nearest_feature(africa_cities_ports, nodes_df)] # Initial consolidation graph <- consolidate_graph(graph, keep = nearest_nodes, w = ~ passes) # Method 1: Shortest-paths simplification (keeps only traversed edges) graph_simple <- simplify_network(graph, nearest_nodes, method = "shortest-paths", cost.column = ".length") nrow(graph_simple) # Reduced number of edges # Method 2: Cluster-based simplification (contracts graph spatially) # Compute node weights for clustering node_weights <- collapse::rowbind( collapse::fselect(graph, node = from, gravity_rd), collapse::fselect(graph, to, gravity_rd), use.names = FALSE) |> collapse::collap(~ node, "fsum") graph_cluster <- simplify_network(graph, nearest_nodes, method = "cluster", cost.column = node_weights$gravity_rd, radius_km = list(nodes = 30, cluster = 27), w = ~ passes) nrow(graph_cluster)