Skip to content

feat(CCH) : support for customizable contraction hierarchies#32

Open
vlarmet wants to merge 14 commits into
masterfrom
feat/cch
Open

feat(CCH) : support for customizable contraction hierarchies#32
vlarmet wants to merge 14 commits into
masterfrom
feat/cch

Conversation

@vlarmet

@vlarmet vlarmet commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Integrates Customizable Contraction Hierarchies (CCH) support from the cppRoutingCCH fork into cppRouting, fixing correctness and performance bugs found along the way, and bringing CCH to full functional parity with classical CH (contracted graphs) wherever that made sense.

Guiding principle: every R function that supports a CH graph should also support a CCH graph, and vice versa where the underlying algorithm allows it.

CCH correctness and performance fixes

The fork's CCH backend had several bugs and unoptimized code paths that predate this integration:

  • get_path_pair() returned wrong nodes on a CCH graph. Root cause: CCHPathPairWorker didn't reset its touched-node state between pairs in a batch, and self-pairs (from == to) weren't handled. Both fixed; CCHDistancePairWorker/CCHPathPairWorker were also rewritten to use the elimination-tree query algorithm (ascend via for_ancestors/relax_outgoing_elimination, descend pinned ancestors via relax_incoming_elimination) instead of a bidirectional priority-queue search, matching the already-correct CCHPathValuesPairWorker pattern. get_path_pair() on a CCH graph is new — the fork never supported this.
  • min_degree_order() (the elimination ordering heuristic) was O(n²) — a linear scan for the minimum-degree node at every one of the n elimination steps. Replaced with a bucket-by-degree queue (buckets indexed by current degree, a monotonic-with-backward-reset pointer to the smallest non-empty bucket), bringing this to near-linear time while preserving the exact same tie-breaking (smallest node id among degree ties), verified against the original algorithm on real data.
  • distance_matrix_cch() rescanned all buckets for every destination on every cell (O(n·b)). Rewritten RPHAST-style: downward searches build per-node buckets of (destination, distance) once, then upward searches combine buckets directly.
  • Stall-on-demand pruning (Stall_par), which is standard for classical CH, does not apply to CCH's dense clique-closure topology (confirmed via instrumentation: 0% trigger rate) — left out of the CCH query path rather than adding dead-weight checks.

cpp_simplify() fixes

  • The output of cpp_simplify() was an unclassed list. This silently broke every downstream function that checks inherits(Graph, "cppRouting_graph") (get_distance_pair, get_distance_matrix, aggregate_aux, etc. on a simplified graph), producing confusing "object not found"-style errors instead of a clear validation message. Fixed by classing the result c("cppRouting_simplified", "cppRouting_graph") — multi-class inheritance keeps every existing inherits(x, "cppRouting_graph") check working while allowing new simplified-graph-specific dispatch.
  • Extended to correctly sum multiple auxiliary columns across merged/collapsed edge chains during simplification (previously single-column only), by tracking, for every surviving edge, which original edge rows it represents (edge_origin, mirrored through every merge in simp()/simplify()), and summing the requested aux columns once at the end.

aggregate_aux: unified multi-column auxiliary weights

This is the main feature of this integration. makegraph()'s aux argument now accepts a data.frame with one or more named columns (in addition to the historical single vector), and aggregate_aux on get_distance_pair()/get_distance_matrix() accepts TRUE (all columns) or a character vector of column names — consistently across normal graphs, CH, CCH, and simplified graphs.

The fork's equivalent (get_path_values_pair(), CCH-only, values supplied at query time rather than declared on the graph) is superseded by this: auxiliary weights are now a graph-level property declared once at makegraph() time, reused across every query function, not a special-purpose CCH-only query.

