From 381152b91c54ab40d24564468d5e04fe4ec046d7 Mon Sep 17 00:00:00 2001 From: David Schoch Date: Tue, 29 Sep 2026 21:15:53 +0200 Subject: [PATCH 1/9] feat: Add `is_same_graph()` to compare graphs regardless of vertex and edge order `is_same_graph()` checks whether two graphs have the same vertex and edge sets. Vertices are matched by name when both graphs are named. It uses the existing `is_same_graph_impl()` from the C core. Closes #349. Co-Authored-By: Claude Opus 5.5 (1M context) --- NAMESPACE | 1 + R/iterators.R | 80 +++++++++++++++++++++++++++ _pkgdown.yml | 1 + tests/testthat/_snaps/aaa-auto.md | 15 +++++ tests/testthat/_snaps/iterators.md | 27 +++++++++ tests/testthat/test-aaa-auto.R | 25 +++++++++ tests/testthat/test-iterators.R | 89 ++++++++++++++++++++++++++++++ tools/stimulus/functions-R.yaml | 4 +- 8 files changed, 240 insertions(+), 2 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 6735ed9b748..dbfeb04b9c0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -577,6 +577,7 @@ export(is_max_matching) export(is_min_separator) export(is_named) export(is_printer_callback) +export(is_same_graph) export(is_separator) export(is_simple) export(is_subgraph_isomorphic_to) diff --git a/R/iterators.R b/R/iterators.R index 15e1875e434..a30baeb157d 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -78,6 +78,8 @@ get_es_graph_id <- get_vs_graph_id <- function(seq) { #' @inheritParams rlang::args_dots_empty #' @param attrs Whether to compare the attributes of the graphs #' @return Logical scalar +#' @seealso [is_same_graph()] to ignore the order of vertices and edges, +#' [isomorphic()] to ignore vertex labels altogether. #' @export identical_graphs <- function( g1, @@ -124,6 +126,84 @@ identical_graphs <- function( .Call(Rx_igraph_identical_graphs, g1, g2, as.logical(attrs)) } +#' Decide if two graphs are the same as labelled graphs +#' +#' @description +#' `r lifecycle::badge("experimental")` +#' +#' Two graphs are the same if they have the same directedness, +#' the same vertex set and the same edge set. +#' Unlike [identical_graphs()], the order in which vertices and edges are +#' stored is ignored, +#' and unlike [isomorphic()], vertices are not relabelled. +#' +#' @details +#' If `use_names` is `TRUE` and both graphs have a `name` vertex attribute, +#' vertices are matched by name, so the two graphs may store their vertices +#' in a different order. +#' Vertex names must be unique in both graphs in this case. +#' Otherwise, vertices are matched by their IDs. +#' +#' Edges are compared as a multiset: +#' the order of the edges and, in undirected graphs, the order of the +#' endpoints of an edge do not matter, +#' but the multiplicity of each edge does. +#' +#' Graph, vertex and edge attributes other than `name` are not compared. +#' +#' @param g1,g2 The two graphs. +#' @inheritParams rlang::args_dots_empty +#' @param use_names Logical scalar, whether to match vertices by their names +#' if both graphs have a `name` vertex attribute. +#' @return A logical scalar, `TRUE` if the two graphs are the same. +#' @seealso [identical_graphs()] for comparing the internal representation, +#' [isomorphic()] for comparing graphs up to relabelling of the vertices. +#' @cdocs igraph_is_same_graph +#' @export +#' @examples +#' # Same edges, stored in a different order +#' g1 <- make_graph(c(1, 2, 2, 3, 3, 1), directed = FALSE) +#' g2 <- make_graph(c(3, 2, 1, 3, 2, 1), directed = FALSE) +#' identical_graphs(g1, g2) +#' is_same_graph(g1, g2) +#' +#' # Same named graph, vertices stored in a different order +#' edges <- data.frame(from = c("a", "b"), to = c("b", "c")) +#' g3 <- graph_from_data_frame(edges, vertices = c("a", "b", "c")) +#' g4 <- graph_from_data_frame(edges, vertices = c("c", "b", "a")) +#' identical_graphs(g3, g4) +#' is_same_graph(g3, g4) +#' is_same_graph(g3, g4, use_names = FALSE) +#' +#' # Isomorphic, but not the same +#' star1 <- make_star(4, mode = "undirected") +#' star2 <- make_graph(c(2, 1, 2, 3, 2, 4), directed = FALSE) +#' is_same_graph(star1, star2) +#' isomorphic(star1, star2) +is_same_graph <- function(g1, g2, ..., use_names = TRUE) { + check_dots_empty() + ensure_igraph(g1) + ensure_igraph(g2) + check_bool(use_names) + + if (use_names && is_named(g1) && is_named(g2)) { + names1 <- V(g1)$name + names2 <- V(g2)$name + if (anyDuplicated(names1) || anyDuplicated(names2)) { + cli::cli_abort(c( + "Vertex names must be unique to match vertices by name.", + i = "Use {.code use_names = FALSE} to match vertices by their IDs." + )) + } + if (length(names1) != length(names2) || !setequal(names1, names2)) { + return(FALSE) + } + g2 <- permute(g2, match(names2, names1)) + } + + is_same_graph_impl(graph1 = g1, graph2 = g2) +} + add_vses_graph_ref <- function(vses, graph) { ref <- get_vs_ref(graph) if (!is.null(ref)) { diff --git a/_pkgdown.yml b/_pkgdown.yml index 22fbbfcc819..807e42dadd1 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -93,6 +93,7 @@ reference: - contents: - graph_id - identical_graphs + - is_same_graph - is_igraph - is_named - is_weighted diff --git a/tests/testthat/_snaps/aaa-auto.md b/tests/testthat/_snaps/aaa-auto.md index b8315efdb62..0e7e285a432 100644 --- a/tests/testthat/_snaps/aaa-auto.md +++ b/tests/testthat/_snaps/aaa-auto.md @@ -56,6 +56,21 @@ Error in `ensure_igraph()`: ! Must provide a graph object (provided `NULL`). +# is_same_graph_impl basic + + Code + is_same_graph_impl(graph1 = g1, graph2 = g2) + Output + [1] TRUE + +# is_same_graph_impl errors + + Code + is_same_graph_impl(graph1 = NULL, graph2 = make_ring(3)) + Condition + Error in `ensure_igraph()`: + ! Must provide a graph object (provided `NULL`). + # delete_vertices_idx_impl basic Code diff --git a/tests/testthat/_snaps/iterators.md b/tests/testthat/_snaps/iterators.md index 14bfdb75dfe..b28b4f615be 100644 --- a/tests/testthat/_snaps/iterators.md +++ b/tests/testthat/_snaps/iterators.md @@ -1,3 +1,30 @@ +# is_same_graph() errors + + Code + is_same_graph(dup, dup) + Condition + Error in `is_same_graph()`: + ! Vertex names must be unique to match vertices by name. + i Use `use_names = FALSE` to match vertices by their IDs. + Code + is_same_graph(g, g, TRUE) + Condition + Error in `is_same_graph()`: + ! `...` must be empty. + x Problematic argument: + * ..1 = TRUE + i Did you forget to name an argument? + Code + is_same_graph(g, g, use_names = NA) + Condition + Error in `is_same_graph()`: + ! `use_names` must be `TRUE` or `FALSE`, not `NA`. + Code + is_same_graph(g, NULL) + Condition + Error in `ensure_igraph()`: + ! Must provide a graph object (provided `NULL`). + # printing connected vs/es works Code diff --git a/tests/testthat/test-aaa-auto.R b/tests/testthat/test-aaa-auto.R index f55d16dea98..476c33788e7 100644 --- a/tests/testthat/test-aaa-auto.R +++ b/tests/testthat/test-aaa-auto.R @@ -104,6 +104,31 @@ test_that("copy_impl errors", { )) }) +# 3a. is_same_graph_impl + +test_that("is_same_graph_impl basic", { + igraph_local_seed(20250909) + g1 <- make_graph(c(1, 2, 2, 3), directed = FALSE) + g2 <- make_graph(c(3, 2, 2, 1), directed = FALSE) + + expect_snapshot(is_same_graph_impl( + graph1 = g1, + graph2 = g2 + )) + + # Structured tests + expect_true(is_same_graph_impl(graph1 = g1, graph2 = g2)) + expect_false(is_same_graph_impl(graph1 = g1, graph2 = make_ring(3))) +}) + +test_that("is_same_graph_impl errors", { + igraph_local_seed(20250909) + expect_snapshot_igraph_error(is_same_graph_impl( + graph1 = NULL, + graph2 = make_ring(3) + )) +}) + # 4. delete_vertices_idx_impl test_that("delete_vertices_idx_impl basic", { diff --git a/tests/testthat/test-iterators.R b/tests/testthat/test-iterators.R index 5e41c086170..a3a5c0aabf0 100644 --- a/tests/testthat/test-iterators.R +++ b/tests/testthat/test-iterators.R @@ -199,6 +199,95 @@ test_that("identical_graphs considers attributes", { expect_identical_graphs(g, g2, attrs = FALSE) }) +test_that("is_same_graph() ignores edge order", { + g <- make_ring(10) + g_edit <- g + edge(7, 9) + edge(8, 10) - edge("7|8") - edge("9|10") + + adj <- as_adjacency_matrix(g, sparse = FALSE) + adj[7, 8] <- adj[8, 7] <- adj[9, 10] <- adj[10, 9] <- 0 + adj[7, 9] <- adj[9, 7] <- adj[8, 10] <- adj[10, 8] <- 1 + g_adj <- graph_from_adjacency_matrix(adj, mode = "undirected") + + expect_not_identical_graphs(g_edit, g_adj) + expect_true(is_same_graph(g_edit, g_adj)) +}) + +test_that("is_same_graph() ignores endpoint order in undirected graphs only", { + g1 <- make_graph(c(1, 2, 2, 3), directed = FALSE) + g2 <- make_graph(c(3, 2, 2, 1), directed = FALSE) + expect_true(is_same_graph(g1, g2)) + + d1 <- make_graph(c(1, 2, 2, 3), directed = TRUE) + d2 <- make_graph(c(3, 2, 2, 1), directed = TRUE) + expect_false(is_same_graph(d1, d2)) + expect_false(is_same_graph(g1, d1)) +}) + +test_that("is_same_graph() respects edge multiplicities", { + g1 <- make_graph(c(1, 2, 1, 2, 2, 3), directed = FALSE) + g2 <- make_graph(c(2, 3, 2, 1, 1, 2), directed = FALSE) + g3 <- make_graph(c(1, 2, 2, 3, 2, 3), directed = FALSE) + expect_true(is_same_graph(g1, g2)) + expect_false(is_same_graph(g1, g3)) +}) + +test_that("is_same_graph() distinguishes isomorphic graphs", { + g1 <- make_star(4, mode = "undirected") + g2 <- permute(g1, c(2, 1, 3, 4)) + expect_true(isomorphic(g1, g2)) + expect_false(is_same_graph(g1, g2)) + expect_false(is_same_graph(make_ring(5), make_ring(6))) +}) + +test_that("is_same_graph() matches vertices by name", { + igraph_local_seed(50) + edges <- unique(cbind( + paste0("v", sample(100:200, 10)), + paste0("v", sample(100:200, 10)) + )) + vertices <- unique(as.vector(edges)) + + g1 <- graph_from_data_frame(edges, directed = TRUE) + g2 <- graph_from_data_frame(edges, directed = TRUE, vertices = sort(vertices)) + + expect_not_identical_graphs(g1, g2) + expect_true(is_same_graph(g1, g2)) + expect_false(is_same_graph(g1, g2, use_names = FALSE)) +}) + +test_that("is_same_graph() compares vertex name sets", { + g1 <- make_graph(~ a - b, b - c) + g2 <- make_graph(~ a - b, b - d) + g3 <- make_graph(~ a - b, b - c, d) + expect_false(is_same_graph(g1, g2)) + expect_false(is_same_graph(g1, g3)) +}) + +test_that("is_same_graph() uses IDs unless both graphs are named", { + g1 <- make_ring(3) + g2 <- set_vertex_attr(make_ring(3), "name", value = c("c", "b", "a")) + expect_true(is_same_graph(g1, g2)) +}) + +test_that("is_same_graph() ignores attributes other than names", { + g1 <- set_edge_attr(make_ring(3), "weight", value = 1:3) + g2 <- set_graph_attr(make_ring(3), "name", "ring") + expect_true(is_same_graph(g1, g2)) +}) + +test_that("is_same_graph() errors", { + g <- make_ring(3) + dup <- set_vertex_attr(g, "name", value = c("a", "a", "b")) + + expect_snapshot(error = TRUE, { + is_same_graph(dup, dup) + is_same_graph(g, g, TRUE) + is_same_graph(g, g, use_names = NA) + is_same_graph(g, NULL) + }) + expect_true(is_same_graph(dup, dup, use_names = FALSE)) +}) + test_that("we can create vertex/edge seqs", { g <- make_ring(10) V(g) %&&% expect_true(TRUE) diff --git a/tools/stimulus/functions-R.yaml b/tools/stimulus/functions-R.yaml index f5e95746274..efb46b71a3b 100644 --- a/tools/stimulus/functions-R.yaml +++ b/tools/stimulus/functions-R.yaml @@ -24,8 +24,8 @@ igraph_get_all_eids_between: DEPS: from ON graph, to ON graph, eids ON graph igraph_is_same_graph: - # Not needed in R; we can simply compare things in the R layer without - # calling into C + # Used by is_same_graph(), which matches vertices by name before calling + # into C ####################################### # Constructors, deterministic From a228258d41edf387676cf62a742c478f6971142a Mon Sep 17 00:00:00 2001 From: David Schoch Date: Tue, 29 Sep 2026 21:18:55 +0200 Subject: [PATCH 2/9] docs: Drop experimental badge from `is_same_graph()` Co-Authored-By: Claude Opus 5.5 (1M context) --- R/iterators.R | 2 -- 1 file changed, 2 deletions(-) diff --git a/R/iterators.R b/R/iterators.R index a30baeb157d..faee3b048ed 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -129,8 +129,6 @@ identical_graphs <- function( #' Decide if two graphs are the same as labelled graphs #' #' @description -#' `r lifecycle::badge("experimental")` -#' #' Two graphs are the same if they have the same directedness, #' the same vertex set and the same edge set. #' Unlike [identical_graphs()], the order in which vertices and edges are From 57d404a8928d906fc22336bce36a9e6e05368393 Mon Sep 17 00:00:00 2001 From: schochastics Date: Tue, 29 Sep 2026 19:28:54 +0000 Subject: [PATCH 3/9] chore: Auto-update from GitHub Actions Run: https://github.com/igraph/rigraph/actions/runs/36618367425 --- man/identical_graphs.Rd | 4 +++ man/is_same_graph.Rd | 69 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 man/is_same_graph.Rd diff --git a/man/identical_graphs.Rd b/man/identical_graphs.Rd index ed60154f8c6..3de63d6d4fc 100644 --- a/man/identical_graphs.Rd +++ b/man/identical_graphs.Rd @@ -32,3 +32,7 @@ different even if the two graphs are identical. Attribute comparison can be turned off with the \code{attrs} parameter if the attributes of the two graphs are allowed to be different. } +\seealso{ +\code{\link[=is_same_graph]{is_same_graph()}} to ignore the order of vertices and edges, +\code{\link[=isomorphic]{isomorphic()}} to ignore vertex labels altogether. +} diff --git a/man/is_same_graph.Rd b/man/is_same_graph.Rd new file mode 100644 index 00000000000..a62d7396255 --- /dev/null +++ b/man/is_same_graph.Rd @@ -0,0 +1,69 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/iterators.R +\name{is_same_graph} +\alias{is_same_graph} +\title{Decide if two graphs are the same as labelled graphs} +\usage{ +is_same_graph(g1, g2, ..., use_names = TRUE) +} +\arguments{ +\item{g1, g2}{The two graphs.} + +\item{...}{These dots are for future extensions and must be empty.} + +\item{use_names}{Logical scalar, whether to match vertices by their names +if both graphs have a \code{name} vertex attribute.} +} +\value{ +A logical scalar, \code{TRUE} if the two graphs are the same. +} +\description{ +Two graphs are the same if they have the same directedness, +the same vertex set and the same edge set. +Unlike \code{\link[=identical_graphs]{identical_graphs()}}, the order in which vertices and edges are +stored is ignored, +and unlike \code{\link[=isomorphic]{isomorphic()}}, vertices are not relabelled. +} +\details{ +If \code{use_names} is \code{TRUE} and both graphs have a \code{name} vertex attribute, +vertices are matched by name, so the two graphs may store their vertices +in a different order. +Vertex names must be unique in both graphs in this case. +Otherwise, vertices are matched by their IDs. + +Edges are compared as a multiset: +the order of the edges and, in undirected graphs, the order of the +endpoints of an edge do not matter, +but the multiplicity of each edge does. + +Graph, vertex and edge attributes other than \code{name} are not compared. +} +\section{Related documentation in the C library}{ +\href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_is_same_graph}{\code{is_same_graph()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Isomorphism.html#igraph_permute_vertices}{\code{permute_vertices()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}} +} + +\examples{ +# Same edges, stored in a different order +g1 <- make_graph(c(1, 2, 2, 3, 3, 1), directed = FALSE) +g2 <- make_graph(c(3, 2, 1, 3, 2, 1), directed = FALSE) +identical_graphs(g1, g2) +is_same_graph(g1, g2) + +# Same named graph, vertices stored in a different order +edges <- data.frame(from = c("a", "b"), to = c("b", "c")) +g3 <- graph_from_data_frame(edges, vertices = c("a", "b", "c")) +g4 <- graph_from_data_frame(edges, vertices = c("c", "b", "a")) +identical_graphs(g3, g4) +is_same_graph(g3, g4) +is_same_graph(g3, g4, use_names = FALSE) + +# Isomorphic, but not the same +star1 <- make_star(4, mode = "undirected") +star2 <- make_graph(c(2, 1, 2, 3, 2, 4), directed = FALSE) +is_same_graph(star1, star2) +isomorphic(star1, star2) +} +\seealso{ +\code{\link[=identical_graphs]{identical_graphs()}} for comparing the internal representation, +\code{\link[=isomorphic]{isomorphic()}} for comparing graphs up to relabelling of the vertices. +} From 5b700d547ec11f49a0b0e51671c08bc20f966202 Mon Sep 17 00:00:00 2001 From: David Schoch Date: Thu, 1 Oct 2026 12:40:57 +0200 Subject: [PATCH 4/9] docs: Address review of `is_same_graph()`; warn when only one graph is named Clarify how `use_names` identifies vertices, drop the term "multiset", contrast `identical_graphs()`, `is_same_graph()` and `isomorphic()`, use the example from the C docs and remove the obsolete `@cdocs` tag. `is_same_graph()` now warns when `use_names = TRUE` but only one graph has vertex names. Co-Authored-By: Claude Opus 5.5 (1M context) --- R/iterators.R | 77 +++++++++++++++++++----------- tests/testthat/_snaps/iterators.md | 9 ++++ tests/testthat/test-iterators.R | 6 ++- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/R/iterators.R b/R/iterators.R index faee3b048ed..6d0d886f32a 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -129,61 +129,80 @@ identical_graphs <- function( #' Decide if two graphs are the same as labelled graphs #' #' @description -#' Two graphs are the same if they have the same directedness, -#' the same vertex set and the same edge set. +#' Two graphs are the same as labelled graphs if they have the same +#' directedness, the same vertices and the same edges. #' Unlike [identical_graphs()], the order in which vertices and edges are -#' stored is ignored, -#' and unlike [isomorphic()], vertices are not relabelled. +#' stored does not matter. +#' Unlike [isomorphic()], vertices are not relabelled. #' #' @details +#' Vertices can be identified by their index or by their name, +#' and `use_names` controls which one is used. #' If `use_names` is `TRUE` and both graphs have a `name` vertex attribute, -#' vertices are matched by name, so the two graphs may store their vertices -#' in a different order. +#' a vertex of `g1` corresponds to the vertex with the same name in `g2`, +#' wherever it is stored. #' Vertex names must be unique in both graphs in this case. -#' Otherwise, vertices are matched by their IDs. +#' Otherwise, the \eqn{i}-th vertex of `g1` corresponds to the +#' \eqn{i}-th vertex of `g2`. #' -#' Edges are compared as a multiset: -#' the order of the edges and, in undirected graphs, the order of the -#' endpoints of an edge do not matter, -#' but the multiplicity of each edge does. +#' Two graphs have the same edges if every edge occurs equally often in both. +#' The order of the edges does not matter, +#' and neither does the order of the two endpoints of an undirected edge. #' #' Graph, vertex and edge attributes other than `name` are not compared. #' +#' The three ways of comparing graphs answer different questions: +#' - [identical_graphs()]: do the two objects store exactly the same data, +#' in the same order? +#' This is mostly useful in tests, +#' or before transferring edge attributes from one graph to the other by +#' position. +#' - `is_same_graph()`: do the same vertices have the same connections? +#' - [isomorphic()]: is there any way to relabel the vertices of one graph +#' so that it becomes the same as the other? +#' #' @param g1,g2 The two graphs. #' @inheritParams rlang::args_dots_empty -#' @param use_names Logical scalar, whether to match vertices by their names -#' if both graphs have a `name` vertex attribute. +#' @param use_names Logical scalar, whether to identify vertices by their +#' names if both graphs have a `name` vertex attribute. #' @return A logical scalar, `TRUE` if the two graphs are the same. #' @seealso [identical_graphs()] for comparing the internal representation, #' [isomorphic()] for comparing graphs up to relabelling of the vertices. -#' @cdocs igraph_is_same_graph #' @export #' @examples -#' # Same edges, stored in a different order -#' g1 <- make_graph(c(1, 2, 2, 3, 3, 1), directed = FALSE) -#' g2 <- make_graph(c(3, 2, 1, 3, 2, 1), directed = FALSE) +#' # 1-2, 3-2 and 2-3, 1-2 only differ in the order of the edges and of +#' # their endpoints, so they are the same graph. +#' g1 <- make_graph(c(1, 2, 3, 2), directed = FALSE) +#' g2 <- make_graph(c(2, 3, 1, 2), directed = FALSE) #' identical_graphs(g1, g2) #' is_same_graph(g1, g2) #' -#' # Same named graph, vertices stored in a different order +#' # 1-3, 2-3 is isomorphic to them, but not the same: +#' # it contains the edge 1-3, which g1 does not. +#' g3 <- make_graph(c(1, 3, 2, 3), directed = FALSE) +#' is_same_graph(g1, g3) +#' isomorphic(g1, g3) +#' +#' # Named vertices stored in a different order #' edges <- data.frame(from = c("a", "b"), to = c("b", "c")) -#' g3 <- graph_from_data_frame(edges, vertices = c("a", "b", "c")) -#' g4 <- graph_from_data_frame(edges, vertices = c("c", "b", "a")) -#' identical_graphs(g3, g4) -#' is_same_graph(g3, g4) -#' is_same_graph(g3, g4, use_names = FALSE) -#' -#' # Isomorphic, but not the same -#' star1 <- make_star(4, mode = "undirected") -#' star2 <- make_graph(c(2, 1, 2, 3, 2, 4), directed = FALSE) -#' is_same_graph(star1, star2) -#' isomorphic(star1, star2) +#' g4 <- graph_from_data_frame(edges, vertices = c("a", "b", "c")) +#' g5 <- graph_from_data_frame(edges, vertices = c("c", "b", "a")) +#' identical_graphs(g4, g5) +#' is_same_graph(g4, g5) +#' is_same_graph(g4, g5, use_names = FALSE) is_same_graph <- function(g1, g2, ..., use_names = TRUE) { check_dots_empty() ensure_igraph(g1) ensure_igraph(g2) check_bool(use_names) + if (use_names && xor(is_named(g1), is_named(g2))) { + cli::cli_warn(c( + "Only one of the graphs has vertex names, matching vertices by their IDs.", + i = "Use {.code use_names = FALSE} to silence this warning." + )) + } + if (use_names && is_named(g1) && is_named(g2)) { names1 <- V(g1)$name names2 <- V(g2)$name diff --git a/tests/testthat/_snaps/iterators.md b/tests/testthat/_snaps/iterators.md index b28b4f615be..584c7773b93 100644 --- a/tests/testthat/_snaps/iterators.md +++ b/tests/testthat/_snaps/iterators.md @@ -1,3 +1,12 @@ +# is_same_graph() uses IDs unless both graphs are named + + Code + res <- is_same_graph(g1, g2) + Condition + Warning: + Only one of the graphs has vertex names, matching vertices by their IDs. + i Use `use_names = FALSE` to silence this warning. + # is_same_graph() errors Code diff --git a/tests/testthat/test-iterators.R b/tests/testthat/test-iterators.R index a3a5c0aabf0..92e0bb5e49a 100644 --- a/tests/testthat/test-iterators.R +++ b/tests/testthat/test-iterators.R @@ -266,7 +266,11 @@ test_that("is_same_graph() compares vertex name sets", { test_that("is_same_graph() uses IDs unless both graphs are named", { g1 <- make_ring(3) g2 <- set_vertex_attr(make_ring(3), "name", value = c("c", "b", "a")) - expect_true(is_same_graph(g1, g2)) + + expect_snapshot(res <- is_same_graph(g1, g2)) + expect_true(res) + expect_true(is_same_graph(g1, g2, use_names = FALSE)) + expect_no_warning(is_same_graph(g1, g1)) }) test_that("is_same_graph() ignores attributes other than names", { From 6d35327d060d4b977880e5f1acc6b75caa9cf5e5 Mon Sep 17 00:00:00 2001 From: schochastics Date: Thu, 1 Oct 2026 10:50:09 +0000 Subject: [PATCH 5/9] chore: Auto-update from GitHub Actions Run: https://github.com/igraph/rigraph/actions/runs/36850665226 --- man/is_same_graph.Rd | 71 +++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/man/is_same_graph.Rd b/man/is_same_graph.Rd index a62d7396255..52139597a6f 100644 --- a/man/is_same_graph.Rd +++ b/man/is_same_graph.Rd @@ -11,57 +11,72 @@ is_same_graph(g1, g2, ..., use_names = TRUE) \item{...}{These dots are for future extensions and must be empty.} -\item{use_names}{Logical scalar, whether to match vertices by their names -if both graphs have a \code{name} vertex attribute.} +\item{use_names}{Logical scalar, whether to identify vertices by their +names if both graphs have a \code{name} vertex attribute.} } \value{ A logical scalar, \code{TRUE} if the two graphs are the same. } \description{ -Two graphs are the same if they have the same directedness, -the same vertex set and the same edge set. +Two graphs are the same as labelled graphs if they have the same +directedness, the same vertices and the same edges. Unlike \code{\link[=identical_graphs]{identical_graphs()}}, the order in which vertices and edges are -stored is ignored, -and unlike \code{\link[=isomorphic]{isomorphic()}}, vertices are not relabelled. +stored does not matter. +Unlike \code{\link[=isomorphic]{isomorphic()}}, vertices are not relabelled. } \details{ +Vertices can be identified by their index or by their name, +and \code{use_names} controls which one is used. If \code{use_names} is \code{TRUE} and both graphs have a \code{name} vertex attribute, -vertices are matched by name, so the two graphs may store their vertices -in a different order. +a vertex of \code{g1} corresponds to the vertex with the same name in \code{g2}, +wherever it is stored. Vertex names must be unique in both graphs in this case. -Otherwise, vertices are matched by their IDs. +Otherwise, the \eqn{i}-th vertex of \code{g1} corresponds to the +\eqn{i}-th vertex of \code{g2}. -Edges are compared as a multiset: -the order of the edges and, in undirected graphs, the order of the -endpoints of an edge do not matter, -but the multiplicity of each edge does. +Two graphs have the same edges if every edge occurs equally often in both. +The order of the edges does not matter, +and neither does the order of the two endpoints of an undirected edge. Graph, vertex and edge attributes other than \code{name} are not compared. + +The three ways of comparing graphs answer different questions: +\itemize{ +\item \code{\link[=identical_graphs]{identical_graphs()}}: do the two objects store exactly the same data, +in the same order? +This is mostly useful in tests, +or before transferring edge attributes from one graph to the other by +position. +\item \code{is_same_graph()}: do the same vertices have the same connections? +\item \code{\link[=isomorphic]{isomorphic()}}: is there any way to relabel the vertices of one graph +so that it becomes the same as the other? +} } \section{Related documentation in the C library}{ \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_is_same_graph}{\code{is_same_graph()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Isomorphism.html#igraph_permute_vertices}{\code{permute_vertices()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}} } \examples{ -# Same edges, stored in a different order -g1 <- make_graph(c(1, 2, 2, 3, 3, 1), directed = FALSE) -g2 <- make_graph(c(3, 2, 1, 3, 2, 1), directed = FALSE) +# 1-2, 3-2 and 2-3, 1-2 only differ in the order of the edges and of +# their endpoints, so they are the same graph. +g1 <- make_graph(c(1, 2, 3, 2), directed = FALSE) +g2 <- make_graph(c(2, 3, 1, 2), directed = FALSE) identical_graphs(g1, g2) is_same_graph(g1, g2) -# Same named graph, vertices stored in a different order -edges <- data.frame(from = c("a", "b"), to = c("b", "c")) -g3 <- graph_from_data_frame(edges, vertices = c("a", "b", "c")) -g4 <- graph_from_data_frame(edges, vertices = c("c", "b", "a")) -identical_graphs(g3, g4) -is_same_graph(g3, g4) -is_same_graph(g3, g4, use_names = FALSE) +# 1-3, 2-3 is isomorphic to them, but not the same: +# it contains the edge 1-3, which g1 does not. +g3 <- make_graph(c(1, 3, 2, 3), directed = FALSE) +is_same_graph(g1, g3) +isomorphic(g1, g3) -# Isomorphic, but not the same -star1 <- make_star(4, mode = "undirected") -star2 <- make_graph(c(2, 1, 2, 3, 2, 4), directed = FALSE) -is_same_graph(star1, star2) -isomorphic(star1, star2) +# Named vertices stored in a different order +edges <- data.frame(from = c("a", "b"), to = c("b", "c")) +g4 <- graph_from_data_frame(edges, vertices = c("a", "b", "c")) +g5 <- graph_from_data_frame(edges, vertices = c("c", "b", "a")) +identical_graphs(g4, g5) +is_same_graph(g4, g5) +is_same_graph(g4, g5, use_names = FALSE) } \seealso{ \code{\link[=identical_graphs]{identical_graphs()}} for comparing the internal representation, From 4f3964518774b62d39675bdfb57a668505807665 Mon Sep 17 00:00:00 2001 From: David Schoch Date: Thu, 1 Oct 2026 13:07:09 +0200 Subject: [PATCH 6/9] docs: add canonical form example and strictness explanation --- R/iterators.R | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/R/iterators.R b/R/iterators.R index 6d0d886f32a..fe2e94b2932 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -161,6 +161,11 @@ identical_graphs <- function( #' - [isomorphic()]: is there any way to relabel the vertices of one graph #' so that it becomes the same as the other? #' +#' Each check is stricter than the next one: +#' identical graphs are always the same, +#' and graphs that are the same are always isomorphic, +#' but not the other way round. +#' #' @param g1,g2 The two graphs. #' @inheritParams rlang::args_dots_empty #' @param use_names Logical scalar, whether to identify vertices by their @@ -183,6 +188,13 @@ identical_graphs <- function( #' is_same_graph(g1, g3) #' isomorphic(g1, g3) #' +#' # After bringing both graphs into canonical form, +#' # is_same_graph() tests for isomorphism. +#' # For named graphs, use `use_names = FALSE`: +#' # matching vertices by name would undo the reordering. +#' canon <- function(g) permute(g, canonical_permutation(g)$labeling) +#' is_same_graph(canon(g1), canon(g3)) +#' #' # Named vertices stored in a different order #' edges <- data.frame(from = c("a", "b"), to = c("b", "c")) #' g4 <- graph_from_data_frame(edges, vertices = c("a", "b", "c")) From 843c3c49bd8635c6ae35feb9f708b1dc374fcac6 Mon Sep 17 00:00:00 2001 From: schochastics Date: Thu, 1 Oct 2026 11:17:34 +0000 Subject: [PATCH 7/9] chore: Auto-update from GitHub Actions Run: https://github.com/igraph/rigraph/actions/runs/36853374759 --- man/is_same_graph.Rd | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/man/is_same_graph.Rd b/man/is_same_graph.Rd index 52139597a6f..9ba34e2de88 100644 --- a/man/is_same_graph.Rd +++ b/man/is_same_graph.Rd @@ -51,6 +51,11 @@ position. \item \code{\link[=isomorphic]{isomorphic()}}: is there any way to relabel the vertices of one graph so that it becomes the same as the other? } + +Each check is stricter than the next one: +identical graphs are always the same, +and graphs that are the same are always isomorphic, +but not the other way round. } \section{Related documentation in the C library}{ \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_is_same_graph}{\code{is_same_graph()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Isomorphism.html#igraph_permute_vertices}{\code{permute_vertices()}}, \href{https://igraph.org/c/html/0.10.17/igraph-Basic.html#igraph_vcount}{\code{vcount()}} @@ -70,6 +75,13 @@ g3 <- make_graph(c(1, 3, 2, 3), directed = FALSE) is_same_graph(g1, g3) isomorphic(g1, g3) +# After bringing both graphs into canonical form, +# is_same_graph() tests for isomorphism. +# For named graphs, use `use_names = FALSE`: +# matching vertices by name would undo the reordering. +canon <- function(g) permute(g, canonical_permutation(g)$labeling) +is_same_graph(canon(g1), canon(g3)) + # Named vertices stored in a different order edges <- data.frame(from = c("a", "b"), to = c("b", "c")) g4 <- graph_from_data_frame(edges, vertices = c("a", "b", "c")) From df44d423532219c7cc646db7d3ce87699e349db9 Mon Sep 17 00:00:00 2001 From: David Schoch Date: Thu, 1 Oct 2026 13:20:23 +0200 Subject: [PATCH 8/9] feat: Add `vertex_attrs` to `is_same_graph()`; document canonical forms `is_same_graph()` gains `vertex_attrs`, a character vector of vertex attributes that must also be identical after matching up the vertices. Attributes missing from either graph are an error. The docs add an example that uses `canonical_permutation()` to test for isomorphism, and state that identical, same and isomorphic are increasingly loose checks. Co-Authored-By: Claude Opus 5.5 (1M context) --- R/iterators.R | 44 ++++++++++++++++++++++++++-- tests/testthat/_snaps/iterators.md | 23 +++++++++++++++ tests/testthat/test-iterators.R | 47 ++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/R/iterators.R b/R/iterators.R index fe2e94b2932..ae7da6d1cd3 100644 --- a/R/iterators.R +++ b/R/iterators.R @@ -149,7 +149,12 @@ identical_graphs <- function( #' The order of the edges does not matter, #' and neither does the order of the two endpoints of an undirected edge. #' -#' Graph, vertex and edge attributes other than `name` are not compared. +#' Vertex attributes listed in `vertex_attrs` must also be identical, +#' after matching up the vertices. +#' They are compared exactly, with [identical()], +#' so for example the integer `1L` and the double `1` are different values. +#' Graph and edge attributes are never compared, +#' and neither are vertex attributes other than those in `vertex_attrs`. #' #' The three ways of comparing graphs answer different questions: #' - [identical_graphs()]: do the two objects store exactly the same data, @@ -170,6 +175,11 @@ identical_graphs <- function( #' @inheritParams rlang::args_dots_empty #' @param use_names Logical scalar, whether to identify vertices by their #' names if both graphs have a `name` vertex attribute. +#' @param vertex_attrs Character vector of vertex attribute names to compare, +#' or `NULL` to compare none. +#' Every attribute must exist in both graphs. +#' Including `"name"` is only meaningful with `use_names = FALSE`, +#' otherwise names match by construction. #' @return A logical scalar, `TRUE` if the two graphs are the same. #' @seealso [identical_graphs()] for comparing the internal representation, #' [isomorphic()] for comparing graphs up to relabelling of the vertices. @@ -202,11 +212,29 @@ identical_graphs <- function( #' identical_graphs(g4, g5) #' is_same_graph(g4, g5) #' is_same_graph(g4, g5, use_names = FALSE) -is_same_graph <- function(g1, g2, ..., use_names = TRUE) { +#' +#' # Vertex-coloured graphs: same edges, different colours +#' star <- make_star(4, mode = "undirected") +#' c1 <- set_vertex_attr(star, "color", value = c(1, 2, 2, 2)) +#' c2 <- set_vertex_attr(star, "color", value = c(1, 3, 3, 3)) +#' is_same_graph(c1, c2) +#' is_same_graph(c1, c2, vertex_attrs = "color") +is_same_graph <- function(g1, g2, ..., use_names = TRUE, vertex_attrs = NULL) { check_dots_empty() ensure_igraph(g1) ensure_igraph(g2) check_bool(use_names) + check_character(vertex_attrs, allow_na = FALSE, allow_null = TRUE) + + missing1 <- setdiff(vertex_attrs, vertex_attr_names(g1)) + missing2 <- setdiff(vertex_attrs, vertex_attr_names(g2)) + if (length(missing1) > 0 || length(missing2) > 0) { + cli::cli_abort(c( + "All {.arg vertex_attrs} must exist in both graphs.", + x = if (length(missing1) > 0) "Missing in {.arg g1}: {.val {missing1}}.", + x = if (length(missing2) > 0) "Missing in {.arg g2}: {.val {missing2}}." + )) + } if (use_names && xor(is_named(g1), is_named(g2))) { cli::cli_warn(c( @@ -230,7 +258,17 @@ is_same_graph <- function(g1, g2, ..., use_names = TRUE) { g2 <- permute(g2, match(names2, names1)) } - is_same_graph_impl(graph1 = g1, graph2 = g2) + if (!is_same_graph_impl(graph1 = g1, graph2 = g2)) { + return(FALSE) + } + + for (attr in vertex_attrs) { + if (!identical(vertex_attr(g1, attr), vertex_attr(g2, attr))) { + return(FALSE) + } + } + + TRUE } add_vses_graph_ref <- function(vses, graph) { diff --git a/tests/testthat/_snaps/iterators.md b/tests/testthat/_snaps/iterators.md index 584c7773b93..dda12b9b1df 100644 --- a/tests/testthat/_snaps/iterators.md +++ b/tests/testthat/_snaps/iterators.md @@ -33,6 +33,29 @@ Condition Error in `ensure_igraph()`: ! Must provide a graph object (provided `NULL`). + Code + is_same_graph(g, g, vertex_attrs = 1) + Condition + Error in `is_same_graph()`: + ! `vertex_attrs` must be a character vector or `NULL`, not the number 1. + Code + is_same_graph(g, g, vertex_attrs = NA_character_) + Condition + Error in `is_same_graph()`: + ! `vertex_attrs` can't contain NA values. + Code + is_same_graph(g, set_vertex_attr(g, "color", value = 1:3), vertex_attrs = "color") + Condition + Error in `is_same_graph()`: + ! All `vertex_attrs` must exist in both graphs. + x Missing in `g1`: "color". + Code + is_same_graph(g, g, vertex_attrs = c("color", "size")) + Condition + Error in `is_same_graph()`: + ! All `vertex_attrs` must exist in both graphs. + x Missing in `g1`: "color" and "size". + x Missing in `g2`: "color" and "size". # printing connected vs/es works diff --git a/tests/testthat/test-iterators.R b/tests/testthat/test-iterators.R index 92e0bb5e49a..50e8cb5a1d4 100644 --- a/tests/testthat/test-iterators.R +++ b/tests/testthat/test-iterators.R @@ -279,6 +279,45 @@ test_that("is_same_graph() ignores attributes other than names", { expect_true(is_same_graph(g1, g2)) }) +test_that("is_same_graph() compares selected vertex attributes", { + star <- make_star(4, mode = "undirected") + g1 <- set_vertex_attr(star, "color", value = c(1, 2, 2, 2)) + g1 <- set_vertex_attr(g1, "size", value = 1:4) + g2 <- set_vertex_attr(star, "color", value = c(1, 3, 3, 3)) + g2 <- set_vertex_attr(g2, "size", value = 1:4) + + expect_true(is_same_graph(g1, g2)) + expect_true(is_same_graph(g1, g2, vertex_attrs = "size")) + expect_false(is_same_graph(g1, g2, vertex_attrs = "color")) + expect_false(is_same_graph(g1, g2, vertex_attrs = c("size", "color"))) + expect_true(is_same_graph(g1, g1, vertex_attrs = c("size", "color"))) + expect_true(is_same_graph(g1, g2, vertex_attrs = character())) +}) + +test_that("is_same_graph() compares vertex attributes after matching names", { + edges <- data.frame(from = c("a", "b"), to = c("b", "c")) + vertices1 <- data.frame(name = c("a", "b", "c"), color = c(1, 2, 3)) + vertices2 <- data.frame(name = c("c", "b", "a"), color = c(3, 2, 1)) + g1 <- graph_from_data_frame(edges, directed = FALSE, vertices = vertices1) + g2 <- graph_from_data_frame(edges, directed = FALSE, vertices = vertices2) + + expect_true(is_same_graph(g1, g2, vertex_attrs = "color")) +}) + +test_that("is_same_graph() compares vertex attributes exactly", { + g1 <- set_vertex_attr(make_ring(3), "color", value = c(1, 2, 3)) + g2 <- set_vertex_attr(make_ring(3), "color", value = 1:3) + expect_false(is_same_graph(g1, g2, vertex_attrs = "color")) +}) + +test_that("is_same_graph() compares names as attributes when not matching by them", { + g1 <- set_vertex_attr(make_ring(3), "name", value = c("a", "b", "c")) + g2 <- set_vertex_attr(make_ring(3), "name", value = c("c", "b", "a")) + + expect_true(is_same_graph(g1, g2, use_names = FALSE)) + expect_false(is_same_graph(g1, g2, use_names = FALSE, vertex_attrs = "name")) +}) + test_that("is_same_graph() errors", { g <- make_ring(3) dup <- set_vertex_attr(g, "name", value = c("a", "a", "b")) @@ -288,6 +327,14 @@ test_that("is_same_graph() errors", { is_same_graph(g, g, TRUE) is_same_graph(g, g, use_names = NA) is_same_graph(g, NULL) + is_same_graph(g, g, vertex_attrs = 1) + is_same_graph(g, g, vertex_attrs = NA_character_) + is_same_graph( + g, + set_vertex_attr(g, "color", value = 1:3), + vertex_attrs = "color" + ) + is_same_graph(g, g, vertex_attrs = c("color", "size")) }) expect_true(is_same_graph(dup, dup, use_names = FALSE)) }) From 98c0d3430b97a4413f326af60a8a1bd467f7d309 Mon Sep 17 00:00:00 2001 From: schochastics Date: Thu, 1 Oct 2026 11:29:34 +0000 Subject: [PATCH 9/9] chore: Auto-update from GitHub Actions Run: https://github.com/igraph/rigraph/actions/runs/36854758087 --- man/is_same_graph.Rd | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/man/is_same_graph.Rd b/man/is_same_graph.Rd index 9ba34e2de88..d249fa77516 100644 --- a/man/is_same_graph.Rd +++ b/man/is_same_graph.Rd @@ -4,7 +4,7 @@ \alias{is_same_graph} \title{Decide if two graphs are the same as labelled graphs} \usage{ -is_same_graph(g1, g2, ..., use_names = TRUE) +is_same_graph(g1, g2, ..., use_names = TRUE, vertex_attrs = NULL) } \arguments{ \item{g1, g2}{The two graphs.} @@ -13,6 +13,12 @@ is_same_graph(g1, g2, ..., use_names = TRUE) \item{use_names}{Logical scalar, whether to identify vertices by their names if both graphs have a \code{name} vertex attribute.} + +\item{vertex_attrs}{Character vector of vertex attribute names to compare, +or \code{NULL} to compare none. +Every attribute must exist in both graphs. +Including \code{"name"} is only meaningful with \code{use_names = FALSE}, +otherwise names match by construction.} } \value{ A logical scalar, \code{TRUE} if the two graphs are the same. @@ -38,7 +44,12 @@ Two graphs have the same edges if every edge occurs equally often in both. The order of the edges does not matter, and neither does the order of the two endpoints of an undirected edge. -Graph, vertex and edge attributes other than \code{name} are not compared. +Vertex attributes listed in \code{vertex_attrs} must also be identical, +after matching up the vertices. +They are compared exactly, with \code{\link[=identical]{identical()}}, +so for example the integer \code{1L} and the double \code{1} are different values. +Graph and edge attributes are never compared, +and neither are vertex attributes other than those in \code{vertex_attrs}. The three ways of comparing graphs answer different questions: \itemize{ @@ -89,6 +100,13 @@ g5 <- graph_from_data_frame(edges, vertices = c("c", "b", "a")) identical_graphs(g4, g5) is_same_graph(g4, g5) is_same_graph(g4, g5, use_names = FALSE) + +# Vertex-coloured graphs: same edges, different colours +star <- make_star(4, mode = "undirected") +c1 <- set_vertex_attr(star, "color", value = c(1, 2, 2, 2)) +c2 <- set_vertex_attr(star, "color", value = c(1, 3, 3, 3)) +is_same_graph(c1, c2) +is_same_graph(c1, c2, vertex_attrs = "color") } \seealso{ \code{\link[=identical_graphs]{identical_graphs()}} for comparing the internal representation,