routingkit-cch 0.1.4

Rust bindings for RoutingKit's Customizable Contraction Hierarchies (CCH)
Documentation
from dataclasses import dataclass
import routingkit_cch as rk
import geopandas as gpd
from tqdm import tqdm
import more_itertools
from concurrent.futures import ThreadPoolExecutor
import os
import time
import pickle


@dataclass
class Data:
    x: list[float]
    y: list[float]
    tail: list[int]
    head: list[int]
    weights: list[int]
    trips: list[list[int]]


def read_data(path: str):
    edge_df = gpd.read_file(os.path.join(path, "map/edges.shp"), ignore_geometry=True)
    node_df = gpd.read_file(os.path.join(path, "map/nodes.shp"), ignore_geometry=True)
    map_node_osmid_to_id = {
        j: i for i, j in enumerate(node_df[["osmid"]].to_numpy().flatten())
    }
    edge_df["u"] = edge_df["u"].map(map_node_osmid_to_id)
    edge_df["v"] = edge_df["v"].map(map_node_osmid_to_id)

    with open(os.path.join(path, "preprocessed_train_trips_all.pkl"), "rb") as f:
        data_train: list[tuple[tuple[str, int], list[int], tuple[int, int]]] = (
            pickle.load(f)
        )

    x: list[float] = node_df["x"].tolist()
    y: list[float] = node_df["y"].tolist()
    tail: list[int] = edge_df["u"].tolist()
    head: list[int] = edge_df["v"].tolist()
    weights = (edge_df["length"] * 1e3).astype(int).tolist()
    trips: list[list[int]] = [trip for _, trip, _ in data_train]
    return Data(x, y, tail, head, weights, trips)


def sequential(path: str = "data/beijing_data"):
    data = read_data(path)
    order = rk.compute_order_inertial(
        len(data.x),
        data.tail,
        data.head,
        data.x,
        data.y,
    )

    cch = rk.CCH(order, data.tail, data.head, False)
    metric = rk.CCHMetric(cch, data.weights)
    q = rk.CCHQuery(metric)

    start = time.time()
    pbar = tqdm(total=len(data.trips))
    for trip in data.trips:
        pbar.update(1)
        u = data.tail[trip[0]]
        v = data.head[trip[-1]]

        res = q.run(u, v)
        node_path = res.node_path
        assert node_path[0] == u and node_path[-1] == v
        assert sum(data.weights[i] for i in res.arc_path) == res.distance
        assert sum(data.weights[i] for i in trip) >= res.distance
        del res
    return time.time() - start


def parallel(path: str = "data/beijing_data", max_workers: int = 2):
    data = read_data(path)
    order = rk.compute_order_inertial(
        len(data.x),
        data.tail,
        data.head,
        data.x,
        data.y,
    )

    cch = rk.CCH(order, data.tail, data.head, False)
    metric = rk.CCHMetric(cch, data.weights)

    executor = ThreadPoolExecutor(max_workers=max_workers)

    def worker(trips: list[list[int]]):
        q = rk.CCHQuery(metric)
        for trip in trips:
            u = data.tail[trip[0]]
            v = data.head[trip[-1]]

            res = q.run(u, v)
            node_path = res.node_path
            assert node_path[0] == u and node_path[-1] == v
            assert sum(data.weights[i] for i in res.arc_path) == res.distance
            assert sum(data.weights[i] for i in trip) >= res.distance
            del res

    start = time.time()
    futures = [
        executor.submit(worker, list(chunk))
        for chunk in more_itertools.chunked(
            data.trips, len(data.trips) // max_workers + 1
        )
    ]
    for future in futures:
        future.result()
    return time.time() - start


if __name__ == "__main__":
    for city in os.listdir("data"):
        print(f"Processing city: {city}")
        path = os.path.join("data", city)
        print(f"Sequential took {sequential(path)} seconds")
        print(f"Parallel took {parallel(path)} seconds")