Aggregation happens inline in C++, during the same search that computes distances — no path reconstruction, no R round-trip:

  • Normal graph, all 4 algorithms (Dijkstra, bi, A*, NBA): distancePairAdd / distanceMatAdd, generalizing the existing single-column add/addr running-sum mechanism (distancePair/distanceMat) from one running sum to N, using a flattened column-major nbedge × n_aux layout.
  • CH (contracted graph): distancePairCAdd / phastCAdd, fed by a per-shortcut N-column precompute pass (aggCAdd, generalizing aggC) so the query itself never unpacks shortcuts.
  • CCH: wired to the existing cpppathvaluescch engine.

Requesting more than one aux column changes the return shape, and this is new: get_distance_pair() returns a data.frame (one column per requested aux name) instead of a plain vector, and get_distance_matrix() returns a named list of matrices (one per requested aux name) instead of a single matrix. The historical single-column shape (plain vector / single matrix) is preserved whenever exactly one aux value is requested, whether aux was declared as a single vector (aggregate_aux = TRUE) or as a one-column selection out of a data.frame aux (aggregate_aux = TRUE with one column, or aggregate_aux = "name") - both distinguished internally by single_legacy in resolve_aggregate_aux()/shape_aggregate_result() (aux_helpers.R).

API cleanup

  • cpp_cch_prepare() merged into cpp_contract(): new customizable (default FALSE) and order arguments. customizable = FALSE is the unchanged classical CH path; customizable = TRUE runs what used to be cpp_cch_prepare() and returns a CCH topology object. One entry point for both contraction flavors.
  • cpp_cch_customize() renamed to cpp_customize().
  • cpp_cch_prepare() removed. All call sites updated (assign_traffic()'s auto-preparation, error messages across get_aon/get_distance_pair/get_distance_matrix), NAMESPACE and generated man/ pages regenerated with roxygen2.

CCH preparation progress bar

cpp_contract(..., customizable = TRUE) previously had no progress feedback at all (unlike classical cpp_contract(), which shows one via RcppProgress). Added the same mechanism to both phases of CCH preparation — elimination-order computation (min_degree_order) and the node-elimination/shortcut-construction loop (build_cch) — controlled by the existing silent argument, which is no longer ignored when customizable = TRUE. Also added Rcpp::checkUserInterrupt() to both loops, matching the classical CH path.

Fixes found during a final integration review

A systematic diff against cppRoutingCCH (every exported R function, every C++ file) turned up a couple of things that had been ported partially:

  • validate_cch_for_graph() — which checks that a user-supplied cch object was actually prepared from the same graph topology before assign_traffic() uses it — existed in cch.R but its call site in assign_traffic() had never been ported. A cch prepared for the wrong graph previously hit undefined behavior in the C++ layer instead of a clear R error. Wired back in and tested against both a matching and a mismatched topology.
  • assign_traffic()'s roxygen docs didn't mention aon_method = "cch" at all — missing from the @param aon_method enum, no @param cch entry, and the AON algorithm list in @details omitted it entirely, even though the code has fully supported it. Docs restored (adapted for the cpp_contract/cpp_customize renames) and man/assign_traffic.Rd regenerated.
  • Two smaller latent issues fixed opportunistically: aggc.cpp printed a diagnostic then indexed with -1 (undefined behavior) when a shortcut's original edge couldn't be found, in both the pre-existing aggC and the new aggCAdd — now raises a clear Rcpp::stop(). A stale roxygen claim that aggregate_aux's character-vector column-selection form was "CCH only" was removed (it's been generally supported for a while).
  • man/*.Rd pages had drifted out of sync with their roxygen source comments across several prior sessions (roxygen2 was not installed in this environment). Installed and regenerated; NAMESPACE (hand-maintained, not roxygen-generated) was left untouched.

Aside from those, the review confirmed cppRouting is at full parity with (and in some places ahead of — CCH support in get_path_pair, the multi-column aggregate_aux, and the performance fixes above are not in the fork) cppRoutingCCH. The only remaining gaps are get_multi_paths()/get_isochrone()/get_detour(), which support neither CH nor CCH in either codebase — a pre-existing, deliberately out-of-scope limitation, not something this integration missed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant