diff --git a/R/attributes.R b/R/attributes.R index 5db68c0962..a413190b8c 100644 --- a/R/attributes.R +++ b/R/attributes.R @@ -1290,7 +1290,7 @@ is_bipartite <- function(graph) { ############# -igraph.i.attribute.combination <- function(comb) { +igraph.i.attribute.combination <- function(comb, allow_rename = FALSE) { if (is.function(comb)) { comb <- list(comb) } @@ -1310,34 +1310,40 @@ igraph.i.attribute.combination <- function(comb) { if (anyDuplicated(names(comb)) > 0) { cli::cli_warn("Some attributes are duplicated") } + known_names <- c( + "ignore", + "sum", + "prod", + "min", + "max", + "random", + "first", + "last", + "mean", + "median", + "concat" + ) + known_codes <- c(0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) + if (allow_rename) { + known_names <- c(known_names, "rename") + known_codes <- c(known_codes, NA_integer_) + } comb <- lapply(comb, function(x) { if (!is.character(x)) { x } else { - known <- data.frame( - n = c( - "ignore", - "sum", - "prod", - "min", - "max", - "random", - "first", - "last", - "mean", - "median", - "concat" - ), - i = c(0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), - stringsAsFactors = FALSE - ) - x <- pmatch(tolower(x), known[, 1]) - if (is.na(x)) { + idx <- pmatch(tolower(x), known_names) + if (is.na(idx)) { + if (!allow_rename && identical(tolower(x), "rename")) { + cli::cli_abort( + "{.val rename} is only supported by graph operators ({.fn union}, {.fn intersection}, {.fn compose}, {.fn disjoint_union}), not by this function." + ) + } cli::cli_abort( "Unknown/unambigous attribute combination specification." ) } - known[, 2][x] + if (is.na(known_codes[idx])) "rename" else known_codes[idx] } }) @@ -1435,6 +1441,15 @@ igraph.i.attribute.combination <- function(comb) { #' Concatenate the attributes, using the [c()] function. #' This results almost always a complex attribute. #' } +#' \item{"rename"}{ +#' Keep clashing attributes side-by-side under disambiguated names by +#' appending `_1`, `_2`, ... suffixes. This is the default for the +#' graph operators [union()], [intersection()], [compose()] and +#' [disjoint_union()] and preserves their historical behaviour. +#' Only those operators accept `"rename"`; [simplify()] and +#' [contract()] will reject it because the rename strategy has no +#' per-element interpretation when many input values collapse into one. +#' } #' } #' @author Gabor Csardi \email{csardi.gabor@@gmail.com} #' @seealso [graph_attr()], [vertex_attr()], diff --git a/R/operators.R b/R/operators.R index 8782cfb061..5a6caf9a9e 100644 --- a/R/operators.R +++ b/R/operators.R @@ -118,13 +118,14 @@ graph.complementer <- function(graph, loops = FALSE) { # ################################################################### -rename.attr.if.needed <- function( +combine.attrs <- function( type = c("g", "v", "e"), graphs, newsize = NULL, maps = NULL, maps2 = NULL, - ignore = character() + ignore = character(), + comb = list("rename") ) { type <- igraph_match_arg(type) @@ -146,6 +147,9 @@ rename.attr.if.needed <- function( getval <- function(which, name) { newval <- getfun(graphs[[which]], name) + if (type == "g") { + return(newval) + } if (!is.null(maps)) { tmpval <- newval[maps[[which]] >= 0] mm <- maps[[which]][maps[[which]] >= 0] + 1 @@ -161,22 +165,85 @@ rename.attr.if.needed <- function( newval } + default_idx <- which(names(comb) == "" | is.na(names(comb))) + default_comb <- if (length(default_idx) > 0) { + comb[[default_idx[1]]] + } else { + "rename" + } + resolve_comb <- function(name) { + if (nzchar(name) && name %in% names(comb)) { + comb[[name]] + } else { + default_comb + } + } + attr <- list() for (name in an) { w <- which(sapply(alist, function(x) name %in% x)) - if (length(w) == 1) { - attr[[name]] <- getval(w, name) - } else { - for (w2 in w) { - nname <- paste(name, sep = "_", w2) - newval <- getval(w2, name) - attr[[nname]] <- newval + this_comb <- resolve_comb(name) + + if (identical(this_comb, "rename")) { + if (length(w) == 1) { + attr[[name]] <- getval(w, name) + } else { + for (w2 in w) { + nname <- paste(name, sep = "_", w2) + attr[[nname]] <- getval(w2, name) + } } + } else if (identical(this_comb, 0) || identical(this_comb, 0L)) { + # ignore: drop the attribute + } else { + vals <- lapply(w, function(w2) getval(w2, name)) + attr[[name]] <- apply_attr_combiner(this_comb, vals, type) } } attr } +apply_attr_combiner <- function(comb, vals, type) { + if (type == "g") { + x <- unlist(vals, recursive = FALSE) + return(apply_one_combiner(comb, x)) + } + m <- do.call(cbind, vals) + out <- lapply(seq_len(nrow(m)), function(i) { + x <- m[i, ] + x <- x[!is.na(x)] + apply_one_combiner(comb, x) + }) + if (all(vapply(out, length, integer(1)) == 1L)) { + unlist(out) + } else { + out + } +} + +apply_one_combiner <- function(comb, x) { + if (is.function(comb)) { + return(comb(x)) + } + if (length(x) == 0) { + return(NA) + } + switch( + as.character(comb), + "3" = sum(x), + "4" = prod(x), + "5" = min(x), + "6" = max(x), + "7" = sample(x, 1), + "8" = x[[1]], + "9" = x[[length(x)]], + "10" = mean(x), + "11" = stats::median(x), + "12" = if (length(x) == 1) x[[1]] else x, + cli::cli_abort("Unknown attribute combiner code: {.val {comb}}") + ) +} + #' Disjoint union of graphs #' @@ -192,9 +259,10 @@ rename.attr.if.needed <- function( #' particular, it merges vertex and edge attributes using the [vctrs::vec_c()] #' function. For graphs that lack some vertex/edge attribute, the corresponding #' values in the new graph are set to a missing value (`NA` for scalar attributes, -#' `NULL` for list attributes). Graph attributes are simply -#' copied to the result. If this would result a name clash, then they are -#' renamed by adding suffixes: _1, _2, etc. +#' `NULL` for list attributes). Graph attributes are combined according to +#' `graph.attr.comb`; by default any name clash is resolved by adding +#' suffixes (`_1`, `_2`, ...). See [igraph-attribute-combination] for the +#' available combiners. #' #' Note that if both graphs have vertex names (i.e. a `name` vertex #' attribute), then the concatenated vertex names might be non-unique in the @@ -206,6 +274,10 @@ rename.attr.if.needed <- function( #' @aliases %du% #' @param \dots Graph objects or lists of graph objects. #' @param x,y Graph objects. +#' @param graph.attr.comb Specification for combining shared graph attributes. +#' Defaults to `"rename"`, which preserves the historical behaviour of +#' appending `_1`, `_2`, ... suffixes to clashing attribute names. See +#' [igraph-attribute-combination] for the available combiners. #' @return A new graph object. #' @author Gabor Csardi \email{csardi.gabor@@gmail.com} #' @export @@ -219,7 +291,7 @@ rename.attr.if.needed <- function( #' V(g2)$name <- letters[11:20] #' print_all(g1 %du% g2) #' @export -disjoint_union <- function(...) { +disjoint_union <- function(..., graph.attr.comb = "rename") { graphs <- unlist( recursive = FALSE, lapply(list(...), function(l) { @@ -232,7 +304,11 @@ disjoint_union <- function(...) { res <- .Call(Rx_igraph_disjoint_union, graphs) ## Graph attributes - graph.attributes(res) <- rename.attr.if.needed("g", graphs) + graph.attr.comb <- igraph.i.attribute.combination( + graph.attr.comb, + allow_rename = TRUE + ) + graph.attributes(res) <- combine.attrs("g", graphs, comb = graph.attr.comb) ## Vertex attributes attr <- list() @@ -306,7 +382,10 @@ disjoint_union <- function(...) { call, ..., byname, - keep.all.vertices + keep.all.vertices, + graph.attr.comb = "rename", + vertex.attr.comb = "rename", + edge.attr.comb = "rename" ) { graphs <- unlist( recursive = FALSE, @@ -330,6 +409,19 @@ disjoint_union <- function(...) { cli::cli_abort("Some graphs are not named.") } + graph.attr.comb <- igraph.i.attribute.combination( + graph.attr.comb, + allow_rename = TRUE + ) + vertex.attr.comb <- igraph.i.attribute.combination( + vertex.attr.comb, + allow_rename = TRUE + ) + edge.attr.comb <- igraph.i.attribute.combination( + edge.attr.comb, + allow_rename = TRUE + ) + edgemaps <- length(unlist(lapply(graphs, edge_attr_names))) != 0 if (byname) { @@ -357,23 +449,28 @@ disjoint_union <- function(...) { maps <- res$edgemaps res <- res$graph - ## We might need to rename all attributes - graph.attributes(res) <- rename.attr.if.needed("g", newgraphs) - vertex.attributes(res) <- rename.attr.if.needed( + graph.attributes(res) <- combine.attrs( + "g", + newgraphs, + comb = graph.attr.comb + ) + vertex.attributes(res) <- combine.attrs( "v", newgraphs, vcount(res), - ignore = "name" + ignore = "name", + comb = vertex.attr.comb ) V(res)$name <- uninames ## Edges are a bit more difficult, we need a mapping if (edgemaps) { - edge.attributes(res) <- rename.attr.if.needed( + edge.attributes(res) <- combine.attrs( "e", newgraphs, ecount(res), - maps = maps + maps = maps, + comb = edge.attr.comb ) } } else { @@ -397,21 +494,26 @@ disjoint_union <- function(...) { maps <- res$edgemaps res <- res$graph - ## We might need to rename all attributes - graph.attributes(res) <- rename.attr.if.needed("g", graphs) - vertex.attributes(res) <- rename.attr.if.needed( + graph.attributes(res) <- combine.attrs( + "g", + graphs, + comb = graph.attr.comb + ) + vertex.attributes(res) <- combine.attrs( "v", graphs, - vcount(res) + vcount(res), + comb = vertex.attr.comb ) ## Edges are a bit more difficult, we need a mapping if (edgemaps) { - edge.attributes(res) <- rename.attr.if.needed( + edge.attributes(res) <- combine.attrs( "e", graphs, ecount(res), - maps = maps + maps = maps, + comb = edge.attr.comb ) } } @@ -459,9 +561,12 @@ union.default <- function(...) { #' of the internal numeric vertex IDs. #' #' `union()` keeps the attributes of all graphs. All graph, vertex and -#' edge attributes are copied to the result. If an attribute is present in -#' multiple graphs and would result a name clash, then this attribute is -#' renamed by adding suffixes: _1, _2, etc. +#' edge attributes are copied to the result. By default, if an attribute is +#' present in multiple graphs and would result in a name clash, that attribute +#' is renamed by adding suffixes: `_1`, `_2`, etc. Pass `graph.attr.comb`, +#' `vertex.attr.comb` or `edge.attr.comb` to combine clashing attributes +#' instead, e.g. by summing or by taking the first non-`NA` value. See +#' [igraph-attribute-combination] for the available combiners. #' #' The `name` vertex attribute is treated specially if the operation is #' performed based on symbolic vertex names. In this case `name` must be @@ -477,6 +582,11 @@ union.default <- function(...) { #' `auto`, that means `TRUE` if all graphs are named and `FALSE` #' otherwise. A warning is generated if `auto` and some (but not all) #' graphs are named. +#' @param graph.attr.comb,vertex.attr.comb,edge.attr.comb Specification for +#' combining clashing graph, vertex and edge attributes. Each defaults to +#' `"rename"`, which preserves the historical behaviour of appending +#' `_1`, `_2`, ... suffixes. See [igraph-attribute-combination] for the +#' available combiners. #' @return A new graph object. #' @author Gabor Csardi \email{csardi.gabor@@gmail.com} #' @method union igraph @@ -492,12 +602,21 @@ union.default <- function(...) { #' ) #' net2 <- graph_from_literal(D - A:F:Y, B - A - X - F - H - Z, F - Y) #' print_all(net1 %u% net2) -union.igraph <- function(..., byname = "auto") { +union.igraph <- function( + ..., + byname = "auto", + graph.attr.comb = "rename", + vertex.attr.comb = "rename", + edge.attr.comb = "rename" +) { .igraph.graph.union.or.intersection( "union", ..., byname = byname, - keep.all.vertices = TRUE + keep.all.vertices = TRUE, + graph.attr.comb = graph.attr.comb, + vertex.attr.comb = vertex.attr.comb, + edge.attr.comb = edge.attr.comb ) } @@ -540,9 +659,12 @@ intersection <- function(...) { #' of the internal numeric vertex IDs. #' #' `intersection()` keeps the attributes of all graphs. All graph, -#' vertex and edge attributes are copied to the result. If an attribute is -#' present in multiple graphs and would result a name clash, then this -#' attribute is renamed by adding suffixes: _1, _2, etc. +#' vertex and edge attributes are copied to the result. By default, if an +#' attribute is present in multiple graphs and would result in a name clash, +#' that attribute is renamed by adding suffixes: `_1`, `_2`, etc. Pass +#' `graph.attr.comb`, `vertex.attr.comb` or `edge.attr.comb` to combine +#' clashing attributes instead; see [igraph-attribute-combination] for the +#' available combiners. #' #' The `name` vertex attribute is treated specially if the operation is #' performed based on symbolic vertex names. In this case `name` must be @@ -560,6 +682,10 @@ intersection <- function(...) { #' graphs are named. #' @param keep.all.vertices Logical scalar, whether to keep vertices that only #' appear in a subset of the input graphs. +#' @param graph.attr.comb,vertex.attr.comb,edge.attr.comb Specification for +#' combining clashing graph, vertex and edge attributes. Each defaults to +#' `"rename"`. See [igraph-attribute-combination] for the available +#' combiners. #' @return A new graph object. #' @author Gabor Csardi \email{csardi.gabor@@gmail.com} #' @method intersection igraph @@ -578,13 +704,19 @@ intersection <- function(...) { intersection.igraph <- function( ..., byname = "auto", - keep.all.vertices = TRUE + keep.all.vertices = TRUE, + graph.attr.comb = "rename", + vertex.attr.comb = "rename", + edge.attr.comb = "rename" ) { .igraph.graph.union.or.intersection( "intersection", ..., byname = byname, - keep.all.vertices = keep.all.vertices + keep.all.vertices = keep.all.vertices, + graph.attr.comb = graph.attr.comb, + vertex.attr.comb = vertex.attr.comb, + edge.attr.comb = edge.attr.comb ) } @@ -765,9 +897,11 @@ complementer <- function(graph, loops = FALSE) { #' names. Otherwise numeric vertex IDs are used. #' #' `compose()` keeps the attributes of both graphs. All graph, vertex -#' and edge attributes are copied to the result. If an attribute is present in -#' multiple graphs and would result a name clash, then this attribute is -#' renamed by adding suffixes: _1, _2, etc. +#' and edge attributes are copied to the result. By default, if an attribute +#' is present in both graphs and would result in a name clash, that attribute +#' is renamed by adding suffixes: `_1`, `_2`. Pass `graph.attr.comb`, +#' `vertex.attr.comb` or `edge.attr.comb` to combine clashing attributes +#' instead; see [igraph-attribute-combination] for the available combiners. #' #' The `name` vertex attribute is treated specially if the operation is #' performed based on symbolic vertex names. In this case `name` must be @@ -796,6 +930,10 @@ complementer <- function(graph, loops = FALSE) { #' `auto`, that means `TRUE` if both graphs are named and #' `FALSE` otherwise. A warning is generated if `auto` and one graph, #' but not both graphs are named. +#' @param graph.attr.comb,vertex.attr.comb,edge.attr.comb Specification for +#' combining clashing graph, vertex and edge attributes. Each defaults to +#' `"rename"`. See [igraph-attribute-combination] for the available +#' combiners. #' @return A new graph object. #' @author Gabor Csardi \email{csardi.gabor@@gmail.com} #' @family functions for manipulating graph structure @@ -809,7 +947,14 @@ complementer <- function(graph, loops = FALSE) { #' print_all(gc) #' print_all(simplify(gc)) #' -compose <- function(g1, g2, byname = "auto") { +compose <- function( + g1, + g2, + byname = "auto", + graph.attr.comb = "rename", + vertex.attr.comb = "rename", + edge.attr.comb = "rename" +) { ensure_igraph(g1) ensure_igraph(g2) @@ -828,6 +973,19 @@ compose <- function(g1, g2, byname = "auto") { cli::cli_abort("Some graphs are not named.") } + graph.attr.comb <- igraph.i.attribute.combination( + graph.attr.comb, + allow_rename = TRUE + ) + vertex.attr.comb <- igraph.i.attribute.combination( + vertex.attr.comb, + allow_rename = TRUE + ) + edge.attr.comb <- igraph.i.attribute.combination( + edge.attr.comb, + allow_rename = TRUE + ) + if (byname) { uninames <- unique(c(V(g1)$name, V(g2)$name)) if (vcount(g1) < length(uninames)) { @@ -852,28 +1010,34 @@ compose <- function(g1, g2, byname = "auto") { maps <- list(res$edge_map1, res$edge_map2) res <- res$graph - ## We might need to rename all attributes graphs <- list(g1, g2) - graph.attributes(res) <- rename.attr.if.needed("g", graphs) + graph.attributes(res) <- combine.attrs("g", graphs, comb = graph.attr.comb) if (byname) { - vertex.attributes(res) <- - rename.attr.if.needed("v", graphs, vcount(res), ignore = "name") + vertex.attributes(res) <- combine.attrs( + "v", + graphs, + vcount(res), + ignore = "name", + comb = vertex.attr.comb + ) V(res)$name <- uninames } else { - vertex.attributes(res) <- rename.attr.if.needed( + vertex.attributes(res) <- combine.attrs( "v", graphs, - vcount(res) + vcount(res), + comb = vertex.attr.comb ) } if (edgemaps) { - edge.attributes(res) <- rename.attr.if.needed( + edge.attributes(res) <- combine.attrs( "e", graphs, ecount(res), - maps2 = maps + maps2 = maps, + comb = edge.attr.comb ) } diff --git a/man/compose.Rd b/man/compose.Rd index f26ae5aad8..2500c1620d 100644 --- a/man/compose.Rd +++ b/man/compose.Rd @@ -5,7 +5,14 @@ \alias{\%c\%} \title{Compose two graphs as binary relations} \usage{ -compose(g1, g2, byname = "auto") +compose( + g1, + g2, + byname = "auto", + graph.attr.comb = "rename", + vertex.attr.comb = "rename", + edge.attr.comb = "rename" +) } \arguments{ \item{g1}{The first input graph.} @@ -17,6 +24,11 @@ to perform the operation based on symbolic vertex names. If it is \code{auto}, that means \code{TRUE} if both graphs are named and \code{FALSE} otherwise. A warning is generated if \code{auto} and one graph, but not both graphs are named.} + +\item{graph.attr.comb, vertex.attr.comb, edge.attr.comb}{Specification for +combining clashing graph, vertex and edge attributes. Each defaults to +\code{"rename"}. See \link{igraph-attribute-combination} for the available +combiners.} } \value{ A new graph object. @@ -38,9 +50,11 @@ are all named), then the operation is performed based on symbolic vertex names. Otherwise numeric vertex IDs are used. \code{compose()} keeps the attributes of both graphs. All graph, vertex -and edge attributes are copied to the result. If an attribute is present in -multiple graphs and would result a name clash, then this attribute is -renamed by adding suffixes: _1, _2, etc. +and edge attributes are copied to the result. By default, if an attribute +is present in both graphs and would result in a name clash, that attribute +is renamed by adding suffixes: \verb{_1}, \verb{_2}. Pass \code{graph.attr.comb}, +\code{vertex.attr.comb} or \code{edge.attr.comb} to combine clashing attributes +instead; see \link{igraph-attribute-combination} for the available combiners. The \code{name} vertex attribute is treated specially if the operation is performed based on symbolic vertex names. In this case \code{name} must be diff --git a/man/disjoint_union.Rd b/man/disjoint_union.Rd index 220e39c546..23f301d792 100644 --- a/man/disjoint_union.Rd +++ b/man/disjoint_union.Rd @@ -5,13 +5,18 @@ \alias{\%du\%} \title{Disjoint union of graphs} \usage{ -disjoint_union(...) +disjoint_union(..., graph.attr.comb = "rename") x \%du\% y } \arguments{ \item{\dots}{Graph objects or lists of graph objects.} +\item{graph.attr.comb}{Specification for combining shared graph attributes. +Defaults to \code{"rename"}, which preserves the historical behaviour of +appending \verb{_1}, \verb{_2}, ... suffixes to clashing attribute names. See +\link{igraph-attribute-combination} for the available combiners.} + \item{x, y}{Graph objects.} } \value{ @@ -31,9 +36,10 @@ function can also be used via the \verb{\%du\%} operator. particular, it merges vertex and edge attributes using the \code{\link[vctrs:vec_c]{vctrs::vec_c()}} function. For graphs that lack some vertex/edge attribute, the corresponding values in the new graph are set to a missing value (\code{NA} for scalar attributes, -\code{NULL} for list attributes). Graph attributes are simply -copied to the result. If this would result a name clash, then they are -renamed by adding suffixes: _1, _2, etc. +\code{NULL} for list attributes). Graph attributes are combined according to +\code{graph.attr.comb}; by default any name clash is resolved by adding +suffixes (\verb{_1}, \verb{_2}, ...). See \link{igraph-attribute-combination} for the +available combiners. Note that if both graphs have vertex names (i.e. a \code{name} vertex attribute), then the concatenated vertex names might be non-unique in the diff --git a/man/igraph-attribute-combination.Rd b/man/igraph-attribute-combination.Rd index f6e2e22a5b..20354f0163 100644 --- a/man/igraph-attribute-combination.Rd +++ b/man/igraph-attribute-combination.Rd @@ -93,6 +93,15 @@ Calls the R \code{\link[=median]{median()}} function for all attribute types. Concatenate the attributes, using the \code{\link[=c]{c()}} function. This results almost always a complex attribute. } +\item{"rename"}{ +Keep clashing attributes side-by-side under disambiguated names by +appending \verb{_1}, \verb{_2}, ... suffixes. This is the default for the +graph operators \code{\link[=union]{union()}}, \code{\link[=intersection]{intersection()}}, \code{\link[=compose]{compose()}} and +\code{\link[=disjoint_union]{disjoint_union()}} and preserves their historical behaviour. +Only those operators accept \code{"rename"}; \code{\link[=simplify]{simplify()}} and +\code{\link[=contract]{contract()}} will reject it because the rename strategy has no +per-element interpretation when many input values collapse into one. +} } } diff --git a/man/intersection.igraph.Rd b/man/intersection.igraph.Rd index e7345de60f..06b265b118 100644 --- a/man/intersection.igraph.Rd +++ b/man/intersection.igraph.Rd @@ -5,7 +5,14 @@ \alias{\%s\%} \title{Intersection of graphs} \usage{ -\method{intersection}{igraph}(..., byname = "auto", keep.all.vertices = TRUE) +\method{intersection}{igraph}( + ..., + byname = "auto", + keep.all.vertices = TRUE, + graph.attr.comb = "rename", + vertex.attr.comb = "rename", + edge.attr.comb = "rename" +) } \arguments{ \item{\dots}{Graph objects or lists of graph objects.} @@ -18,6 +25,11 @@ graphs are named.} \item{keep.all.vertices}{Logical scalar, whether to keep vertices that only appear in a subset of the input graphs.} + +\item{graph.attr.comb, vertex.attr.comb, edge.attr.comb}{Specification for +combining clashing graph, vertex and edge attributes. Each defaults to +\code{"rename"}. See \link{igraph-attribute-combination} for the available +combiners.} } \value{ A new graph object. @@ -36,9 +48,12 @@ are named), then the operation is performed on symbolic vertex names instead of the internal numeric vertex IDs. \code{intersection()} keeps the attributes of all graphs. All graph, -vertex and edge attributes are copied to the result. If an attribute is -present in multiple graphs and would result a name clash, then this -attribute is renamed by adding suffixes: _1, _2, etc. +vertex and edge attributes are copied to the result. By default, if an +attribute is present in multiple graphs and would result in a name clash, +that attribute is renamed by adding suffixes: \verb{_1}, \verb{_2}, etc. Pass +\code{graph.attr.comb}, \code{vertex.attr.comb} or \code{edge.attr.comb} to combine +clashing attributes instead; see \link{igraph-attribute-combination} for the +available combiners. The \code{name} vertex attribute is treated specially if the operation is performed based on symbolic vertex names. In this case \code{name} must be diff --git a/man/union.igraph.Rd b/man/union.igraph.Rd index 86f77adc40..c3ae729ed6 100644 --- a/man/union.igraph.Rd +++ b/man/union.igraph.Rd @@ -5,7 +5,13 @@ \alias{\%u\%} \title{Union of graphs} \usage{ -\method{union}{igraph}(..., byname = "auto") +\method{union}{igraph}( + ..., + byname = "auto", + graph.attr.comb = "rename", + vertex.attr.comb = "rename", + edge.attr.comb = "rename" +) } \arguments{ \item{\dots}{Graph objects or lists of graph objects.} @@ -15,6 +21,12 @@ to perform the operation based on symbolic vertex names. If it is \code{auto}, that means \code{TRUE} if all graphs are named and \code{FALSE} otherwise. A warning is generated if \code{auto} and some (but not all) graphs are named.} + +\item{graph.attr.comb, vertex.attr.comb, edge.attr.comb}{Specification for +combining clashing graph, vertex and edge attributes. Each defaults to +\code{"rename"}, which preserves the historical behaviour of appending +\verb{_1}, \verb{_2}, ... suffixes. See \link{igraph-attribute-combination} for the +available combiners.} } \value{ A new graph object. @@ -33,9 +45,12 @@ are named), then the operation is performed on symbolic vertex names instead of the internal numeric vertex IDs. \code{union()} keeps the attributes of all graphs. All graph, vertex and -edge attributes are copied to the result. If an attribute is present in -multiple graphs and would result a name clash, then this attribute is -renamed by adding suffixes: _1, _2, etc. +edge attributes are copied to the result. By default, if an attribute is +present in multiple graphs and would result in a name clash, that attribute +is renamed by adding suffixes: \verb{_1}, \verb{_2}, etc. Pass \code{graph.attr.comb}, +\code{vertex.attr.comb} or \code{edge.attr.comb} to combine clashing attributes +instead, e.g. by summing or by taking the first non-\code{NA} value. See +\link{igraph-attribute-combination} for the available combiners. The \code{name} vertex attribute is treated specially if the operation is performed based on symbolic vertex names. In this case \code{name} must be diff --git a/tests/testthat/test-operators.R b/tests/testthat/test-operators.R index 6620c26008..d1ac897eb4 100644 --- a/tests/testthat/test-operators.R +++ b/tests/testthat/test-operators.R @@ -1311,3 +1311,112 @@ test_that("unique on detached vs, names", { expect_equal(ignore_attr = TRUE, vg, vr) }) }) + +# attribute combination on graph operators ------------------------------------- + +make_named_pair <- function() { + g1 <- graph_from_literal(A - B, B - C, C - A) + g2 <- graph_from_literal(A - B, B - C, C - A) + V(g1)$weight <- c(1, 2, 3) + V(g2)$weight <- c(10, 20, 30) + E(g1)$weight <- c(1, 2, 3) + E(g2)$weight <- c(10, 20, 30) + list(g1 = g1, g2 = g2) +} + +test_that("union() defaults to rename behaviour", { + gs <- make_named_pair() + u <- union(gs$g1, gs$g2) + expect_setequal(vertex_attr_names(u), c("name", "weight_1", "weight_2")) + expect_setequal(edge_attr_names(u), c("weight_1", "weight_2")) +}) + +test_that("union() combines vertex attributes with sum", { + gs <- make_named_pair() + u <- union(gs$g1, gs$g2, vertex.attr.comb = "sum") + expect_setequal(vertex_attr_names(u), c("name", "weight")) + expect_equal(sort(V(u)$weight), sort(c(11, 22, 33))) +}) + +test_that("union() combines edge attributes with sum", { + gs <- make_named_pair() + u <- union(gs$g1, gs$g2, edge.attr.comb = "sum") + expect_setequal(edge_attr_names(u), c("weight")) + expect_equal(sort(E(u)$weight), sort(c(11, 22, 33))) +}) + +test_that("union() honours per-attribute list spec with rename fallback", { + gs <- make_named_pair() + V(gs$g1)$color <- letters[1:3] + V(gs$g2)$color <- LETTERS[1:3] + u <- union( + gs$g1, + gs$g2, + vertex.attr.comb = list(weight = "sum", "rename") + ) + expect_setequal( + vertex_attr_names(u), + c("name", "weight", "color_1", "color_2") + ) +}) + +test_that("union() can drop clashing attributes with ignore", { + gs <- make_named_pair() + u <- union(gs$g1, gs$g2, edge.attr.comb = "ignore") + expect_length(edge_attr_names(u), 0) +}) + +test_that("union() supports custom function combiner", { + gs <- make_named_pair() + u <- union( + gs$g1, + gs$g2, + vertex.attr.comb = list(weight = function(x) mean(x)) + ) + expect_equal(sort(V(u)$weight), sort(c(5.5, 11, 16.5))) +}) + +test_that("union() picks first non-NA when only one input has the attr", { + gs <- make_named_pair() + u <- union(gs$g1, gs$g2, vertex.attr.comb = "first", byname = TRUE) + expect_setequal(vertex_attr_names(u), c("name", "weight")) + expect_equal( + V(u)$weight[match(c("A", "B", "C"), V(u)$name)], + c(1, 2, 3) + ) +}) + +test_that("intersection() takes attr.comb args", { + gs <- make_named_pair() + i <- intersection(gs$g1, gs$g2, edge.attr.comb = "sum") + expect_setequal(edge_attr_names(i), c("weight")) + expect_equal(sort(E(i)$weight), sort(c(11, 22, 33))) +}) + +test_that("compose() takes attr.comb args", { + g1 <- graph_from_literal(A - B:D:E, B - C:D, C - D, D - E) + g2 <- graph_from_literal(A - B - E - A) + V(g1)$foo <- seq_len(vcount(g1)) + V(g2)$foo <- 10 * seq_len(vcount(g2)) + g <- compose(g1, g2, vertex.attr.comb = "sum") + expect_true("foo" %in% vertex_attr_names(g)) + expect_false("foo_1" %in% vertex_attr_names(g)) +}) + +test_that("disjoint_union() combines graph attrs via comb", { + g1 <- make_ring(3) + g2 <- make_ring(3) + g1$label <- "first" + g2$label <- "second" + u <- disjoint_union(g1, g2, graph.attr.comb = "concat") + expect_equal(u$label, c("first", "second")) +}) + +test_that("simplify() rejects 'rename' combiner", { + g <- make_graph(c(1, 2, 1, 2, 1, 2, 2, 3, 3, 4)) + E(g)$weight <- 1:5 + expect_error( + simplify(g, edge.attr.comb = "rename"), + "rename" + ) +})