This report reads the outputs produced by scripts/test_decoupled_pipeline_star_fullsize.sh (or the BWA variant) and compares the joint -W 1 pipeline’s BSJ/FSJ matrices against the decoupled SCAN1 → BUILD_UNIVERSE → SCAN2 → FINALIZE matrices.

To rerender after a new test run:

Rscript -e "rmarkdown::render('scripts/validation_report.Rmd')"

or, to point at a different output directory:

Rscript -e "rmarkdown::render('scripts/validation_report.Rmd', params=list(out_root='/path/to/out_root'))"

Test configuration

out_root        <- params$out_root
orig_dir        <- file.path(out_root, "original")
decoupled_dir   <- file.path(out_root, "decoupled")
finalize_dir    <- file.path(decoupled_dir, "finalize")
bench_dir       <- file.path(out_root, "bench")

required <- c(
  "original/result.BSJ_Matrix",
  "original/result.FSJ_Matrix",
  "decoupled/finalize/result.BSJ_Matrix",
  "decoupled/finalize/result.FSJ_Matrix"
)
missing <- required[!file.exists(file.path(out_root, required))]
if (length(missing) > 0) {
  stop("Missing expected input files:\n  ",
       paste(file.path(out_root, missing), collapse = "\n  "))
}
tibble(key = c("out_root", "orig_dir", "decoupled_dir"),
       value = c(out_root, orig_dir, decoupled_dir)) |>
  kable(caption = "Paths used by this report")
Paths used by this report
key value
out_root /pastel/tools/circRNA_tools/test_data/decoupled_comparison
orig_dir /pastel/tools/circRNA_tools/test_data/decoupled_comparison/original
decoupled_dir /pastel/tools/circRNA_tools/test_data/decoupled_comparison/decoupled

Load matrices

joint_BSJ     <- read_tsv(file.path(orig_dir, "result.BSJ_Matrix"))
joint_FSJ     <- read_tsv(file.path(orig_dir, "result.FSJ_Matrix"))
decoupled_BSJ <- read_tsv(file.path(finalize_dir, "result.BSJ_Matrix"))
decoupled_FSJ <- read_tsv(file.path(finalize_dir, "result.FSJ_Matrix"))

# Joint writes sample columns by input-file basename (e.g. "bwa.sam");
# decoupled uses the sampleName column from finalize_samples.tsv. They mean
# the same thing in the same order, so align positionally using the decoupled
# names — those are more informative.
align_cols <- function(df, ref_names) {
  stopifnot(ncol(df) == length(ref_names) + 1)
  colnames(df) <- c("circRNA_ID", ref_names)
  df
}
ref_names    <- colnames(decoupled_BSJ)[-1]
joint_BSJ    <- align_cols(joint_BSJ,    ref_names)
joint_FSJ    <- align_cols(joint_FSJ,    ref_names)

tibble(
  matrix     = c("BSJ joint", "BSJ decoupled", "FSJ joint", "FSJ decoupled"),
  nrow       = c(nrow(joint_BSJ), nrow(decoupled_BSJ),
                 nrow(joint_FSJ), nrow(decoupled_FSJ)),
  n_samples  = length(ref_names)
) |> kable(caption = "Matrix dimensions (sample columns aligned positionally)")
Matrix dimensions (sample columns aligned positionally)
matrix nrow n_samples
BSJ joint 35869 4
BSJ decoupled 35869 4
FSJ joint 35869 4
FSJ decoupled 35869 4
cat("Sample column names used:\n")
## Sample column names used:
cat(paste0("  ", ref_names, collapse = "\n"), "\n")
##   Div_100_S91
##   Div_101_S92
##   PARDOS_1_S1
##   PARDOS_2_S2

circRNA set agreement

both   <- intersect(joint_BSJ$circRNA_ID, decoupled_BSJ$circRNA_ID)
only_j <- setdiff(joint_BSJ$circRNA_ID, decoupled_BSJ$circRNA_ID)
only_d <- setdiff(decoupled_BSJ$circRNA_ID, joint_BSJ$circRNA_ID)

tibble(
  set = c("circRNAs in both pipelines",
          "joint only",
          "decoupled only",
          "Jaccard (intersection / union)"),
  n   = c(length(both),
          length(only_j),
          length(only_d),
          round(length(both) /
                  length(union(joint_BSJ$circRNA_ID,
                               decoupled_BSJ$circRNA_ID)), 4))
) |> kable(caption = "circRNA ID set agreement")
circRNA ID set agreement
set n
circRNAs in both pipelines 35869
joint only 0
decoupled only 0
Jaccard (intersection / union) 1

Sample joint-only IDs (first 20)

head(only_j, 20) |> as_tibble() |> kable(col.names = "joint-only circRNA_ID")
joint-only circRNA_ID

Sample decoupled-only IDs (first 20)

head(only_d, 20) |> as_tibble() |> kable(col.names = "decoupled-only circRNA_ID")
decoupled-only circRNA_ID

BSJ matrix agreement

bsj_merged <- full_join(joint_BSJ, decoupled_BSJ,
                        by = "circRNA_ID",
                        suffix = c(".joint", ".decoupled"))

bsj_long <- bsj_merged |>
  pivot_longer(cols = -circRNA_ID,
               names_to = c("Sample", "Run"),
               names_pattern = "(.*)\\.(joint|decoupled)$") |>
  pivot_wider(names_from = Run, values_from = value) |>
  replace_na(list(joint = 0, decoupled = 0))

bsj_long |>
  group_by(Sample) |>
  summarise(
    n_rows            = n(),
    exactly_equal     = sum(joint == decoupled),
    joint_greater     = sum(joint > decoupled),
    decoupled_greater = sum(decoupled > joint),
    max_abs_diff      = max(abs(joint - decoupled)),
    pct_exact         = round(100 * mean(joint == decoupled), 2)
  ) |> kable(caption = "BSJ matrix: per-sample cell-level agreement")
BSJ matrix: per-sample cell-level agreement
Sample n_rows exactly_equal joint_greater decoupled_greater max_abs_diff pct_exact
Div_100_S91 35869 35869 0 0 0 100
Div_101_S92 35869 35869 0 0 0 100
PARDOS_1_S1 35869 35869 0 0 0 100
PARDOS_2_S2 35869 35869 0 0 0 100

BSJ scatter: joint vs. decoupled

ggplot(bsj_long, aes(x = joint, y = decoupled)) +
  geom_point(alpha = 0.4, size = 0.6) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "red") +
  facet_wrap(~ Sample, scales = "free") +
  labs(x = "Joint pipeline BSJ count",
       y = "Decoupled pipeline BSJ count",
       title = "BSJ counts per circRNA per sample") +
  theme_bw()

BSJ cell-level discordance (top 20)

bsj_long |>
  mutate(abs_diff = abs(joint - decoupled)) |>
  filter(abs_diff > 0) |>
  arrange(desc(abs_diff)) |>
  head(20) |> kable(caption = "Largest per-cell BSJ disagreements")
Largest per-cell BSJ disagreements
circRNA_ID Sample joint decoupled abs_diff

FSJ matrix agreement

fsj_merged <- full_join(joint_FSJ, decoupled_FSJ,
                        by = "circRNA_ID",
                        suffix = c(".joint", ".decoupled"))
fsj_long <- fsj_merged |>
  pivot_longer(cols = -circRNA_ID,
               names_to = c("Sample", "Run"),
               names_pattern = "(.*)\\.(joint|decoupled)$") |>
  pivot_wider(names_from = Run, values_from = value) |>
  replace_na(list(joint = 0, decoupled = 0))

fsj_long |>
  group_by(Sample) |>
  summarise(
    n_rows            = n(),
    exactly_equal     = sum(joint == decoupled),
    joint_greater     = sum(joint > decoupled),
    decoupled_greater = sum(decoupled > joint),
    max_abs_diff      = max(abs(joint - decoupled)),
    pct_exact         = round(100 * mean(joint == decoupled), 2)
  ) |> kable(caption = "FSJ matrix: per-sample cell-level agreement")
FSJ matrix: per-sample cell-level agreement
Sample n_rows exactly_equal joint_greater decoupled_greater max_abs_diff pct_exact
Div_100_S91 35869 35869 0 0 0 100
Div_101_S92 35869 35869 0 0 0 100
PARDOS_1_S1 35869 35869 0 0 0 100
PARDOS_2_S2 35869 35869 0 0 0 100

FSJ scatter: joint vs. decoupled

ggplot(fsj_long, aes(x = joint, y = decoupled)) +
  geom_point(alpha = 0.4, size = 0.6) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "red") +
  facet_wrap(~ Sample, scales = "free") +
  labs(x = "Joint pipeline FSJ count",
       y = "Decoupled pipeline FSJ count",
       title = "FSJ counts per circRNA per sample") +
  theme_bw()

FSJ cell-level discordance (top 20)

fsj_long |>
  mutate(abs_diff = abs(joint - decoupled)) |>
  filter(abs_diff > 0) |>
  arrange(desc(abs_diff)) |>
  head(20) |> kable(caption = "Largest per-cell FSJ disagreements")
Largest per-cell FSJ disagreements
circRNA_ID Sample joint decoupled abs_diff

Detection-correlation per sample

Per-sample Pearson and Spearman correlation between joint and decoupled counts — a value of 1.00 means per-cell agreement is perfect; values under ~0.99 flag a real divergence.

sample_corr <- function(df, what) {
  df |>
    group_by(Sample) |>
    summarise(
      pearson  = round(cor(joint, decoupled, method = "pearson"),  4),
      spearman = round(cor(joint, decoupled, method = "spearman"), 4)
    ) |>
    mutate(matrix = what, .before = 1)
}
bind_rows(sample_corr(bsj_long, "BSJ"),
          sample_corr(fsj_long, "FSJ")) |>
  kable(caption = "Joint vs. decoupled correlation (per sample)")
Joint vs. decoupled correlation (per sample)
matrix Sample pearson spearman
BSJ Div_100_S91 1 1
BSJ Div_101_S92 1 1
BSJ PARDOS_1_S1 1 1
BSJ PARDOS_2_S2 1 1
FSJ Div_100_S91 1 1
FSJ Div_101_S92 1 1
FSJ PARDOS_1_S1 1 1
FSJ PARDOS_2_S2 1 1

Benchmark (if present)

bench_files <- list.files(bench_dir, pattern = "\\.bench$", full.names = TRUE)
if (length(bench_files) == 0) {
  cat("No benchmark files found in", bench_dir, "\n")
} else {
  parse_bench <- function(f) {
    txt <- readLines(f, warn = FALSE)
    if (length(txt) == 0) return(NULL)
    kv <- strsplit(txt[1], " ")[[1]]
    as_tibble(setNames(
      lapply(kv, function(x) strsplit(x, "=")[[1]][2]),
      sapply(kv, function(x) strsplit(x, "=")[[1]][1])
    )) |> mutate(stage = sub("\\.bench$", "", basename(f)), .before = 1)
  }
  bench_df <- bind_rows(lapply(bench_files, parse_bench))
  # Best-effort numeric coercion
  for (col in c("wall_sec", "user_sec", "sys_sec", "max_rss_kb")) {
    if (col %in% names(bench_df)) {
      bench_df[[col]] <- suppressWarnings(as.numeric(bench_df[[col]]))
    }
  }
  bench_df <- bench_df |> arrange(stage)
  if ("max_rss_kb" %in% names(bench_df)) {
    bench_df$max_rss_gb <- round(bench_df$max_rss_kb / 1024 / 1024, 2)
  }
  bench_df |> kable(caption = "Per-stage benchmark")
}
Per-stage benchmark
stage wall_sec user_sec sys_sec max_rss_kb cpu_pct exit max_rss_gb
00_original_joint 250.16 12447.89 561.28 13782240 5200% 0 13.14
10_scan1_01_Div_100_S91 119.94 1886.27 104.28 11848004 1659% 0 11.30
10_scan1_02_Div_101_S92 123.21 3735.27 80.45 11722928 3096% 0 11.18
10_scan1_03_PARDOS_1_S1 32.97 462.75 54.18 12309132 1567% 0 11.74
10_scan1_04_PARDOS_2_S2 34.56 499.20 78.16 11574668 1670% 0 11.04
20_build_universe 20.15 30.43 20.60 6727544 253% 0 6.42
30_scan2_01_Div_100_S91 158.10 2813.38 241.65 20013064 1932% 0 19.09
30_scan2_02_Div_101_S92 131.88 3113.51 264.95 22656412 2561% 0 21.61
30_scan2_03_PARDOS_1_S1 90.29 2580.47 185.58 12933104 3063% 0 12.33
30_scan2_04_PARDOS_2_S2 95.84 2065.58 200.04 21966236 2363% 0 20.95
40_finalize 28.15 48.67 33.87 9593792 293% 0 9.15

Session info

sessionInfo()
## R version 4.1.2 (2021-11-01)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: CentOS Stream 8
## 
## Matrix products: default
## BLAS/LAPACK: /usr/lib64/libopenblasp-r0.3.15.so
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
##  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
##  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] knitr_1.51      lubridate_1.9.5 forcats_1.0.1   stringr_1.6.0  
##  [5] dplyr_1.2.0     purrr_1.2.1     readr_2.2.0     tidyr_1.3.2    
##  [9] tibble_3.3.1    ggplot2_4.0.2   tidyverse_2.0.0
## 
## loaded via a namespace (and not attached):
##  [1] bslib_0.10.0       compiler_4.1.2     pillar_1.11.1      RColorBrewer_1.1-3
##  [5] jquerylib_0.1.4    tools_4.1.2        bit_4.6.0          digest_0.6.39     
##  [9] timechange_0.4.0   jsonlite_2.0.0     evaluate_1.0.5     lifecycle_1.0.5   
## [13] gtable_0.3.6       pkgconfig_2.0.3    rlang_1.1.7        cli_3.6.5         
## [17] rstudioapi_0.18.0  parallel_4.1.2     yaml_2.3.12        xfun_0.57         
## [21] fastmap_1.2.0      withr_3.0.2        hms_1.1.4          generics_0.1.4    
## [25] sass_0.4.10        vctrs_0.7.2        bit64_4.6.0-1      grid_4.1.2        
## [29] tidyselect_1.2.1   glue_1.8.0         R6_2.6.1           otel_0.2.0        
## [33] vroom_1.7.1        rmarkdown_2.31     tzdb_0.5.0         farver_2.1.2      
## [37] magrittr_2.0.4     scales_1.4.0       htmltools_0.5.9    dichromat_2.0-0.1 
## [41] labeling_0.4.3     S7_0.2.1           stringi_1.8.7      cachem_1.1.0      
## [45] crayon_1.5.3