---
title: "iscream vs Rsamtools::scanTabix"
output:
  BiocStyle::html_document:
    toc_float: true
vignette: >
  %\VignetteIndexEntry{iscream vs Rsamtools::scanTabix}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
link-citations: yes
---

```{r, include=FALSE}
knitr::opts_chunk$set(
  error = TRUE,
  fig.path = "../man/figures/",
  fig.width = 10,
  fig.height = 8,
  fig.align = "center"
)
```

Both iscream and Rsamtools can be used to query records from tabixed BED files
in R. Here we compare their usability and performance using the methscan
dataset.

This vignette uses mouse WGBS data from *methscan* as in the [Plotting TSS
profiles tutorial](./TSS.Rmd) and mouse promoter regions. Running it requires
downloading 18MB of BED files and tabix indices from this Zenodo record:
<https://zenodo.org/records/14733834>

```{r download_data}
library("BiocFileCache") |> suppressPackageStartupMessages()
cachedir <- BiocFileCache()
methscan_zip_path <- bfcrpath(cachedir, "https://zenodo.org/records/14733834/files/methscan_data.zip")
methscan_unzip <- file.path(tempdir(), "methscan")
unzip(methscan_zip_path, exdir = methscan_unzip)
methscan_dir <- file.path(methscan_unzip, "scbs_tutorial_data")
```

iscream normally uses htslib to query BED files and store the data in memory.
However, iscream's `tabix()` function can use the tabix executable to make tabix
queries because the shell program's stream-to-file approach is faster than
allocating and storing strings in memory. By default, the `"tabix.method"`
option is set to "shell" and iscream's `tabix()` will look for the tabix
executable, falling back to using htslib only if it's not found. Here, the
`"tabix.method"` option is set to "htslib" to make fair comparisons between
Rsamtools and iscream since `scanTabix` does not use the executable, but stores
the strings in memory.

```{r}
library(iscream)
options("tabix.method" = 'htslib')
options("iscream.threads" = 8)
```

Using `scanTabix` requires the input regions to be `GRanges`. `iscream::tabix`
accepts strings, data frames and GRanges.

```{r}
library(data.table)
library(GenomicRanges) |> suppressPackageStartupMessages()
library(Rsamtools) |> suppressPackageStartupMessages()
library(microbenchmark)
library(pbapply)
library(ggplot2)
```

```{r}
methscan_files <- list.files(
  methscan_dir,
  full.names = T,
  pattern = "*.cov.gz$"
)
mouse_promoters <- fread(file.path(methscan_dir, "mouse_promoters.bed"), col.names = c("chr", "start", "end"))
mouse_promoters.gr <- GRanges(mouse_promoters)
```


# One file

`tabix_raw()` and `scanTabix` produce a list of unparsed or raw strings. The
result of both are identical.

```{r bench_1_raw, fig.cap="tabix vs scanTabix raw string output on 1 file", dpi=300}
bench_1_raw <- microbenchmark(
  `rsamtools 1 file raw` = rq <- scanTabix(methscan_files[1], param = mouse_promoters.gr),
  `iscream 1 file raw` = iq <- tabix_raw(methscan_files[1], mouse_promoters),
  times = 3
)
bench_1_raw
autoplot(bench_1_raw)
iq[1:5]
all.equal(iq, rq)
```


# Multiple files

`tabix()` can also take multiple files in the same call. It returns the raw
strings, but each input file is a list of it's own. `scanTabix()` does not
support this.

```{r}
bench_30_raw <- microbenchmark(
  `iscream 30 files raw` = iq <- tabix_raw(methscan_files, mouse_promoters),
  times = 3
)
bench_30_raw
names(iq)
iq[["cell_01"]][1:5]

scanTabix(methscan_files, param = GRanges(mouse_promoters))
```

# Parsing records into a data frame

A parsed data frame is more useful than a list of raw strings - use `tabix()`
instead of `tabix_raw()`. For `scanTabix`, this custom function parses the list
of strings to make a similar data frame:

```{r}
rtbx <- function(bed) {
  q <- scanTabix(bed, param = GRanges(mouse_promoters)) |>
    lapply(strsplit, split = "\t") |>
    Filter(function(i) length(i) != 0, x = _) |>
    unlist(recursive = FALSE) |>
    do.call(rbind, args = _) |>
    as.data.table() |>
    setnames(c("chr", "start", "end", paste0("V", 1:3)))
  q
}

bench_1_df <- microbenchmark(
  `rsamtools 1 file data frame` = rq <- rtbx(methscan_files[1]),
  `iscream 1 file data frame` = iq <- tabix(methscan_files[1], mouse_promoters),
  times = 3
)
rq
iq
all.equal(iq, rq, check.attributes = F)
```

The column types are different but the data is identical.

```{r bench_1_df, fig.cap="tabix vs scanTabix parsed data frame from 1 file", dpi=300}
bench_1_df
autoplot(bench_1_df)
```

# Multiple files as data frame

We can try to query multiple BED files using Rsamtools with this function that
8 cores like iscream does:

```{r bench_30_df, fig.cap="tabix vs scanTabix parsed data frame from 30 files", dpi=300}
partbx <- function(bedfiles) {
  pblapply(
    methscan_files,
    function(bed) {
      query <- rtbx(bed)[, file := tools::file_path_sans_ext(basename(bed), compression = TRUE)][]
      setnames(query, c("chr", "start", "end", "beta", "M", "U", "file"))
      query
    },
    cl = 8
  ) |>
    rbindlist()
}

bench_30_df <- microbenchmark(
  `rsamtools 30 file data frame` = rq <- partbx(methscan_files),
  `iscream 30 file data frame` = iq <- tabix(methscan_files, mouse_promoters, col.names = c("beta", "M", "U")),
  times = 3
)
bench_30_df
autoplot(bench_30_df)
```


## All benchmarks {.tabset}

```{r}
bench_all <- rbind(bench_1_raw, bench_30_raw, bench_1_df, bench_30_raw, bench_30_df)

bench.dt <- as.data.table(bench_all)[, .(
  time       = time / 1000000,
  package    = fifelse(grepl("iscream", expr), "iscream", "rsamtools"),
  files      = fifelse(grepl("1", expr), 1, 30),
  data.frame = fifelse(grepl("raw", expr), FALSE, TRUE)
)][,
  `data structure` := fifelse(data.frame, "data.frame", "raw-strings")
]
```


### Runtime

```{r all, fig.cap="Comparing all benchmarked iscream and Rsamtools querying runtimes", dpi=300}
ggplot(bench.dt, aes(x = as.factor(files), y = time / 1000, color = as.factor(package))) +
  geom_boxplot(position = position_dodge(preserve = "single")) +
  scale_color_discrete(name = "Package") +
  scale_y_continuous(limits = c(0, 35), breaks = scales::extended_breaks(10)) +
  facet_wrap(~ `data structure`, labeller = "label_both") +
  labs(x = "File count", y = "Runtime (seconds)") +
  theme_bw()
```

## Session info

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

<!-- vim: set filetype=rmd: -->
