From 1b47b2659d6514522e0d171d859b1846ad9f62f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 14:08:55 +0000 Subject: [PATCH 1/2] docs: Announce the `weights = NULL` default change, with migration guidance #2677 made `weights = NULL` pick up the `weight` edge attribute in the adjacency matrix functions and in `power_centrality()`, which brings them in line with `distances()` and the rest of igraph. It is intentional and consistency is worth it, but it changes what those functions return for any graph carrying a `weight` attribute, silently and with no warning, and NEWS had it only as a Features line about retiring `attr`. The entry names every affected function, shows the before and after, and gives the migration: `weights = NA`. `power_centrality()` gets called out separately. It had no weight argument at all before #2677, so for it this is a new default rather than a renamed one. It also says what *not* to write. `weights = numeric()` gets the old behaviour from `as_adjacency_matrix()` -- `all(is.na(numeric()))` is `TRUE` -- but it is not a supported spelling, and `distances()` rejects it against `ecount()`. Recommending it would hand people something that works in one function and errors in the next. Neither spelling exists on 2.3.3, where these functions took `attr`, so the entry gives two expressions that work on both versions for code that has to span them. This edits a fledge-managed file by hand, which the header asks contributors not to do. A breaking change that needs a migration path does not fit in a generated one-line bullet, and fledge prepends new versions rather than rewriting old ones, so the section survives the next bump. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D1xpHRV7yVfgtJg4vp9P7z --- NEWS.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/NEWS.md b/NEWS.md index a24bf34615d..060e30db5b3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,74 @@ # igraph 2.3.3.9031 +## Breaking changes + +- `weights = NULL` now picks up the `weight` edge attribute in the adjacency + matrix functions and in `power_centrality()` (#906, #1137, #2677). + + This brings them in line with the rest of igraph — `distances()`, + `shortest_paths()` and friends have always read `weight` by default — but it + changes what these functions return for a graph that carries a `weight` + edge attribute, silently and without a warning: + + ```r + g <- make_ring(4) + E(g)$weight <- c(0.5, 0.7, 0.2, 0.9) + + as_adjacency_matrix(g) # was a 0/1 matrix, now carries the weights + power_centrality(g) # was -1 -1 -1 -1, now -1.211 -0.931 -0.727 -1.067 + ``` + + Affected: `as_adjacency_matrix()` and its aliases `as_adj()` and + `get.adjacency()`; `as_biadjacency_matrix()` and its aliases + `as_incidence_matrix()` and `get.incidence()`; and `power_centrality()` with + its alias `bonpow()`. `alpha_centrality()` is unchanged — it already read + `weight`. + + `power_centrality()` is the sharpest case: it had no weight argument at all + before, so this is a new default rather than a renamed one. + + **To get the old behaviour, pass `weights = NA`**, which is how igraph spells + "explicitly unweighted" everywhere: + + ```r + as_adjacency_matrix(g, weights = NA) + power_centrality(g, weights = NA) + ``` + + Do not use `weights = numeric()` for this. It happens to work in + `as_adjacency_matrix()`, because `all(is.na(numeric()))` is `TRUE`, but it is + not a supported spelling and it errors in `distances()` and the other + weighted algorithms, which check the vector against `ecount()`. + + `weights = NA` is not available on igraph 2.3.3 and earlier, where these + functions took `attr` instead. Code that has to work on both can either drop + the attribute: + + ```r + as_adjacency_matrix(delete_edge_attr(g, "weight")) + ``` + + or binarise the result: + + ```r + (as_adjacency_matrix(g) != 0) * 1 + ``` + + The deprecated `attr = NULL` also still means unweighted on both, since + igraph 2.3.3.9032 — see below. + +- `as_adjacency_matrix(attr = NULL)` and `as_biadjacency_matrix(attr = NULL)` + are unweighted again (#2842). + + The deprecation shim forwarded `attr` to `weights` unchanged, but the two + spell "unweighted" differently: `attr = NULL` was the documented way to ask + for a plain 0/1 matrix, while `weights = NULL` means the opposite. Every call + that spelled out `attr = NULL` was returning a weighted matrix. `attr = NULL` + now maps to `weights = NA`, and the deprecation warning says so. + +## Continuous integration + ## Continuous integration ### revdep2 From e60867052743ae9baf3794d831aa2e751aae67bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 14:26:11 +0000 Subject: [PATCH 2/2] feat: Any all-NA `weights` means unweighted, in `distances()` too `distances()` and `shortest_paths()` tested `length(weights) == 1 && is.na(weights)`, so a single `NA` meant unweighted but `rep(NA, ecount(g))` and `numeric()` were errors -- while meaning "unweighted" in every generated wrapper in `R/aaa-*.R`, which have always tested `all(is.na(weights))`. Both now test `all(is.na(weights))`. `all(is.na(numeric()))` is `TRUE`, so the empty vector comes along with it, and the rule is one sentence: an all-NA `weights` is unweighted. A vector in which only *some* entries are NA is still rejected, by the C core, which reports `Weights must not contain NaN values` -- R's `NA_real_` reaches C as a NaN, so `NA` and `NaN` are the same thing there. A vector of the wrong length is still the wrong length. The NEWS entry from the previous commit now says "all NA" rather than naming the single `NA`, and no longer warns people off `numeric()`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01D1xpHRV7yVfgtJg4vp9P7z --- NEWS.md | 21 ++++++++++++----- R/structural-properties.R | 16 +++++++++++-- tests/testthat/test-structural-properties.R | 25 +++++++++++++++++++++ 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/NEWS.md b/NEWS.md index 060e30db5b3..37091c212ca 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,18 +29,18 @@ `power_centrality()` is the sharpest case: it had no weight argument at all before, so this is a new default rather than a renamed one. - **To get the old behaviour, pass `weights = NA`**, which is how igraph spells - "explicitly unweighted" everywhere: + **To get the old behaviour, pass an all-NA `weights`**, which is how igraph + spells "explicitly unweighted": ```r as_adjacency_matrix(g, weights = NA) power_centrality(g, weights = NA) ``` - Do not use `weights = numeric()` for this. It happens to work in - `as_adjacency_matrix()`, because `all(is.na(numeric()))` is `TRUE`, but it is - not a supported spelling and it errors in `distances()` and the other - weighted algorithms, which check the vector against `ecount()`. + "All NA" is the rule, not the single `NA`: any vector for which + `all(is.na(weights))` holds means unweighted, so `rep(NA, ecount(g))` works + too, and so does `numeric()` — `all(is.na(numeric()))` is `TRUE`. A vector in + which only *some* entries are NA is still an error, in every function. `weights = NA` is not available on igraph 2.3.3 and earlier, where these functions took `attr` instead. Code that has to work on both can either drop @@ -59,6 +59,15 @@ The deprecated `attr = NULL` also still means unweighted on both, since igraph 2.3.3.9032 — see below. +- `distances()` and `shortest_paths()` accept any all-NA `weights`, not only a + single `NA`. + + They tested `length(weights) == 1 && is.na(weights)`, so `rep(NA, ecount(g))` + and `numeric()` were errors there while meaning "unweighted" everywhere else + in igraph — the generated wrappers in `R/aaa-*.R` have always tested + `all(is.na(weights))`. They now do too. A vector in which only some entries + are NA is still rejected, by the C core, in both. + - `as_adjacency_matrix(attr = NULL)` and `as_biadjacency_matrix(attr = NULL)` are unweighted again (#2842). diff --git a/R/structural-properties.R b/R/structural-properties.R index c2970ebc5a9..83687fb51d2 100644 --- a/R/structural-properties.R +++ b/R/structural-properties.R @@ -1532,7 +1532,13 @@ distances <- function( weights <- as.numeric(E(graph)$weight) } } else { - if (length(weights) == 1 && is.na(weights)) { + # `all(is.na())` rather than `length() == 1 && is.na()`: "unweighted" is + # spelled with any all-NA vector everywhere else in igraph -- that is what + # the generated blocks in R/aaa-*.R have always tested -- and a vector of + # NAs the right length for the graph, or a zero-length one, meant the same + # thing there and an error here. `all(is.na(numeric()))` is `TRUE`, so the + # empty vector comes along with it. + if (all(is.na(weights))) { weights <- NULL } else { weights <- as.numeric(weights) @@ -1665,7 +1671,13 @@ shortest_paths <- function( weights <- as.numeric(E(graph)$weight) } } else { - if (length(weights) == 1 && is.na(weights)) { + # `all(is.na())` rather than `length() == 1 && is.na()`: "unweighted" is + # spelled with any all-NA vector everywhere else in igraph -- that is what + # the generated blocks in R/aaa-*.R have always tested -- and a vector of + # NAs the right length for the graph, or a zero-length one, meant the same + # thing there and an error here. `all(is.na(numeric()))` is `TRUE`, so the + # empty vector comes along with it. + if (all(is.na(weights))) { weights <- NULL } else { weights <- as.numeric(weights) diff --git a/tests/testthat/test-structural-properties.R b/tests/testthat/test-structural-properties.R index 6c4e5752024..74bdb4b02b1 100644 --- a/tests/testthat/test-structural-properties.R +++ b/tests/testthat/test-structural-properties.R @@ -462,6 +462,31 @@ test_that("shortest_paths() works", { expect_true(s1$vpath %in% all1) }) +test_that("any all-NA `weights` means unweighted", { + # "Unweighted" is spelled with an all-NA vector everywhere in igraph, which + # is what the generated wrappers in R/aaa-*.R test. `distances()` and + # `shortest_paths()` tested `length(weights) == 1 && is.na(weights)`, so a + # vector of NAs the right length for the graph -- or a zero-length one -- + # was an error here and meant "unweighted" there. + g <- make_ring(4) + E(g)$weight <- c(0.5, 0.7, 0.2, 0.9) + + # Weighted, the short way round is 0.9 + 0.2 via vertex 4; unweighted it is 2. + expect_equal(distances(g)[1, 3], 1.1) + for (w in list(NA, rep(NA, 4), rep(NA_real_, 4), NaN, numeric())) { + expect_equal(distances(g, weights = w)[1, 3], 2) + expect_equal( + as.integer(shortest_paths(g, 1, 3, weights = w)$vpath[[1]]), + c(1L, 2L, 3L) + ) + } + + # Only *some* of them NA is still an error, from the C core. + expect_error(distances(g, weights = c(1, NA, 1, 1)), "NaN") + # And a vector of the wrong length is still the wrong length. + expect_error(distances(g, weights = c(1, 2)), "length") +}) + test_that("shortest_paths() can handle negative weights", { g <- make_tree(7) E(g)$weight <- -1