Skip to content

Commit

Permalink
refactor: enhance readability and performance of graph.adjacency.spar…
Browse files Browse the repository at this point in the history
…se (#1667)
  • Loading branch information
schochastics authored Feb 6, 2025
1 parent 723e7d1 commit 178bb74
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 138 deletions.
186 changes: 50 additions & 136 deletions R/adjacency.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

#' Create graphs from adjacency matrices
#'
#' @description
Expand Down Expand Up @@ -119,15 +118,15 @@ graph.adjacency <- function(adjmatrix, mode = c("directed", "undirected", "max",
#' @examples
#'
#' g1 <- sample(
#' x = 0:1, size = 100, replace = TRUE,
#' prob = c(0.9, 0.1)
#' ) %>%
#' x = 0:1, size = 100, replace = TRUE,
#' prob = c(0.9, 0.1)
#' ) %>%
#' matrix(ncol = 10) %>%
#' graph_from_adjacency_matrix()
#'
#' g2 <- sample(
#' x = 0:5, size = 100, replace = TRUE,
#' prob = c(0.9, 0.02, 0.02, 0.02, 0.02, 0.02)
#' x = 0:5, size = 100, replace = TRUE,
#' prob = c(0.9, 0.02, 0.02, 0.02, 0.02, 0.02)
#' ) %>%
#' matrix(ncol = 10) %>%
#' graph_from_adjacency_matrix(weighted = TRUE)
Expand Down Expand Up @@ -193,8 +192,8 @@ graph.adjacency <- function(adjmatrix, mode = c("directed", "undirected", "max",
#' x
#' }
#' expected_g8_weights <- non_zero_sort(
#' halve_diag(adj_matrix + t(adj_matrix)
#' )[lower.tri(adj_matrix, diag = TRUE)])
#' halve_diag(adj_matrix + t(adj_matrix))[lower.tri(adj_matrix, diag = TRUE)]
#' )
#' actual_g8_weights <- sort(E(g8)$weight)
#' all(expected_g8_weights == actual_g8_weights)
#'
Expand Down Expand Up @@ -229,7 +228,6 @@ graph_from_adjacency_matrix <- function(adjmatrix,
),
weighted = NULL, diag = TRUE,
add.colnames = NULL, add.rownames = NA) {

mode <- igraph.match.arg(mode)

if (!is.matrix(adjmatrix) && !inherits(adjmatrix, "Matrix")) {
Expand Down Expand Up @@ -377,157 +375,68 @@ mysummary <- function(x) {
result
}

pmax_AB <- function(A,B) {
change <- A < B
A[change] <- B[change]
A
}

graph.adjacency.sparse <- function(adjmatrix, mode, weighted = NULL, diag = TRUE) {
pmin_AB <- function(A, B) {
change <- A > B
A[change] <- B[change]
A
}

graph.adjacency.sparse <- function(adjmatrix, mode, weighted = NULL, diag = TRUE, call = rlang::caller_env()) {
if (!is.null(weighted)) {
if (is.logical(weighted) && weighted) {
weighted <- "weight"
}
if (!is.character(weighted)) {
stop("invalid value supplied for `weighted' argument, please see docs.")
cli::cli_abort("Invalid value supplied for `weighted' argument, please see docs.", call = call)
}
}

if (nrow(adjmatrix) != ncol(adjmatrix)) {
stop("not a square matrix")
cli::cli_abort("Not a square matrix", call = call)
}

vc <- nrow(adjmatrix)
# Exit early for empty graphs
if (vc == 1 || Matrix::nnzero(adjmatrix) == 0) {
return(make_empty_graph(n = vc, directed = (mode == "directed")))
}

## to remove non-redundancies that can persist in a dgtMatrix
if (inherits(adjmatrix, "dgTMatrix")) {
adjmatrix <- as(adjmatrix, "CsparseMatrix")
} else if (inherits(adjmatrix, "ddiMatrix")) {
adjmatrix <- as(adjmatrix, "CsparseMatrix")
}

if (mode == "directed") {
## DIRECTED
el <- mysummary(adjmatrix)
if (!diag) {
el <- el[el[, 1] != el[, 2], ]
}
} else if (mode == "undirected") {
## UNDIRECTED, must be symmetric if weighted
if (mode == "undirected") {
if (!is.null(weighted) && !Matrix::isSymmetric(adjmatrix)) {
stop("Please supply a symmetric matrix if you want to create a weighted graph with mode=UNDIRECTED.")
}
if (diag) {
adjmatrix <- Matrix::tril(adjmatrix)
} else {
if (vc == 1) {
# Work around Matrix glitch
adjmatrix <- as(matrix(0), "dgCMatrix")
} else {
adjmatrix <- Matrix::tril(adjmatrix, -1)
}
}
el <- mysummary(adjmatrix)
rm(adjmatrix)
} else if (mode == "max") {
## MAXIMUM
el <- mysummary(adjmatrix)
rm(adjmatrix)
if (!diag) {
el <- el[el[, 1] != el[, 2], ]
}
el <- el[el[, 3] != 0, ]
w <- el[, 3]
el <- el[, 1:2]
el <- cbind(pmin(el[, 1], el[, 2]), pmax(el[, 1], el[, 2]))
o <- order(el[, 1], el[, 2])
el <- el[o, , drop = FALSE]
w <- w[o]
if (nrow(el) > 1) {
dd <- el[2:nrow(el), 1] == el[1:(nrow(el) - 1), 1] &
el[2:nrow(el), 2] == el[1:(nrow(el) - 1), 2]
dd <- which(dd)
if (length(dd) > 0) {
mw <- pmax(w[dd], w[dd + 1])
w[dd] <- mw
w[dd + 1] <- mw
el <- el[-dd, , drop = FALSE]
w <- w[-dd]
}
}
el <- cbind(el, w)
} else if (mode == "upper") {
## UPPER
if (diag) {
adjmatrix <- Matrix::triu(adjmatrix)
} else {
adjmatrix <- Matrix::triu(adjmatrix, 1)
}
el <- mysummary(adjmatrix)
rm(adjmatrix)
if (!diag) {
el <- el[el[, 1] != el[, 2], ]
cli::cli_abort(
"Please supply a symmetric matrix if you want to create a weighted graph with mode=UNDIRECTED.",
call = call
)
}
adjmatrix <- Matrix::tril(adjmatrix)
} else if (mode == "lower") {
## LOWER
if (diag) {
adjmatrix <- Matrix::tril(adjmatrix)
} else {
if (vc == 1) {
# Work around Matrix glitch
adjmatrix <- as(matrix(0), "dgCMatrix")
} else {
adjmatrix <- Matrix::tril(adjmatrix, -1)
}
}
el <- mysummary(adjmatrix)
rm(adjmatrix)
if (!diag) {
el <- el[el[, 1] != el[, 2], ]
}
} else if (mode == "min") {
## MINIMUM
adjmatrix <- sign(adjmatrix) * sign(Matrix::t(adjmatrix)) * adjmatrix
el <- mysummary(adjmatrix)
rm(adjmatrix)
if (!diag) {
el <- el[el[, 1] != el[, 2], ]
}
el <- el[el[, 3] != 0, ]
w <- el[, 3]
el <- el[, 1:2]
el <- cbind(pmin(el[, 1], el[, 2]), pmax(el[, 1], el[, 2]))
o <- order(el[, 1], el[, 2])
el <- el[o, ]
w <- w[o]
if (nrow(el) > 1) {
dd <- el[2:nrow(el), 1] == el[1:(nrow(el) - 1), 1] &
el[2:nrow(el), 2] == el[1:(nrow(el) - 1), 2]
dd <- which(dd)
if (length(dd) > 0) {
mw <- pmin(w[dd], w[dd + 1])
w[dd] <- mw
w[dd + 1] <- mw
el <- el[-dd, ]
w <- w[-dd]
}
}
el <- cbind(el, w)
adjmatrix <- Matrix::tril(adjmatrix)
} else if (mode == "upper") {
adjmatrix <- Matrix::triu(adjmatrix)
} else if (mode == "plus") {
## PLUS
adjmatrix <- adjmatrix + Matrix::t(adjmatrix)
if (diag) {
adjmatrix <- Matrix::tril(adjmatrix)
} else {
if (vc == 1) {
# Work around Matrix glitch
adjmatrix <- as(matrix(0), "dgCMatrix")
} else {
adjmatrix <- Matrix::tril(adjmatrix, -1)
}
}
el <- mysummary(adjmatrix)
rm(adjmatrix)
if (diag) {
loop <- el[, 1] == el[, 2]
el[loop, 3] <- el[loop, 3] / 2
}
el <- el[el[, 3] != 0, ]
adjmatrix <- Matrix::tril(adjmatrix)
} else if (mode == "max") {
adjmatrix <- pmax_AB(Matrix::tril(adjmatrix), Matrix::t(Matrix::triu(adjmatrix)))
} else if (mode == "min") {
adjmatrix <- pmin_AB(Matrix::tril(adjmatrix), Matrix::t(Matrix::triu(adjmatrix)))
adjmatrix <- Matrix::drop0(adjmatrix)
}
el <- mysummary(adjmatrix)
if (!diag) {
el <- el[el[, 1] != el[, 2], ]
}

if (!is.null(weighted)) {
Expand All @@ -536,7 +445,12 @@ graph.adjacency.sparse <- function(adjmatrix, mode, weighted = NULL, diag = TRUE
names(weight) <- weighted
res <- add_edges(res, edges = t(as.matrix(el[, 1:2])), attr = weight)
} else {
edges <- unlist(apply(el, 1, function(x) rep(unname(x[1:2]), x[3])))
if (max(el[, 3]) == 1) {
edges <- as.vector(t(el[, 1:2]))
} else {
row_repeats <- rep(seq_len(nrow(el)), times = el[, 3])
edges <- as.vector(t(el[row_repeats, 1:2]))
}
res <- make_graph(n = vc, edges, directed = (mode == "directed"))
}
res
Expand Down
61 changes: 59 additions & 2 deletions tests/testthat/test-adjacency.R
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ test_that("graph_from_adjacency_matrix() works", {
})

test_that("graph_from_adjacency_matrix() works -- dgCMatrix", {
skip_if_not_installed("Matrix")
skip_if_not_installed("Matrix",minimum_version = "1.6.0")

M1 <- rbind(
c(0, 0, 1, 1),
Expand Down Expand Up @@ -597,7 +597,7 @@ test_that("graph_from_adjacency_matrix() snapshot", {
})

test_that("graph_from_adjacency_matrix() snapshot for sparse matrices", {
skip_if_not_installed("Matrix")
skip_if_not_installed("Matrix", minimum_version = "1.6.0")

rlang::local_options(lifecycle_verbosity = "warning")

Expand Down Expand Up @@ -657,3 +657,60 @@ test_that("weighted graph_from_adjacency_matrix() works on integer matrices", {
g <- graph_from_adjacency_matrix(data, weighted = TRUE)
expect_equal(as.matrix(g[]), data)
})

test_that("sparse/dense matrices no loops works",{
skip_if_not_installed("Matrix", minimum_version = "1.6.0")
A <- diag(1, 5)
A[1, 2] <- 1
g <- graph_from_adjacency_matrix(A, diag = FALSE)
expect_equal(ecount(g), 1)
expect_equal(get_edge_ids(g, c(1, 2)), 1)

A <- as(A, "dgCMatrix")
g <- graph_from_adjacency_matrix(A, diag = FALSE)
expect_equal(ecount(g), 1)
expect_equal(get_edge_ids(g,c(1, 2)), 1)

})

test_that("sparse/dense matrices multiple works",{
skip_if_not_installed("Matrix", minimum_version = "1.6.0")
A <- matrix(0, 5, 5)
A[1, 2] <- 3
g <- graph_from_adjacency_matrix(A, diag = FALSE, weighted = FALSE)
expect_equal(ecount(g), 3)
expect_equal(as_edgelist(g), matrix(c(1, 2), 3, 2, byrow = TRUE))

A <- as(A,"dgCMatrix")
g <- graph_from_adjacency_matrix(A,diag = FALSE)
expect_equal(ecount(g), 3)
expect_equal(as_edgelist(g), matrix(c(1, 2), 3, 2, byrow = TRUE))

})

test_that("sparse/dense matrices min/max/plus",{
skip_if_not_installed("Matrix", minimum_version = "1.6.0")
A <- matrix(0, 5, 5)
A[1, 2] <- 3
A[2, 1] <- 2
g <- graph_from_adjacency_matrix(A, diag = FALSE, mode = "max", weighted = TRUE)
expect_equal(E(g)$weight[1], 3)

g <- graph_from_adjacency_matrix(A, diag = FALSE, mode = "min", weighted = TRUE)
expect_equal(E(g)$weight[1], 2)

g <- graph_from_adjacency_matrix(A, diag = FALSE, mode = "plus", weighted = TRUE)
expect_equal(E(g)$weight[1], 5)

A <- as(A,"dgCMatrix")
g <- graph_from_adjacency_matrix(A, diag = FALSE, mode = "max", weighted = TRUE)
expect_equal(E(g)$weight[1], 3)

g <- graph_from_adjacency_matrix(A, diag = FALSE, mode = "min", weighted = TRUE)
expect_equal(E(g)$weight[1], 2)

g <- graph_from_adjacency_matrix(A, diag = FALSE, mode = "plus", weighted = TRUE)
expect_equal(E(g)$weight[1], 5)


})

0 comments on commit 178bb74

Please sign in to comment.