---
title: "fastRanges Benchmark Result Interpretation"
subtitle: "Post hoc analysis of saved benchmark tables"
format:
  html:
    toc: true
    number-sections: true
    code-fold: true
    code-summary: "Show code"
execute:
  echo: true
  warning: false
  message: false
params:
  results_dir: "benchmark_result/results"
  figures_dir: "benchmark_result/figures_interpretation"
---

## Goal

This report interprets previously generated benchmark results for `fastRanges`
and Bioconductor baselines. Unlike the main benchmark document, this file does
not rerun the workloads. It reads saved tables from
`inst/benchmarks/benchmark_result/results`, regenerates publication-oriented
plots, and explains what the observed patterns mean.

This separation is useful when:

- large benchmarks were run on a dedicated server
- only CSV results are available locally
- manuscript figures need multiple alternate visual summaries
- interpretation should remain stable even when the heavy benchmark is not rerun

## Why This Interpretation Layer Matters

A raw timing table is not enough for a paper. Readers usually need four kinds
of interpretation:

1. Absolute runtime: which engine finishes first?
2. Relative speedup: how much faster is one engine than the baseline?
3. Scaling behavior: does additional threading continue to help?
4. Workload dependence: does the winner change for repeated-query or
   summary-style tasks?

The plots below are organized around those questions.

## Setup

```{r setup}
required_pkgs <- c("ggplot2", "dplyr", "tidyr", "scales")
missing_pkgs <- required_pkgs[!vapply(required_pkgs, requireNamespace, logical(1), quietly = TRUE)]
if (length(missing_pkgs) > 0) {
  stop("Missing required packages: ", paste(missing_pkgs, collapse = ", "))
}

library(ggplot2)
library(dplyr)
library(tidyr)
library(scales)

input_dir <- dirname(knitr::current_input())
resolve_path <- function(path, mustWork = FALSE) {
  candidates <- c(
    path,
    file.path(input_dir, path),
    file.path(getwd(), path)
  )
  for (candidate in unique(candidates)) {
    if (file.exists(candidate) || dir.exists(candidate)) {
      return(normalizePath(candidate, winslash = "/", mustWork = mustWork))
    }
  }
  normalizePath(file.path(input_dir, path), winslash = "/", mustWork = mustWork)
}

results_dir <- resolve_path(params$results_dir, mustWork = TRUE)
figures_dir <- resolve_path(params$figures_dir, mustWork = FALSE)
if (!dir.exists(figures_dir)) {
  dir.create(figures_dir, recursive = TRUE)
}

read_tbl <- function(name) {
  path <- file.path(results_dir, paste0(name, ".csv"))
  if (!file.exists(path)) {
    stop("Missing result table: ", path)
  }
  utils::read.csv(path, stringsAsFactors = FALSE, check.names = FALSE)
}

save_plot <- function(plot, filename, width = 10, height = 6, dpi = 320) {
  path <- file.path(figures_dir, filename)
  ggplot2::ggsave(path, plot = plot, width = width, height = height, dpi = dpi)
  path
}

summarise_runtime <- function(tbl, group_cols) {
  tbl %>%
    group_by(across(all_of(group_cols))) %>%
    summarise(
      median_sec = median(elapsed_sec),
      q25_sec = quantile(elapsed_sec, 0.25),
      q75_sec = quantile(elapsed_sec, 0.75),
      min_sec = min(elapsed_sec),
      max_sec = max(elapsed_sec),
      .groups = "drop"
    )
}

add_speedup_vs_baseline <- function(tbl, group_cols, baseline_engine_col, baseline_engine_value) {
  if (length(group_cols) == 0L) {
    baseline_sec <- tbl %>%
      filter(.data[[baseline_engine_col]] == baseline_engine_value) %>%
      pull(median_sec)
    baseline_sec <- unique(baseline_sec)
    if (length(baseline_sec) != 1L) {
      stop("Expected exactly one baseline median for ", baseline_engine_value)
    }
    return(tbl %>%
      mutate(
        baseline_sec = baseline_sec,
        speedup = baseline_sec / median_sec,
        speedup_low = baseline_sec / q75_sec,
        speedup_high = baseline_sec / q25_sec
      ))
  }

  baseline_tbl <- tbl %>%
    filter(.data[[baseline_engine_col]] == baseline_engine_value) %>%
    select(all_of(group_cols), baseline_sec = median_sec)

  tbl %>%
    left_join(baseline_tbl, by = group_cols) %>%
    mutate(
      speedup = baseline_sec / median_sec,
      speedup_low = baseline_sec / q75_sec,
      speedup_high = baseline_sec / q25_sec
    )
}

add_speedup_vs_one_thread <- function(tbl, engine_col = "engine") {
  baseline_tbl <- tbl %>%
    filter(threads == 1L) %>%
    select(all_of(engine_col), baseline_sec = median_sec)

  tbl %>%
    left_join(baseline_tbl, by = setNames(engine_col, engine_col)) %>%
    mutate(
      speedup_vs_1_thread = baseline_sec / median_sec,
      speedup_low = baseline_sec / q75_sec,
      speedup_high = baseline_sec / q25_sec
    )
}

theme_pub <- function() {
  theme_bw(base_size = 13) +
    theme(
      panel.grid.minor = element_blank(),
      plot.title.position = "plot",
      legend.position = "bottom",
      strip.background = element_rect(fill = "grey95"),
      axis.text.x = element_text(color = "black"),
      axis.text.y = element_text(color = "black")
    )
}

engine_palette <- c(
  "fastRanges_indexed" = "#00798C",
  "fastRanges_direct" = "#D1495B",
  "fastRanges_indexed_GRanges" = "#00798C",
  "fastRanges_direct_GRanges" = "#D1495B",
  "GenomicRanges_findOverlaps" = "#6C757D",
  "GenomicRanges_GNCList" = "#EDAe49",
  "fastRanges_index_reuse" = "#00798C",
  "IRanges_nthread" = "#6C757D",
  "fastRanges_indexed_IRanges" = "#00798C",
  "fastRanges" = "#00798C",
  "Bioconductor_baseline" = "#6C757D"
)

engine_levels <- c(
  "GenomicRanges_findOverlaps",
  "GenomicRanges_GNCList",
  "fastRanges_direct",
  "fastRanges_indexed",
  "fastRanges_direct_GRanges",
  "fastRanges_indexed_GRanges",
  "GenomicRanges_findOverlaps",
  "GenomicRanges_GNCList",
  "fastRanges_direct",
  "fastRanges_index_reuse",
  "IRanges_nthread",
  "fastRanges_indexed_IRanges",
  "Bioconductor_baseline",
  "fastRanges"
)
engine_levels <- unique(engine_levels)

thread_palette <- c(
  "1" = "#DCEAF2",
  "2" = "#BDD7E7",
  "8" = "#9ECAE1",
  "32" = "#6BAED6",
  "64" = "#3182BD",
  "96" = "#08519C"
)

fast_color <- unname(engine_palette[["fastRanges"]])

meta <- read_tbl("benchmark_run_metadata")
package_versions <- read_tbl("package_versions")
gr_raw <- read_tbl("gr_bench_raw")
gr_summary <- read_tbl("gr_summary")
gr_speedup <- read_tbl("gr_speedup")
gr_scaling_raw <- read_tbl("gr_scaling_raw")
gr_scaling_summary <- read_tbl("gr_scaling_summary")
ir_raw <- read_tbl("ir_scaling_raw")
ir_summary <- read_tbl("ir_scaling_summary")
ir_winner <- read_tbl("ir_absolute_winner")
repeat_raw <- read_tbl("repeat_bench_raw")
repeat_summary <- read_tbl("repeat_bench_summary")
derived_raw <- read_tbl("derived_bench_raw")
derived_summary <- read_tbl("derived_summary")
publication_table <- read_tbl("publication_table")

gr_stats <- summarise_runtime(gr_raw, c("scenario", "query_n", "subject_n", "engine")) %>%
  add_speedup_vs_baseline(c("scenario", "query_n", "subject_n"), "engine", "GenomicRanges_findOverlaps")

gr_scaling_stats <- summarise_runtime(gr_scaling_raw, c("engine", "threads")) %>%
  add_speedup_vs_one_thread("engine") %>%
  left_join(
    gr_scaling_summary %>%
      select(engine, threads, speedup_vs_genomicranges),
    by = c("engine", "threads")
  )

repeat_stats <- summarise_runtime(repeat_raw, c("engine")) %>%
  add_speedup_vs_baseline(character(), "engine", "GenomicRanges_findOverlaps")

derived_stats <- summarise_runtime(derived_raw, c("task", "engine")) %>%
  add_speedup_vs_baseline(c("task"), "engine", "Bioconductor_baseline")

ir_stats <- summarise_runtime(ir_raw, c("engine", "threads")) %>%
  add_speedup_vs_one_thread("engine")

meta
```

