feat: retire attr in favor of weights for adjacency matrix functions#2677
feat: retire attr in favor of weights for adjacency matrix functions#2677schochastics wants to merge 11 commits into
attr in favor of weights for adjacency matrix functions#2677Conversation
Introduces a `weights` argument on `as_adjacency_matrix()` / `as_adj()` and `as_biadjacency_matrix()` following the standard rigraph weight convention (`NULL` auto-picks `weight`, `NA` forces unweighted, numeric vector is used directly, character scalar names an attribute). The legacy `attr` argument is now soft-deprecated via `lifecycle::deprecate_warn()`. Centrality functions that previously routed weights through a temporary `weight` edge attribute have been simplified to use the new vector path directly: `alpha_centrality()` drops the graph-mutation workaround (#910), and `power_centrality()` gains a `weights` argument (#903). This also structurally fixes the bug class behind #915. Closes: #906, #903, #910, partially #1137
|
To me this looks good. @krlmlr do we run revdeps on each potentially breaking PR? If so, this definitely needs it 😬 |
krlmlr
left a comment
There was a problem hiding this comment.
I'm not sure I like it. Could we keep two arguments, attr and weight, and resolve them in unison?
The previous commit hand-edited four Rd files for the functions whose signatures changed. The package's CI regenerates `man/*.Rd` from the roxygen comments in `R/`, so committing those edits adds nothing — the source-of-truth roxygen blocks are already updated in the same commit. Removing them keeps the diff minimal and avoids divergence from the canonical generator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
You mean no deprecation warning for attr and silently forward attr to weight? |
krlmlr
left a comment
There was a problem hiding this comment.
Took another look. Let's discuss.
| weights = NULL, | ||
| attr = deprecated(), | ||
| names = TRUE, | ||
| sparse = FALSE |
There was a problem hiding this comment.
How about adding an ellipsis after types, while we're at it? Adding arguments mid-signature calls for trouble.
There was a problem hiding this comment.
I add the ellipses but I think this can also lead some (soft) breaking changes
…nd extract edge_attr_as_weights helper; add `...` for future extensions to as_adjacency_matrix and as_biadjacency_matrix
krlmlr
left a comment
There was a problem hiding this comment.
Thanks, great! I skimmed the patch, let's accumulate breaking PRs (by merging into main) and run revdepchecks in bulk.
| sparse = igraph_opt("sparsematrices") | ||
| ) { | ||
| ensure_igraph(graph) | ||
| rlang::check_dots_empty() |
There was a problem hiding this comment.
It is possible to implement this in a non-breaking way. I did this for dm 1.0.0, the idea is to detect non-empty dots and create a function that matches the args according to the old signature. We can do this depending on revdepcheck results (maybe faster now), or adopt this practice as a rule (allows us to easily add dots without breaking downstream other than soft-deprecation warnings).
There was a problem hiding this comment.
I like your suggestion because I had some headaches over this. Let me try and come up with something
There was a problem hiding this comment.
hmm @krlmlr Claude tells me that what we are doing here is what dm 1.0.0 does
There was a problem hiding this comment.
We had a substantial interface change for dm_filter() , I think this is the most relevant one. We don't have to do it exactly like with that function, but a design along these lines might be useful.
There was a problem hiding this comment.
I used dm_filter as inspiration. There is now a function that handles this similarly here
There was a problem hiding this comment.
@schochastics @krlmlr side-note, this would be a very nice short blog post. Adding the empty dot checks in a non breaking way.
…d as_biadjacency_matrix() via `...` with soft deprecation
krlmlr
left a comment
There was a problem hiding this comment.
Thanks. I had hoped we could recover other unnamed arguments as well. I wonder if we can create a very generic helper function that accepts the old signature and returns a list of values from a call with the new signature that we can then deconstruct again. That function would also issue lifecycle warnings. With such a function, we could add dots everywhere in one swoop: once the helper exists, Claude can embed it everywhere.
as_biadjacency_matrix <- function(graph, types, ..., weights, attrs, names, sparse) {
args <- handle_args(
graph, types, ..., weights, attrs, names, sparse,
.old_signature = list(graph, types, attrs, names, sparse)
)
graph <- args$graph
types <- args$types
...
}We could also create a dedicated helper for each such function (deterministic code generation) to simplify debugging and enable custom changes in the helper code.
as_biadjacency_matrix <- function(graph, types, ..., weights, attrs, names, sparse) {
args <- handle_args_as_biadjacency_matrix(
graph, types, ..., weights, attrs, names, sparse
)
graph <- args$graph
types <- args$types
...
}If we do this, we can do a separate PR before this. Well in scope for 3.0.0 if we agree on that; an alternative is a hard break and dealing with the revdepchecks.
| ensure_igraph(graph) | ||
|
|
||
| user_env <- rlang::caller_env() | ||
| recovered_attr <- recover_positional_attr( |
There was a problem hiding this comment.
How does this code handle as_adjacency_matrix(graph, type, attr, names = letters) ?
maelle
left a comment
There was a problem hiding this comment.
a few (most off PR topic?) comments, thank you!!
| nodes <- as_igraph_vs(graph, nodes) | ||
| if (sparse) { | ||
| res <- bonpow.sparse(graph, nodes, loops, exponent, rescale, tol) | ||
| res <- bonpow.sparse( |
There was a problem hiding this comment.
since bonpow.sparse() is not exported, could we give it a better name? Or do we want to keep dot names for unexported functions?
| @@ -1966,25 +1986,7 @@ alpha.centrality.dense <- function( | |||
| exo <- rep(exo, length.out = vcount(graph)) | |||
There was a problem hiding this comment.
What does exo mean? In my French-speaking head it's an abbreviation of exercise so I'm confused.
| } | ||
|
|
||
| d <- t(as_adjacency_matrix(graph, attr = attr, sparse = FALSE)) | ||
| d <- t(as_adjacency_matrix(graph, weights = weights, sparse = FALSE)) |
| } | ||
|
|
||
| M <- Matrix::t(as_adjacency_matrix(graph, attr = attr, sparse = TRUE)) | ||
| M <- Matrix::t(as_adjacency_matrix(graph, weights = weights, sparse = TRUE)) |
There was a problem hiding this comment.
or are these single letters sort of convention?
| # | ||
| ################################################################### | ||
|
|
||
| # Resolve the user-facing `weights` argument into a numeric vector |
There was a problem hiding this comment.
is this now valid in all of igraph?
| call = call | ||
| ) | ||
| } | ||
| if (!weights %in% edge_attr_names(graph)) { |
There was a problem hiding this comment.
I can't wait for %notin% to be old enough for us to use it.
| return(edge_attr_as_weights(graph, weights, call)) | ||
| } | ||
|
|
||
| if (!is.numeric(weights) && !is.logical(weights)) { |
There was a problem hiding this comment.
couldn't we check the types earlier?
| # would otherwise hard-fail via rlang::check_dots_empty(). | ||
| # | ||
| # Dispatcher is dm-style: discriminate on whether dots are named or not | ||
| # (cynkra/dm R/filter-dm.R, dm_filter_api1()). Named extras stay an error |
There was a problem hiding this comment.
better to use a GitHub permalink?
| #' also allowed. The reason for the difference is that the `Matrix` | ||
| #' package does not support character sparse matrices yet. | ||
| #' @param ... These dots are for future extensions and must be empty. | ||
| #' @param weights One of the following: |
There was a problem hiding this comment.
so this will be inherited all over the place?
| #' is included in the matrix. | ||
| #' @param attr `r lifecycle::badge("deprecated")` Use `weights` instead. If | ||
| #' supplied, the value is forwarded to `weights` as a character edge | ||
| #' attribute name. |
There was a problem hiding this comment.
but this will be removed in future versions
Summary
Resolves milestone #30 — retire
attrforweightin one PR. Bundles the API change with the dependent centrality function migrations so no temporary translation shims are needed.Closes: #906, #903, #910 — partially closes #1137 (the sparse path already worked; this PR audits and adds parity tests).
Changes
1.
as_adjacency_matrix()/as_adj()(#906)New
weightsargument follows the standard rigraph weight convention:weights =NULL(default)weightedge attribute if present, else unweightedNAweightattribute)<numeric vector>ecount())<character scalar>The legacy
attrargument is nowlifecycle::deprecate_warn()-ed againstigraph 3.0.0. Whenattris supplied it is forwarded intoweights, so all existing user code keeps working with a soft warning.2.
as_biadjacency_matrix()Same migration mirrored for the bipartite sibling. The C binding
R_igraph_get_biadjacencydoesn't have a weights slot, so the four-mode resolution happens entirely in R via the helper functions.3.
alpha_centrality()(#910)The previous implementation laundered a numeric
weightsvector through aset_edge_attr(graph, "weight", value = ...)mutation just to use the oldattr =API. That workaround is gone — both the dense and sparse helpers now passweightsstraight toas_adjacency_matrix(). This also structurally fixes the bug class behind #915 (custom weight attribute names other than"weight").4.
power_centrality()(#903)Gained a
weightsargument. Previously it silently dropped edge weights, returning identical results for a weighted graph and its unweighted version. Weights now thread through bothbonpow.dense()andbonpow.sparse(). Drive-by: the sparse path useddegree(graph, mode = "out")where the dense path usedrowSums(d); these agree only for unweighted graphs. The sparse path now usesMatrix::rowSums(d)for consistency.5. Internal call site
R/indexing.R:269(the[operator on graphs) now passesweights = if (is.null(attr)) NA else attrinstead ofattr = attr— preserving its existing "unweighted by default" semantics rather than tripping the new auto-pickup.6. Tests
attr =test calls toweights =(test-conversion.R, test-incidence.R, test-structural-properties.R, plus the matching snapshot).weightsmodes, length/type errors, dense/sparse parity for arbitrary numeric vectors, and the new deprecation warnings (with snapshots).power_centrality()reprex from power_centrality() should support edge weights #903 (weighted graph equivalent to unweighted graph with parallel edges).weightsargument is a string other than "weight" #915 (custom weight attribute name).Soft-breaking change
as_adjacency_matrix(g)on a graph that already has aweightedge attribute now returns a weighted matrix by default. Old behavior: passweights = NA. This brings the function into line with the rest of rigraph (shortest_paths(),cluster_leiden(), etc.).Out of scope (followups)
get.adjacency(),get.incidence(),as_incidence_matrix()are intentionally not touched. They forwardattr = attrto the migrated functions, so calling them now emits the outer "function is deprecated" warning plus the newattr =warning. That doubling is acceptable; users are supposed to be migrating off these anyway.lifecycle::deprecate_warn()→lifecycle::deprecate_stop()forattr =after one release cycle on CRAN.Test plan
devtools::test()— 0 failures across the full suite (NOT_CRAN=true).🤖 Generated with Claude Code