Benchmarking rcrisp across European city-river cases

This vignette documents how rcrisp performs across a diverse set of European city-river cases. The goal is to identify which pipeline steps are most computationally intensive and how run time varies with the scale and complexity of the urban river system being delineated.

Benchmarks were run on a single machine in a single session using OSM and SRTM data downloaded live; the get_osm() and get_dem() timings therefore include network latency and reflect the volume of spatial data available for each case, not just CPU work.

library(dplyr)
library(ggplot2)

Cases

raw <- read.csv(
  system.file("extdata", "benchmark_timings.csv", package = "rcrisp"),
  stringsAsFactors = FALSE
)

# Pipeline steps in processing order
io_steps      <- c("get_osm", "get_dem")
compute_steps <- c("delineate_valley", "as_network", "delineate_corridor",
                   "delineate_segments", "delineate_riverspace")
all_steps     <- c(io_steps, compute_steps)

step_labels <- c(
  get_osm              = "get_osm()",
  get_dem              = "get_dem()",
  delineate_valley     = "delineate_valley()",
  as_network           = "as_network()",
  delineate_corridor   = "delineate_corridor()",
  delineate_segments   = "delineate_segments()",
  delineate_riverspace = "delineate_riverspace()"
)

raw <- raw |>
  mutate(case = paste0(city_name, " (", river_name, ")"))

# Cases that completed the full pipeline
case_order <- raw |>
  filter(step == "total", status == "success") |>
  arrange(elapsed_s) |>
  pull(case)

The table below summarises the 36 city-river cases included in the benchmark. Cases marked Incomplete did not produce a final delineation; the step at which the run was interrupted is shown in the Note column.

City River Total (s) Note
Iași Bahlui 15.6
Malmö Sege å 15.9
Porto Rio Douro 17.2
Plovdiv Марица 17.2
Tallinn Pirita jõgi 19.6
Timișoara Bega 23.7
Cluj-Napoca Someșul Mic 24.6
Münster Werse 26
Geneva L’Arve 26.2
Brașov Timiș 26.4
København Mølleåen 29.9
Montpellier Le Lez 31.8
Århus Aarhus Å 35.3
Trondheim Nidelva 38.1
Helsinki Vantaanjoki 40.1
Ankara Ankara Çayı 40.6
Ljubljana Sava 47.7
Oslo Alna 57.7
Dublin Tolka 60.9
Poznań Warta 65
Rīga Daugava 65.9
Vilnius Neris 67.9
Liège La Meuse 72.1
Zagreb Sava 102.3
Bratislava Dunaj 104.4
Amsterdam Amstel 113.1
Praha Vltava 123.3
Madrid Río Manzanares 200.9
Budapest Duna 225.5
Roma Tevere 282.6
Paris La Seine 446
Athina Κηφισός Incomplete Failed at get_osm(): No city boundary found. The city name may be incorrect.
Craiova Jiu Incomplete Failed at delineate_corridor(): Corridor start- and end-points coincide!
Galați Prut Incomplete Failed at delineate_corridor(): No river crossings found. Corridor cannot be delineated.
Sheffield River Don Incomplete Failed at delineate_riverspace(): TopologyException: side location conflict at 598983.54113248398 5920051.9688930158. This can occur if the input geometry is invalid.
Wien Donau Incomplete Failed at delineate_corridor(): No river crossings found. Corridor cannot be delineated.
Athina Κηφισός Incomplete Failed at get_osm(): No city boundary found. The city name may be incorrect.

Run time by case

The chart below shows total run time per case, broken into data download (get_osm() + get_dem()) and computation (all delineation steps). Only the 31 cases that completed the full pipeline are included. Cases are sorted from fastest (bottom) to slowest (top).

bar_data <- raw |>
  filter(case %in% case_order, step %in% all_steps, status == "success") |>
  mutate(
    step_type = if_else(step %in% io_steps, "Data download", "Computation"),
    case = factor(case, levels = case_order)
  ) |>
  group_by(case, step_type) |>
  summarise(elapsed_s = sum(elapsed_s), .groups = "drop") |>
  mutate(step_type = factor(step_type, levels = c("Data download", "Computation")))

ggplot(bar_data, aes(x = elapsed_s, y = case, fill = step_type)) +
  geom_bar(stat = "identity") +
  scale_x_continuous(expand = expansion(mult = c(0, 0.03))) +
  scale_fill_manual(
    values = c("Data download" = "#4C72B0", "Computation" = "#DD8452"),
    breaks = c("Data download", "Computation")
  ) +
  labs(
    x    = "Elapsed time (s)",
    y    = NULL,
    fill = NULL
  ) +
  theme_minimal(base_size = 11) +
  theme(
    legend.position       = "top",
    panel.grid.major.y    = element_blank()
  )
plot of chunk bar-chart
plot of chunk bar-chart

Data download accounts for the majority of wall-clock time in most cases. The variation in get_osm() time is driven primarily by the volume of OSM data within the area of interest: large metropolitan areas (Paris, Roma, Budapest) have substantially more street and building data than smaller or less densely documented cities.

Run time by step

The heatmap below shows elapsed time for each pipeline step individually. The colour scale is logarithmic so that the fast computation steps remain visible alongside the much longer download steps. Grey tiles indicate steps that were not reached due to an earlier failure.

# Include incomplete runs (except Athina where get_osm itself failed)
heatmap_cases <- c(case_order, setdiff(
  raw |>
    filter(step %in% all_steps) |>
    distinct(case) |>
    filter(!case %in% case_order) |>
    pull(case),
  "Athina (Κηφισός)"
))

heatmap_data <- raw |>
  filter(case %in% heatmap_cases, step %in% all_steps) |>
  mutate(
    step_type    = factor(
      if_else(step %in% io_steps, "Data download", "Computation"),
      levels = c("Data download", "Computation")
    ),
    step_label   = factor(step_labels[step], levels = step_labels[all_steps]),
    elapsed_plot = if_else(status == "success", elapsed_s, NA_real_),
    case         = factor(case, levels = rev(heatmap_cases))
  )

ggplot(heatmap_data, aes(x = step_label, y = case, fill = elapsed_plot)) +
  geom_tile(colour = "white", linewidth = 0.4) +
  facet_grid(~ step_type, scales = "free_x", space = "free_x") +
  scale_fill_viridis_c(
    option    = "plasma",
    na.value  = "grey85",
    trans     = "log10",
    name      = "Seconds\n(log scale)",
    labels    = \(x) formatC(x, format = "g", digits = 2)
  ) +
  labs(x = NULL, y = NULL) +
  theme_minimal(base_size = 10) +
  theme(
    axis.text.x       = element_text(angle = 40, hjust = 1),
    strip.text        = element_text(face = "bold"),
    panel.grid        = element_blank(),
    legend.key.height = unit(1.5, "cm")
  )
plot of chunk heatmap
plot of chunk heatmap

Key findings

Looking across all cases, a few patterns stand out: