diff --git a/NAMESPACE b/NAMESPACE index 95be07fb203..f12bff5673c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -894,6 +894,8 @@ export(weighted_cliques) export(which_loop) export(which_multiple) export(which_mutual) +export(widest_path_widths) +export(widest_paths) export(with_dh) export(with_drl) export(with_edge_) diff --git a/R/paths.R b/R/paths.R index 857bbf8bf04..a13cdc7658b 100644 --- a/R/paths.R +++ b/R/paths.R @@ -406,3 +406,255 @@ graph_center <- function( distance_table <- function(graph, directed = TRUE) { path_length_hist_impl(graph = graph, directed = directed) } +#' Widest paths between vertices +#' +#' `widest_path_widths()` calculates the widths of all the widest paths from +#' or to the vertices in the network. +#' `widest_paths()` calculates one widest path (the path itself, and not just its width) from or to the +#' given vertex. +#' +#' The widest path between two vertices is the path with the maximum bottleneck width, +#' where the bottleneck width is defined as the minimum edge weight along the path. +#' The functions documented in this manual page all calculate widest paths between vertex pairs. +#' +#' `widest_path_widths()` calculates the widths of pairwise widest paths from +#' a set of vertices (`from`) to another set of vertices (`to`). +#' It uses different algorithms, depending on the `algorithm` argument. +#' The implemented algorithms are Dijkstra's algorithm (\sQuote{`dijkstra`}), +#' which is faster for sparse graphs, and the Floyd-Warshall algorithm +#' (\sQuote{`floyd-warshall`}), which is faster for dense graphs. +#' +#' igraph can choose automatically between algorithms. For automatic algorithm selection, +#' supply \sQuote{`automatic`} as the `algorithm` argument. (This is also the default.) +#' +#' `widest_paths()` calculates a single widest path (i.e. the path +#' itself, not just its width) between the source vertex given in `from`, +#' to the target vertices given in `to`. +#' `widest_paths()` uses a modified Dijkstra's algorithm. +#' +#' @param graph The graph to work on. +#' @param from The source vertex for `widest_paths()`, or a set of source vertices +#' for `widest_path_widths()`. +#' @param v Numeric vector, the vertices from which the widest paths will be +#' calculated. This is an alias for `from` in `widest_path_widths()`. +#' @param to Numeric vector, the vertices to which the widest paths will be +#' calculated. By default it includes all vertices. +#' @param mode Character constant, gives whether the widest paths to or from +#' the given vertices should be calculated for directed graphs. If `out` +#' then the widest paths *from* the vertex, if `in` then *to* +#' it will be considered. If `all`, the default, then the graph is treated +#' as undirected, i.e. edge directions are not taken into account. This +#' argument is ignored for undirected graphs. +#' @param weights A numeric vector giving edge weights (interpreted as widths). +#' If this is `NULL` and the graph has a `weight` edge attribute, then the +#' attribute is used. Widest path functions require edge weights, so if no +#' `weight` attribute exists and `weights` is not provided, an error is raised. +#' In a weighted graph, the width of a path is the minimum weight along the path. +#' @param algorithm Which algorithm to use for the calculation. By default +#' igraph selects automatically between Dijkstra's algorithm and Floyd-Warshall. +#' For sparse graphs, Dijkstra is faster; for dense graphs, Floyd-Warshall is faster. +#' You can override igraph's choice by explicitly giving this parameter. +#' @param output Character scalar, defines how to report the widest paths for `widest_paths()`. +#' \dQuote{vpath} means that the vertices along the paths are reported, +#' \dQuote{epath} means that the edges along the paths are reported. +#' \dQuote{both} means that both forms are returned, in a named list with components +#' \dQuote{vpath} and \dQuote{epath}. +#' @param predecessors Logical scalar, whether to return the predecessor vertex +#' for each vertex. The predecessor of vertex `i` in the tree is the +#' vertex from which vertex `i` was reached. The predecessor of the start +#' vertex (in the `from` argument) is -1. If the predecessor is -2, it means +#' that the given vertex was not reached from the source during the search. +#' Note that the search terminates if all the vertices in `to` are reached. +#' @param inbound.edges Logical scalar, whether to return the inbound edge for +#' each vertex. The inbound edge of vertex `i` in the tree is the edge via +#' which vertex `i` was reached. The start vertex and vertices that were +#' not reached during the search will have -1 in the corresponding entry of +#' the vector. Note that the search terminates if all the vertices in `to` +#' are reached. +#' +#' @return For `widest_path_widths()` a numeric matrix with `length(to)` +#' columns and `length(from)` rows. The widest path width from a vertex to +#' itself is `Inf`. For unreachable vertices `-Inf` is included. +#' +#' For `widest_paths()` a named list is returned: +#' \describe{ +#' \item{vpath}{ +#' A list of length `length(to)`. List element `i` contains the vertex ids on +#' the path from vertex `from` to vertex `to[i]` (or the other way for directed +#' graphs depending on the `mode` argument). +#' The vector also contains `from` and `to[i]` as the first and last elements. +#' If `from` is the same as `to[i]` then it is only included once. +#' If there is no path between two vertices then a numeric vector of length zero is +#' returned as the list element. +#' If this output is not requested in the `output` argument, then it will be `NULL`. +#' } +#' \item{epath}{ +#' A list similar to `vpath`, but the vectors contain the edge ids along the widest +#' paths, instead of the vertex ids. This entry is set to `NULL` if it is not +#' requested in the `output` argument. +#' } +#' \item{predecessors}{ +#' Numeric vector, the predecessor of each vertex, or `NULL` if it was not requested. +#' } +#' \item{inbound_edges}{ +#' Numeric vector, the inbound edge for each vertex, or `NULL`, if it was not requested. +#' } +#' } +#' +#' @author Gabor Csardi \email{csardi.gabor@@gmail.com} +#' @family paths +#' @export +#' @keywords graphs +#' @examples +#' +#' g <- make_ring(10) +#' E(g)$weight <- seq_len(ecount(g)) +#' widest_path_widths(g) +#' widest_paths(g, 5) +#' +#' @cdocs igraph_widest_path_widths_dijkstra +widest_path_widths <- function( + graph, + v = V(graph), + to = V(graph), + mode = c("all", "out", "in"), + weights = NULL, + algorithm = c("automatic", "dijkstra", "floyd-warshall") +) { + ensure_igraph(graph) + + # make sure that the lower-level function in C gets mode == "out" + # unconditionally when the graph is undirected + if (!is_directed(graph)) { + mode <- "out" + } + + v <- as_igraph_vs(graph, v) + to <- as_igraph_vs(graph, to) + mode <- igraph.match.arg(mode) + algorithm <- igraph.match.arg(algorithm) + + if (is.null(weights)) { + if ("weight" %in% edge_attr_names(graph)) { + weights <- as.numeric(E(graph)$weight) + } else { + cli::cli_abort( + "Widest path functions require edge weights. Please provide {.arg weights} or add a {.field weight} edge attribute.", + call = rlang::caller_env() + ) + } + } else { + if (length(weights) == 1 && is.na(weights)) { + cli::cli_abort( + "Widest path functions require edge weights. {.arg weights = NA} is not supported.", + call = rlang::caller_env() + ) + } else { + weights <- as.numeric(weights) + } + } + + # Select algorithm automatically if requested + if (algorithm == "automatic") { + # Use heuristic: Dijkstra for sparse graphs, Floyd-Warshall for dense + ecount_val <- ecount(graph) + vcount_val <- vcount(graph) + if (ecount_val < vcount_val * vcount_val / 10) { + algorithm <- "dijkstra" + } else { + algorithm <- "floyd-warshall" + } + } + + if (algorithm == "dijkstra") { + res <- widest_path_widths_dijkstra_impl( + graph = graph, + from = v, + to = to, + weights = weights, + mode = mode + ) + } else { + res <- widest_path_widths_floyd_warshall_impl( + graph = graph, + from = v, + to = to, + weights = weights, + mode = mode + ) + } + + if (igraph_opt("add.vertex.names") && is_named(graph)) { + rownames(res) <- V(graph)$name[v] + colnames(res) <- V(graph)$name[to] + } + res +} + +#' @rdname widest_path_widths +#' @export +#' @cdocs igraph_get_widest_paths +widest_paths <- function( + graph, + from, + to = V(graph), + mode = c("out", "all", "in"), + weights = NULL, + output = c("vpath", "epath", "both"), + predecessors = FALSE, + inbound.edges = FALSE +) { + ensure_igraph(graph) + mode <- igraph.match.arg(mode) + output <- igraph.match.arg(output) + + if (is.null(weights)) { + if ("weight" %in% edge_attr_names(graph)) { + weights <- as.numeric(E(graph)$weight) + } else { + cli::cli_abort( + "Widest path functions require edge weights. Please provide {.arg weights} or add a {.field weight} edge attribute.", + call = rlang::caller_env() + ) + } + } else { + if (length(weights) == 1 && is.na(weights)) { + cli::cli_abort( + "Widest path functions require edge weights. {.code weights = NA} is not supported.", + call = rlang::caller_env() + ) + } else { + weights <- as.numeric(weights) + } + } + + # Get the results from the implementation function + res <- get_widest_paths_impl( + graph = graph, + from = from, + to = to, + weights = weights, + mode = mode + ) + + # Process output based on the output parameter + result <- list() + + if (output == "vpath" || output == "both") { + result$vpath <- res$vertices + } + + if (output == "epath" || output == "both") { + result$epath <- res$edges + } + + if (predecessors) { + result$predecessors <- res$parents + } + + if (inbound.edges) { + result$inbound_edges <- res$inbound_edges + } + + result +} diff --git a/man/all_simple_paths.Rd b/man/all_simple_paths.Rd index 2f83d84a3dd..8a1af33750c 100644 --- a/man/all_simple_paths.Rd +++ b/man/all_simple_paths.Rd @@ -58,7 +58,8 @@ Other paths: \code{\link{distance_table}()}, \code{\link{eccentricity}()}, \code{\link{graph_center}()}, -\code{\link{radius}()} +\code{\link{radius}()}, +\code{\link{widest_path_widths}()} } \concept{paths} \keyword{graphs} diff --git a/man/diameter.Rd b/man/diameter.Rd index 9fd3594546b..758a0b96298 100644 --- a/man/diameter.Rd +++ b/man/diameter.Rd @@ -79,7 +79,8 @@ Other paths: \code{\link{distance_table}()}, \code{\link{eccentricity}()}, \code{\link{graph_center}()}, -\code{\link{radius}()} +\code{\link{radius}()}, +\code{\link{widest_path_widths}()} } \author{ Gabor Csardi \email{csardi.gabor@gmail.com} diff --git a/man/distances.Rd b/man/distances.Rd index 96ffa1c2860..dbe7b5610ca 100644 --- a/man/distances.Rd +++ b/man/distances.Rd @@ -320,7 +320,8 @@ Other paths: \code{\link{diameter}()}, \code{\link{eccentricity}()}, \code{\link{graph_center}()}, -\code{\link{radius}()} +\code{\link{radius}()}, +\code{\link{widest_path_widths}()} } \author{ Gabor Csardi \email{csardi.gabor@gmail.com} diff --git a/man/eccentricity.Rd b/man/eccentricity.Rd index a4678a78d2d..6c742affc57 100644 --- a/man/eccentricity.Rd +++ b/man/eccentricity.Rd @@ -65,7 +65,8 @@ Other paths: \code{\link{diameter}()}, \code{\link{distance_table}()}, \code{\link{graph_center}()}, -\code{\link{radius}()} +\code{\link{radius}()}, +\code{\link{widest_path_widths}()} } \concept{paths} \section{Related documentation in the C library}{\href{https://igraph.org/c/html/latest/igraph-Structural.html#igraph_eccentricity_dijkstra}{\code{eccentricity_dijkstra()}}.} diff --git a/man/graph_center.Rd b/man/graph_center.Rd index edaa1075a6e..7956e499ade 100644 --- a/man/graph_center.Rd +++ b/man/graph_center.Rd @@ -54,7 +54,8 @@ Other paths: \code{\link{diameter}()}, \code{\link{distance_table}()}, \code{\link{eccentricity}()}, -\code{\link{radius}()} +\code{\link{radius}()}, +\code{\link{widest_path_widths}()} } \concept{paths} \section{Related documentation in the C library}{\href{https://igraph.org/c/html/latest/igraph-Structural.html#igraph_graph_center_dijkstra}{\code{graph_center_dijkstra()}}.} diff --git a/man/radius.Rd b/man/radius.Rd index 16cb54ccbe5..4ffd579fc0b 100644 --- a/man/radius.Rd +++ b/man/radius.Rd @@ -58,7 +58,8 @@ Other paths: \code{\link{diameter}()}, \code{\link{distance_table}()}, \code{\link{eccentricity}()}, -\code{\link{graph_center}()} +\code{\link{graph_center}()}, +\code{\link{widest_path_widths}()} } \concept{paths} \section{Related documentation in the C library}{\href{https://igraph.org/c/html/latest/igraph-Structural.html#igraph_radius_dijkstra}{\code{radius_dijkstra()}}.} diff --git a/man/widest_path_widths.Rd b/man/widest_path_widths.Rd new file mode 100644 index 00000000000..aa9600d385b --- /dev/null +++ b/man/widest_path_widths.Rd @@ -0,0 +1,157 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/paths.R +\name{widest_path_widths} +\alias{widest_path_widths} +\alias{widest_paths} +\title{Widest paths between vertices} +\usage{ +widest_path_widths( + graph, + v = V(graph), + to = V(graph), + mode = c("all", "out", "in"), + weights = NULL, + algorithm = c("automatic", "dijkstra", "floyd-warshall") +) + +widest_paths( + graph, + from, + to = V(graph), + mode = c("out", "all", "in"), + weights = NULL, + output = c("vpath", "epath", "both"), + predecessors = FALSE, + inbound.edges = FALSE +) +} +\arguments{ +\item{graph}{The graph to work on.} + +\item{v}{Numeric vector, the vertices from which the widest paths will be +calculated. This is an alias for \code{from} in \code{widest_path_widths()}.} + +\item{to}{Numeric vector, the vertices to which the widest paths will be +calculated. By default it includes all vertices.} + +\item{mode}{Character constant, gives whether the widest paths to or from +the given vertices should be calculated for directed graphs. If \code{out} +then the widest paths \emph{from} the vertex, if \verb{in} then \emph{to} +it will be considered. If \code{all}, the default, then the graph is treated +as undirected, i.e. edge directions are not taken into account. This +argument is ignored for undirected graphs.} + +\item{weights}{A numeric vector giving edge weights (interpreted as widths). +If this is \code{NULL} and the graph has a \code{weight} edge attribute, then the +attribute is used. Widest path functions require edge weights, so if no +\code{weight} attribute exists and \code{weights} is not provided, an error is raised. +In a weighted graph, the width of a path is the minimum weight along the path.} + +\item{algorithm}{Which algorithm to use for the calculation. By default +igraph selects automatically between Dijkstra's algorithm and Floyd-Warshall. +For sparse graphs, Dijkstra is faster; for dense graphs, Floyd-Warshall is faster. +You can override igraph's choice by explicitly giving this parameter.} + +\item{from}{The source vertex for \code{widest_paths()}, or a set of source vertices +for \code{widest_path_widths()}.} + +\item{output}{Character scalar, defines how to report the widest paths for \code{widest_paths()}. +\dQuote{vpath} means that the vertices along the paths are reported, +\dQuote{epath} means that the edges along the paths are reported. +\dQuote{both} means that both forms are returned, in a named list with components +\dQuote{vpath} and \dQuote{epath}.} + +\item{predecessors}{Logical scalar, whether to return the predecessor vertex +for each vertex. The predecessor of vertex \code{i} in the tree is the +vertex from which vertex \code{i} was reached. The predecessor of the start +vertex (in the \code{from} argument) is -1. If the predecessor is -2, it means +that the given vertex was not reached from the source during the search. +Note that the search terminates if all the vertices in \code{to} are reached.} + +\item{inbound.edges}{Logical scalar, whether to return the inbound edge for +each vertex. The inbound edge of vertex \code{i} in the tree is the edge via +which vertex \code{i} was reached. The start vertex and vertices that were +not reached during the search will have -1 in the corresponding entry of +the vector. Note that the search terminates if all the vertices in \code{to} +are reached.} +} +\value{ +For \code{widest_path_widths()} a numeric matrix with \code{length(to)} +columns and \code{length(from)} rows. The widest path width from a vertex to +itself is \code{Inf}. For unreachable vertices \code{-Inf} is included. + +For \code{widest_paths()} a named list is returned: +\describe{ +\item{vpath}{ +A list of length \code{length(to)}. List element \code{i} contains the vertex ids on +the path from vertex \code{from} to vertex \code{to[i]} (or the other way for directed +graphs depending on the \code{mode} argument). +The vector also contains \code{from} and \code{to[i]} as the first and last elements. +If \code{from} is the same as \code{to[i]} then it is only included once. +If there is no path between two vertices then a numeric vector of length zero is +returned as the list element. +If this output is not requested in the \code{output} argument, then it will be \code{NULL}. +} +\item{epath}{ +A list similar to \code{vpath}, but the vectors contain the edge ids along the widest +paths, instead of the vertex ids. This entry is set to \code{NULL} if it is not +requested in the \code{output} argument. +} +\item{predecessors}{ +Numeric vector, the predecessor of each vertex, or \code{NULL} if it was not requested. +} +\item{inbound_edges}{ +Numeric vector, the inbound edge for each vertex, or \code{NULL}, if it was not requested. +} +} +} +\description{ +\code{widest_path_widths()} calculates the widths of all the widest paths from +or to the vertices in the network. +\code{widest_paths()} calculates one widest path (the path itself, and not just its width) from or to the +given vertex. +} +\details{ +The widest path between two vertices is the path with the maximum bottleneck width, +where the bottleneck width is defined as the minimum edge weight along the path. +The functions documented in this manual page all calculate widest paths between vertex pairs. + +\code{widest_path_widths()} calculates the widths of pairwise widest paths from +a set of vertices (\code{from}) to another set of vertices (\code{to}). +It uses different algorithms, depending on the \code{algorithm} argument. +The implemented algorithms are Dijkstra's algorithm (\sQuote{\code{dijkstra}}), +which is faster for sparse graphs, and the Floyd-Warshall algorithm +(\sQuote{\code{floyd-warshall}}), which is faster for dense graphs. + +igraph can choose automatically between algorithms. For automatic algorithm selection, +supply \sQuote{\code{automatic}} as the \code{algorithm} argument. (This is also the default.) + +\code{widest_paths()} calculates a single widest path (i.e. the path +itself, not just its width) between the source vertex given in \code{from}, +to the target vertices given in \code{to}. +\code{widest_paths()} uses a modified Dijkstra's algorithm. +} +\examples{ + +g <- make_ring(10) +E(g)$weight <- seq_len(ecount(g)) +widest_path_widths(g) +widest_paths(g, 5) + +} +\seealso{ +Other paths: +\code{\link{all_simple_paths}()}, +\code{\link{diameter}()}, +\code{\link{distance_table}()}, +\code{\link{eccentricity}()}, +\code{\link{graph_center}()}, +\code{\link{radius}()} +} +\author{ +Gabor Csardi \email{csardi.gabor@gmail.com} +} +\concept{paths} +\keyword{graphs} +\section{Related documentation in the C library}{\href{https://igraph.org/c/html/latest/igraph-Structural.html#igraph_widest_path_widths_dijkstra}{\code{widest_path_widths_dijkstra()}}, \href{https://igraph.org/c/html/latest/igraph-Structural.html#igraph_get_widest_paths}{\code{get_widest_paths()}}.} + diff --git a/tests/testthat/test-paths.R b/tests/testthat/test-paths.R index 85f76ce094c..79c473d138a 100644 --- a/tests/testthat/test-paths.R +++ b/tests/testthat/test-paths.R @@ -71,3 +71,202 @@ test_that("all_simple_paths() passes on cutoff argument", { c(2, 3, 4, 5, 6, 7, 2, 3, 4, 5, 6, 7) ) }) + +test_that("widest_path_widths() works", { + # Simple ring graph with weights + g <- make_ring(5) + E(g)$weight <- c(1, 2, 3, 4, 5) + + # Widest path from vertex 1 to all vertices + result <- widest_path_widths(g, v = 1, to = V(g)) + + # Width to itself is Inf + expect_equal(result[1, 1], Inf) + + # Width from 1 to 2: direct edge has weight 1, but going the other way + # 1->5->4->3->2 has bottleneck min(5,4,3,2) = 2, which is wider + expect_equal(result[1, 2], 2) + + # Width from 1 to 3: 1->2->3 has bottleneck min(1,2) = 1, or + # 1->5->4->3 has bottleneck min(5,4,3) = 3, so choose 3 + expect_equal(result[1, 3], 3) +}) + +test_that("widest_path_widths() works -- algorithm selection", { + g <- make_ring(5) + E(g)$weight <- c(1, 2, 3, 4, 5) + + # Test Dijkstra + result_dijkstra <- widest_path_widths( + g, + v = 1, + to = V(g), + algorithm = "dijkstra" + ) + + # Test Floyd-Warshall + result_floyd <- widest_path_widths( + g, + v = 1, + to = V(g), + algorithm = "floyd-warshall" + ) + + # Both should give the same result + expect_equal(result_dijkstra, result_floyd) + + # Test automatic selection + result_auto <- widest_path_widths( + g, + v = 1, + to = V(g), + algorithm = "automatic" + ) + expect_equal(result_auto, result_dijkstra) +}) + +test_that("widest_path_widths() works -- mode parameter", { + # Create a directed graph + g <- make_ring(5, directed = TRUE) + E(g)$weight <- c(1, 2, 3, 4, 5) + + # Test out mode (default) + result_out <- widest_path_widths(g, v = 1, to = V(g), mode = "out") + + # Test in mode + result_in <- widest_path_widths(g, v = 1, to = V(g), mode = "in") + + # Results should be different for directed graph + expect_false(identical(result_out, result_in)) + + # Test all mode (treats as undirected) + result_all <- widest_path_widths(g, v = 1, to = V(g), mode = "all") + expect_true(is.matrix(result_all)) +}) + +test_that("widest_path_widths() works -- weight attribute", { + g <- make_ring(5) + E(g)$weight <- c(1, 2, 3, 4, 5) + + # Use weight attribute + result_with_attr <- widest_path_widths(g, v = 1, to = V(g)) + + # Explicitly pass weights + result_with_explicit <- widest_path_widths( + g, + v = 1, + to = V(g), + weights = E(g)$weight + ) + + expect_equal(result_with_attr, result_with_explicit) +}) + +test_that("widest_path_widths() requires weights", { + # Graph without weight attribute + g <- make_ring(5) + + # Should error when no weights provided + expect_error( + widest_path_widths(g, v = 1, to = V(g)), + "Widest path functions require edge weights" + ) + + # Should error when weights = NA + E(g)$weight <- c(1, 2, 3, 4, 5) + expect_error( + widest_path_widths(g, v = 1, to = V(g), weights = NA), + "weights = NA" + ) +}) + +test_that("widest_paths() works", { + g <- make_ring(5) + E(g)$weight <- c(1, 2, 3, 4, 5) + + # Test vpath output + result <- widest_paths(g, from = 1, to = 3, output = "vpath") + expect_true("vpath" %in% names(result)) + expect_false("epath" %in% names(result)) + expect_true(length(result$vpath) == 1) + + # The widest path from 1 to 3 should go through 5->4->3 (bottleneck 3) + # rather than 1->2->3 (bottleneck 1) + path <- as.numeric(result$vpath[[1]]) + expect_true(1 %in% path) + expect_true(3 %in% path) +}) + +test_that("widest_paths() works -- output modes", { + g <- make_ring(5) + E(g)$weight <- c(1, 2, 3, 4, 5) + + # Test epath output + result_epath <- widest_paths(g, from = 1, to = 3, output = "epath") + expect_false("vpath" %in% names(result_epath)) + expect_true("epath" %in% names(result_epath)) + + # Test both output + result_both <- widest_paths(g, from = 1, to = 3, output = "both") + expect_true("vpath" %in% names(result_both)) + expect_true("epath" %in% names(result_both)) +}) + +test_that("widest_paths() works -- predecessors and inbound_edges", { + g <- make_ring(5) + E(g)$weight <- c(1, 2, 3, 4, 5) + + # Test with predecessors + result <- widest_paths(g, from = 1, to = V(g), predecessors = TRUE) + expect_true("predecessors" %in% names(result)) + + # Test with inbound_edges + result2 <- widest_paths(g, from = 1, to = V(g), inbound.edges = TRUE) + expect_true("inbound_edges" %in% names(result2)) + + # Test with both + result3 <- widest_paths( + g, + from = 1, + to = V(g), + predecessors = TRUE, + inbound.edges = TRUE + ) + expect_true("predecessors" %in% names(result3)) + expect_true("inbound_edges" %in% names(result3)) +}) + +test_that("widest_paths() works -- multiple targets", { + g <- make_ring(5) + E(g)$weight <- c(1, 2, 3, 4, 5) + + # Test with multiple targets + result <- widest_paths(g, from = 1, to = c(2, 3, 4)) + expect_equal(length(result$vpath), 3) +}) + +test_that("widest_path_widths() handles unreachable vertices", { + # Create a disconnected graph + g <- make_graph(~ 1-2-3, 4-5) + E(g)$weight <- c(1, 2, 3) + + result <- widest_path_widths(g, v = 1, to = V(g)) + + # Vertex 4 and 5 are unreachable from vertex 1 + expect_equal(result[1, 4], -Inf) + expect_equal(result[1, 5], -Inf) +}) + +test_that("widest_paths() works with directed graphs", { + # Create a directed graph + g <- make_ring(5, directed = TRUE) + E(g)$weight <- c(1, 2, 3, 4, 5) + + # Test out mode + result_out <- widest_paths(g, from = 1, to = 3, mode = "out") + expect_true(length(result_out$vpath) == 1) + + # Test in mode + result_in <- widest_paths(g, from = 1, to = 3, mode = "in") + expect_true(length(result_in$vpath) == 1) +}) diff --git a/tests/testthat/testthat-problems.rds b/tests/testthat/testthat-problems.rds deleted file mode 100644 index 95cf1759297..00000000000 Binary files a/tests/testthat/testthat-problems.rds and /dev/null differ