diff --git a/DESCRIPTION b/DESCRIPTION index d092b85a615..1801cb14e41 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -60,7 +60,7 @@ Suggests: scales, stats4, tcltk, - testthat, + testthat (>= 3.3.0), vdiffr, withr Enhances: diff --git a/R/aaa-operators.R b/R/aaa-operators.R index 1c81fcd5262..94994feeaad 100644 --- a/R/aaa-operators.R +++ b/R/aaa-operators.R @@ -35,7 +35,7 @@ connect_neighborhood_impl <- function( contract_vertices_impl <- function( graph, mapping, - vertex_attr_comb = igraph_opt("vertex.attr.comb") + vertex_attr_comb = igraph_opt("vertex_attr_combine") ) { # Argument checks ensure_igraph(graph) @@ -245,7 +245,7 @@ simplify_impl <- function( graph, remove_multiple = TRUE, remove_loops = TRUE, - edge_attr_comb = igraph_opt("edge.attr.comb") + edge_attr_comb = igraph_opt("edge_attr_combine") ) { # Argument checks ensure_igraph(graph) diff --git a/R/aaa-structural.R b/R/aaa-structural.R index de9543e847b..21d3795e887 100644 --- a/R/aaa-structural.R +++ b/R/aaa-structural.R @@ -166,7 +166,7 @@ to_directed_impl <- function( to_undirected_impl <- function( graph, mode = c("collapse", "each", "mutual"), - edge_attr_comb = igraph_opt("edge.attr.comb") + edge_attr_comb = igraph_opt("edge_attr_combine") ) { # Argument checks ensure_igraph(graph) diff --git a/R/attributes.R b/R/attributes.R index 5db68c0962d..57be26b72a8 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,45 @@ igraph.i.attribute.combination <- function(comb) { if (anyDuplicated(names(comb)) > 0) { cli::cli_warn("Some attributes are duplicated") } + # `known_codes` are the numeric values of the `igraph_attribute_combination_type_t` + # enum in the C library (see src/vendor/cigraph/include/igraph_attributes.h). + # Each code must stay aligned with its name in `known_names`. The DEFAULT (1) and + # FUNCTION (2) enum values are intentionally absent: FUNCTION is handled by the + # `!is.character(x)` branch below, and DEFAULT is not selectable by name. + known_names <- c( + "concat", + "first", + "ignore", + "last", + "max", + "mean", + "median", + "min", + "prod", + "random", + "sum" + ) + known_codes <- c(12L, 8L, 0L, 9L, 6L, 10L, 11L, 5L, 4L, 7L, 3L) + 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 (identical(tolower(x), "rename") && !allow_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] } }) @@ -1353,7 +1364,7 @@ igraph.i.attribute.combination <- function(comb) { #' vertex/edge attributes in these cases. #' #' The functions that support the combination of attributes have one or two -#' extra arguments called `vertex.attr.comb` and/or `edge.attr.comb` +#' extra arguments called `vertex_attr_combine` and/or `edge_attr_combine` #' that specify how to perform the mapping of the attributes. E.g. #' [contract()] contracts many vertices into a single one, the #' attributes of the vertices can be combined and stores as the vertex @@ -1435,6 +1446,18 @@ 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. For example, if two graphs each +#' have an attribute called `group`, the resulting graph will have +#' attributes `group_1` and `group_2`, corresponding to the first and +#' second input graph, respectively. 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()], @@ -1452,22 +1475,22 @@ igraph.i.attribute.combination <- function(comb) { #' igraph_options(print.edge.attributes = TRUE) #' #' ## new attribute is the sum of the old ones -#' simplify(g, edge.attr.comb = "sum") +#' simplify(g, edge_attr_combine = "sum") #' #' ## collect attributes into a string -#' simplify(g, edge.attr.comb = toString) +#' simplify(g, edge_attr_combine = toString) #' #' ## concatenate them into a vector, this creates a complex #' ## attribute -#' simplify(g, edge.attr.comb = "concat") +#' simplify(g, edge_attr_combine = "concat") #' #' E(g)$name <- letters[seq_len(ecount(g))] #' #' ## both attributes are collected into strings -#' simplify(g, edge.attr.comb = toString) +#' simplify(g, edge_attr_combine = toString) #' #' ## harmonic average of weights, names are dropped -#' simplify(g, edge.attr.comb = list( +#' simplify(g, edge_attr_combine = list( #' weight = function(x) length(x) / sum(1 / x), #' name = "ignore" #' )) diff --git a/R/community.R b/R/community.R index 65889c3c3e9..a12e2d46f80 100644 --- a/R/community.R +++ b/R/community.R @@ -3201,13 +3201,14 @@ communities <- groups.communities #' #' The attributes of the graph are kept. Graph and edge attributes are #' unchanged, vertex attributes are combined, according to the -#' `vertex.attr.comb` parameter. +#' `vertex_attr_combine` parameter. #' #' @param graph The input graph, it can be directed or undirected. #' @param mapping A numeric vector that specifies the mapping. Its elements #' correspond to the vertices, and for each element the ID in the new graph is #' given. -#' @param vertex.attr.comb Specifies how to combine the vertex attributes in +#' @inheritParams rlang::args_dots_empty +#' @param vertex_attr_combine Specifies how to combine the vertex attributes in #' the new graph. Please see [attribute.combination()] for details. #' @return A new graph object. #' @author Gabor Csardi \email{csardi.gabor@@gmail.com} @@ -3220,7 +3221,7 @@ communities <- groups.communities #' E(g)$weight <- runif(ecount(g)) #' #' g2 <- contract(g, rep(1:5, each = 2), -#' vertex.attr.comb = toString +#' vertex_attr_combine = toString #' ) #' #' ## graph and edge attributes are kept, vertex attributes are @@ -3232,12 +3233,35 @@ communities <- groups.communities contract <- function( graph, mapping, - vertex.attr.comb = igraph_opt("vertex.attr.comb") + ..., + vertex_attr_combine = igraph_opt("vertex_attr_combine") ) { + # BEGIN GENERATED ARG_HANDLE: contract, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(vertex_attr_combine = vertex_attr_combine), + recover_new = c("vertex_attr_combine"), + recover_old = c("vertex.attr.comb"), + match_names = c("vertex.attr.comb", "vertex_attr_combine"), + match_to = c("vertex_attr_combine", "vertex_attr_combine"), + defaults = list(vertex_attr_combine = igraph_opt("vertex_attr_combine")), + head_args = c("graph", "mapping"), + fn_name = "contract" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + contract_vertices_impl( graph = graph, mapping = mapping, - vertex_attr_comb = vertex.attr.comb + vertex_attr_comb = vertex_attr_combine ) } diff --git a/R/conversion.R b/R/conversion.R index c978a051e50..1b3ddce83f1 100644 --- a/R/conversion.R +++ b/R/conversion.R @@ -670,7 +670,7 @@ as_edgelist <- function(graph, names = TRUE) { #' E(g4)$weight <- seq_len(ecount(g4)) #' ug4 <- as_undirected(g4, #' mode = "mutual", -#' edge.attr.comb = list(weight = length) +#' edge_attr_combine = list(weight = length) #' ) #' print(ug4, e = TRUE) #' @@ -685,7 +685,8 @@ as_directed <- function( } #' @rdname as_directed -#' @param edge.attr.comb Specifies what to do with edge attributes, if +#' @inheritParams rlang::args_dots_empty +#' @param edge_attr_combine Specifies what to do with edge attributes, if #' `mode="collapse"` or `mode="mutual"`. In these cases many edges #' might be mapped to a single one in the new graph, and their attributes are #' combined. Please see [attribute.combination()] for details on @@ -694,8 +695,31 @@ as_directed <- function( as_undirected <- function( graph, mode = c("collapse", "each", "mutual"), - edge.attr.comb = igraph_opt("edge.attr.comb") + ..., + edge_attr_combine = igraph_opt("edge_attr_combine") ) { + # BEGIN GENERATED ARG_HANDLE: as_undirected, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(edge_attr_combine = edge_attr_combine), + recover_new = c("edge_attr_combine"), + recover_old = c("edge.attr.comb"), + match_names = c("edge.attr.comb", "edge_attr_combine"), + match_to = c("edge_attr_combine", "edge_attr_combine"), + defaults = list(edge_attr_combine = igraph_opt("edge_attr_combine")), + head_args = c("graph", "mode"), + fn_name = "as_undirected" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + # Argument checks ensure_igraph(graph) mode <- igraph_match_arg(mode) @@ -704,7 +728,7 @@ as_undirected <- function( res <- to_undirected_impl( graph = graph, mode = mode, - edge_attr_comb = edge.attr.comb + edge_attr_comb = edge_attr_combine ) res diff --git a/R/operators.R b/R/operators.R index 8782cfb0612..9e69759a7ef 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,100 @@ 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 { + # Backward-compatible standard + "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")) { + renamed <- rename_attr_if_needed(name, w, getval) + # Assign element-wise (not `c()`) so a later clash that resolves to an + # already-used `name_` overwrites rather than duplicating it -- the + # historical behaviour, e.g. under a chain of `%du%`. + for (nm in names(renamed)) { + attr[[nm]] <- renamed[[nm]] } + } 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 } +# Historical behaviour for clashing attributes: a value present in a single +# input graph is copied as-is, otherwise each copy is kept side-by-side under a +# disambiguated `name_1`, `name_2`, ... name. Returns a named list to splice +# into the result; `getval` is the per-graph accessor closure from +# `combine.attrs()`. +rename_attr_if_needed <- function(name, w, getval) { + if (length(w) == 1) { + stats::setNames(list(getval(w, name)), name) + } else { + out <- lapply(w, function(w2) getval(w2, name)) + names(out) <- paste(name, w, sep = "_") + out + } +} + +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 +274,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_combine`; 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 +289,11 @@ rename.attr.if.needed <- function( #' @aliases %du% #' @param \dots Graph objects or lists of graph objects. #' @param x,y Graph objects. +#' @param graph_attr_combine Specification for combining shared graph attributes. +#' Defaults to the `graph_attr_combine` igraph option (`"rename"` unless changed +#' via [igraph_options()]), 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 +307,10 @@ 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_combine = igraph_opt("graph_attr_combine") +) { graphs <- unlist( recursive = FALSE, lapply(list(...), function(l) { @@ -232,7 +323,11 @@ disjoint_union <- function(...) { res <- .Call(Rx_igraph_disjoint_union, graphs) ## Graph attributes - graph.attributes(res) <- rename.attr.if.needed("g", graphs) + graph_attr_combine <- igraph.i.attribute.combination( + graph_attr_combine, + allow_rename = TRUE + ) + graph.attributes(res) <- combine.attrs("g", graphs, comb = graph_attr_combine) ## Vertex attributes attr <- list() @@ -306,7 +401,10 @@ disjoint_union <- function(...) { call, ..., byname, - keep.all.vertices + keep.all.vertices, + graph_attr_combine = "rename", + vertex_attr_combine = "rename", + edge_attr_combine = "rename" ) { graphs <- unlist( recursive = FALSE, @@ -330,6 +428,19 @@ disjoint_union <- function(...) { cli::cli_abort("Some graphs are not named.") } + graph_attr_combine <- igraph.i.attribute.combination( + graph_attr_combine, + allow_rename = TRUE + ) + vertex_attr_combine <- igraph.i.attribute.combination( + vertex_attr_combine, + allow_rename = TRUE + ) + edge_attr_combine <- igraph.i.attribute.combination( + edge_attr_combine, + allow_rename = TRUE + ) + edgemaps <- length(unlist(lapply(graphs, edge_attr_names))) != 0 if (byname) { @@ -357,23 +468,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_combine + ) + vertex.attributes(res) <- combine.attrs( "v", newgraphs, vcount(res), - ignore = "name" + ignore = "name", + comb = vertex_attr_combine ) 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_combine ) } } else { @@ -397,21 +513,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_combine + ) + vertex.attributes(res) <- combine.attrs( "v", graphs, - vcount(res) + vcount(res), + comb = vertex_attr_combine ) ## 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_combine ) } } @@ -459,9 +580,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_combine`, +#' `vertex_attr_combine` or `edge_attr_combine` 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 +601,13 @@ 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_combine,vertex_attr_combine,edge_attr_combine Specification for +#' combining clashing graph, vertex and edge attributes. `vertex_attr_combine` +#' and `edge_attr_combine` default to `"rename"`; `graph_attr_combine` defaults to +#' the `graph_attr_combine` igraph option (`"rename"` unless changed via +#' [igraph_options()]). `"rename"` 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 +623,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_combine = igraph_opt("graph_attr_combine"), + vertex_attr_combine = "rename", + edge_attr_combine = "rename" +) { .igraph.graph.union.or.intersection( "union", ..., byname = byname, - keep.all.vertices = TRUE + keep.all.vertices = TRUE, + graph_attr_combine = graph_attr_combine, + vertex_attr_combine = vertex_attr_combine, + edge_attr_combine = edge_attr_combine ) } @@ -540,9 +680,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_combine`, `vertex_attr_combine` or `edge_attr_combine` 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 +703,12 @@ 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_combine,vertex_attr_combine,edge_attr_combine Specification for +#' combining clashing graph, vertex and edge attributes. `vertex_attr_combine` +#' and `edge_attr_combine` default to `"rename"`; `graph_attr_combine` defaults to +#' the `graph_attr_combine` igraph option (`"rename"` unless changed via +#' [igraph_options()]). 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 +727,19 @@ intersection <- function(...) { intersection.igraph <- function( ..., byname = "auto", - keep.all.vertices = TRUE + keep.all.vertices = TRUE, + graph_attr_combine = igraph_opt("graph_attr_combine"), + vertex_attr_combine = "rename", + edge_attr_combine = "rename" ) { .igraph.graph.union.or.intersection( "intersection", ..., byname = byname, - keep.all.vertices = keep.all.vertices + keep.all.vertices = keep.all.vertices, + graph_attr_combine = graph_attr_combine, + vertex_attr_combine = vertex_attr_combine, + edge_attr_combine = edge_attr_combine ) } @@ -765,9 +920,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_combine`, +#' `vertex_attr_combine` or `edge_attr_combine` 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 +953,12 @@ 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_combine,vertex_attr_combine,edge_attr_combine Specification for +#' combining clashing graph, vertex and edge attributes. `vertex_attr_combine` +#' and `edge_attr_combine` default to `"rename"`; `graph_attr_combine` defaults to +#' the `graph_attr_combine` igraph option (`"rename"` unless changed via +#' [igraph_options()]). 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 +972,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_combine = igraph_opt("graph_attr_combine"), + vertex_attr_combine = "rename", + edge_attr_combine = "rename" +) { ensure_igraph(g1) ensure_igraph(g2) @@ -828,6 +998,19 @@ compose <- function(g1, g2, byname = "auto") { cli::cli_abort("Some graphs are not named.") } + graph_attr_combine <- igraph.i.attribute.combination( + graph_attr_combine, + allow_rename = TRUE + ) + vertex_attr_combine <- igraph.i.attribute.combination( + vertex_attr_combine, + allow_rename = TRUE + ) + edge_attr_combine <- igraph.i.attribute.combination( + edge_attr_combine, + allow_rename = TRUE + ) + if (byname) { uninames <- unique(c(V(g1)$name, V(g2)$name)) if (vcount(g1) < length(uninames)) { @@ -852,28 +1035,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_combine) 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_combine + ) 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_combine ) } 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_combine ) } diff --git a/R/par.R b/R/par.R index b76f25024a4..1a5beadd02c 100644 --- a/R/par.R +++ b/R/par.R @@ -61,8 +61,9 @@ getIgraphOpt <- function(x, default = NULL) { "print.edge.attributes" = FALSE, "print.graph.attributes" = FALSE, "verbose" = FALSE, - "vertex.attr.comb" = list(name = "concat", "ignore"), - "edge.attr.comb" = list(weight = "sum", name = "concat", "ignore"), + "graph_attr_combine" = "rename", + "vertex_attr_combine" = list(name = "concat", "ignore"), + "edge_attr_combine" = list(weight = "sum", name = "concat", "ignore"), "sparsematrices" = TRUE, "add.params" = TRUE, "add.vertex.names" = TRUE, @@ -75,6 +76,31 @@ getIgraphOpt <- function(x, default = NULL) { "print.style" = "cli" ) +# Option keys that were renamed to snake_case. Reading or setting an option by +# its old dotted name still works, but maps to the canonical key and emits a +# soft-deprecation. +.igraph.pars.aliases <- c( + "vertex.attr.comb" = "vertex_attr_combine", + "edge.attr.comb" = "edge_attr_combine" +) + +# Map any deprecated option names in `x` to their canonical keys, warning once +# per deprecated name encountered. +igraph_normalize_par_name <- function(x) { + for (i in seq_along(x)) { + new <- unname(.igraph.pars.aliases[x[i]]) + if (!is.na(new)) { + lifecycle::deprecate_soft( + "3.0.0", + I(paste0("The igraph option `", x[i], "`")), + I(paste0("the `", new, "` option")) + ) + x[i] <- new + } + } + x +} + igraph.pars.set.verbose <- function(verbose) { if (is.logical(verbose)) { .Call(Rx_igraph_set_verbose, verbose) @@ -138,9 +164,17 @@ igraph.pars.callbacks <- list("verbose" = igraph.pars.set.verbose) #' Possible values are \sQuote{auto} (the default), \sQuote{phylo}, \sQuote{hclust} and \sQuote{dendrogram}. #' See [plot_dendrogram()] for details. #' } -#' \item{edge.attr.comb}{ +#' \item{edge_attr_combine}{ #' Specifies what to do with the edge attributes if the graph is modified. #' The default value is `list(weight="sum", name="concat", "ignore")`. +#' See [attribute.combination()] for details on this. The former dotted +#' name `edge.attr.comb` still works but is soft-deprecated. +#' } +#' \item{graph_attr_combine}{ +#' Specifies what to do with the graph attributes when graphs are +#' combined, e.g. via [union()], [intersection()], [disjoint_union()] +#' or [compose()]. The default value is `"rename"`, which resolves any +#' name clash by appending `_1`, `_2`, ... suffixes. #' See [attribute.combination()] for details on this. #' } #' \item{print.edge.attributes}{ @@ -180,10 +214,11 @@ igraph.pars.callbacks <- list("verbose" = igraph.pars.set.verbose) #' Logical constant, whether igraph functions should talk more than minimal. #' E.g. if `TRUE` then some functions will use progress bars while computing. Defaults to `FALSE`. #' } -#' \item{vertex.attr.comb}{ +#' \item{vertex_attr_combine}{ #' Specifies what to do with the vertex attributes if the graph is modified. #' The default value is `list(name="concat", "ignore")`. -#' See [attribute.combination()] for details on this. +#' See [attribute.combination()] for details on this. The former dotted +#' name `vertex.attr.comb` still works but is soft-deprecated. #' } #' } #' @@ -235,7 +270,7 @@ igraph_i_options <- function(..., .in = parent.frame()) { arg <- temp[[1]] if (mode(arg) == "character") { - return(.igraph.pars[arg]) + return(.igraph.pars[igraph_normalize_par_name(arg)]) } if (mode(arg) != "list") { @@ -254,6 +289,8 @@ igraph_i_options <- function(..., .in = parent.frame()) { if (is.null(n)) { cli::cli_abort("options must be given by name.") } + names(temp) <- igraph_normalize_par_name(n) + n <- names(temp) cb <- intersect(names(igraph.pars.callbacks), n) for (cn in cb) { temp[[cn]] <- igraph.pars.callbacks[[cn]](temp[[cn]]) @@ -289,6 +326,7 @@ get_all_options <- function() { #' @rdname igraph_options #' @export igraph_opt <- function(x, default = NULL) { + x <- igraph_normalize_par_name(x) if (missing(default)) { get_config(paste0("igraph::", x), .igraph.pars[[x]]) } else { diff --git a/R/simple.R b/R/simple.R index dcc91205422..5b1680a7edd 100644 --- a/R/simple.R +++ b/R/simple.R @@ -68,7 +68,8 @@ is.simple <- function(graph) { #' @param remove.loops Logical, whether the loop edges are to be removed. #' @param remove.multiple Logical, whether the multiple edges are to be #' removed. -#' @param edge.attr.comb Specifies what to do with edge attributes, if +#' @inheritParams rlang::args_dots_empty +#' @param edge_attr_combine Specifies what to do with edge attributes, if #' `remove.multiple=TRUE`. In this case many edges might be mapped to a #' single one in the new graph, and their attributes are combined. Please see #' [attribute.combination()] for details on this. @@ -94,11 +95,34 @@ simplify <- function( graph, remove.multiple = TRUE, remove.loops = TRUE, - edge.attr.comb = igraph_opt("edge.attr.comb") + ..., + edge_attr_combine = igraph_opt("edge_attr_combine") ) { + # BEGIN GENERATED ARG_HANDLE: simplify, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(edge_attr_combine = edge_attr_combine), + recover_new = c("edge_attr_combine"), + recover_old = c("edge.attr.comb"), + match_names = c("edge.attr.comb", "edge_attr_combine"), + match_to = c("edge_attr_combine", "edge_attr_combine"), + defaults = list(edge_attr_combine = igraph_opt("edge_attr_combine")), + head_args = c("graph", "remove.multiple", "remove.loops"), + fn_name = "simplify" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + # A graph that is already simple has no loops and no multiple edges, so # simplify_impl() would not change its structure regardless of the - # remove.* / edge.attr.comb arguments. Short-circuiting here avoids the + # remove.* / edge_attr_combine arguments. Short-circuiting here avoids the # cost of rebuilding the graph in the (common) already-simple case; # is_simple() is orders of magnitude cheaper than simplify(). if (is_simple(graph)) { @@ -108,7 +132,7 @@ simplify <- function( graph = graph, remove_multiple = remove.multiple, remove_loops = remove.loops, - edge_attr_comb = edge.attr.comb + edge_attr_comb = edge_attr_combine ) } diff --git a/R/structural-properties.R b/R/structural-properties.R index 9182950592e..5dacd27cb24 100644 --- a/R/structural-properties.R +++ b/R/structural-properties.R @@ -2517,7 +2517,7 @@ girth <- function(graph, circle = TRUE) { #' # Remove multiple edges but keep multiplicity #' g <- sample_pa(10, m = 3, algorithm = "bag") #' E(g)$weight <- count_multiple(g) -#' g <- simplify(g, edge.attr.comb = list(weight = "min")) +#' g <- simplify(g, edge_attr_combine = list(weight = "min")) #' any(which_multiple(g)) #' E(g)$weight #' diff --git a/man/as.undirected.Rd b/man/as.undirected.Rd index 1a0ae66f3f4..66f36118514 100644 --- a/man/as.undirected.Rd +++ b/man/as.undirected.Rd @@ -17,12 +17,6 @@ as.undirected( \code{as_directed()} it can be \code{mutual} or \code{arbitrary}. For \code{as_undirected()} it can be \code{each}, \code{collapse} or \code{mutual}. See details below.} - -\item{edge.attr.comb}{Specifies what to do with edge attributes, if -\code{mode="collapse"} or \code{mode="mutual"}. In these cases many edges -might be mapped to a single one in the new graph, and their attributes are -combined. Please see \code{\link[=attribute.combination]{attribute.combination()}} for details on -this.} } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} diff --git a/man/as_directed.Rd b/man/as_directed.Rd index e496f6022b0..df00951531a 100644 --- a/man/as_directed.Rd +++ b/man/as_directed.Rd @@ -10,7 +10,8 @@ as_directed(graph, mode = c("mutual", "arbitrary", "random", "acyclic")) as_undirected( graph, mode = c("collapse", "each", "mutual"), - edge.attr.comb = igraph_opt("edge.attr.comb") + ..., + edge_attr_combine = igraph_opt("edge_attr_combine") ) } \arguments{ @@ -21,7 +22,9 @@ as_undirected( \code{as_undirected()} it can be \code{each}, \code{collapse} or \code{mutual}. See details below.} -\item{edge.attr.comb}{Specifies what to do with edge attributes, if +\item{...}{These dots are for future extensions and must be empty.} + +\item{edge_attr_combine}{Specifies what to do with edge attributes, if \code{mode="collapse"} or \code{mode="mutual"}. In these cases many edges might be mapped to a single one in the new graph, and their attributes are combined. Please see \code{\link[=attribute.combination]{attribute.combination()}} for details on @@ -115,7 +118,7 @@ g4 <- make_graph(c( E(g4)$weight <- seq_len(ecount(g4)) ug4 <- as_undirected(g4, mode = "mutual", - edge.attr.comb = list(weight = length) + edge_attr_combine = list(weight = length) ) print(ug4, e = TRUE) diff --git a/man/compose.Rd b/man/compose.Rd index f26ae5aad8f..061b9e2ecc5 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_combine = igraph_opt("graph_attr_combine"), + vertex_attr_combine = "rename", + edge_attr_combine = "rename" +) } \arguments{ \item{g1}{The first input graph.} @@ -17,6 +24,13 @@ 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_combine, vertex_attr_combine, edge_attr_combine}{Specification for +combining clashing graph, vertex and edge attributes. \code{vertex_attr_combine} +and \code{edge_attr_combine} default to \code{"rename"}; \code{graph_attr_combine} defaults to +the \code{graph_attr_combine} igraph option (\code{"rename"} unless changed via +\code{\link[=igraph_options]{igraph_options()}}). See \link{igraph-attribute-combination} for the available +combiners.} } \value{ A new graph object. @@ -38,9 +52,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_combine}, +\code{vertex_attr_combine} or \code{edge_attr_combine} 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/contract.Rd b/man/contract.Rd index 677567c3d0c..05488daa625 100644 --- a/man/contract.Rd +++ b/man/contract.Rd @@ -4,7 +4,12 @@ \alias{contract} \title{Contract several vertices into a single one} \usage{ -contract(graph, mapping, vertex.attr.comb = igraph_opt("vertex.attr.comb")) +contract( + graph, + mapping, + ..., + vertex_attr_combine = igraph_opt("vertex_attr_combine") +) } \arguments{ \item{graph}{The input graph, it can be directed or undirected.} @@ -13,7 +18,9 @@ contract(graph, mapping, vertex.attr.comb = igraph_opt("vertex.attr.comb")) correspond to the vertices, and for each element the ID in the new graph is given.} -\item{vertex.attr.comb}{Specifies how to combine the vertex attributes in +\item{...}{These dots are for future extensions and must be empty.} + +\item{vertex_attr_combine}{Specifies how to combine the vertex attributes in the new graph. Please see \code{\link[=attribute.combination]{attribute.combination()}} for details.} } \value{ @@ -26,7 +33,7 @@ vertices in the new graph correspond to sets of vertices in the input graph. \details{ The attributes of the graph are kept. Graph and edge attributes are unchanged, vertex attributes are combined, according to the -\code{vertex.attr.comb} parameter. +\code{vertex_attr_combine} parameter. } \section{Related documentation in the C library}{ \href{https://igraph.org/c/html/0.10.17/igraph-Operators.html#igraph_contract_vertices}{\code{contract_vertices()}} @@ -40,7 +47,7 @@ V(g)$name <- letters[1:vcount(g)] E(g)$weight <- runif(ecount(g)) g2 <- contract(g, rep(1:5, each = 2), - vertex.attr.comb = toString + vertex_attr_combine = toString ) ## graph and edge attributes are kept, vertex attributes are diff --git a/man/contract.vertices.Rd b/man/contract.vertices.Rd index 41c87032023..cbb9cd10580 100644 --- a/man/contract.vertices.Rd +++ b/man/contract.vertices.Rd @@ -16,9 +16,6 @@ contract.vertices( \item{mapping}{A numeric vector that specifies the mapping. Its elements correspond to the vertices, and for each element the ID in the new graph is given.} - -\item{vertex.attr.comb}{Specifies how to combine the vertex attributes in -the new graph. Please see \code{\link[=attribute.combination]{attribute.combination()}} for details.} } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} diff --git a/man/disjoint_union.Rd b/man/disjoint_union.Rd index 220e39c546a..f72701ae8ad 100644 --- a/man/disjoint_union.Rd +++ b/man/disjoint_union.Rd @@ -5,13 +5,19 @@ \alias{\%du\%} \title{Disjoint union of graphs} \usage{ -disjoint_union(...) +disjoint_union(..., graph_attr_combine = igraph_opt("graph_attr_combine")) x \%du\% y } \arguments{ \item{\dots}{Graph objects or lists of graph objects.} +\item{graph_attr_combine}{Specification for combining shared graph attributes. +Defaults to the \code{graph_attr_combine} igraph option (\code{"rename"} unless changed +via \code{\link[=igraph_options]{igraph_options()}}), 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 +37,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_combine}; 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 f6e2e22a5b3..b36ff376c89 100644 --- a/man/igraph-attribute-combination.Rd +++ b/man/igraph-attribute-combination.Rd @@ -13,7 +13,7 @@ vertex/edge attributes in these cases. } \details{ The functions that support the combination of attributes have one or two -extra arguments called \code{vertex.attr.comb} and/or \code{edge.attr.comb} +extra arguments called \code{vertex_attr_combine} and/or \code{edge_attr_combine} that specify how to perform the mapping of the attributes. E.g. \code{\link[=contract]{contract()}} contracts many vertices into a single one, the attributes of the vertices can be combined and stores as the vertex @@ -93,6 +93,18 @@ 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. For example, if two graphs each +have an attribute called \code{group}, the resulting graph will have +attributes \code{group_1} and \code{group_2}, corresponding to the first and +second input graph, respectively. 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. +} } } @@ -107,22 +119,22 @@ igraph_options(print.vertex.attributes = TRUE) igraph_options(print.edge.attributes = TRUE) ## new attribute is the sum of the old ones -simplify(g, edge.attr.comb = "sum") +simplify(g, edge_attr_combine = "sum") ## collect attributes into a string -simplify(g, edge.attr.comb = toString) +simplify(g, edge_attr_combine = toString) ## concatenate them into a vector, this creates a complex ## attribute -simplify(g, edge.attr.comb = "concat") +simplify(g, edge_attr_combine = "concat") E(g)$name <- letters[seq_len(ecount(g))] ## both attributes are collected into strings -simplify(g, edge.attr.comb = toString) +simplify(g, edge_attr_combine = toString) ## harmonic average of weights, names are dropped -simplify(g, edge.attr.comb = list( +simplify(g, edge_attr_combine = list( weight = function(x) length(x) / sum(1 / x), name = "ignore" )) diff --git a/man/igraph_options.Rd b/man/igraph_options.Rd index cc8af78745f..23faeeab1f3 100644 --- a/man/igraph_options.Rd +++ b/man/igraph_options.Rd @@ -65,9 +65,17 @@ The plotting function to use when plotting community structure dendrograms via \ Possible values are \sQuote{auto} (the default), \sQuote{phylo}, \sQuote{hclust} and \sQuote{dendrogram}. See \code{\link[=plot_dendrogram]{plot_dendrogram()}} for details. } -\item{edge.attr.comb}{ +\item{edge_attr_combine}{ Specifies what to do with the edge attributes if the graph is modified. The default value is \code{list(weight="sum", name="concat", "ignore")}. +See \code{\link[=attribute.combination]{attribute.combination()}} for details on this. The former dotted +name \code{edge.attr.comb} still works but is soft-deprecated. +} +\item{graph_attr_combine}{ +Specifies what to do with the graph attributes when graphs are +combined, e.g. via \code{\link[=union]{union()}}, \code{\link[=intersection]{intersection()}}, \code{\link[=disjoint_union]{disjoint_union()}} +or \code{\link[=compose]{compose()}}. The default value is \code{"rename"}, which resolves any +name clash by appending \verb{_1}, \verb{_2}, ... suffixes. See \code{\link[=attribute.combination]{attribute.combination()}} for details on this. } \item{print.edge.attributes}{ @@ -107,10 +115,11 @@ It is recommended, if the user works with larger graphs. Logical constant, whether igraph functions should talk more than minimal. E.g. if \code{TRUE} then some functions will use progress bars while computing. Defaults to \code{FALSE}. } -\item{vertex.attr.comb}{ +\item{vertex_attr_combine}{ Specifies what to do with the vertex attributes if the graph is modified. The default value is \code{list(name="concat", "ignore")}. -See \code{\link[=attribute.combination]{attribute.combination()}} for details on this. +See \code{\link[=attribute.combination]{attribute.combination()}} for details on this. The former dotted +name \code{vertex.attr.comb} still works but is soft-deprecated. } } } diff --git a/man/intersection.igraph.Rd b/man/intersection.igraph.Rd index e7345de60f7..2c313890870 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_combine = igraph_opt("graph_attr_combine"), + vertex_attr_combine = "rename", + edge_attr_combine = "rename" +) } \arguments{ \item{\dots}{Graph objects or lists of graph objects.} @@ -18,6 +25,13 @@ 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_combine, vertex_attr_combine, edge_attr_combine}{Specification for +combining clashing graph, vertex and edge attributes. \code{vertex_attr_combine} +and \code{edge_attr_combine} default to \code{"rename"}; \code{graph_attr_combine} defaults to +the \code{graph_attr_combine} igraph option (\code{"rename"} unless changed via +\code{\link[=igraph_options]{igraph_options()}}). See \link{igraph-attribute-combination} for the available +combiners.} } \value{ A new graph object. @@ -36,9 +50,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_combine}, \code{vertex_attr_combine} or \code{edge_attr_combine} 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/simplify.Rd b/man/simplify.Rd index bf1643497a5..fc4b7c6c4dc 100644 --- a/man/simplify.Rd +++ b/man/simplify.Rd @@ -10,7 +10,8 @@ simplify( graph, remove.multiple = TRUE, remove.loops = TRUE, - edge.attr.comb = igraph_opt("edge.attr.comb") + ..., + edge_attr_combine = igraph_opt("edge_attr_combine") ) is_simple(graph) @@ -25,7 +26,9 @@ removed.} \item{remove.loops}{Logical, whether the loop edges are to be removed.} -\item{edge.attr.comb}{Specifies what to do with edge attributes, if +\item{...}{These dots are for future extensions and must be empty.} + +\item{edge_attr_combine}{Specifies what to do with edge attributes, if \code{remove.multiple=TRUE}. In this case many edges might be mapped to a single one in the new graph, and their attributes are combined. Please see \code{\link[=attribute.combination]{attribute.combination()}} for details on this.} diff --git a/man/union.igraph.Rd b/man/union.igraph.Rd index 86f77adc40d..60a0357a8f3 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_combine = igraph_opt("graph_attr_combine"), + vertex_attr_combine = "rename", + edge_attr_combine = "rename" +) } \arguments{ \item{\dots}{Graph objects or lists of graph objects.} @@ -15,6 +21,14 @@ 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_combine, vertex_attr_combine, edge_attr_combine}{Specification for +combining clashing graph, vertex and edge attributes. \code{vertex_attr_combine} +and \code{edge_attr_combine} default to \code{"rename"}; \code{graph_attr_combine} defaults to +the \code{graph_attr_combine} igraph option (\code{"rename"} unless changed via +\code{\link[=igraph_options]{igraph_options()}}). \code{"rename"} 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 +47,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_combine}, +\code{vertex_attr_combine} or \code{edge_attr_combine} 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/man/which_multiple.Rd b/man/which_multiple.Rd index ac17d8dfe4f..ab6d858f424 100644 --- a/man/which_multiple.Rd +++ b/man/which_multiple.Rd @@ -88,7 +88,7 @@ which_multiple(make_graph(c(1, 2, 2, 1), dir = FALSE)) # Remove multiple edges but keep multiplicity g <- sample_pa(10, m = 3, algorithm = "bag") E(g)$weight <- count_multiple(g) -g <- simplify(g, edge.attr.comb = list(weight = "min")) +g <- simplify(g, edge_attr_combine = list(weight = "min")) any(which_multiple(g)) E(g)$weight diff --git a/tests/testthat/_snaps/conversion.md b/tests/testthat/_snaps/conversion.md index dd796ca286b..7e39ca56ba6 100644 --- a/tests/testthat/_snaps/conversion.md +++ b/tests/testthat/_snaps/conversion.md @@ -17,6 +17,13 @@ Warning: `as.undirected()` was deprecated in igraph 2.1.0. i Please use `as_undirected()` instead. + Warning: + The igraph option `edge.attr.comb` was deprecated in igraph 3.0.0. + i Please use the `edge_attr_combine` option instead. + Warning: + Calling `as_undirected()` with positional or abbreviated arguments was deprecated in igraph 3.0.0. + i Detected call: as_undirected(graph, mode, edge.attr.comb) + i Use instead: as_undirected(graph, mode, edge_attr_combine = ) Output [1] FALSE diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R index f2a6a00c7aa..a31cd873b5b 100644 --- a/tests/testthat/helper.R +++ b/tests/testthat/helper.R @@ -62,3 +62,15 @@ expect_snapshot_igraph <- function(x, ...) { ... )) } + +# Two identically-shaped named triangles sharing a `weight` vertex/edge +# attribute, used by the attribute-combination tests for 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) +} diff --git a/tests/testthat/test-attributes.R b/tests/testthat/test-attributes.R index 9b2bd83021b..ab39eec359b 100644 --- a/tests/testthat/test-attributes.R +++ b/tests/testthat/test-attributes.R @@ -208,11 +208,11 @@ test_that("attribute combinations handle errors correctly", { g <- make_graph(c(1, 2, 2, 1)) E(g)$weight <- c("a", "b") expect_error( - as_undirected(g, edge.attr.comb = list(weight = "sum")), + as_undirected(g, edge_attr_combine = list(weight = "sum")), "invalid 'type'" ) expect_error( - as_undirected(g, edge.attr.comb = list(weight = sum)), + as_undirected(g, edge_attr_combine = list(weight = sum)), "invalid 'type'" ) }) diff --git a/tests/testthat/test-community.R b/tests/testthat/test-community.R index 389d1d3801e..2aae424e240 100644 --- a/tests/testthat/test-community.R +++ b/tests/testthat/test-community.R @@ -645,7 +645,7 @@ test_that("contract works", { V(g)$name <- letters[1:vcount(g)] E(g)$weight <- sample(ecount(g)) - g2 <- contract(g, rep(1:5, each = 2), vertex.attr.comb = toString) + g2 <- contract(g, rep(1:5, each = 2), vertex_attr_combine = toString) expect_equal(g2$name, g$name) expect_equal(V(g2)$name, c("a, b", "c, d", "e, f", "g, h", "i, j")) diff --git a/tests/testthat/test-operators.R b/tests/testthat/test-operators.R index b79725b8a90..142af4f49f6 100644 --- a/tests/testthat/test-operators.R +++ b/tests/testthat/test-operators.R @@ -1311,3 +1311,122 @@ test_that("unique on detached vs, names", { expect_equal(ignore_attr = TRUE, vg, vr) }) }) + +# attribute combination on graph operators ------------------------------------- + +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_combine = "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_combine = "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_combine = 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_combine = "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_combine = 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_combine = "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_combine = "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_combine = "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_combine = "concat") + expect_equal(u$label, c("first", "second")) +}) + +test_that("graph_attr_combine defaults to the graph_attr_combine igraph option", { + expect_equal(igraph_opt("graph_attr_combine"), "rename") + + g1 <- make_ring(3) + g2 <- make_ring(3) + g1$label <- "first" + g2$label <- "second" + + # Default option ("rename") preserves the historical suffixing behaviour. + u <- union(g1, g2) + expect_all_true(c("label_1", "label_2") %in% graph_attr_names(u)) + + # Setting the option changes the default for the graph operators. + local_igraph_options(graph_attr_combine = "ignore") + expect_length(graph_attr_names(union(g1, g2)), 0) + expect_length(graph_attr_names(intersection(g1, g2)), 0) + expect_length(graph_attr_names(disjoint_union(g1, g2)), 0) + expect_length(graph_attr_names(compose(g1, g2)), 0) +}) + +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_combine = "rename"), + "rename" + ) +}) diff --git a/tools/migrations.R b/tools/migrations.R index e18c7283b99..a9afe932e2f 100644 --- a/tools/migrations.R +++ b/tools/migrations.R @@ -95,6 +95,46 @@ migrations <- list( when = "3.0.0" ), + # --- real migrations ----------------------------------------------------- + # Dotted `*.attr.comb` arguments renamed to snake_case. The old dotted names + # keep working (recovered from `...`) under a single soft-deprecation. + simplify = list( + old = function( + graph, + remove.multiple, + remove.loops, + edge.attr.comb = edge_attr_combine + ) {}, + new = function( + graph, + remove.multiple = TRUE, + remove.loops = TRUE, + ..., + edge_attr_combine = igraph_opt("edge_attr_combine") + ) {}, + when = "3.0.0" + ), + as_undirected = list( + old = function(graph, mode, edge.attr.comb = edge_attr_combine) {}, + new = function( + graph, + mode = c("collapse", "each", "mutual"), + ..., + edge_attr_combine = igraph_opt("edge_attr_combine") + ) {}, + when = "3.0.0" + ), + contract = list( + old = function(graph, mapping, vertex.attr.comb = vertex_attr_combine) {}, + new = function( + graph, + mapping, + ..., + vertex_attr_combine = igraph_opt("vertex_attr_combine") + ) {}, + when = "3.0.0" + ), + # --- test fixture -------------------------------------------------------- # Exercises the generator end-to-end without migrating a real function. The # arg names are chosen to cover every recovery path: two renames (`weight -> diff --git a/tools/stimulus/types-RR.yaml b/tools/stimulus/types-RR.yaml index bfb5ca69ed8..0594b749682 100644 --- a/tools/stimulus/types-RR.yaml +++ b/tools/stimulus/types-RR.yaml @@ -531,12 +531,12 @@ SUBGRAPH_IMPL: EDGE_ATTRIBUTE_COMBINATION: DEFAULT: - Default: igraph_opt("edge.attr.comb") + Default: igraph_opt("edge_attr_combine") INCONV: '%I% <- igraph.i.attribute.combination(%I%)' VERTEX_ATTRIBUTE_COMBINATION: DEFAULT: - Default: igraph_opt("vertex.attr.comb") + Default: igraph_opt("vertex_attr_combine") INCONV: '%I% <- igraph.i.attribute.combination(%I%)' ADD_WEIGHTS: