fix: simplify() honours edge.attr.comb on an already-simple graph again - #2843
Merged
Conversation
… again `edge.attr.comb` does not only combine attributes across merged edges, it decides which survive at all: an attribute the combination list does not name is dropped even when every group has exactly one member. The default list ends in `"ignore"`, so `simplify(g)` keeps `weight` and drops everything else, and `edge.attr.comb = "ignore"` leaves no edge attributes at all. #1981 opened `simplify()` with `if (is_simple(graph)) return(graph)`, on the grounds that a graph with no loops and no multiple edges has nothing for `simplify_impl()` to remove. That is true of its structure and false of its attributes, so a simple graph came back with every attribute intact -- silently, and regardless of what `edge.attr.comb` asked for. Guarding the short-circuit on the graph having no edge attributes does not fix it, which is the part worth writing down. `is_simple()` populates the C core's property cache, and `simplify.c` has a cache fast path of its own that returns early without applying `edge_comb` once the cache says there is nothing to remove. So merely asking whether a graph is simple changes what simplifying it does. 2.3.3 behaves that way too; opening `simplify()` with `is_simple()` made the cache-warm answer the only answer. The check therefore moves to the call site that wants it. `graph_from_literal_i()` is what #824 and #1981 were about, and it simplifies a graph it has only just built from the formula, before any attribute is set on it -- so skipping `simplify()` there is unobservable beyond the edge order, which is exactly the property #824 asked for. Everywhere else `simplify()` goes through `simplify_impl()` as it always did, which also restores the canonical edge order for direct callers. The residual cache sensitivity is pinned by a test so that fixing it upstream is noticed here; it belongs in src/vendor/cigraph/src/operators/simplify.c. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1xpHRV7yVfgtJg4vp9P7z
Contributor
|
This is how benchmark results would change (along with a 95% confidence interval in relative change) if 15e906e is merged into main:
|
This was referenced Aug 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
edge.attr.combdoes not only combine attributes across merged edges — it decides which survive at all. An edge attribute the combination list does not name is dropped even when every group has exactly one member. The default list ends in"ignore", sosimplify(g)is meant to keepweightand drop everything else, andedge.attr.comb = "ignore"is meant to leave no edge attributes at all.#1981 opened
simplify()withif (is_simple(graph)) return(graph), on the grounds that a graph with no loops and no multiple edges has nothing forsimplify_impl()to remove. That is true of its structure and false of its attributes.On
mainboth returnc("weight", "foo")— silently, with no warning.Guarding on "has no edge attributes" does not fix it
This is the part worth writing down, because it is the obvious fix and it does not work:
is_simple()populates the C core's property cache, andsimplify.chas a cache fast path of its own that returns early — without applyingedge_comb— once the cache says there is nothing to remove. So merely asking whether a graph is simple changes what simplifying it does. Verified against CRAN 2.3.3:2.3.3 has this divergence too. What #1981 changed was making the cache-warm answer the only answer, by warming the cache on every call.
The fix
The check moves to the call site that wants it.
graph_from_literal_i()is what #824 and #1981 were about, and it simplifies a graph it has only just built from the formula, before any attribute is set on it — so skippingsimplify()there is unobservable beyond the edge order, which is exactly the property #824 asked for.Everywhere else
simplify()goes throughsimplify_impl()as it always did. That restoresedge.attr.combfor every caller, and restores the canonical edge order for direct callers:Not fixed here
The residual cache sensitivity is a C-core issue and belongs in
src/vendor/cigraph/src/operators/simplify.c, upstream: the fast path taken when the cache says there is nothing to remove should still applyedge_comb. A test pins the current behaviour so that fixing it upstream is noticed here rather than surfacing as a mystery diff.Where this came from
Triaging the reverse-dependency failures in #2646. Two of the
[NEEDS TRIAGE]packages — archeofrag and gemtc — callsimplify()directly on already-simple, attribute-free graphs and broke on the edge-order half of this. Both were reproduced against 2.3.3 and againstmain, and both are restored by this change. (Both also turned out to have genuine bugs of their own that this merely exposed, so they are being reported upstream regardless.)Testing
Full
testthat::test_local()green apart fromtest-foreign.R:69, which downloads fromgithub.com/igraph/graphsdband has no network here.Three new tests in
test-operators.R:edge.attr.combapplied to an already-simple graph for the default list,"ignore", and an explicit per-attribute list; the cache sensitivity above; andgraph_from_literal()'s edge order, both when the formula needs simplifying and when it does not.🤖 Generated with Claude Code
https://claude.ai/code/session_01D1xpHRV7yVfgtJg4vp9P7z
Generated by Claude Code