Skip to content
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Changelog

All notable changes to this crate are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-07-09

### Added

- `Customizer` (via `Cch::customizer`) — a reusable customizer that derives the
elimination-tree level partition once and reuses output buffers across many
metrics through `Customizer::customize_into`, avoiding per-call allocation on
the hot re-customization path.

### Changed

- Customization now runs in parallel. Phase-1 reset parallelizes over
independent arcs; the phase-2 lower-triangle relaxation runs level by level
(barrier between levels, nodes parallel within a level). Output remains
**bit-identical** to the previous serial implementation and to the C++
RoutingKit oracle.
- `rayon` is now a dependency. It is pure Rust, so the crate remains free of any
C++ or FFI.
- `Cch::customize` is unchanged in signature and output (byte-for-byte); it is
now a thin wrapper over `Customizer`.

### Notes

- The parallel relaxation's data-race-freedom is contracted to a well-formed
(chordal) CCH as produced by `Cch::build` or a faithful `load_struct`
round-trip; a one-time bounds validation additionally guards against
out-of-bounds access for any bounds-valid structure.

## [0.1.1] - 2026-06-25

### Changed

- Query paths reach parity with the C++ RoutingKit oracle by eliding per-arc
bounds checks in the hot relaxation loops.

## [0.1.0] - 2026-06-25

### Added

- Initial release: the complete CCH pipeline in pure, safe Rust — contraction
order (`degree_order`, `inertial_order`), structure build (`Cch::build`),
per-metric customization, memory-mappable bundle read/write, and
distance / distance-matrix / shortest-path queries — validated bit-for-bit
against RoutingKit.

[0.2.0]: https://github.com/Rodeapps/cch/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/Rodeapps/cch/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/Rodeapps/cch/releases/tag/v0.1.0
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[package]
name = "cch"
version = "0.1.1"
version = "0.2.0"
edition = "2024"
license = "MIT"
description = "Pure-Rust Customizable Contraction Hierarchies (CCH): build, customize, and serve fast shortest-path distance, many-to-many matrix, and path queries on road networks"
description = "Pure-Rust Customizable Contraction Hierarchies (CCH): build, customize in parallel, and serve fast shortest-path distance, many-to-many matrix, and path queries on road networks"
repository = "https://github.com/Rodeapps/cch"
homepage = "https://github.com/Rodeapps/cch"
documentation = "https://docs.rs/cch"
Expand All @@ -19,6 +19,7 @@ exclude = ["oracle/", ".github/", ".superpowers/", ".plans/", ".claude/"]

[dependencies]
memmap2 = "0.9"
rayon = "1.10"

[dev-dependencies]
routingkit-cch = { path = "oracle/routingkit-cch" }
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ It is a from-scratch Rust reimplementation of [RoutingKit](https://github.com/Ro

```toml
[dependencies]
cch = "0.1"
cch = "0.2"
```

## Quick start
Expand All @@ -41,6 +41,10 @@ let cch = Cch::build(&graph, &order);
// 3. Customize a metric (cheap — repeat per weight profile).
let metric = cch.customize(&graph.weight);

// For repeated re-customization, reuse buffers:
// let cust = cch.customizer();
// cust.customize_into(&graph.weight, &mut metric);

// 4. Query in memory, via zero-copy views.
let dm = distance_matrix(&cch.view(), &metric.view(), &[0], &[3]);
assert_eq!(dm[0], 3); // shortest distance 0 -> 3
Expand All @@ -61,8 +65,9 @@ The expensive build is amortized across many cheap customizations, which is exac

## Highlights

- **Pure Rust, no FFI.** The published library has zero C++ in its dependency tree — embed it in a Rust service with no C/C++ toolchain. (A C++ RoutingKit build is used *only* as a dev-time differential-test oracle; it is not part of the crate you depend on.)
- **Pure Rust, no FFI.** The published library has zero C++ in its dependency tree — embed it in a Rust service with no C/C++ toolchain. (Parallel customization uses [rayon](https://github.com/rayon-rs/rayon), also pure Rust. A C++ RoutingKit build is used *only* as a dev-time differential-test oracle; it is not part of the crate you depend on.)
- **The complete pipeline** — contraction order, structure build, per-metric customization, bundle reader **and** writer, distance / distance-matrix / path queries, and shortcut unpacking.
- **Parallel, reusable customization.** `Cch::customizer` builds a [`Customizer`](https://docs.rs/cch/latest/cch/struct.Customizer.html) once per structure; `Customizer::customize_into` re-customizes for a new weight profile without reallocating output buffers, with both phases of customization running in parallel — bit-identical to the serial, single-shot `Cch::customize`.
- **Two ordering strategies** — a lightweight `degree_order`, and **`inertial_order`**: a full geometric nested-dissection (inertial-flow max-flow / min-cut) that produces hierarchies of the *same quality as RoutingKit* (identical shortcut counts in testing).
- **Zero-copy mmap bundles.** Build once, write `.cch-struct` / `.cch-metric` files, then serve them memory-mapped — the OS page cache backs the query slices directly, so many regions can be served within a bounded memory budget. The format is byte-compatible with RoutingKit-produced bundles.
- **Proven correct.** Every stage is gated by a differential test against the C++ oracle (see [Correctness](#correctness)).
Expand Down Expand Up @@ -113,6 +118,8 @@ Indicative numbers, Rust vs the C++ RoutingKit oracle, on a 24×24 bidirectional

Every operation is at parity with (or faster than) RoutingKit. The query paths reach parity by eliding the per-arc bounds checks in the hot relaxation loops — the elision-able accesses via sliced iterators, and the data-dependent distance-array access via a `get_unchecked` guarded by a one-time structural validation. (Numbers are indicative; hardware is not standardized — run `cargo bench` for your own.)

A `customize_reuse` bench compares a fresh `Cch::customize` per call against a reused `Customizer::customize_into` on the same grid; the reused path avoids re-deriving the level partition and re-allocating output buffers, so it is never slower and is typically a few percent faster (`cargo bench --bench cch -- customize` to reproduce). The gain grows with structure size and call frequency; on this modest 24×24 fixture the parallel overhead largely offsets the savings.

## Correctness

The reference C++ RoutingKit is vendored as a **dev-only** differential-test oracle, and each stage of the pipeline is gated against it:
Expand All @@ -127,9 +134,9 @@ The whole crate maintains **100% line coverage**, enforced by a CI gate (`cargo

## Status & roadmap

`0.1` ships the full pipeline — both orderings, build, customize, bundle read/write, and all query types — validated against RoutingKit. The API may still evolve before `1.0`.
`0.1` ships the full pipeline — both orderings, build, customize, bundle read/write, and all query types — validated against RoutingKit. Customization runs in parallel (via rayon) and supports buffer reuse across repeated calls through `Cch::customizer` / `Customizer::customize_into`, with no change to the bit-identical output. The API may still evolve before `1.0`.

Planned: narrowing the query-path performance gap, parallel customization, and broader benchmark coverage on real continental graphs.
Planned: narrowing the query-path performance gap and broader benchmark coverage on real continental graphs.

## Relationship to RoutingKit

Expand Down
34 changes: 34 additions & 0 deletions benches/cch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,39 @@ fn bench_customize(c: &mut Criterion) {
g.finish();
}

// ---------------------------------------------------------------------------
// Bench: customize_reuse (fresh Cch::customize per call vs a reused
// Customizer::customize_into, both on the Rust side only)
// ---------------------------------------------------------------------------

fn bench_customize_reuse(c: &mut Criterion) {
let (n, tail, head, weights) = make_grid(24, 24);
let graph = csr_from_arcs(n, &tail, &head);
let order = cch::degree_order(&graph);
let cch = cch::Cch::build(&graph, &order);

let mut g: BenchmarkGroup<WallTime> = c.benchmark_group("customize_reuse/24x24");
g.sample_size(50);

g.bench_function(BenchmarkId::new("fresh_each_call", ""), |b| {
b.iter(|| {
let m = cch.customize(black_box(&weights));
black_box(m);
});
});

g.bench_function(BenchmarkId::new("reused_customizer", ""), |b| {
let cust = cch.customizer();
let mut metric = cch.customize(&weights);
b.iter(|| {
cust.customize_into(black_box(&weights), &mut metric);
black_box(&metric);
});
});

g.finish();
}

// ---------------------------------------------------------------------------
// Bench: distance_matrix (all 576 nodes as sources AND targets = 576×576)
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -336,6 +369,7 @@ criterion_group!(
bench_degree_order,
bench_build,
bench_customize,
bench_customize_reuse,
bench_distance_matrix,
bench_node_path,
);
Expand Down
Loading
Loading