---
title: "SpectralTAD Vignette"
author:
- name: Kellen Cresswell
  affiliation:
  - &1 Department of Biostatistics, Virginia Commonwealth University, Richmond, VA
- name: John Stansfield
  affiliation:
  - *1
- name: Mikhail Dozmorov
  affiliation:
  - *1

date: '`r format(Sys.Date(), "%B %e, %Y")`'
abstract: >
    SpectralTAD is an R package designed to identify Topologically Associated 
    Domains (TADs) from Hi-C contact matrices. It uses a modified version of 
    spectral clustering that uses a sliding window to quickly detect TADs. 
    The function works on a range of different formats of contact matrices 
    and returns a list of data frames or GRanges in BED format with TAD coordinates,
    seperated by hierarchy level. The method does not require users to adjust any
    parameters to work and gives them control over the number of levels to 
    be returned.
package: SpectralTAD
output:
    BiocStyle::html_document
vignette: >
    %\VignetteIndexEntry{SpectralTAD}
    %\VignetteEncoding{UTF-8}
    %\VignetteEngine{knitr::rmarkdown}
editor_options:
    chunk_output_type: console
bibliography: pack_ref.bib
---

```{r set-options, echo=FALSE, cache=FALSE}
options(width = 400)
```

# Introduction

`SpectralTAD` is a package designed to allow for fast hierarchical TAD calling on a wide-variety of chromatin conformation capture (Hi-C) data. `SpectralTAD` takes a contact matrix as an input and outputs a list of data frames or GRange objects in BED format containing coordinates corresponding to TAD locations. The package contains two functions, `SpectralTAD()` and `SpectralTAD_Par()` with SpectralTAD being a single-CPU TAD-caller and `SpectralTAD_Par` being the parallelized version. This package provides flexibility in the data it accepts, allowing for $n \times n$ (square numerical matrix), $n \times (n+3)$ (square numerical matrix with the additional chromosome, start, end columns), and 3-column sparse matrices (described below). There are no parameters required for running the functions, though certain methods for customizing the results are available. The idea is to give users the freedom to control how they call TADs while giving them the option to perform it in an unsupervised manner.

The package is based around the windowed spectral clustering algorithm, introduced by [@cresswell:2019aa] , which is designed to be robust to sparsity, noise, and sequencing depth of Hi-C data. 

# Getting Started

## Installation

```{r, eval = FALSE, message=FALSE}
# if (!requireNamespace("BiocManager", quietly=TRUE))
#     install.packages("BiocManager")
# BiocManager::install("SpectralTAD")
devtools::install_github("cresswellkg/SpectralTAD")
library(SpectralTAD)
```


```{r, echo=FALSE, message=FALSE, warning=FALSE}
library(SpectralTAD)
```


## Input data

### Working with $n \times n$ matrices

$n \times n$ contact matrices, are most commonly associated with data coming from the Bing Ren lab (http://chromosome.sdsc.edu/mouse/hi-c/download.html). These contact matrices are square and symmetric with entry $ij$ corresponding to the number of contacts between region $i$ and region $j$. Below is an example of a $5 \times 5$ region of an $n \times n$ contact matrix. Derived from [@Rao:2014aa], chromosome 20 data at 25kb resolution. Note the symmetry around the diagonal - the typical shape of chromatin interaction matrix.

```{r, echo = FALSE, warning = FALSE, message = FALSE}
data("rao_chr20_25_rep")
rao_chr20_25_rep = HiCcompare::sparse2full(rao_chr20_25_rep)
row.names(rao_chr20_25_rep) = colnames(rao_chr20_25_rep) = format(as.numeric(row.names(rao_chr20_25_rep)), scientific = FALSE)
rao_chr20_25_rep[1:5, 1:5]
```

### Working with $n \times (n+3)$ matrices

$n \times (n+3)$ matrices are commonly associated with the TopDom tad-caller (http://zhoulab.usc.edu/TopDom/). These matrices consist of a normal $n \times n$ matrix but with 3 additional leading columns containg the chromosome, the start of the region and the end of the region. Regions in this case are determined by the resolution of the data. The typical $n \times (n+3)$ matrix is shown below.

```{r, echo = FALSE, warning = FALSE}
row.names(rao_chr20_25_rep) = NULL
sub_mat = cbind.data.frame("chr19", as.numeric(colnames(rao_chr20_25_rep)), as.numeric(colnames(rao_chr20_25_rep))+25000, rao_chr20_25_rep)[1:10, 1:10]
colnames(sub_mat) = NULL

sub_mat
```

### Working with sparse 3-column matrices

Sparse 3-column matrices, sometimes referred to as a coordinated lists, are matrices where the first and second column refer to region $i$ and region $j$ of the chromosome, and the third column is the number of contacts between them. This style is becoming increasingly popular and is associated with raw data from Rao (https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=GSE63525), and is the data output produced by the Juicer tool [@Durand:2016aa]. 3-column matrices are handled internally in the package by converting them to $n \times n$ matrices using the `HiCcompare` package's `sparse2full()` function. The first 5 rows of a typical sparse 3-column matrix is shown below.

```{r, echo = FALSE, warning = FALSE}
head(HiCcompare::full2sparse(rao_chr20_25_rep), 5)
```

### Working with other data types

Users can also find TADs from data output by `cooler` (http://cooler.readthedocs.io/en/latest/index.html) and HiC-Pro (https://github.com/nservant/HiC-Pro) with minor pre-processing using the `HiCcompare` package. 

#### Working with .cool files

The cooler software can be downloaded from http://cooler.readthedocs.io/en/latest/index.html. It essentially provides access to a catalog of popular HiC datasets. We can pre-process and use .cool files that are associated with cooler files using the following steps:

1. Download `.cool` file from (ftp://cooler.csail.mit.edu/coolers)
2. Convert to text file using `cooler dump --join Rao2014-GM12878-DpnII-allreps-filtered.50kb.cool > Rao.GM12878.50kb.txt`
3. Run the code below

```{r, eval = FALSE}
#Read in data
cool_mat = read.table("Rao.GM12878.50kb.txt")
#Convert to sparse 3-column matrix using cooler2sparse from HiCcompare
sparse_mats = HiCcompare::cooler2sparse(cool_mat)
#Remove empty matrices if necessary
#sparse_mats = sparse_mats$cis[sapply(sparse_mats, nrow) != 0]
#Run SpectralTAD
spec_tads = lapply(names(sparse_mats), function(x) {
  SpectralTAD(sparse_mats[[x]], chr = x)
})

```

#### Working with HiC-Pro files

HiC-Pro data comes with 2 files, the `.matrix` file and the `.bed` file. The `.matrix` file is a 3-column matrix where instead of coordinates as the 1st and 2nd column, there is an ID. The `.bed` file maps these IDs to genomic coordinates. The steps for analyzing these files is shown below:

```{r, eval = FALSE}
#Read in both files
mat = read.table("amyg_100000.matrix")
bed = read.table("amyg_100000_abs.bed")
#Convert to modified bed format
sparse_mats = HiCcompare::hicpro2bedpe(mat,bed)
#Remove empty matrices if necessary
#sparse_mats$cis = sparse_mats$cis[sapply(sparse_mats, nrow) != 0]
#Go through all matrices
sparse_tads = lapply(sparse_mats$cis, function(x) {
  #Pull out chromosome
  chr = x[,1][1]
  #Subset to make three column matrix
  x = x[,c(2,5,7)]
  #Run SpectralTAD
  SpectralTAD(x, chr=chr)
})
```

## Running SpectralTAD

Once matrices are in an acceptable format, SpectralTAD can be run with as little as two parameters or as many as eight. Below we show how to run the algorithm with just TAD detection and no quality filtering.

```{r, message = FALSE, warning = FALSE}
#Get the rao contact matrix built into the package
data("rao_chr20_25_rep")
head(rao_chr20_25_rep)
#We see that this is a sparse 3-column contact matrix
#Running the algorithm with resolution specified
results = SpectralTAD(rao_chr20_25_rep, chr = "chr20", resolution = 25000, qual_filter = FALSE, z_clust = FALSE)
#Printing the top 5 TADs
head(results$Level_1, 5)
#Repeating without specifying resolution
no_res = SpectralTAD(rao_chr20_25_rep, chr = "chr20", qual_filter = FALSE, z_clust = FALSE)
#We can see below that resolution can be estimated automatically if necessary
identical(results, no_res)
```

## Filtering TADs

### Silhouette score filtering

One method for filtering TADs is using group-wise silhouette scores. This is done by getting the overall silhouette for each group and removing those with a score of less than .25. A low silhouette score for a given TAD indicates a poor level of connectivity within it.

```{r, message = FALSE}
#Running SpectralTAD with silhouette score filtering
qual_filt = SpectralTAD(rao_chr20_25_rep, chr = "chr20", qual_filter = TRUE, z_clust = FALSE, resolution = 25000)
#Showing quality filtered results
head(qual_filt$Level_1,5)
#Quality filtering generally has different dimensions
dim(qual_filt$Level_1)
dim(results$Level_1)
```

The results when using the quality filtering option are altered to include a new column called `Sil_Score`. This column includes a group-wise silhouette score. In this example a single TAD was removed due to having poor organization (Low silhouette score).

### Z-score filtering

Z-score filtering is based on observations about the log-normality of the distance between eigenvector gaps from [@cresswell:2019aa]. Z-score filtering bypasses the uses of silhouette score and defines a TAD boundary as the area between consecutive regions where the z-score of the eigenvector gap is greater than 2.  

```{r, message = FALSE}
z_filt = SpectralTAD(rao_chr20_25_rep, chr = "chr20", qual_filter = FALSE, z_clust = TRUE, resolution = 25000)
head(z_filt$Level_1, 5)
dim(z_filt$Level_1)
```

We can see that TADs found using this method are generally more fine than those found using the silhouette score based filtering. Z-score filtering is more suited for people not interested in TAD hierarchies because the initial TADs detected are less broad than those by quality filtering. 

## Finding hierarchical TADS

Our method is specifically designed to find hierarchical TADs. To do so, users must specify how many levels they are interested in using the `levels` parameter. There is no limit to the number of levels but after a certain point no new TADs will be found due to limitations in TAD size or TAD quality. Hierarchies are found by running the algorithm initially and then iteratively running it on sub-TADs until none can be found. At the levels below the initial level we use z-score filtering by default.

```{r, message = FALSE}
#Running SpectralTAD with 3 levels and no quality filtering
spec_hier = SpectralTAD(rao_chr20_25_rep, chr = "chr20", resolution = 25000, qual_filter = FALSE, levels = 3)
#Level 1 remains unchanged
head(spec_hier$Level_1,5)
#Level 2 contains the sub-TADs for level 1
head(spec_hier$Level_2,5)
#Level 3 contains even finer sub-TADs for level 1 and level 2
head(spec_hier$Level_3,5)

```

Note that as we move down levels, more gaps form indicating regions where sub-TADs are not present.

## Removing gaps

Though, it has been shown that windowed spectral clustering is more robust to sparsity than other common methods[@cresswell:2019aa], there is still some instability caused by consecutive regions of gaps. To counter this, we use a `gap_threshold` parameter to allow users to exclude regions based on how many zeros are included. By default this value is set to 1 which means only columns/rows with 100% of values being zero are removed before analysis. Accordingly, setting this value to .7 would mean rows/columns with 70% zeros would be removed. Since we are not interested in long-range contacts for TAD identification, this percentage only applies to the number of zeros within a specific distance of the diagonal (Defined by the maximum TAD size). Users must be careful not to filter too much as this can remove informative regions of the matrix.

## Running SpectralTAD with parallelization

It is sometimes the case that people want to run SpectralTAD on multiple chromosomes at once in parallel. This can be done using the `SpectralTAD_Par` function. SpectralTAD_Par is identical to SpectralTAD but takes a list of contact matrices as an input. These matrices can be of any of the allowable types and mixing of types is allowed. Users are required to provide a vector of chromosomes, `chr_over`, where it is ordered such that each entry matches up with its corresponding contact matrix in the list of matrices. Users can also input list names using the `labels` parameter. In terms of parallelization, users can either input the number of cores they would like to use or the function will automatically use 1 less than the total number of cores on the computer on which it is ran. The function works on Linux/Mac and Windows with automatic OS detection built in. We show the steps below.

```{r, eval = FALSE}
#Creating replicates of our HiC data for demonstration
cont_list = replicate(3,rao_chr20_25_rep, simplify = FALSE)
#Creating a vector of chromosomes
chr_over = c("chr20", "chr20", "chr20")
#Creating a list of labels
labels = c("Replicate 1", "Replicate 2", "Replicate 3")
SpectralTAD_Par(cont_list = cont_list, chr_over = chr_over, labels = labels, cores = 3)

```

## Effect of matrix type on runtime

The type of matrix input into the algorithm can affect runtimes for the algorithm. $n \times n$ matrices require no conversion and are the fastest. Meanwhile, $n \times (n+3)$ matrices take slightly longer to run due to the need to remove the first 3 columns. Sparse 3-column matrices have the highest runtimes due to the complexity of converting them to an $n \times n$ matrix. The times are summarized below, holding all other parameters constant.

```{r, message = FALSE}
library(microbenchmark)
#Converting to nxn
n_n = HiCcompare::sparse2full(rao_chr20_25_rep)
#Converting to nxn+3
n_n_3 = cbind.data.frame("chr20", as.numeric(colnames(n_n)), as.numeric(colnames(n_n))+25000, n_n)
#Defining each function
sparse = SpectralTAD(cont_mat = rao_chr20_25_rep, chr = "chr20", qual_filter = FALSE)
n_by_n = SpectralTAD(cont_mat = n_n, chr = "chr20", qual_filter = FALSE)
n_by_n_3 =SpectralTAD(cont_mat = n_n_3, chr = "chr20", qual_filter = FALSE)

#Benchmarking different parameters
microbenchmark(sparse = SpectralTAD(cont_mat = rao_chr20_25_rep, chr = "chr20", qual_filter = FALSE),
n_by_n = SpectralTAD(cont_mat = n_n, chr = "chr20", qual_filter = FALSE),
n_by_n_3 =SpectralTAD(cont_mat = n_n_3, chr = "chr20", qual_filter = FALSE), unit = "s", times = 3)
```

## Effect of parameters on runtime

Just as the type of data affects runtime, the parameters used to detect TADs do as well. The main bottleneck is the quality filter function which requires the inversion of a matrix and the calculation of silhouette score.

```{r, message = FALSE}
microbenchmark(quality_filter = SpectralTAD(cont_mat = n_n, chr = "chr20", qual_filter = TRUE, z_clust = FALSE), no_filter = SpectralTAD(cont_mat = n_n, chr = "chr20", qual_filter = FALSE, z_clust = FALSE), z_clust = SpectralTAD(cont_mat = n_n, chr = "chr20", qual_filter = FALSE, z_clust = TRUE), times = 3, unit = "s")
```

As can be seen using the z-score method is the fastest.

# References
