Skip to content

Commit daf96de

Browse files
Greatly speed up calculation of permutation ES
Switched to a method that eliminates the need to compute matrix dot products when deriving permutation ES. This new version can generate results faster than the previous version when R was linked with the OpenBLAS library, and the new approach also requires less memory. Unfortunately, this change breaks support for directional databases, such as PTMsigDB.
1 parent 569913b commit daf96de

File tree

9 files changed

+255
-266
lines changed

9 files changed

+255
-266
lines changed

DESCRIPTION

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
Package: fast.ssgsea
22
Type: Package
33
Title: Fast Single-Sample Gene Set Enrichment Analysis (ssGSEA)
4-
Version: 0.1.0.9017
5-
Date: 2025-09-14
4+
Version: 0.1.0.9018
5+
Date: 2025-10-30
66
Authors@R:
77
person(given = "Tyler", family = "Sagendorf",
88
email = "tyler.sagendorf@pnnl.gov",

R/RcppExports.R

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
11
# Generated by using Rcpp::compileAttributes() -> do not edit by hand
22
# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393
33

4+
#' @title Multiplication of an unseen binary matrix and real-valued matrix
5+
#'
6+
#' @description Compute the product of an unseen binary and real-valued matrix
7+
#' when the binary matrix is in a specific format. See details.
8+
#'
9+
#' @param set_sizes integer vector of unique set sizes, sorted in ascending
10+
#' order.
11+
#' @param Y real-valued matrix. The number of rows is equal to
12+
#' \code{max(set_sizes)}.
13+
#'
14+
#' @details The set_sizes vector acts as a stand-in for the unseen binary
15+
#' matrix, which has the following characteristics:
16+
#'
17+
#' 1. The first `set_sizes[i]` elements of the i-th row are all 1 and the
18+
#' remaining entries of that row are 0.
19+
#'
20+
#' 2. If the i-th row has n ones, then the i + 1 row will have at least (n +
21+
#' 1) ones.
22+
#'
23+
#' 3. All elements of the last row are 1.
24+
#'
25+
#' @author Tyler Sagendorf
26+
#'
27+
#' @references Sanderson, C., & Curtin, R. (2016). Armadillo: A template-based
28+
#' C++ library for linear algebra. The Journal of Open Source Software, 1(2),
29+
#' 26. \url{https://doi.org/10.21105/joss.00026}
30+
#'
31+
#' @noRd
32+
NULL
33+
434
#' @title Find Index of First Positive Value in Sorted Vector
535
#'
636
#' @description Given a sorted vector of real-valued numbers, find the index of
@@ -170,8 +200,8 @@ NULL
170200
#'
171201
#' @noRd
172202
#'
173-
.Rcpp_calcESPermCore <- function(alpha, Y_perm, R_perm, sumRanks_i, A_perm, theta_m_i, theta_w_i) {
174-
.Call(`_fast_ssgsea_Rcpp_calcESPermCore`, alpha, Y_perm, R_perm, sumRanks_i, A_perm, theta_m_i, theta_w_i)
203+
.Rcpp_calcESPermCore <- function(alpha, Y_perm, R_perm, sumRanks_i, theta_m_i, theta_w_i) {
204+
.Call(`_fast_ssgsea_Rcpp_calcESPermCore`, alpha, Y_perm, R_perm, sumRanks_i, theta_m_i, theta_w_i)
175205
}
176206

177207
#' @title Extract Information from a Permutation Enrichment Score Matrix

R/globals.R

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
utils::globalVariables(
22
c(
3+
"A",
4+
"A_d",
35
"adj_p_value",
46
"A_perm",
57
"A_perm_d",
68
"direction_down",
79
"ES",
10+
"ES_d",
811
"ES_idx",
12+
"ES_u",
913
"i",
1014
"j",
1115
"M",

R/utils.R

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,16 @@
213213
dims <- lengths(dim_names)
214214

215215
# Keep genes expected to be "up"
216-
dt_u <- dt[direction_down == FALSE]
216+
if (any(dt[["direction_down"]])) {
217+
stop(
218+
"fast_ssgsea does not currently support directional databases. ",
219+
"This will be fixed in a future update."
220+
)
221+
222+
dt_u <- dt[direction_down == FALSE]
223+
} else {
224+
dt_u <- dt
225+
}
217226

218227
# Incidence matrix where a 1 indicates that the element is in the set. If x
219228
# is a directional database, then A will only contain elements that are
@@ -533,13 +542,16 @@
533542
y_i,
534543
r_i,
535544
sumRanks_i,
536-
A_perm,
537545
theta_m_i,
538546
theta_w_i,
539-
A_perm_d = NULL,
540547
theta_m_d_i = NULL,
541548
theta_w_d_i = NULL) {
542-
max_set_size <- ncol(A_perm)
549+
# max_set_size <- ncol(A_perm)
550+
if (is.null(theta_m_d_i)) {
551+
max_set_size <- max(theta_m_i)
552+
} else {
553+
max_set_size <- max(theta_m_i + theta_m_d_i)
554+
}
543555

544556
n_elements <- length(element_indices) # number of nonmissing values
545557

@@ -571,18 +583,16 @@
571583
Y_perm,
572584
R_perm,
573585
sumRanks_i,
574-
A_perm,
575586
theta_m_i,
576587
theta_w_i
577588
)
578589

579-
if (!is.null(A_perm_d)) { # directional sets
590+
if (!is.null(theta_m_d_i)) { # directional sets
580591
ES_perm_d <- .Rcpp_calcESPermCore(
581592
alpha,
582593
Y_perm,
583594
R_perm,
584595
sumRanks_i,
585-
A_perm_d,
586596
theta_m_d_i,
587597
theta_w_d_i
588598
)
@@ -787,22 +797,22 @@
787797

788798
A_perm_d <- NULL
789799

790-
A_perm <- .Rcpp_calcAPerm(
791-
end = theta_m_i,
792-
MAX_SET_SIZE = max_set_size,
793-
check = FALSE
794-
)
800+
# A_perm <- .Rcpp_calcAPerm(
801+
# end = theta_m_i,
802+
# MAX_SET_SIZE = max_set_size,
803+
# check = FALSE
804+
# )
795805

796806
rep_idx <- match(x = m_i, table = theta_m_i)
797807
}
798808

799809
out <- list(
800810
"rep_idx" = rep_idx,
801-
"A_perm" = A_perm,
811+
# "A_perm" = A_perm,
802812
"theta_m_i" = theta_m_i,
803813
"theta_w_i" = theta_w_i,
804814
# These may be NULL
805-
"A_perm_d" = A_perm_d,
815+
# "A_perm_d" = A_perm_d,
806816
"theta_m_d_i" = theta_m_d_i,
807817
"theta_w_d_i" = theta_w_d_i
808818
)
@@ -895,6 +905,7 @@
895905
tab_i <- data.table(
896906
set = sets,
897907
set_size = m_i,
908+
m_d_i = m_d_i,
898909
ES = ES_i,
899910
# Initialize vectors of 0's. These 3 vectors will be updated using the
900911
# results from each batch of permutations.
@@ -905,6 +916,16 @@
905916
stringsAsFactors = FALSE
906917
)
907918

919+
setorderv(
920+
x = tab_i,
921+
cols = c("set_size", "ES", "row_order"),
922+
order = c(1L, 1L, 1L)
923+
)
924+
925+
# Reordered
926+
m_i <- tab_i[["set_size"]]
927+
m_d_i <- tab_i[["m_d_i"]]
928+
908929
if (nperm != 0L) {
909930
# Incidence matrices and other information for permutations
910931
A_list_perm <- .permIncidenceMatrix(
@@ -913,14 +934,12 @@
913934
m_d_i = m_d_i
914935
)
915936

916-
# Extract list components: rep_idx, A_perm, theta_m_i, theta_w_i,
917-
# A_perm_d, theta_m_d_i, theta_w_d_i
937+
# Extract list components: rep_idx, theta_m_i, theta_w_i, theta_m_d_i,
938+
# theta_w_d_i
918939
list2env(x = A_list_perm, envir = environment())
919940

920941
tab_i[, rep_idx := rep_idx]
921942

922-
setorderv(x = tab_i, cols = c("rep_idx", "ES"), order = c(1L, 1L))
923-
924943
ES_ls <- split(
925944
x = tab_i[["ES"]],
926945
f = tab_i[["rep_idx"]]
@@ -941,10 +960,8 @@
941960
y_i = y_i,
942961
r_i = r_i,
943962
sumRanks_i = sumRanks_i,
944-
A_perm = A_perm,
945963
theta_m_i = theta_m_i,
946964
theta_w_i = theta_w_i,
947-
A_perm_d = A_perm_d,
948965
theta_m_d_i = theta_m_d_i,
949966
theta_w_d_i = theta_w_d_i
950967
)
@@ -954,7 +971,7 @@
954971
ES_perm = ES_perm
955972
)
956973

957-
perm_dt <- rbindlist(perm_dt)
974+
perm_dt <- rbindlist(perm_dt, use.names = FALSE)
958975

959976
# Update summary vectors
960977
tab_i[, `:=`(

README.Rmd

Lines changed: 6 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ X <- matrix(
7575
7676
## Simulate list of gene sets
7777
n_sets <- 20000L # number of gene sets
78-
min_size <- 10L # size of smallest gene set
79-
max_size <- 500L # size of largest gene set
78+
min_size <- 5L # size of smallest gene set
79+
max_size <- 1000L # size of largest gene set
8080
8181
size_range <- max_size - min_size + 1L
8282
n_reps <- ceiling(n_sets / size_range)
@@ -91,7 +91,7 @@ names(gene_sets) <- paste0("set", seq_along(gene_sets))
9191

9292
### Runtime and Results
9393

94-
This shows the runtime of `fast_ssgsea` with the reference Basic Linear Algebra Subprograms (BLAS) library [@blas] (single-threaded) running on an AMD Ryzen 5 7600X CPU with a clock speed of 4.7 GHz.
94+
This shows the runtime of `fast_ssgsea` running on an AMD Ryzen 5 7600X CPU with a clock speed of 4.7 GHz.
9595

9696
```{r time-results}
9797
library(fast.ssgsea)
@@ -123,78 +123,17 @@ print(sessionInfo(), locale = FALSE, tzone = FALSE)
123123

124124
## Performance
125125

126-
The `fast.ssgsea` R package utilizes linear algebra and ideas from Fast Gene Set Enrichment Analysis [@korotkevich-fast-2021] to greatly reduce the runtime of ssGSEA and PTM-SEA while also properly controlling the type I error rate.
126+
The `fast.ssgsea` R package utilizes linear algebra and ideas from Fast Gene Set Enrichment Analysis [@korotkevich-fast-2021] to greatly reduce the runtime of gene permutation GSEA and PTM-SEA.
127127

128128
Tests were performed on a desktop computer with an AMD Ryzen 5 7600X CPU (6 cores, 12 threads) at 4.7 GHz. Different combinations of the number of samples, gene sets, maximum gene set size, number of permutations, and value of the $\alpha$ parameter (the weighting exponent) were tested in a random order (3 replicates each) to minimize the influence of previous runs.
129129

130130
```{r, echo=FALSE}
131-
fig1_cap <- "Runtime of fast_ssgsea with A) 1,000 or B) 10,000 permutations. R was linked to the reference BLAS library, so only a single thread was used."
132-
133-
fig2_cap <- "Runtime of fast_ssgsea with A) 1,000 or B) 10,000 permutations. R was linked to OpenBLAS, and all 12 threads were used."
131+
fig_cap <- "Runtime of fast_ssgsea with A) 1,000 or B) 10,000 permutations."
134132
```
135133

136134

137-
```{r, echo=FALSE, fig.cap=fig1_cap}
135+
```{r, echo=FALSE, fig.cap=fig_cap}
138136
knitr::include_graphics("./man/figures/README-figure-1.png")
139137
```
140138

141-
## Switching the BLAS Library
142-
143-
Linking R to an external BLAS, such as the optimized, open-source OpenBLAS library [@openblas-1; @openblas-2], can greatly reduce the runtime (though it may affect precision):
144-
145-
```{r, echo=FALSE, fig.cap=fig2_cap}
146-
knitr::include_graphics("./man/figures/README-figure-2.png")
147-
```
148-
149-
### Linux
150-
151-
Linux users can follow [these instructions](https://docs.posit.co/resources/install-r-source.html#optional-configure-r-to-use-a-different-blas-library) from Posit to easily switch the BLAS library.
152-
153-
### macOS
154-
155-
For Mac computers with an Apple Silicon chip (M1 and beyond), follow [these instructions](https://cran.r-project.org/bin/macosx/RMacOSX-FAQ.html#Which-BLAS-is-used-and-how-can-it-be-changed_003f) to switch to Apple's optimized BLAS library.
156-
157-
### Windows
158-
159-
To install OpenBLAS for R on Windows, users may follow [this tutorial](https://github.com/david-cortes/R-openblas-in-windows).
160-
161-
162-
## Further Improvements
163-
164-
If using an external BLAS library, the runtime can be reduced by ~1/3 by switching from double-precision to single-precision floating point arithmetic for the permutation tests. If the BLAS supports multi-threading, the difference in runtime will be negligible, so the switch is largely unnecessary. While using floats will slightly affect the precision of the normalized enrichment scores (NES), the differences are small compared to differences observed from changing the value of the `seed` parameter. Unfortunately, the R BLAS does not support floats (see [this issue](https://github.com/RcppCore/RcppArmadillo/issues/197) in RcppArmadillo). Since Windows has no default BLAS/LAPACK library, it was not possible to implement this in `fast.ssgsea` without complicating the installation process or making it impossible for Windows users.
165-
166-
For users looking to implement this change, please follow these instructions:
167-
168-
1. Link R to an external BLAS library, such as OpenBLAS. Verify success by examining the result of `sessionInfo()["BLAS"]`.
169-
2. Clone pnnl/fast.ssgsea (e.g., with `git clone https://github.com/pnnl/fast.ssgsea` in a terminal) and open the fast.ssgsea.Rproj file.
170-
3. In src/Rcpp_functions.cpp, replace the `Rcpp_calcESPermCore` C++ function with the following:
171-
172-
```
173-
arma::fmat Rcpp_calcESPermCore(const float alpha,
174-
const arma::fmat& Y_perm,
175-
const arma::fmat& R_perm,
176-
const float sumRanks_i,
177-
const arma::fmat& A_perm,
178-
const arma::fvec& theta_m_i,
179-
const arma::fvec& theta_w_i)
180-
{
181-
arma::fmat AR_perm = A_perm * R_perm;
182-
183-
arma::fmat ES_perm(A_perm.n_rows, Y_perm.n_cols, arma::fill::zeros);
184-
185-
if (alpha == 0.0f) {
186-
ES_perm = arma::diagmat(1.0f / theta_m_i) * AR_perm;
187-
} else {
188-
ES_perm = (A_perm * (Y_perm % R_perm)) / (A_perm * Y_perm);
189-
}
190-
191-
ES_perm += arma::diagmat(1.0f / theta_w_i) * (AR_perm - sumRanks_i);
192-
193-
return ES_perm;
194-
}
195-
```
196-
197-
4. Build and install `fast.ssgea`.
198-
199-
200139
## References

0 commit comments

Comments
 (0)