Skip to contents

The data

ChrAccRex ships an ATAC-seq example dataset of sorted immune cells, aligned to hg38 and stored as a DsATAC object with fragment level data. That is what chromTFR needs: footprints are built from single base Tn5 cut positions, which only fragments provide.

remotes::install_github("EpigenomeInformatics/ChrAccRex")
library(chromTFR)
#> Loading required package: data.table
#> Loading required package: SummarizedExperiment
#> Loading required package: MatrixGenerics
#> Loading required package: matrixStats
#> 
#> Attaching package: 'MatrixGenerics'
#> The following objects are masked from 'package:matrixStats':
#> 
#>     colAlls, colAnyNAs, colAnys, colAvgsPerRowSet, colCollapse,
#>     colCounts, colCummaxs, colCummins, colCumprods, colCumsums,
#>     colDiffs, colIQRDiffs, colIQRs, colLogSumExps, colMadDiffs,
#>     colMads, colMaxs, colMeans2, colMedians, colMins, colOrderStats,
#>     colProds, colQuantiles, colRanges, colRanks, colSdDiffs, colSds,
#>     colSums2, colTabulates, colVarDiffs, colVars, colWeightedMads,
#>     colWeightedMeans, colWeightedMedians, colWeightedSds,
#>     colWeightedVars, rowAlls, rowAnyNAs, rowAnys, rowAvgsPerColSet,
#>     rowCollapse, rowCounts, rowCummaxs, rowCummins, rowCumprods,
#>     rowCumsums, rowDiffs, rowIQRDiffs, rowIQRs, rowLogSumExps,
#>     rowMadDiffs, rowMads, rowMaxs, rowMeans2, rowMedians, rowMins,
#>     rowOrderStats, rowProds, rowQuantiles, rowRanges, rowRanks,
#>     rowSdDiffs, rowSds, rowSums2, rowTabulates, rowVarDiffs, rowVars,
#>     rowWeightedMads, rowWeightedMeans, rowWeightedMedians,
#>     rowWeightedSds, rowWeightedVars
#> Loading required package: GenomicRanges
#> Loading required package: stats4
#> Loading required package: BiocGenerics
#> 
#> Attaching package: 'BiocGenerics'
#> The following objects are masked from 'package:stats':
#> 
#>     IQR, mad, sd, var, xtabs
#> The following objects are masked from 'package:base':
#> 
#>     Filter, Find, Map, Position, Reduce, anyDuplicated, aperm, append,
#>     as.data.frame, basename, cbind, colnames, dirname, do.call,
#>     duplicated, eval, evalq, get, grep, grepl, intersect, is.unsorted,
#>     lapply, mapply, match, mget, order, paste, pmax, pmax.int, pmin,
#>     pmin.int, rank, rbind, rownames, sapply, setdiff, sort, table,
#>     tapply, union, unique, unsplit, which.max, which.min
#> Loading required package: S4Vectors
#> 
#> Attaching package: 'S4Vectors'
#> The following objects are masked from 'package:data.table':
#> 
#>     first, second
#> The following object is masked from 'package:utils':
#> 
#>     findMatches
#> The following objects are masked from 'package:base':
#> 
#>     I, expand.grid, unname
#> Loading required package: IRanges
#> 
#> Attaching package: 'IRanges'
#> The following object is masked from 'package:data.table':
#> 
#>     shift
#> Loading required package: GenomeInfoDb
#> Loading required package: Biobase
#> Welcome to Bioconductor
#> 
#>     Vignettes contain introductory material; view with
#>     'browseVignettes()'. To cite Bioconductor, see
#>     'citation("Biobase")', and for packages 'citation("pkgname")'.
#> 
#> Attaching package: 'Biobase'
#> The following object is masked from 'package:MatrixGenerics':
#> 
#>     rowMedians
#> The following objects are masked from 'package:matrixStats':
#> 
#>     anyMissing, rowMedians
library(data.table)
library(ggplot2)
library(BSgenome.Hsapiens.UCSC.hg38)
#> Loading required package: BSgenome
#> Loading required package: Biostrings
#> Loading required package: XVector
#> 
#> Attaching package: 'Biostrings'
#> The following object is masked from 'package:base':
#> 
#>     strsplit
#> Loading required package: BiocIO
#> Loading required package: rtracklayer
#> 
#> Attaching package: 'rtracklayer'
#> The following object is masked from 'package:BiocIO':
#> 
#>     FileForFormat

dsa <- ChrAccRex::loadExample("dsAtac_ia_example")
#> 2026-09-28 23:06:35     2.1  STATUS STARTED Loading region count data from HDF5
#> 2026-09-28 23:06:35     2.1  STATUS     Region type: promoters_gc_protein_coding
#> 2026-09-28 23:06:35     2.1  STATUS     Region type: IA_prog_peaks
#> 2026-09-28 23:06:35     2.1  STATUS     Region type: t200
#> 2026-09-28 23:06:35     2.1  STATUS     Region type: t10k
#> 2026-09-28 23:06:35     2.1  STATUS COMPLETED Loading region count data from HDF5
#> 
#> 2026-09-28 23:06:35     2.1  STATUS STARTED Updating fragment RDS file references
#> 2026-09-28 23:06:35     2.1  STATUS COMPLETED Updating fragment RDS file references
ann <- getAccSampleAnnotation(dsa)
# The annotation is in the order of the samples, but its sampleId column
# lists the merged replicates, so take the ids from the object itself
ann$sample <- getAccSamples(dsa)
table(ann$cellType)
#> 
#>    TCD8EM TCD8naive   TeffMem TeffNaive 
#>         8         8         8         9
ChrAccR::getRegionTypes(dsa)
#> [1] "promoters_gc_protein_coding" "IA_prog_peaks"              
#> [3] "t200"                        "t10k"

This vignette compares naive against effector memory CD8 T cells, using three unstimulated samples of each. Restricting to a handful of samples keeps the build short; a real run would use all of them.

groups <- c("TCD8naive", "TCD8EM")
picked <- do.call(rbind, lapply(groups, function(g) {
    rows <- ann[ann$cellType == g & ann$stimulus == "U", , drop = FALSE]
    head(rows, 3)
}))
if (!all(groups %in% picked$cellType)) {
    stop("No unstimulated samples for: ",
        paste(setdiff(groups, picked$cellType), collapse = ", ")
    )
}
picked$sample
#> [1] "TCD8naive_U_1001" "TCD8naive_U_1002" "TCD8naive_U_1003" "TCD8EM_U_1001"   
#> [5] "TCD8EM_U_1002"    "TCD8EM_U_1003"

Insertion sites

The peaks of the example dataset are extended so that they still cover the flanks of a motif window, and the insertions are counted per base inside them. Scores are insertions per million, so samples of different depth stay comparable.

peaks <- getAccRegions(dsa, regionType = "IA_prog_peaks", extend = 500)

cache <- tempfile("chromTFR_example")
dir.create(cache)
files <- vapply(picked$sample, function(s) {
    f <- file.path(cache, paste0("ins_", s, ".RDS"))
    saveRDS(getTn5Insertions(dsa, s, regions = peaks), f)
    f
}, character(1))
#> INFO [2026-09-28 23:06:36] TCD8naive_U_1001: 452102 insertions, 260301 positions
#> INFO [2026-09-28 23:06:37] TCD8naive_U_1002: 663086 insertions, 392195 positions
#> INFO [2026-09-28 23:06:38] TCD8naive_U_1003: 318716 insertions, 205502 positions
#> INFO [2026-09-28 23:06:39] TCD8EM_U_1001: 778908 insertions, 395991 positions
#> INFO [2026-09-28 23:06:39] TCD8EM_U_1002: 526024 insertions, 266680 positions
#> INFO [2026-09-28 23:06:39] TCD8EM_U_1003: 376778 insertions, 203893 positions

mergeInsertionSites() pools the samples of a group into one aggregate, averaging the scores so that groups of unequal size stay on the same scale.

ins <- lapply(groups, function(g) {
    mergeInsertionSites(files[picked$cellType == g])
})
names(ins) <- groups
ins[["TCD8naive"]]
#> GRanges object with 775869 ranges and 2 metadata columns:
#>            seqnames    ranges strand |     score  coverage
#>               <Rle> <IRanges>  <Rle> | <numeric> <integer>
#>        [1]     chr1  23401088      * |  1.045863         1
#>        [2]     chr1  23401103      * |  0.737297         1
#>        [3]     chr1  23401106      * |  1.045863         1
#>        [4]     chr1  23401115      * |  0.502700         1
#>        [5]     chr1  23401122      * |  0.737297         1
#>        ...      ...       ...    ... .       ...       ...
#>   [775865]    chr21  31000002      * |  0.502700         1
#>   [775866]    chr21  31000017      * |  0.737297         1
#>   [775867]    chr21  31000054      * |  0.502700         1
#>   [775868]    chr21  31000171      * |  1.005400         2
#>   [775869]    chr21  31000448      * |  0.737297         1
#>   -------
#>   seqinfo: 4 sequences from an unspecified genome; no seqlengths

Binding sites

The annotation comes from methylTFRAnnotationHg38, unchanged from methylTFR. prepareTFBS() resizes the binding sites to the footprint window; everything downstream works on that object.

motifs <- c("CTCF", "FOS::JUN")
tf_bindsites <- methylTFRAnnotationHg38::getTFbindsites(
    motifSet = "jaspar2020"
)