The benchmark represented here used `r meta$iterations[1]` timing iterations,
thread grid `r meta$thread_grid[1]`, and up to
`r meta$max_threads_used[1]` physical cores.

## Software Versions

```{r software-versions}
package_versions
```

## Result Inventory

```{r inventory}
data.frame(
  table = c(
    "gr_summary", "gr_speedup", "gr_scaling_summary", "ir_scaling_summary",
    "repeat_bench_summary", "derived_summary", "publication_table"
  ),
  rows = c(
    nrow(gr_summary), nrow(gr_speedup), nrow(gr_scaling_summary), nrow(ir_summary),
    nrow(repeat_summary), nrow(derived_summary), nrow(publication_table)
  )
)
```

## GRanges Overlap Runtime

### Absolute runtime by workload

```{r gr-runtime-bar, fig.width=12, fig.height=5}
gr_stats$engine <- factor(gr_stats$engine, levels = engine_levels)

p_gr_runtime <- ggplot(gr_stats, aes(x = engine, y = median_sec, fill = engine)) +
  geom_col(width = 0.72) +
  geom_errorbar(aes(ymin = q25_sec, ymax = q75_sec), width = 0.18, linewidth = 0.7) +
  facet_wrap(~scenario, scales = "free_y") +
  scale_fill_manual(values = engine_palette, guide = "none") +
  scale_y_continuous(labels = label_number(accuracy = 0.01)) +
  labs(
    title = "Absolute runtime for GRanges overlap workloads",
    subtitle = "Bars show medians; error bars show interquartile range across iterations",
    x = NULL,
    y = "Median runtime (sec)"
  ) +
  theme_pub() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

p_gr_runtime
save_plot(p_gr_runtime, "interpret_gr_runtime_bar.png", width = 12, height = 5)
```

Interpretation:

`fastRanges_indexed` is the absolute winner in every stored `GRanges`
scenario. The gap is not marginal: indexed `fastRanges` reduces median runtime
from `r sprintf('%.3f', min(publication_table$median_sec[publication_table$engine == "GenomicRanges_findOverlaps"]))`
 to `r sprintf('%.3f', min(publication_table$median_sec[publication_table$engine == "fastRanges_indexed"]))`
 seconds in the smallest saved scenario and remains clearly ahead at the
 largest one. `fastRanges_direct` is still faster than
 `GenomicRanges_findOverlaps`, but the largest improvement comes from avoiding
 repeated subject preprocessing through index reuse.

### Relative speedup against `GenomicRanges::findOverlaps()`

```{r gr-speedup-bar, fig.width=12, fig.height=5}
gr_stats$engine <- factor(gr_stats$engine, levels = engine_levels)

p_gr_speedup <- ggplot(gr_stats, aes(x = engine, y = speedup, fill = engine)) +
  geom_col(width = 0.72) +
  geom_errorbar(aes(ymin = speedup_low, ymax = speedup_high), width = 0.18, linewidth = 0.7) +
  facet_wrap(~scenario, scales = "free_y") +
  geom_hline(yintercept = 1, linetype = 2, color = "grey40") +
  scale_fill_manual(values = engine_palette, guide = "none") +
  scale_y_continuous(labels = label_number(accuracy = 0.1)) +
  labs(
    title = "Speedup relative to GenomicRanges baseline",
    subtitle = "Dashed line marks parity with GenomicRanges::findOverlaps()",
    x = NULL,
    y = "Speedup vs baseline (x)"
  ) +
  theme_pub() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

p_gr_speedup
save_plot(p_gr_speedup, "interpret_gr_speedup_bar.png", width = 12, height = 5)
```

Interpretation:

The speedup framing makes the hierarchy clearer. `GenomicRanges_GNCList`
already improves materially over plain `findOverlaps()`, which confirms that
subject preprocessing matters. `fastRanges_direct` adds another layer of
improvement. `fastRanges_indexed` is the strongest mode because it combines
fast overlap execution with reusable subject organization. Across the saved
scenarios, its observed gain ranges from about
`r sprintf('%.2f', min(gr_stats$speedup[gr_stats$engine == "fastRanges_indexed"]))`x
to
`r sprintf('%.2f', max(gr_stats$speedup[gr_stats$engine == "fastRanges_indexed"]))`x
relative to the GenomicRanges baseline.

### Speedup heatmap across workload size

