Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 33 additions & 23 deletions R/indexing.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

## IGraph library.
## Copyright (C) 2010-2012 Gabor Csardi <csardi.gabor@gmail.com>
## 334 Harvard street, Cambridge, MA 02139 USA
Expand Down Expand Up @@ -329,14 +328,27 @@ length.igraph <- function(x) {
vcount(x)
}

expand.grid.unordered <- function(i, j, loops = FALSE, directed = FALSE) {
grid <- vctrs::vec_expand_grid(i = i, j = j)
if (!directed) {
grid <- vctrs::vec_unique(data.frame(
i = pmin(grid$i, grid$j),
j = pmax(grid$i, grid$j)
))
}
if (!loops) {
grid <- grid[grid[, 1] != grid[, 2], ]
}
grid
}

#' @method [<- igraph
#' @family functions for manipulating graph structure
#' @export
`[<-.igraph` <- function(x, i, j, ..., from, to,
attr = if (is_weighted(x)) "weight" else NULL,
loops = FALSE,
value) {
## TODO: rewrite this in C to make it faster

################################################################
## Argument checks
if ((!missing(from) || !missing(to)) &&
Expand Down Expand Up @@ -373,16 +385,16 @@ length.igraph <- function(x) {
(is.logical(value) && !value) ||
(is.null(attr) && is.numeric(value) && value == 0)) {
## Delete edges
todel <- x[from = from, to = to, ..., edges = TRUE]
todel <- get_edge_ids(x, c(rbind(from, to)))
x <- delete_edges(x, todel)
} else {
## Addition or update of an attribute (or both)
ids <- x[from = from, to = to, ..., edges = TRUE]
ids <- get_edge_ids(x, c(rbind(from, to)))
if (any(ids == 0)) {
x <- add_edges(x, rbind(from[ids == 0], to[ids == 0]))
}
if (!is.null(attr)) {
ids <- x[from = from, to = to, ..., edges = TRUE]
ids <- get_edge_ids(x, c(rbind(from, to)))
x <- set_edge_attr(x, attr, ids, value = value)
}
}
Expand All @@ -391,37 +403,35 @@ length.igraph <- function(x) {
(is.null(attr) && is.numeric(value) && value == 0)) {
## Delete edges
if (missing(i) && missing(j)) {
todel <- unlist(x[[, , ..., edges = TRUE]])
todel <- seq_len(ecount(x))
Comment thread
krlmlr marked this conversation as resolved.
} else if (missing(j)) {
todel <- unlist(x[[i, , ..., edges = TRUE]])
todel <- unlist(incident_edges(x, v = i, mode = "out"))
} else if (missing(i)) {
todel <- unlist(x[[, j, ..., edges = TRUE]])
todel <- unlist(incident_edges(x, v = j, mode = "in"))
} else {
todel <- unlist(x[[i, j, ..., edges = TRUE]])
edge_pairs <- expand.grid(i, j)
edge_ids <- get_edge_ids(x, c(rbind(edge_pairs[, 1], edge_pairs[, 2])))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this covered by tests?

Suggested change
edge_ids <- get_edge_ids(x, c(rbind(edge_pairs[, 1], edge_pairs[, 2])))
edge_ids <- get_edge_ids(x, as.vector(t(edge_pairs)))

The interface of get_edge_ids() is interesting. Should we extend that to accept two-column data frames?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this covered by tests?

Going through the existing tests, I realize there are some gaps. I will add a set of tests for this functionality

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface of get_edge_ids() is interesting. Should we extend that to accept two-column data frames?

It has been bothering me mildly for years as a user that edges need to be supplied as a vector (same with add_edges() and delete_edges() and probably more). However that's required by the c core. It might be too much of a fundamental change at this point?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The c(rbind(...)) pattern is probably fine, I forgot the semantics for vectors:

c(rbind(1:3, 4:6))
#> [1] 1 4 2 5 3 6
c(t(data.frame(1:3, 4:6)))
#> [1] 1 4 2 5 3 6
as.vector(t(data.frame(1:3, 4:6)))
#> [1] 1 4 2 5 3 6

Created on 2025-01-19 with reprex v2.1.1

There are two layers here: the C core and the R interface. We should provide an idiomatic R user interface that translates to what the C core needs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c(rbind(...)) is faster for data frames by an order of magnitude, but not for matrices:

df <- as.data.frame(cbind(1:30, 4:33))

bench::mark(
  c(t(df)),
  c(rbind(df[, 1], df[, 2])),
  c(rbind(df[[1]], df[[2]]))
)
#> # A tibble: 3 × 6
#>   expression                      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>                 <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 c(t(df))                    12.96µs  14.51µs    57926.    74.4KB     34.8
#> 2 c(rbind(df[, 1], df[, 2]))   5.33µs   5.99µs   157537.      576B     47.3
#> 3 c(rbind(df[[1]], df[[2]]))   3.65µs   4.26µs   219083.      576B     43.8

m <- cbind(1:30, 4:33)

bench::mark(
  c(t(m)),
  c(rbind(m[, 1], m[, 2]))
)
#> # A tibble: 2 × 6
#>   expression                    min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>               <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 c(t(m))                     820ns 983.94ns   964028.      576B     96.4
#> 2 c(rbind(m[, 1], m[, 2]))    984ns   1.15µs   791706.      576B      0

Created on 2025-01-20 with reprex v2.1.1

Draft PR for new UI in #1663.

todel <- edge_ids[edge_ids != 0]
}
x <- delete_edges(x, todel)
} else {
## Addition or update of an attribute (or both)
i <- if (missing(i)) as.numeric(V(x)) else as_igraph_vs(x, i)
j <- if (missing(j)) as.numeric(V(x)) else as_igraph_vs(x, j)
if (length(i) != 0 && length(j) != 0) {
## Existing edges, and their endpoints
exe <- lapply(x[[i, j, ..., edges = TRUE]], as.vector)
exv <- lapply(x[[i, j, ...]], as.vector)
toadd <- unlist(lapply(seq_along(exv), function(idx) {
to <- setdiff(j, exv[[idx]])
if (length(to != 0)) {
rbind(i[idx], setdiff(j, exv[[idx]]))
} else {
numeric()
}
}))
## Do the changes
edge_pairs <- expand.grid.unordered(i, j, loops = loops, directed = is_directed(x))

edge_ids <- get_edge_ids(x, c(rbind(edge_pairs[, 1], edge_pairs[, 2])))
Comment thread
schochastics marked this conversation as resolved.
toadd <- c(rbind(edge_pairs[edge_ids == 0, 1], edge_pairs[edge_ids == 0, 2]))

if (is.null(attr)) {
if (value > 1) {
cli::cli_abort("value greater than one but graph is not weighted and no attribute was specified.")
}
x <- add_edges(x, toadd)
} else {
x <- add_edges(x, toadd, attr = structure(list(value), names = attr))
toupdate <- unlist(exe)
toupdate <- edge_ids[edge_ids != 0]
x <- set_edge_attr(x, attr, toupdate, value)
}
}
Expand Down
128 changes: 125 additions & 3 deletions tests/testthat/test-indexing2.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ test_that("[ can set weights and delete weighted edges", {
A[1, 2] <- g[1, 2] <- 3
expect_equal(canonicalize_matrix(g[]), A)

A[1:2, 2:3] <- g[1:2, 2:3] <- -1
A[1:2, 2:3] <- g[1:2, 2:3, loops = TRUE] <- -1
expect_equal(canonicalize_matrix(g[]), A)

g[1, 2] <- NULL
Expand All @@ -52,12 +52,12 @@ test_that("[ can add edges and ste weights via vertex names", {
A["b", "c"] <- g["b", "c"] <- TRUE
expect_equal(canonicalize_matrix(g[]), canonicalize_matrix(A))

A[c("a", "f"), c("f", "a")] <- g[c("a", "f"), c("f", "a")] <- TRUE
A[c("a", "f"), c("f", "a")] <- g[c("a", "f"), c("f", "a"), loops = TRUE] <- TRUE
expect_equal(canonicalize_matrix(g[]), canonicalize_matrix(A))

A[A == 1] <- NA
A[c("a", "c", "h"), c("a", "b", "c")] <-
g[c("a", "c", "h"), c("a", "b", "c"), attr = "weight"] <- 3
g[c("a", "c", "h"), c("a", "b", "c"), attr = "weight", loops = TRUE] <- 3
expect_equal(canonicalize_matrix(g[]), canonicalize_matrix(A))
})

Expand Down Expand Up @@ -105,3 +105,125 @@ test_that("[ and from-to with multiple values", {
)
expect_equal(canonicalize_matrix(g[]), canonicalize_matrix(A))
})

test_that("[ manipulation works as intended for unweighted", {
# see issue https://github.com/igraph/rigraph/issues/1662
g1 <- make_empty_graph(n = 10, directed = FALSE)
A1 <- matrix(0, 10, 10)
A1[1:5, ] <- A1[, 1:5] <- g1[1:5, ] <- 1
diag(A1) <- 0
expect_equal(canonicalize_matrix(g1[]), A1)

g2 <- make_empty_graph(n = 10, directed = FALSE)
A2 <- matrix(0, 10, 10)
A2[1:5, ] <- A2[, 1:5] <- g2[, 1:5] <- 1
diag(A2) <- 0
expect_equal(canonicalize_matrix(g2[]), A2)

g3 <- make_empty_graph(n = 10, directed = TRUE)
A3 <- matrix(0, 10, 10)
A3[1:5, ] <- g3[1:5, ] <- 1
diag(A3) <- 0
expect_equal(canonicalize_matrix(g3[]), A3)

g4 <- make_empty_graph(n = 10, directed = TRUE)
A4 <- matrix(0, 10, 10)
A4[, 1:5] <- g4[, 1:5] <- 1
diag(A4) <- 0
expect_equal(canonicalize_matrix(g4[]), A4)

g5 <- make_empty_graph(n = 10, directed = TRUE)
A5 <- matrix(0, 10, 10)
g5[1, 2] <- g5[2, 1] <- A5[1, 2] <- A5[2, 1] <- 1
expect_equal(canonicalize_matrix(g5[]), A5)

g6 <- make_empty_graph(n = 10, directed = FALSE)
A6 <- matrix(0, 10, 10)
A6[6:10, 1:5] <- A6[1:5, 6:10] <- g6[6:10, 1:5] <- 1
expect_equal(canonicalize_matrix(g6[]), A6)

g7 <- make_empty_graph(n = 10, directed = TRUE)
A7 <- matrix(0, 10, 10)
g7[6:10, 1:5] <- A7[6:10, 1:5] <- 1
diag(A7) <- 0
expect_equal(canonicalize_matrix(g7[]), A7)

g8 <- make_empty_graph(n = 10, directed = TRUE)
A8 <- matrix(0, 10, 10)
g8[1:5, 6:10] <- A8[1:5, 6:10] <- 1
diag(A8) <- 0
expect_equal(canonicalize_matrix(g8[]), A8)
})

test_that("[ manipulation works as intended for weighted", {
# see issue https://github.com/igraph/rigraph/issues/1662

g1 <- make_empty_graph(n = 10, directed = FALSE)
A1 <- matrix(0, 10, 10)
A1[1:5, 1:5] <- g1[1:5, 1:5, attr = "weight"] <- 2
diag(A1) <- 0
expect_equal(canonicalize_matrix(g1[]), A1)

g2 <- make_empty_graph(n = 10, directed = FALSE)
E(g2)$weight <- 1
A2 <- matrix(0, 10, 10)
A2[1:3, 1:3] <- g2[1:3, 1:3] <- -2
diag(A2) <- 0
expect_equal(canonicalize_matrix(g2[]), A2)
})

test_that("[ manipulation handles errors properly", {
g1 <- make_empty_graph(n = 10, directed = FALSE)
expect_error(g1[1:5, ] <- 2)
})

test_that("[ deletion works as intended", {
# see issue https://github.com/igraph/rigraph/issues/1662
g1 <- make_full_graph(n = 10, directed = FALSE)
A1 <- matrix(1, 10, 10)
diag(A1) <- 0
A1[1:5, ] <- A1[, 1:5] <- g1[1:5, ] <- 0
expect_equal(canonicalize_matrix(g1[]), A1)

g2 <- make_full_graph(n = 10, directed = FALSE)
A2 <- matrix(1, 10, 10)
diag(A2) <- 0
A2[1:5, ] <- A2[, 1:5] <- g2[, 1:5] <- 0
expect_equal(canonicalize_matrix(g2[]), A2)

g3 <- make_full_graph(n = 10, directed = TRUE)
A3 <- matrix(1, 10, 10)
diag(A3) <- 0
A3[1:5, ] <- g3[1:5, ] <- 0
expect_equal(canonicalize_matrix(g3[]), A3)

g4 <- make_full_graph(n = 10, directed = TRUE)
A4 <- matrix(1, 10, 10)
diag(A4) <- 0
A4[, 1:5] <- g4[, 1:5] <- 0
expect_equal(canonicalize_matrix(g4[]), A4)

g5 <- make_full_graph(n = 10, directed = TRUE)
A5 <- matrix(1, 10, 10)
diag(A5) <- 0
g5[1, 2] <- g5[2, 1] <- A5[1, 2] <- A5[2, 1] <- 0
expect_equal(canonicalize_matrix(g5[]), A5)

g6 <- make_full_graph(n = 10, directed = FALSE)
A6 <- matrix(1, 10, 10)
diag(A6) <- 0
A6[6:10, 1:5] <- A6[1:5, 6:10] <- g6[6:10, 1:5] <- 0
expect_equal(canonicalize_matrix(g6[]), A6)

g7 <- make_full_graph(n = 10, directed = TRUE)
A7 <- matrix(1, 10, 10)
diag(A7) <- 0
g7[6:10, 1:5] <- A7[6:10, 1:5] <- 0
expect_equal(canonicalize_matrix(g7[]), A7)

g8 <- make_full_graph(n = 10, directed = TRUE)
A8 <- matrix(1, 10, 10)
diag(A8) <- 0
g8[1:5, 6:10] <- A8[1:5, 6:10] <- 0
expect_equal(canonicalize_matrix(g8[]), A8)
})