Skip to content

Commit 5692970

Browse files
authored
Merge pull request #32 from vlarmet/feat/cch
feat(CCH) : support for customizable contraction hierarchies
2 parents d3b3670 + d04c25e commit 5692970

45 files changed

Lines changed: 4800 additions & 170 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DESCRIPTION

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: cppRouting
22
Type: Package
33
Title: Algorithms for Routing and Solving the Traffic Assignment Problem
4-
Version: 3.2
4+
Version: 4.0
55
Date: 2025-11-28
66
Authors@R: person(given = "Vincent",family = "Larmet",role = c("aut", "cre"),email = "larmet.vincent@gmail.com")
77
Description: Calculation of distances, shortest paths and isochrones on weighted graphs using several variants of Dijkstra algorithm.
@@ -15,15 +15,16 @@ Description: Calculation of distances, shortest paths and isochrones on weighted
1515
Method of Successive Averages,
1616
Frank-Wolfe algorithm (M. Fukushima (1984) <doi:10.1016/0191-2615(84)90029-8>),
1717
Conjugate and Bi-Conjugate Frank-Wolfe algorithms (M. Mitradjieva, P. O. Lindberg (2012) <doi:10.1287/trsc.1120.0409>),
18-
Algorithm-B (R. B. Dial (2006) <doi:10.1016/j.trb.2006.02.008>).
18+
Algorithm-B (R. B. Dial (2006) <doi:10.1016/j.trb.2006.02.008>),
19+
Customizable Contraction Hierarchies (J. Dibbelt, B. Strasser, D. Wagner (2014) <doi:10.1145/2886843>).
1920
License: GPL (>= 2)
2021
Encoding: UTF-8
2122
LazyData: true
2223
Imports: Rcpp (>= 1.0.7), RcppParallel, RcppProgress, data.table
2324
LinkingTo: Rcpp, RcppParallel, RcppProgress
2425
SystemRequirements: GNU make
25-
RoxygenNote: 7.2.1
2626
URL: https://github.com/vlarmet/cppRouting
2727
Suggests: knitr, rmarkdown, igraph
2828
VignetteBuilder: knitr
2929
NeedsCompilation: yes
30+
Config/roxygen2/version: 8.0.0

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export(get_detour)
1111
export(cpp_contract)
1212
export(assign_traffic)
1313
export(get_aon)
14+
export(cpp_customize)
1415
importFrom(Rcpp, evalCpp)
1516
importFrom(RcppParallel, RcppParallelLibs)
1617
importFrom("stats", "setNames")

NEWS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
cppRouting v4.0
2+
===============
3+
Major changes
4+
5+
- Customizable Contraction Hierarchies (CCH) support, via `cpp_contract(..., customizable = TRUE)` and `cpp_customize()`. CCH separates preprocessing (topology) from customization (edge weights), so a prepared topology can be reused across many weight updates (e.g. congestion assignment) without a full re-contraction.
6+
- `get_distance_pair`, `get_distance_matrix`, `get_path_pair` and `get_aon` all support CCH graphs, in addition to normal and classically-contracted graphs.
7+
- `makegraph()`'s `aux` argument now accepts a `data.frame` with one or more named columns, in addition to a single vector. `aggregate_aux` in `get_distance_pair`/`get_distance_matrix` accepts `TRUE` (all columns) or a character vector of column names, and is now supported on normal, contracted (CH) and CCH graphs, and on graphs simplified with `cpp_simplify`. Aggregation runs inline in C++ during the shortest-path search itself, no path reconstruction needed.
8+
- `assign_traffic(..., aon_method = "cch", cch = cch)` uses a prepared CCH topology for repeated traffic assignment iterations, avoiding a full re-contraction at each run.
9+
10+
Minor changes
11+
12+
- `cpp_simplify()` result is now classed (`cppRouting_simplified`), fixing downstream functions that silently failed on a simplified graph.
13+
- `cpp_simplify()` now correctly aggregates multiple auxiliary columns across merged edges (previously single-column only).
14+
- `cpp_contract(..., customizable = TRUE)` shows progress, like classical contraction.
15+
- Various CCH correctness and performance fixes: `get_path_pair` on CCH graphs, elimination ordering (near-linear instead of quadratic), many-to-many distance matrix on CCH graphs.
16+
117
cppRouting v3.2
218
===============
319

R/RcppExports.R

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,42 @@ cpppathmat <- function(gfrom, gto, gw, nb, dict, keep, dep, arr, lim, setdif, al
1717
.Call(`_cppRouting_cpppathmat`, gfrom, gto, gw, nb, dict, keep, dep, arr, lim, setdif, algo, reverse)
1818
}
1919

