diff --git a/R/cycles.R b/R/cycles.R index c3d65b5cae0..4d6c1d8d4b2 100644 --- a/R/cycles.R +++ b/R/cycles.R @@ -31,6 +31,7 @@ #' a specific cycle. #' #' @param graph The input graph. +#' @inheritParams rlang::args_dots_empty #' @param mode Character constant specifying how to handle directed graphs. #' `out` follows edge directions, `in` follows edges in the reverse direction, #' and `all` ignores edge directions. Ignored in undirected graphs. @@ -49,7 +50,33 @@ #' @family cycles #' @export -find_cycle <- function(graph, mode = c("out", "in", "all", "total")) { +find_cycle <- function( + graph, + ..., + mode = c("out", "in", "all", "total") +) { + # BEGIN GENERATED ARG_HANDLE: find_cycle, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(mode = mode), + recover_new = c("mode"), + recover_old = c("mode"), + match_names = c("mode"), + match_to = c("mode"), + defaults = list(mode = c("out", "in", "all", "total")), + head_args = c("graph"), + fn_name = "find_cycle" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + find_cycle_impl( graph = graph, mode = mode diff --git a/R/random_walk.R b/R/random_walk.R index d6252a56838..4d610ccecaa 100644 --- a/R/random_walk.R +++ b/R/random_walk.R @@ -16,6 +16,7 @@ #' @param graph The input graph, might be undirected or directed. #' @param start The start vertex. #' @param steps The number of steps to make. +#' @inheritParams rlang::args_dots_empty #' @param weights The edge weights. Larger edge weights increase the #' probability that an edge is selected by the random walker. In other #' words, larger edge weights correspond to stronger connections. The @@ -51,10 +52,37 @@ random_walk <- function( graph, start, steps, + ..., weights = NULL, mode = c("out", "in", "all", "total"), stuck = c("return", "error") ) { + # BEGIN GENERATED ARG_HANDLE: random_walk, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(weights = weights, mode = mode, stuck = stuck), + recover_new = c("weights", "mode", "stuck"), + recover_old = c("weights", "mode", "stuck"), + match_names = c("weights", "mode", "stuck"), + match_to = c("weights", "mode", "stuck"), + defaults = list( + weights = NULL, + mode = c("out", "in", "all", "total"), + stuck = c("return", "error") + ), + head_args = c("graph", "start", "steps"), + fn_name = "random_walk" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + mode <- match.arg(mode) stuck <- match.arg(stuck) out <- random_walk_impl( @@ -71,15 +99,43 @@ random_walk <- function( } #' @rdname random_walk +#' @inheritParams rlang::args_dots_empty #' @export random_edge_walk <- function( graph, start, steps, + ..., weights = NULL, mode = c("out", "in", "all", "total"), stuck = c("return", "error") ) { + # BEGIN GENERATED ARG_HANDLE: random_edge_walk, do not edit, see tools/generate-migrations.R + if (...length() > 0L) { + .arg_handle <- migrate_recover_args( + list(...), + current = list(weights = weights, mode = mode, stuck = stuck), + recover_new = c("weights", "mode", "stuck"), + recover_old = c("weights", "mode", "stuck"), + match_names = c("weights", "mode", "stuck"), + match_to = c("weights", "mode", "stuck"), + defaults = list( + weights = NULL, + mode = c("out", "in", "all", "total"), + stuck = c("return", "error") + ), + head_args = c("graph", "start", "steps"), + fn_name = "random_edge_walk" + ) + list2env(.arg_handle$values, environment()) + lifecycle::deprecate_soft( + "3.0.0", + what = I(.arg_handle$what), + details = .arg_handle$details + ) + } + # END GENERATED ARG_HANDLE + mode <- match.arg(mode) stuck <- match.arg(stuck) out <- random_walk_impl( diff --git a/man/find_cycle.Rd b/man/find_cycle.Rd index c76504fa175..f35f567bc46 100644 --- a/man/find_cycle.Rd +++ b/man/find_cycle.Rd @@ -4,11 +4,13 @@ \alias{find_cycle} \title{Finds a cycle in a graph, if there is one} \usage{ -find_cycle(graph, mode = c("out", "in", "all", "total")) +find_cycle(graph, ..., mode = c("out", "in", "all", "total")) } \arguments{ \item{graph}{The input graph.} +\item{...}{These dots are for future extensions and must be empty.} + \item{mode}{Character constant specifying how to handle directed graphs. \code{out} follows edge directions, \verb{in} follows edges in the reverse direction, and \code{all} ignores edge directions. Ignored in undirected graphs.} diff --git a/man/random_walk.Rd b/man/random_walk.Rd index 5232e145b23..2b4887a6fa5 100644 --- a/man/random_walk.Rd +++ b/man/random_walk.Rd @@ -9,6 +9,7 @@ random_walk( graph, start, steps, + ..., weights = NULL, mode = c("out", "in", "all", "total"), stuck = c("return", "error") @@ -18,6 +19,7 @@ random_edge_walk( graph, start, steps, + ..., weights = NULL, mode = c("out", "in", "all", "total"), stuck = c("return", "error") @@ -30,6 +32,8 @@ random_edge_walk( \item{steps}{The number of steps to make.} +\item{...}{These dots are for future extensions and must be empty.} + \item{weights}{The edge weights. Larger edge weights increase the probability that an edge is selected by the random walker. In other words, larger edge weights correspond to stronger connections. The diff --git a/tests/testthat/test-cycles.R b/tests/testthat/test-cycles.R index 383a9a3a885..27b32c6de73 100644 --- a/tests/testthat/test-cycles.R +++ b/tests/testthat/test-cycles.R @@ -111,3 +111,24 @@ test_that("simple_cycles_callback handles errors in callback", { "Error in R callback function" ) }) + +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("find_cycle mode variants distinguish edge orientation", { + # A directed acyclic triangle is only cyclic when directions are ignored. + g <- make_graph(c(1, 2, 1, 3, 2, 3), directed = TRUE) + + expect_length(find_cycle(g)$vertices, 0) + expect_length(find_cycle(g, mode = "in")$vertices, 0) + + cyc <- find_cycle(g, mode = "all") + expect_length(cyc$vertices, 3) + expect_length(cyc$edges, 3) +}) + +test_that("find_cycle recovers legacy positional arguments", { + g <- make_graph(c(1, 2, 1, 3, 2, 3), directed = TRUE) + lifecycle::expect_deprecated(res <- find_cycle(g, "all")) + expect_equal(res, find_cycle(g, mode = "all")) + expect_length(res$vertices, 3) +}) diff --git a/tests/testthat/test-random_walk.R b/tests/testthat/test-random_walk.R index f419810e3dd..1322de9d886 100644 --- a/tests/testthat/test-random_walk.R +++ b/tests/testthat/test-random_walk.R @@ -79,3 +79,52 @@ test_that("directed random_edge_walk can return wtih an error when stuck", { "Random walk got stuck" ) }) + +# ---- ellipsis migration: argument coverage ---------------------------- + +test_that("random_walk follows edge weights and returns partial walks when stuck", { + igraph_local_seed(42) + + # The heavy edge to vertex 4 forces the walk to bounce between 1 and 4. + g <- make_star(5, mode = "undirected") + w <- random_walk(g, start = 1, steps = 6, weights = c(1, 1, 1e12, 1)) + expect_equal(as_ids(w), rep_len(c(1, 4), 7)) + + # A walk starting on a sink vertex stops immediately with stuck = "return". + g2 <- make_star(11, mode = "out") + w2 <- random_walk(g2, start = 7, steps = 10, stuck = "return") + expect_equal(as_ids(w2), 7) +}) + +test_that("random_walk recovers legacy positional arguments", { + g <- make_star(5, mode = "undirected") + lifecycle::expect_deprecated( + res <- igraph_with_seed(42, random_walk(g, 1, 6, c(1, 1, 1e12, 1))) + ) + expect_equal( + res, + igraph_with_seed(42, random_walk(g, 1, 6, weights = c(1, 1, 1e12, 1))) + ) + expect_equal(as_ids(res), rep_len(c(1, 4), 7)) +}) + +test_that("random_edge_walk follows edge weights", { + igraph_local_seed(42) + # A two-cycle with one heavy edge keeps the walker crossing that edge. + g <- make_ring(2) + w <- random_edge_walk(g, start = 1, steps = 8, weights = c(1e12, 1)) + expect_length(w, 8) + expect_equal(as.integer(w), rep(1L, 8)) +}) + +test_that("random_edge_walk recovers legacy positional arguments", { + g <- make_ring(2) + lifecycle::expect_deprecated( + res <- igraph_with_seed(42, random_edge_walk(g, 1, 8, c(1e12, 1))) + ) + expect_equal( + res, + igraph_with_seed(42, random_edge_walk(g, 1, 8, weights = c(1e12, 1))) + ) + expect_equal(as.integer(res), rep(1L, 8)) +}) diff --git a/tools/migrations/walks-cycles.R b/tools/migrations/walks-cycles.R new file mode 100644 index 00000000000..88056db8061 --- /dev/null +++ b/tools/migrations/walks-cycles.R @@ -0,0 +1,43 @@ +# Argument-signature migrations: walks-cycles +# Schema: see tools/migrations/README.md. Regenerate with: +# Rscript tools/generate-migrations.R + +migrations <- list( + find_cycle = list( + old = function(graph, mode) {}, + new = function( + graph, + ..., + mode = c("out", "in", "all", "total") + ) {}, + when = "3.0.0" + ), + + random_edge_walk = list( + old = function(graph, start, steps, weights, mode, stuck) {}, + new = function( + graph, + start, + steps, + ..., + weights = NULL, + mode = c("out", "in", "all", "total"), + stuck = c("return", "error") + ) {}, + when = "3.0.0" + ), + + random_walk = list( + old = function(graph, start, steps, weights, mode, stuck) {}, + new = function( + graph, + start, + steps, + ..., + weights = NULL, + mode = c("out", "in", "all", "total"), + stuck = c("return", "error") + ) {}, + when = "3.0.0" + ) +)