```{r gr-speedup-heatmap, fig.width=10, fig.height=5}
heat_tbl <- gr_stats %>%
  filter(engine != "GenomicRanges_findOverlaps") %>%
  mutate(
    scenario = factor(scenario, levels = unique(gr_stats$scenario)),
    engine = factor(engine, levels = engine_levels)
  )

p_gr_heat <- ggplot(heat_tbl, aes(x = scenario, y = engine, fill = speedup)) +
  geom_tile(color = "white") +
  geom_text(aes(label = sprintf("%.2fx", speedup)), size = 4) +
  scale_fill_gradient(low = "#F6D6D6", high = "#00798C") +
  labs(
    title = "Speedup landscape across GRanges workloads",
    x = "Scenario",
    y = NULL,
    fill = "Speedup"
  ) +
  theme_pub() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

p_gr_heat
save_plot(p_gr_heat, "interpret_gr_speedup_heatmap.png", width = 10, height = 5)
```

Interpretation:

This heatmap shows that the engine ranking is stable across increasing problem
size. That matters for a manuscript because it argues against a "small-case
only" effect. The indexed path remains dominant as the workload moves from
`400k vs 2M` to `1.6M vs 8M`, indicating that the performance advantage scales
with subject reuse rather than disappearing at larger input sizes.

### Throughput view

```{r gr-throughput, fig.width=11, fig.height=5}
throughput_tbl <- gr_stats %>%
  mutate(
    comparisons_millions = (query_n * subject_n) / 1e12,
    queries_per_sec = query_n / median_sec,
    queries_per_sec_low = query_n / q75_sec,
    queries_per_sec_high = query_n / q25_sec
  )

p_gr_throughput <- ggplot(
  throughput_tbl,
  aes(x = subject_n / 1e6, y = queries_per_sec / 1e6, color = engine, group = engine)
) +
  geom_errorbar(aes(ymin = queries_per_sec_low / 1e6, ymax = queries_per_sec_high / 1e6), width = 0.08, linewidth = 0.7) +
  geom_line(linewidth = 1) +
  geom_point(size = 3) +
  scale_color_manual(values = engine_palette) +
  scale_x_continuous(labels = label_number(accuracy = 0.1)) +
  scale_y_continuous(labels = label_number(accuracy = 0.1)) +
  labs(
    title = "Effective query throughput across GRanges scenarios",
    subtitle = "Higher is better",
    x = "Subject size (millions of ranges)",
    y = "Queries processed per second (millions)",
    color = "Engine"
  ) +
  theme_pub()

p_gr_throughput
save_plot(p_gr_throughput, "interpret_gr_throughput_line.png", width = 11, height = 5)
```

Interpretation:

Absolute runtime is easiest to report, but throughput is often easier to
translate into pipeline planning. The same ranking appears here:
`fastRanges_indexed` processes the most queries per second across all stored
GRanges workloads. `fastRanges_direct` remains competitive, but its throughput
drops more sharply than the indexed path because indexing work is effectively
paid inside each run.

### Runtime stability across iterations

```{r gr-variability, fig.width=11, fig.height=5}
gr_var <- gr_raw %>%
  group_by(scenario, engine) %>%
  summarise(
    min_sec = min(elapsed_sec),
    median_sec = median(elapsed_sec),
    max_sec = max(elapsed_sec),
    .groups = "drop"
  )

p_gr_var <- ggplot(gr_var, aes(x = engine, y = median_sec, color = engine)) +
  geom_linerange(aes(ymin = min_sec, ymax = max_sec), linewidth = 1) +
  geom_point(size = 3) +
  facet_wrap(~scenario, scales = "free_y") +
  scale_color_manual(values = engine_palette, guide = "none") +
  labs(
    title = "Iteration-level runtime spread for GRanges workloads",
    subtitle = "Points show medians; ranges show min to max",
    x = NULL,
    y = "Elapsed time (sec)"
  ) +
  theme_pub() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

p_gr_var
save_plot(p_gr_var, "interpret_gr_variability.png", width = 11, height = 5)
```

Interpretation:

The min-to-max spread is generally narrow, which supports using median runtime
as a stable summary. A few wider bars in direct mode reflect occasional
index-building overhead or memory-system variation. That pattern reinforces the
design rationale for separate indexed and direct modes: one is for convenience,
the other is for repeated high-throughput execution.

## GRanges Thread Scaling on a Denser Workload

This section interprets the additional GRanges scaling workload added to the
main benchmark. Unlike the broad end-to-end scenarios above, this design uses a
smaller coordinate span and fewer chromosomes, which increases overlap density
and makes traversal cost more visible.

### Absolute runtime versus thread count

```{r gr-scaling-runtime, fig.width=10, fig.height=5}
gr_scaling_fast <- gr_scaling_stats %>% filter(grepl("^fastRanges", engine))
gr_scaling_serial <- gr_scaling_stats %>% filter(grepl("^GenomicRanges", engine))

p_gr_scaling_runtime <- ggplot(gr_scaling_fast, aes(x = threads, y = median_sec, color = engine)) +
  geom_errorbar(aes(ymin = q25_sec, ymax = q75_sec), width = 1.5, linewidth = 0.7) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 3) +
  geom_hline(
    data = gr_scaling_serial,
    aes(yintercept = median_sec, color = engine),
    linetype = 2,
    linewidth = 0.9
  ) +
  geom_point(
    data = gr_scaling_serial,
    aes(x = threads, y = median_sec, color = engine),
    size = 3
  ) +
  scale_color_manual(values = engine_palette) +
  scale_x_continuous(breaks = sort(unique(gr_scaling_stats$threads))) +
  scale_y_continuous(labels = label_number(accuracy = 0.1)) +
  labs(
    title = "GRanges thread scaling on a denser workload",
    subtitle = "GenomicRanges baselines are serial reference lines",
    x = "Threads",
    y = "Median runtime (sec)",
    color = "Engine"
  ) +
  theme_pub()

p_gr_scaling_runtime
save_plot(p_gr_scaling_runtime, "interpret_gr_scaling_runtime.png", width = 10, height = 5)
```

Interpretation:

This plot separates two effects that were blended in the end-to-end GRanges
scenarios. `fastRanges_direct_GRanges` shows the gain from the current overlap
kernel with no prebuilt index reuse. `fastRanges_indexed_GRanges` shows the
additional advantage of reusing subject organization across calls. The serial
GenomicRanges reference lines remain well above the indexed `fastRanges`
trajectory, so the dense workload does not overturn the main ranking.

### Speedup versus one thread

```{r gr-scaling-speedup, fig.width=10, fig.height=5}
p_gr_scaling_speedup <- ggplot(
  gr_scaling_fast,
  aes(x = threads, y = speedup_vs_1_thread, color = engine)
) +
  geom_errorbar(aes(ymin = speedup_low, ymax = speedup_high), width = 1.5, linewidth = 0.7) +
  geom_hline(yintercept = 1, linetype = 2, color = "grey40") +
  geom_line(linewidth = 1.1) +
  geom_point(size = 3) +
  scale_color_manual(values = engine_palette) +
  scale_x_continuous(breaks = sort(unique(gr_scaling_fast$threads))) +
  labs(
    title = "GRanges speedup versus 1 thread",
    subtitle = "Dense scaling workload",
    x = "Threads",
    y = "Speedup vs 1 thread (x)",
    color = "Engine"
  ) +
  theme_pub()

p_gr_scaling_speedup
save_plot(p_gr_scaling_speedup, "interpret_gr_scaling_speedup.png", width = 10, height = 5)
```

Interpretation:

The dense GRanges scaling panel answers a question the older benchmark could
not: how much of the GRanges gain is from threading once the workload becomes
overlap-rich? The indexed path reaches about
`r sprintf('%.2f', max(gr_scaling_fast$speedup_vs_1_thread[gr_scaling_fast$engine == "fastRanges_indexed_GRanges"], na.rm = TRUE))`x
relative to its own 1-thread baseline, while the direct path also improves but
remains slower. That supports a cleaner manuscript claim: `fastRanges` is not
only faster in absolute GRanges runtime, it also benefits from thread scaling
when overlap density is sufficiently high.

## Repeated-Query Workload

This workload is especially important biologically because many pipelines reuse
the same subject annotation across many samples, bootstrap resamples, or
per-chromosome query batches.

### Absolute repeated-query runtime

```{r repeat-runtime, fig.width=9, fig.height=5}
repeat_stats$engine <- factor(repeat_stats$engine, levels = engine_levels)

p_repeat_runtime <- ggplot(repeat_stats, aes(x = engine, y = median_sec, fill = engine)) +
  geom_col(width = 0.72) +
  geom_errorbar(aes(ymin = q25_sec, ymax = q75_sec), width = 0.18, linewidth = 0.7) +
  scale_fill_manual(values = engine_palette, guide = "none") +
  labs(
    title = "Repeated-query workload runtime",
    subtitle = "Bars show medians; error bars show interquartile range",
    x = NULL,
    y = "Median runtime (sec)"
  ) +
  theme_pub() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

p_repeat_runtime
save_plot(p_repeat_runtime, "interpret_repeat_runtime_bar.png", width = 9, height = 5)
```

Interpretation:

This is the workload where indexing should matter most, and the result matches
that expectation. `fastRanges_index_reuse` finishes in about
`r sprintf('%.2f', repeat_stats$median_sec[repeat_stats$engine == "fastRanges_index_reuse"])`
seconds versus
`r sprintf('%.2f', repeat_stats$median_sec[repeat_stats$engine == "GenomicRanges_findOverlaps"])`
seconds for the plain GenomicRanges baseline. That is a large end-to-end
improvement for one of the most realistic deployment modes of the package.

### Relative repeated-query gains

```{r repeat-speedup, fig.width=9, fig.height=5}
p_repeat_speedup <- ggplot(repeat_stats, aes(x = engine, y = speedup, fill = engine)) +
  geom_col(width = 0.72) +
  geom_errorbar(aes(ymin = speedup_low, ymax = speedup_high), width = 0.18, linewidth = 0.7) +
  geom_hline(yintercept = 1, linetype = 2, color = "grey40") +
  scale_fill_manual(values = engine_palette, guide = "none") +
  labs(
    title = "Repeated-query speedup relative to GenomicRanges baseline",
    x = NULL,
    y = "Speedup vs baseline (x)"
  ) +
  theme_pub() +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

p_repeat_speedup
save_plot(p_repeat_speedup, "interpret_repeat_speedup_bar.png", width = 9, height = 5)
```

Interpretation:

The repeated-query benchmark is the clearest argument for index reuse in
practice. The indexed `fastRanges` mode achieves about
`r sprintf('%.2f', repeat_stats$speedup[repeat_stats$engine == "fastRanges_index_reuse"])`x
speedup relative to the baseline and remains well ahead of both direct
`fastRanges` and `GenomicRanges_GNCList`. This is the strongest evidence that
the package is not merely a faster one-shot overlap engine, but a better fit
for subject-reuse workflows.

## IRanges Thread Scaling

The `IRanges` benchmark isolates an interval-heavy workload with lower
genomic-class overhead and emphasizes thread scaling behavior on a 96-core
machine.

### Speedup versus one thread

```{r ir-speedup, fig.width=10, fig.height=5}
ir_stats$engine <- factor(ir_stats$engine, levels = engine_levels)

p_ir_speedup <- ggplot(ir_stats, aes(x = threads, y = speedup_vs_1_thread, color = engine)) +
  geom_errorbar(aes(ymin = speedup_low, ymax = speedup_high), width = 1.5, linewidth = 0.7) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 3) +
  scale_color_manual(values = engine_palette) +
  scale_x_continuous(breaks = sort(unique(ir_summary$threads))) +
  labs(
    title = "Thread scaling on IRanges workloads",
    subtitle = "Speedup is normalized within each engine to its 1-thread runtime",
    x = "Threads",
    y = "Speedup vs 1 thread (x)",
    color = "Engine"
  ) +
  theme_pub()

p_ir_speedup
save_plot(p_ir_speedup, "interpret_ir_speedup_line.png", width = 10, height = 5)
```

Interpretation:

Two separate facts matter here.

First, `fastRanges_indexed_IRanges` is already much faster in absolute time at
one thread, so it starts from a stronger serial baseline.

Second, its own scaling is non-monotone at low thread counts but improves
substantially at high core counts, reaching about
`r sprintf('%.2f', max(ir_stats$speedup_vs_1_thread[ir_stats$engine == "fastRanges_indexed_IRanges"]))`x
at the best stored point. `IRanges_nthread` scales more smoothly at small
thread counts, but because its serial baseline is much slower, its best
absolute runtime remains far above the indexed `fastRanges` path.

### Absolute runtime across thread counts

```{r ir-absolute-runtime, fig.width=10, fig.height=5}
p_ir_abs <- ggplot(ir_stats, aes(x = threads, y = median_sec, color = engine)) +
  geom_errorbar(aes(ymin = q25_sec, ymax = q75_sec), width = 1.5, linewidth = 0.7) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 3) +
  scale_color_manual(values = engine_palette) +
  scale_x_continuous(breaks = sort(unique(ir_stats$threads))) +
  scale_y_continuous(trans = "log10", labels = label_number(accuracy = 0.1)) +
  labs(
    title = "Absolute IRanges runtime across thread counts",
    subtitle = "Log-scaled y-axis to compare widely separated runtimes",
    x = "Threads",
    y = "Median runtime (sec, log10 scale)",
    color = "Engine"
  ) +
  theme_pub()

p_ir_abs
save_plot(p_ir_abs, "interpret_ir_absolute_runtime.png", width = 10, height = 5)
```

Interpretation:

This plot answers the most practical question: "which setting actually
finishes first?" The answer is `fastRanges_indexed_IRanges` at every recorded
thread count. The smallest median runtime in the table is
`r sprintf('%.2f', min(ir_stats$median_sec))` seconds, achieved by
`r ir_stats$engine[which.min(ir_stats$median_sec)]` at
`r ir_stats$threads[which.min(ir_stats$median_sec)]` threads.

### Absolute winner profile

```{r ir-winner-bar, fig.width=9, fig.height=5}
winner_stats <- ir_stats %>% filter(engine == "fastRanges_indexed_IRanges")

p_ir_winner <- ggplot(winner_stats, aes(x = factor(threads), y = median_sec, fill = factor(threads))) +
  geom_col(width = 0.72) +
  geom_errorbar(aes(ymin = q25_sec, ymax = q75_sec), width = 0.18, linewidth = 0.7) +
  scale_fill_manual(values = thread_palette, guide = "none") +
  labs(
    title = "Absolute winner profile for the indexed fastRanges IRanges workload",
    subtitle = "Only the winning engine is shown because it was fastest at every thread point",
    x = "Threads",
    y = "Median runtime (sec)"
  ) +
  theme_pub()

p_ir_winner
save_plot(p_ir_winner, "interpret_ir_winner_bar.png", width = 9, height = 5)
```

Interpretation:

This view isolates the practical operating range for the winning configuration.
On this hardware, the stored results suggest that performance continues to
improve through 96 threads, although the gains from 64 to 96 are smaller than
the gains from 8 to 32 or 32 to 64. That is the usual signature of approaching
hardware saturation rather than a regression.

### Incremental gain between thread steps

```{r ir-delta-gain, fig.width=10, fig.height=5}
delta_tbl <- ir_stats %>%
  arrange(engine, threads) %>%
  group_by(engine) %>%
  mutate(
    prev_threads = lag(threads),
    incremental_speedup = speedup_vs_1_thread / lag(speedup_vs_1_thread)
  ) %>%
  filter(!is.na(prev_threads))

p_ir_delta <- ggplot(delta_tbl, aes(x = threads, y = incremental_speedup, color = engine)) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 3) +
  geom_hline(yintercept = 1, linetype = 2, color = "grey40") +
  scale_color_manual(values = engine_palette) +
  scale_x_continuous(breaks = sort(unique(ir_stats$threads))) +
  labs(
    title = "Marginal gain from adding more threads",
    subtitle = "Values above 1 indicate additional speedup relative to the previous thread point",
    x = "Threads",
    y = "Incremental gain",
    color = "Engine"
  ) +
  theme_pub()

p_ir_delta
save_plot(p_ir_delta, "interpret_ir_incremental_gain.png", width = 10, height = 5)
```

Interpretation:

This plot is useful for deployment decisions. It shows where extra cores still
buy meaningful improvement and where they mostly add cost. For the stored
indexed `fastRanges` run, the most valuable jump occurs after the low-thread
region, which is consistent with a fixed-overhead model: once enough threads
are available, the large interval workload begins to dominate and scaling
becomes worthwhile.

## Derived API Benchmarks

These benchmarks test whether higher-level overlap summaries retain the same
performance advantage or whether the gains disappear once downstream work is
included.

### Absolute runtime across derived tasks

```{r derived-runtime, fig.width=10, fig.height=5}
derived_stats$task <- factor(
  derived_stats$task,
  levels = c("aggregate_sum", "count_by_group", "self_overlaps", "window_count")
)
derived_stats$engine <- factor(derived_stats$engine, levels = engine_levels)

p_derived_runtime <- ggplot(derived_stats, aes(x = task, y = median_sec, fill = engine)) +
  geom_col(position = position_dodge(width = 0.75), width = 0.7) +
  geom_errorbar(
    aes(ymin = q25_sec, ymax = q75_sec, group = engine),
    position = position_dodge(width = 0.75),
    width = 0.18,
    linewidth = 0.7
  ) +
  scale_fill_manual(values = engine_palette) +
  labs(
    title = "Absolute runtime for derived overlap tasks",
    subtitle = "Bars show medians; error bars show interquartile range",
    x = "Task",
    y = "Median runtime (sec)",
    fill = "Engine"
  ) +
  theme_pub()

p_derived_runtime
save_plot(p_derived_runtime, "interpret_derived_runtime_bar.png", width = 10, height = 5)
```

Interpretation:

The performance advantage is not limited to raw overlap search. For
`aggregate_sum`, `count_by_group`, `self_overlaps`, and `window_count`,
`fastRanges` remains faster than the Bioconductor baseline. This matters
because real pipelines often spend substantial time in summary steps after
overlap detection. A faster overlap engine is less valuable if downstream
wrappers immediately give that gain back, but that is not what the saved
results show here.

### Derived-task speedup plot

```{r derived-speedup, fig.width=10, fig.height=5}
p_derived_speedup <- ggplot(
  derived_stats %>% filter(engine == "fastRanges"),
  aes(x = task, y = speedup)
) +
  geom_segment(aes(xend = task, y = 1, yend = speedup), linewidth = 1, color = fast_color) +
  geom_errorbar(aes(ymin = speedup_low, ymax = speedup_high), width = 0.18, linewidth = 0.7, color = fast_color) +
  geom_point(size = 4, color = fast_color) +
  geom_hline(yintercept = 1, linetype = 2, color = "grey40") +
  scale_y_continuous(labels = label_number(accuracy = 0.1)) +
  labs(
    title = "Derived-task speedup of fastRanges over Bioconductor baselines",
    x = "Task",
    y = "Speedup (x)"
  ) +
  theme_pub()

p_derived_speedup
save_plot(p_derived_speedup, "interpret_derived_speedup_dot.png", width = 10, height = 5)
```

Interpretation:

The strongest gains appear in `aggregate_sum` and `count_by_group`, where the
speedups are approximately
`r sprintf('%.2f', max(derived_stats$speedup[derived_stats$engine == "fastRanges"]))`x
at best. `self_overlaps` and `window_count` still improve, but more modestly.
That pattern is biologically plausible: some tasks remain dominated by overlap
search, while others include more fixed-cost summarization work that reduces
the visible speedup.

### Derived-task gain heatmap

```{r derived-heatmap, fig.width=8, fig.height=4}
derived_fast <- derived_stats %>%
  filter(engine == "fastRanges") %>%
  mutate(
    label = sprintf("%.2fx", speedup)
  )

p_derived_heat <- ggplot(derived_fast, aes(x = task, y = "fastRanges", fill = speedup)) +
  geom_tile(color = "white") +
  geom_text(aes(label = label), size = 4) +
  scale_fill_gradient(low = "#F6D6D6", high = "#00798C") +
  labs(
    title = "Heatmap view of derived-task gains",
    x = "Task",
    y = NULL,
    fill = "Speedup"
  ) +
  theme_pub()

p_derived_heat
save_plot(p_derived_heat, "interpret_derived_heatmap.png", width = 8, height = 4)
```

Interpretation:

This compact panel is useful in a manuscript supplement because it compresses
the downstream-task story into a single glance. The gains are consistently
above parity, which supports the claim that the package advantage propagates
beyond the core overlap call.

## Publication Summary Table

```{r publication-summary}
publication_table
```

Interpretation:

This table is the shortest defensible summary of the end-to-end GRanges engine
comparison. In every stored scenario:

- `fastRanges_indexed` is fastest
- `GenomicRanges_GNCList` is the strongest Bioconductor-native comparator
- `fastRanges_direct` improves over plain `GenomicRanges_findOverlaps`, but not
  as strongly as indexed reuse

That is a coherent performance story rather than a mixed result.

## Practical Conclusions

### Main conclusions

1. For one-off `GRanges` overlap calls, `fastRanges_direct` is already faster
   than plain `GenomicRanges::findOverlaps()` in the stored scenarios.
2. For repeated-query workflows, index reuse is the dominant optimization.
   `fastRanges_indexed` and `fastRanges_index_reuse` deliver the strongest
   gains and are the most relevant modes for production pipelines.
3. On the large `IRanges` workload, indexed `fastRanges` is the absolute
   winner at every saved thread count, even though its low-thread scaling is not
   perfectly monotone.
4. The performance gains extend to derived APIs such as grouped counts,
   aggregation, and windowed summaries, which strengthens the case for using
   `fastRanges` as a workflow package rather than only a kernel replacement.


## Figure Outputs

```{r figure-inventory}
data.frame(file = list.files(figures_dir, full.names = FALSE))
```

## Session Info

```{r session-info}
sessionInfo()
```