20-
cppsimplify <- function(gfrom, gto, gw, nb, keep, rm_loop, iterate, progress) {
21-
.Call(`_cppRouting_cppsimplify`, gfrom, gto, gw, nb, keep, rm_loop, iterate, progress)
20+
cppsimplify <- function(gfrom, gto, gw, nb, keep, rm_loop, iterate, progress, aux) {
21+
.Call(`_cppRouting_cppsimplify`, gfrom, gto, gw, nb, keep, rm_loop, iterate, progress, aux)
2222
}
2323

2424
cppcontract <- function(gfrom, gto, gw, NbNodes, display_progress) {
2525
.Call(`_cppRouting_cppcontract`, gfrom, gto, gw, NbNodes, display_progress)
2626
}
2727

28+
cppcchprepare <- function(gfrom, gto, NbNodes, order, display_progress = FALSE) {
29+
.Call(`_cppRouting_cppcchprepare`, gfrom, gto, NbNodes, order, display_progress)
30+
}
31+
32+
cppcchcustomize <- function(gfrom, gto, gw, NbNodes, rank, tail, head, rank_first_out, rank_adj_head, rank_adj_arc, input_arc, input_forward) {
33+
.Call(`_cppRouting_cppcchcustomize`, gfrom, gto, gw, NbNodes, rank, tail, head, rank_first_out, rank_adj_head, rank_adj_arc, input_arc, input_forward)
34+
}
35+
36+
cppdistcch <- function(NbNodes, rank, first_out, adj_head, adj_arc, elimination_tree_parent, forward, backward, dep, arr) {
37+
.Call(`_cppRouting_cppdistcch`, NbNodes, rank, first_out, adj_head, adj_arc, elimination_tree_parent, forward, backward, dep, arr)
38+
}
39+
40+
cppdistmatcch <- function(NbNodes, first_out, adj_head, adj_arc, forward, backward, dep, arr) {
41+
.Call(`_cppRouting_cppdistmatcch`, NbNodes, first_out, adj_head, adj_arc, forward, backward, dep, arr)
42+
}
43+
44+
cpppathvaluescch <- function(gfrom, NbNodes, rank, first_out, adj_head, adj_arc, elimination_tree_parent, forward, backward, forward_first_arc, forward_first_dir, forward_second_arc, forward_second_dir, forward_original, backward_first_arc, backward_first_dir, backward_second_arc, backward_second_dir, backward_original, dep, arr, values) {
45+
.Call(`_cppRouting_cpppathvaluescch`, gfrom, NbNodes, rank, first_out, adj_head, adj_arc, elimination_tree_parent, forward, backward, forward_first_arc, forward_first_dir, forward_second_arc, forward_second_dir, forward_original, backward_first_arc, backward_first_dir, backward_second_arc, backward_second_dir, backward_original, dep, arr, values)
46+
}
47+
48+
cpppathcch <- function(NbNodes, rank, gfrom, gto, first_out, adj_head, adj_arc, elimination_tree_parent, forward, backward, forward_first_arc, forward_first_dir, forward_second_arc, forward_second_dir, forward_original, backward_first_arc, backward_first_dir, backward_second_arc, backward_second_dir, backward_original, dict, keep, dep, arr) {
49+
.Call(`_cppRouting_cpppathcch`, NbNodes, rank, gfrom, gto, first_out, adj_head, adj_arc, elimination_tree_parent, forward, backward, forward_first_arc, forward_first_dir, forward_second_arc, forward_second_dir, forward_original, backward_first_arc, backward_first_dir, backward_second_arc, backward_second_dir, backward_original, dict, keep, dep, arr)
50+
}
51+
52+
cppaoncchelim <- function(gfrom, gto, gw, NbNodes, rank, first_out, adj_head, adj_arc, elimination_tree_parent, forward, backward, forward_first_arc, forward_first_dir, forward_second_arc, forward_second_dir, forward_original, backward_first_arc, backward_first_dir, backward_second_arc, backward_second_dir, backward_original, dep, arr, dem) {
53+
.Call(`_cppRouting_cppaoncchelim`, gfrom, gto, gw, NbNodes, rank, first_out, adj_head, adj_arc, elimination_tree_parent, forward, backward, forward_first_arc, forward_first_dir, forward_second_arc, forward_second_dir, forward_original, backward_first_arc, backward_first_dir, backward_second_arc, backward_second_dir, backward_original, dep, arr, dem)
54+
}
55+
2856
cppdistC <- function(gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr, algo) {
2957
.Call(`_cppRouting_cppdistC`, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr, algo)
3058
}
@@ -45,18 +73,34 @@ cpppadd <- function(gfrom, gto, gw, gadd, nb, dep, arr) {
4573
.Call(`_cppRouting_cpppadd`, gfrom, gto, gw, gadd, nb, dep, arr)
4674
}
4775

76+
cppmataddmulti <- function(gfrom, gto, gw, aux, nb, dep, arr) {
77+
.Call(`_cppRouting_cppmataddmulti`, gfrom, gto, gw, aux, nb, dep, arr)
78+
}
79+
4880
cppdistadd <- function(gfrom, gto, gw, gadd, nb, lat, lon, k, dep, arr, algo) {
4981
.Call(`_cppRouting_cppdistadd`, gfrom, gto, gw, gadd, nb, lat, lon, k, dep, arr, algo)
5082
}
5183

84+
cppdistaddmulti <- function(gfrom, gto, gw, aux, nb, lat, lon, k, dep, arr, algo) {
85+
.Call(`_cppRouting_cppdistaddmulti`, gfrom, gto, gw, aux, nb, lat, lon, k, dep, arr, algo)
86+
}
87+
5288
cppdistaddC <- function(orfrom, orto, orw, gadd, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr, algo) {
5389
.Call(`_cppRouting_cppdistaddC`, orfrom, orto, orw, gadd, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr, algo)
5490
}
5591

92+
cppdistaddCmulti <- function(orfrom, orto, orw, aux, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr) {
93+
.Call(`_cppRouting_cppdistaddCmulti`, orfrom, orto, orw, aux, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr)
94+
}
95+
5696
cppaddC <- function(orfrom, orto, orw, gadd, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr, algo) {
5797
.Call(`_cppRouting_cppaddC`, orfrom, orto, orw, gadd, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr, algo)
5898
}
5999

100+
cppmataddCmulti <- function(orfrom, orto, orw, aux, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr) {
101+
.Call(`_cppRouting_cppmataddCmulti`, orfrom, orto, orw, aux, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr)
102+
}
103+
60104
cppaon <- function(gfrom, gto, gw, nb, lat, lon, k, dep, arr, dem, algo) {
61105
.Call(`_cppRouting_cppaon`, gfrom, gto, gw, nb, lat, lon, k, dep, arr, dem, algo)
62106
}
@@ -65,6 +109,10 @@ cppaonC <- function(orfrom, orto, orw, gfrom, gto, gw, nb, rank, shortf, shortt,
65109
.Call(`_cppRouting_cppaonC`, orfrom, orto, orw, gfrom, gto, gw, nb, rank, shortf, shortt, shortc, phast, dep, arr, dem, algo)
66110
}
67111

112+
cpptrafficcch <- function(gfrom, gto, gw, gflow, gaux, gftt, galpha, gbeta, gcap, nb, dep, arr, dem, max_gap, max_it, method, rank, tail, head, first_out, adj_head, adj_arc, rank_first_out, rank_adj_head, rank_adj_arc, input_arc, input_forward, elimination_tree_parent, verbose) {
113+
.Call(`_cppRouting_cpptrafficcch`, gfrom, gto, gw, gflow, gaux, gftt, galpha, gbeta, gcap, nb, dep, arr, dem, max_gap, max_it, method, rank, tail, head, first_out, adj_head, adj_arc, rank_first_out, rank_adj_head, rank_adj_arc, input_arc, input_forward, elimination_tree_parent, verbose)
114+
}
115+
68116
cpptraffic <- function(gfrom, gto, gw, gflow, gaux, gftt, galpha, gbeta, gcap, nb, lat, lon, k, dep, arr, dem, max_gap, max_it, method, aon_method, contract, phast, verbose) {
69117
.Call(`_cppRouting_cpptraffic`, gfrom, gto, gw, gflow, gaux, gftt, galpha, gbeta, gcap, nb, lat, lon, k, dep, arr, dem, max_gap, max_it, method, aon_method, contract, phast, verbose)
70118
}

R/assign_traffic.R

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
#' @param algorithm character. \code{msa}, \code{fw}, \code{cfw}, \code{bfw} or \code{dial}. Default to \code{bfw}. See details.
88
#' @param max_gap Numeric. Relative gap to achieve. Default to 0.001.
99
#' @param max_it Numeric. Maximum number of iterations. Default to \code{.Machine$integer.max}
10-
#' @param aon_method Character.\code{d}, \code{bi}, \code{nba}, \code{cphast} or \code{cbi}. Default to \code{bi}. See details.
10+
#' @param aon_method Character.\code{d}, \code{bi}, \code{nba}, \code{cphast}, \code{cbi} or \code{cch}. Default to \code{bi}. See details.
11+
#' @param cch Optional CCH topology generated by \code{cpp_contract(Graph, customizable = TRUE)}. Use it with \code{aon_method = "cch"} to prepare the road topology once and reuse it across many congestion runs.
1112
#' @param constant numeric. Constant to maintain the heuristic function admissible in NBA* algorithm. Default to 1, when cost is expressed in the same unit than coordinates. See details
1213
#' @param dial_params List. Named list of hyperparameters for \code{dial} algorithm. See details.
1314
#' @param verbose Logical. If \code{TRUE} (default), progression is displayed.
@@ -62,12 +63,20 @@
6263
#' \item \code{d} : Dijkstra algorithm
6364
#' \item \code{cbi} : contraction hierarchies + bidirectional search
6465
#' \item \code{cphast} : contraction hierarchies + phast algorithm
66+
#' \item \code{cch} : customizable contraction hierarchies with RoutingKit-style elimination-tree grouped queries. The topology is prepared once, then edge costs are customized at each assignment iteration.
6567
#' }
6668
#' These AON algorithm can be decomposed into two families, depending the sparsity of origin-destination matrix : \itemize{
6769
#' \item recursive pairwise : \code{bi}, \code{nba} and \code{cbi}. Optimal for high sparsity. One-to-one algorithm is called N times, with N being the length of from.
68-
#' \item recursive one-to-many : \code{d} and \code{cphast}. Optimal for dense matrix. One-to-many algorithm is called N times, with N being the number of unique from (or to) nodes
70+
#' \item recursive one-to-many : \code{d}, \code{cphast} and \code{cch}. Optimal for dense or repeated-endpoint matrices. One-to-many algorithm is called N times, with N being the number of unique from (or to) nodes.
6971
#' }
7072
#'
73+
#' CCH is most useful when the same graph topology is reused with changing edge
74+
#' costs, for example during congestion assignment. In that case, run
75+
#' \code{cch <- cpp_contract(graph, customizable = TRUE)} once, save it if needed, and pass
76+
#' \code{cch = cch} with \code{aon_method = "cch"}. The assignment will
77+
#' then customize the CCH at each iteration instead of rebuilding a full
78+
#' contraction hierarchy.
79+
#'
7180
#' For large instance, it may be appropriate to test different \code{aon_method} for few iterations and choose the fastest one for the final estimation.
7281
#'
7382
#' Hyperparameters for algorithm-b are : \itemize{
@@ -120,7 +129,7 @@
120129

121130

122131
assign_traffic <- function(Graph, from, to, demand, algorithm = "bfw", max_gap = 0.001, max_it = .Machine$integer.max,
123-
aon_method = "bi", constant = 1, dial_params = NULL, verbose = TRUE){
132+
aon_method = "bi", cch = NULL, constant = 1, dial_params = NULL, verbose = TRUE){
124133
if (length(from)!=length(to) | length(from) != length(demand)) stop("From, to and demand have not the same length")
125134
demand <- as.numeric(demand)
126135
if (any(is.na(data.frame(from,to, demand)))) stop("NAs are not allowed in origin/destination trips")
@@ -129,7 +138,7 @@ assign_traffic <- function(Graph, from, to, demand, algorithm = "bfw", max_gap =
129138
to <- to[ind]
130139
demand <- demand[ind]
131140

132-
if (length(Graph) != 5) stop("Graph object must be generated by makegraph() function")
141+
if (!inherits(Graph, "cppRouting_graph")) stop("Graph object must be generated by makegraph() function")
133142
if (is.null(Graph$attrib$alpha) | is.null(Graph$attrib$beta)| is.null(Graph$attrib$cap)) stop("alpha, beta and capacity must be defined during graph construction")
134143

135144

@@ -143,35 +152,30 @@ assign_traffic <- function(Graph, from, to, demand, algorithm = "bfw", max_gap =
143152
to_id <- Graph$dict$id[match(to,Graph$dict$ref)]
144153

145154
if (!algorithm %in% c("msa", "fw", "cfw", "bfw", "dial")) stop("algorithm should be 'msa', 'fw', 'cfw', 'bfw' or 'dial'")
146-
if (!aon_method %in% c("bi", "nba", "d", "cbi", "cphast")) stop("aon_method should be 'bi', 'nba', 'd', 'cbi' or 'cphast'")
155+
if (!aon_method %in% c("bi", "nba", "d", "cbi", "cphast", "cch")) stop("aon_method should be 'bi', 'nba', 'd', 'cbi', 'cphast' or 'cch'")
156+
if (algorithm == "dial" && aon_method == "cch") stop("CCH aon_method is only implemented for link-based algorithms: 'msa', 'fw', 'cfw' and 'bfw'")
157+
if (!is.null(cch) && aon_method != "cch") stop("cch can only be used with aon_method = 'cch'")
158+
if (aon_method == "cch" && !is.null(cch)){
159+
validate_cch_for_graph(cch, Graph)
160+
}
147161

148162
if (aon_method == "nba" & is.null(Graph$coords)){
149163
aon_method <- "bi"
150164
message("nodes coordinates are not provided, using bidirectional Dijkstra")
151165
}
152-
phast <- FALSE
153-
154-
if (aon_method == "d"){
155-
aon <- ifelse(length(unique(from_id)) <= length(unique(to_id)), 0, 1)
156-
contract <- FALSE
157-
}
158-
if (aon_method == "bi") {
159-
aon <- 2
160-
contract <- FALSE
161-
}
162-
if (aon_method == "nba") {
163-
aon <- 3
164-
contract <- FALSE
165-
}
166-
if (aon_method == "cbi") {
167-
aon <- 2
168-
contract <- TRUE
169-
}
170-
if (aon_method == "cphast"){
171-
aon <- ifelse(length(unique(from_id)) <= length(unique(to_id)), 0, 1)
172-
contract <- TRUE
173-
phast <- TRUE
174-
}
166+
n_from <- length(unique(from_id))
167+
n_to <- length(unique(to_id))
168+
grouped_aon <- ifelse(n_from <= n_to, 0, 1)
169+
aon_config <- switch(aon_method,
170+
d = list(aon = grouped_aon, contract = FALSE, phast = FALSE),
171+
bi = list(aon = 2L, contract = FALSE, phast = FALSE),
172+
nba = list(aon = 3L, contract = FALSE, phast = FALSE),
173+
cbi = list(aon = 2L, contract = TRUE, phast = FALSE),
174+
cphast = list(aon = grouped_aon, contract = TRUE, phast = TRUE),
175+
cch = list(aon = 7L, contract = FALSE, phast = FALSE))
176+
aon <- aon_config$aon
177+
contract <- aon_config$contract
178+
phast <- aon_config$phast
175179

176180
vec <- rep(0, nrow(Graph$data))
177181

@@ -188,9 +192,22 @@ assign_traffic <- function(Graph, from, to, demand, algorithm = "bfw", max_gap =
188192
if (algorithm %in% c("msa", "fw", "cfw", "bfw")){
189193
algo <- which(c("msa", "fw", "cfw", "bfw") == algorithm) - 1
190194

191-
res <- cpptraffic(Graph$data$from, Graph$data$to, Graph$data$dist, vec, vec, Graph$data$dist,
192-
Graph$attrib$alpha, Graph$attrib$beta, Graph$attrib$cap, Graph$nbnode,
193-
lat, lon, constant, from_id, to_id, demand, max_gap, max_it, algo, aon, contract, phast, verbose)
195+
if (aon_method == "cch"){
196+
if (is.null(cch)) cch <- cpp_contract(Graph, customizable = TRUE)
197+
if (!inherits(cch, "cppRouting_cch")) stop("cch must be generated by cpp_contract() with customizable = TRUE")
198+
res <- cpptrafficcch(Graph$data$from, Graph$data$to, Graph$data$dist, vec, vec, Graph$data$dist,
199+
Graph$attrib$alpha, Graph$attrib$beta, Graph$attrib$cap, Graph$nbnode,
200+
from_id, to_id, demand, max_gap, max_it, algo,
201+
cch$rank, cch$tail, cch$head,
202+
cch$first_out, cch$adj_head, cch$adj_arc,
203+
cch$rank_first_out, cch$rank_adj_head, cch$rank_adj_arc,
204+
cch$input_arc, cch$input_forward, cch$elimination_tree_parent,
205+
verbose)
206+
} else {
207+
res <- cpptraffic(Graph$data$from, Graph$data$to, Graph$data$dist, vec, vec, Graph$data$dist,
208+
Graph$attrib$alpha, Graph$attrib$beta, Graph$attrib$cap, Graph$nbnode,
209+
lat, lon, constant, from_id, to_id, demand, max_gap, max_it, algo, aon, contract, phast, verbose)
210+
}
194211

195212
}
196213

0 commit comments

Comments
 (0)