Author: Daniel Greene
Date: 2024-02-20

# IntervalSurgeon

#### Daniel Greene

#### 2024-02-20

## Introduction

`IntervalSurgeon` presents functions for intersecting, overlapping, piling and annotating integer-bounded intervals. Underlying algorithms are written in C++ for efficiency where appropriate (with the help of the `Rcpp` package). A typical use case would be for manipulating genomic intervals.

For the purposes of this package, intervals are represented as two-column integer matrices where the inclusive start points are in the first column and the exclusive end points are in the second column.

    library(IntervalSurgeon)

    starts <- 3*1:10
    ends <- 5*1:10

    intervals <- cbind(starts, ends)

The lengths of the intervals are therefore: `intervals[,2]-intervals[,1]`.

A key concept in `IntervalSurgeon` is breaking ranges which contain intervals into ‘sections’ delimited by the unique start/end points in the set. The sections for a set of intervals `x` is therefore a two-column `matrix` representing a set of intervals which partitions the range of `x` by the sorted start and end points. The sorted start and end points can be obtained using the `breaks` function (so named to reflect start/end points of intervals frequently being referred to as ‘breakpoints’ in genomics), which is equivalent to: `sort(unique(as.integer(intervals)))`). The sections can be computed from the sorted start and end points using the `sections` function.

One can use the `depth` and `pile` functions respectively to obtain the depth of intervals over their ‘sections’ (within which the depth is constant), and the row indices of original intervals which cover each section.

The `flatten` function returns a non-touching, non-overlapping set of intervals (as a `matrix`) which overlap at least one of a given set.

    sectioned <- sections(breaks(intervals))
    flattened <- flatten(intervals)
    depths <- depth(intervals)
    piles <- pile(intervals)

## Detached intervals

`IntervalSurgeon` provides functions which are optimised for dealing with detached (i.e. non-overlapping and non-touching) intervals which are sorted and non-empty. Here, we generate two such sets of intervals:

    x_starts <- 10*1:10
    x <- cbind(x_starts, x_starts + 5)

    y_starts <- 20*1:5 + 1
    y <- cbind(y_starts, y_starts + 7)

We can determine that they are indeed detached, sorted and non-empty using the `detached_sorted_nonempty` function.

    detached_sorted_nonempty(x)

    ## [1] TRUE

The `IntervalSurgeon` functions for operating on such sets of detached, sorted, non-empty intervals are analogues of the set operation functions in the `base` package, namely: `intersect`, `union` and `setdiff`. Here, the function names are in plural (i.e. with extra s’s on the end).

For a given set of sorted, non-overlapping intervals, some of the start points might be the same as the end points of adjacent intervals. These intervals are therefore ‘touching’ and can be ‘stitched’ together using the `stitch` function. If there were overlaps, the `flatten` function can be used to generate a set of sorted disjoint intervals. Note that `flatten` will also stitch touching intervals, although the `stitch` function is faster (albeit requiring intervals to be sorted).

## Finding overlaps between sets of intervals

Information about overlaps between sets of intervals can be obtained by ‘joining’ the sets. This is analogous to an SQL inner-join of two tables, and can be performed on sets of intervals using the `join` function. This function returns a matrix containing all overlaps of intervals from each set. Each row in the returned matrix corresponds to a specific overlap of intervals with one from each of the sets passed to the function. The `n`th element in a row contains the row index of the interval in the `n`th set of intervals passed to the function. Depending on the value of the `output` argument, there may two additional columns giving the start and end coordinates of the overlap (the default: `output="intervals"`, no extra columns (`output="indices"`) or one additional column giving the row index of the ‘section’ of the complete set of intervals (`output="sections"`, see `?sections`).

    x <- cbind(3*1:8, 5*1:8)
    y <- cbind(4*1:4, 7*1:4)
    join_xy <- join(x, y)

    head(join_xy)

    ##      [,1] [,2] [,3] [,4]
    ## [1,]    1    1    4    5
    ## [2,]    2    1    6    7
    ## [3,]    2    2    8   10
    ## [4,]    3    2    9   14
    ## [5,]    3    3   12   15
    ## [6,]    4    2   12   14

One common task would be to tag intervals with overlapping intervals, perhaps from a different set. For example, this might be useful for tagging a set of genomic intervals with the names of genes which they overlap. The `annotate` function is provided for this exact purpose.

    x <- cbind(0, c(a=10, b=20, c=30))
    y <- cbind(c(A=0, B=10, C=20), c(5, 15, 25))

    annotate(x, y)

    ## $a
    ## [1] "A"
    ##
    ## $b
    ## [1] "A" "B"
    ##
    ## $c
    ## [1] "A" "B" "C"

## Use with genomic intervals

Genomic intervals are often represented in R as `data.frame`s with columns corresponding to chromosome name, start position and end position. Obviously intervals do not intersect if they are on different chromosomes, so in order to manipulate such intervals with `IntervalSurgeon`, genome-wide operations must be performed chromosome-at-a-time. Using `split` to create a list of chromosome specific `data.frames`, or looping over the names of chromosomes and subsetting the original table, before `cbind`ing/`as.matrix`ing the start and end columns then makes the data accessible to the `IntervalSurgeon` functions.

    regions_annotated_with_genes <- lapply(
        c(1:22, "X", "Y"),
        function(chromosome) {
            regions_on_chr <- as.matrix(regions[regions$chr == chromosome,c("start", "end")])
            genes_on_chr <- as.matrix(genes[genes$chr == chromosome,c("start","end")])
            annotate(regions_on_chr, genes_on_chr)
        }
    )