tfbs <- lapply(motifs, function(m) prepareTFBS(tf_bindsites[[m]]))
names(tfbs) <- motifs
lengths(tfbs)
#>     CTCF FOS::JUN 
#>   554572   244961

The Tn5 k-mer model

Tn5 prefers some short sequences over others, and a motif is a fixed string of letters in the centre of every window, so part of any dip is chemistry rather than protein. The expected profile here measures that preference from the cuts themselves.

kmerBackground() counts every 6-mer of the accessible genome. It depends on the regions only, so it is computed once and reused for both groups.

genome <- BSgenome.Hsapiens.UCSC.hg38
kmer <- 6L
bg <- kmerBackground(peaks, genome, k = kmer)
#> INFO [2026-09-28 23:06:59] Counting background k-mers in 13730 regions

computeKmerBias() compares the 6-mer at each observed cut with that background. A weight above one means Tn5 cuts that word more often than its abundance predicts.

bias <- lapply(ins, computeKmerBias,
    regions = peaks, genome = genome, k = kmer, bg = bg, max.ins = 5e5
)
#> INFO [2026-09-28 23:07:00] Counting k-mers at 500000 insertion sites
#> INFO [2026-09-28 23:07:03] k-mer weights from 0.05 to 8.17
#> INFO [2026-09-28 23:07:03] Counting k-mers at 500000 insertion sites
#> INFO [2026-09-28 23:07:05] k-mer weights from 0.03 to 19.94
summary(bias[["TCD8naive"]])
#>    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
#> 0.04925 0.70813 1.07103 1.29502 1.57287 8.17297

kmerBiasProfile() walks along the binding sites, reads the genomic 6-mer at each position and averages its weight across sites. That curve is what the insertions would look like with no protein bound.

expected <- lapply(groups, function(g) {
    prof <- lapply(motifs, function(m) {
        kmerBiasProfile(tfbs[[m]], genome, bias[[g]],
            k = kmer, max.sites = 5000
        )
    })
    names(prof) <- motifs
    prof
})
#> INFO [2026-09-28 23:07:43] Bias profile over 5000 sites
#> INFO [2026-09-28 23:08:02] Bias profile over 5000 sites
#> INFO [2026-09-28 23:08:11] Bias profile over 5000 sites
#> INFO [2026-09-28 23:08:17] Bias profile over 5000 sites
names(expected) <- groups

Footprints

accFootprintData() stacks the insertions on the centre of every binding site and matches the expected curve to the observed flanks.

profiles <- lapply(motifs, function(m) {
    e <- lapply(groups, function(g) expected[[g]][[m]])
    names(e) <- groups
    accFootprintData(tfbs[[m]], ins, e)
})
names(profiles) <- motifs
head(profiles[["CTCF"]])
#>        x      avg_ins     type     group
#>    <num>        <num>   <char>    <fctr>
#> 1:  -272 0.0003567070 Observed TCD8naive
#> 2:  -271 0.0003552325 Observed TCD8naive
#> 3:  -270 0.0003511040 Observed TCD8naive
#> 4:  -269 0.0003618358 Observed TCD8naive
#> 5:  -268 0.0003618812 Observed TCD8naive
#> 6:  -267 0.0003682324 Observed TCD8naive

The deviation score reads that curve as one number, the insertion density of the central window over the density of the outer flanks, minus the same ratio on the expected curve. A value below zero means the centre is more protected than the sequence alone predicts.

vapply(profiles, function(df) {
    df[group == "TCD8naive", accDeviation(.SD),
        .SDcols = c("x", "avg_ins", "type")]
}, numeric(1))
#>      CTCF  FOS::JUN 
#> 0.1659338 0.4797729
colours <- c(TCD8naive = "#3B6EA5", TCD8EM = "#B23A48")
plotAccFootprintGrid(profiles, tfbs, colours,
    corrected = "k-mer corrected"
)
#> INFO [2026-09-28 23:08:17] CTCF, k-mer corrected deviations: TCD8naive = 0.166, TCD8EM = 0.153
#> INFO [2026-09-28 23:08:17] FOS::JUN, k-mer corrected deviations: TCD8naive = 0.48, TCD8EM = 1.374

CTCF is the control to look at first: it binds stably, so its footprint is deep and the corrected curve dips well below one. AP-1 is the opposite case, a factor with a short residence time whose footprint stays shallow even where it is active.

A GC based expected profile is available as well, through addGCBintoAccessome() and computeAccExpectations(), and needs no genome sequence. Comparing the two on the same motif is a useful check: a dip that survives the k-mer correction is a footprint, one that flattens was Tn5 preferring the flanking sequence.

Session info

sessionInfo()
#> R version 4.3.3 (2024-02-29)
#> Platform: x86_64-conda-linux-gnu (64-bit)
#> Running under: Debian GNU/Linux 12 (bookworm)
#> 
#> Matrix products: default
#> BLAS/LAPACK: /icbb/projects/share/software/packages/miniconda3/envs/chromTFR/lib/libopenblasp-r0.3.34.so;  LAPACK version 3.12.0
#> 
#> locale:
#> [1] C
#> 
#> time zone: Europe/Berlin
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats4    stats     graphics  grDevices utils     datasets  methods  
#> [8] base     
#> 
#> other attached packages:
#>  [1] BSgenome.Hsapiens.UCSC.hg38_1.4.5 BSgenome_1.70.1                  
#>  [3] rtracklayer_1.62.0                BiocIO_1.12.0                    
#>  [5] Biostrings_2.70.1                 XVector_0.42.0                   
#>  [7] ggplot2_3.5.2                     chromTFR_0.99.0                  
#>  [9] SummarizedExperiment_1.32.0       Biobase_2.62.0                   
#> [11] GenomicRanges_1.54.1              GenomeInfoDb_1.38.1              
#> [13] IRanges_2.36.0                    S4Vectors_0.40.2                 
#> [15] BiocGenerics_0.48.1               MatrixGenerics_1.14.0            
#> [17] matrixStats_1.5.0                 data.table_1.17.8                
#> [19] BiocStyle_2.30.0                 
#> 
#> loaded via a namespace (and not attached):
#>  [1] bitops_1.0-9                   logger_0.4.0                  
#>  [3] rlang_1.1.6                    magrittr_2.0.3                
#>  [5] clue_0.3-66                    GetoptLong_1.0.5              
#>  [7] compiler_4.3.3                 png_0.1-8                     
#>  [9] systemfonts_1.2.3              vctrs_0.6.5                   
#> [11] pkgconfig_2.0.3                shape_1.4.6.1                 
#> [13] crayon_1.5.3                   fastmap_1.2.0                 
#> [15] labeling_0.4.3                 Rsamtools_2.18.0              
#> [17] rmarkdown_2.29                 ragg_1.5.0                    
#> [19] xfun_0.61                      zlibbioc_1.48.0               
#> [21] cachem_1.1.0                   jsonlite_2.0.0                
#> [23] rhdf5filters_1.14.1            DelayedArray_0.28.0           
#> [25] muLogR_0.2                     Rhdf5lib_1.24.0               
#> [27] BiocParallel_1.36.0            parallel_4.3.3                
#> [29] cluster_2.1.8.1                R6_2.6.1                      
#> [31] ChrAccRex_0.2                  bslib_0.9.0                   
#> [33] RColorBrewer_1.1-3             jquerylib_0.1.4               
#> [35] bookdown_0.48                  iterators_1.0.14              
#> [37] knitr_1.50                     Matrix_1.6-5                  
#> [39] tidyselect_1.2.1               dichromat_2.0-0.1             
#> [41] abind_1.4-5                    yaml_2.3.10                   
#> [43] doParallel_1.0.17              codetools_0.2-20              
#> [45] lattice_0.22-7                 tibble_3.3.0                  
#> [47] withr_3.0.2                    evaluate_1.0.5                
#> [49] desc_1.4.3                     muRtools_0.9.6                
#> [51] circlize_0.4.16                pillar_1.11.0                 
#> [53] BiocManager_1.30.26            foreach_1.5.2                 
#> [55] ChrAccR_0.9.25                 methylTFRAnnotationHg38_0.99.0
#> [57] generics_0.1.4                 RCurl_1.98-1.17               
#> [59] scales_1.4.0                   glue_1.8.0                    
#> [61] tools_4.3.3                    GenomicAlignments_1.38.0      
#> [63] fs_1.6.6                       XML_3.99-0.17                 
#> [65] rhdf5_2.46.1                   grid_4.3.3                    
#> [67] colorspace_2.1-1               GenomeInfoDbData_1.2.11       
#> [69] patchwork_1.3.2                HDF5Array_1.30.0              
#> [71] restfulr_0.0.15                cli_3.6.5                     
#> [73] textshaping_1.0.3              S4Arrays_1.2.0                
#> [75] ComplexHeatmap_2.18.0          dplyr_1.1.4                   
#> [77] gtable_0.3.6                   sass_0.4.10                   
#> [79] digest_0.6.37                  SparseArray_1.2.2             
#> [81] rjson_0.2.23                   htmlwidgets_1.6.4             
#> [83] farver_2.1.2                   htmltools_0.5.8.1             
#> [85] pkgdown_2.1.3                  lifecycle_1.0.4               
#> [87] GlobalOptions_0.1.2