Skip to content

Add mycielski operator#177

Open
mcognetta wants to merge 5 commits intoJuliaGraphs:masterfrom
mcognetta:mycielski
Open

Add mycielski operator#177
mcognetta wants to merge 5 commits intoJuliaGraphs:masterfrom
mcognetta:mycielski

Conversation

@mcognetta
Copy link
Contributor

This adds the Mycielski operator (wiki), a unary graph operator that increases the chromatic number of a graph by 1.

This can be found in other graph libraries: NetworkX, SageMath.

I wasn't sure if it was best to put it here or in the generators module (which is how it is implemented in NetworkX/SageMath). Happy to move it if necessary.

Copy link
Member

@aurorarossi aurorarossi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! I'm not a maintainer, I'm just contributing. I wrote some suggestions and comments.

src/operators.jl Outdated
"""
@traitfn function mycielski(g::AbstractGraph::(!IsDirected); iterations = 1)
ref = g
out = deepcopy(g)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that deepcopy(g) could slow down and uses a lot of memory especially for big graphs.

Copy link
Contributor Author

@mcognetta mcognetta Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These operators are not supposed to mutate the input right? I see some have ! variants, but not all. Perhaps there should be a mycielski! and mycielski(g; iterations) = mycielski!(deepcopy(g); iterations=iterations) method. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree with you. I was only thinking about the case in which we can mutate the input.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also support having both mycielski! and mycielski. We currently have something similar for transitiveclosure and transitiveclosure! at the moment.

@mcognetta
Copy link
Contributor Author

@aurorarossi thanks for the comments!

@codecov
Copy link

codecov bot commented Oct 5, 2022

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.29%. Comparing base (98942df) to head (051aa48).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #177   +/-   ##
=======================================
  Coverage   97.28%   97.29%           
=======================================
  Files         123      123           
  Lines        7406     7427   +21     
=======================================
+ Hits         7205     7226   +21     
  Misses        201      201           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

src/operators.jl Outdated
Edge 10 => 11
```
"""
@traitfn function mycielski(g::AbstractGraph::(!IsDirected); iterations = 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@traitfn function mycielski(g::AbstractGraph::(!IsDirected); iterations = 1)
@traitfn function mycielski(g::SimpleGraph; iterations::Int = 1)

The problem we have at the moment, that for graphs where we have some kind of edge weights/data, it is not clear what we mean by adding an edge. This is definitely something we should sort out in the future, but in the meantime I would suggest to restrict this function to SimpleGraphs.

Also, would suggest to restrict the type of iterations to to Int or Integer.

src/operators.jl Outdated
"""
@traitfn function mycielski(g::AbstractGraph::(!IsDirected); iterations = 1)
out = deepcopy(g)
for _ in 1:iterations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for _ in 1:iterations
for _ in Base.OneTo(iterations)

To be honest though, I am not sure if that makes a big difference for the case where iterations is not of type Int.

src/operators.jl Outdated
mycielski(g)

Returns a graph after applying the Mycielski operator to the input. The Mycielski operator
returns a new graph with `2n+1` vertices and `3e+n` edges and will increase the chromatic
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using the symbol e for the number of edges is a bit unusual, when we use n for the number of vertices at the same time. Maybe there is a better way?

src/operators.jl Outdated
apply the operator to the previous iterations graph.

For each vertex in the original graph, that vertex and a copy of it are added to the new graph.
Then, for each edge `(x, y)` in the original graph, the edges `(x, y)`, `(x', y)`, and `(x, y')`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, the usual convention for variables that represent vertices are the symbols u, v and w or s and t if we talk about some path finding algorithm.

src/operators.jl Outdated
julia> c = CycleGraph(5)
{5, 5} undirected simple Int64 graph

julia> m = Graphs.mycielski(c)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
julia> m = Graphs.mycielski(c)
julia> gm = Graphs.mycielski(g)

We usually use m to represent some kind of number such as the number of vertices or the number of edges and use symbols such as g and h for graphs.

src/operators.jl Outdated
out = deepcopy(g)
for _ in 1:iterations
N = nv(out)
add_vertices!(out, N + 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might fail, if the graph eltype does not support adding that many vertices. This might be especially bad, if we also create a mutating mycielski! function, as then this function might fail and leave g in some in-between state. So we probably should check at the start of the function if we have enough capacity to add that many vertices.

For the non-mutating mycielski function, we might also allow one to provide another larger eltype as an optional argument, such that we can retu

src/operators.jl Outdated
N = nv(out)
add_vertices!(out, N + 1)
w = nv(out)
for e in collect(edges(out))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we allocate an new vector for all edges, this would need quite a lot of space. Of course it is not possible to just iterate over the edges without collecting them, but maybe we can iterate of the first N vertices and allocate just a vector of the outneighbors of each vertex?

# ensure it is not done in-place
@test nv(g) == 15
@test ne(g) == 50
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are good, but I think we need to cover some additional test cases such as:

  • tests for graphs with self-loops
  • tests for graphs with zero vertices
  • tests for graphs with isolated vertices
  • tests for graphs with different eltypes - the testgraphs utility function can help here

@simonschoelly
Copy link
Member

This is an interesting operator, I added a few suggestions.

@gdalle gdalle added the enhancement New feature or request label Jun 16, 2023
@Krastanov
Copy link
Member

This has stalled and needs the original author to address the reviews.

@mcognetta , if you have the time, your contribution would certainly be very much appreciated! For the moment I will mark this as a draft in order to have easier time organizing and sorting my review queue.

@Krastanov Krastanov marked this pull request as draft February 21, 2026 05:21
@github-actions
Copy link
Contributor

github-actions bot commented Feb 22, 2026

Benchmark Results (Julia v1)

Time benchmarks
master 051aa48... master / 051aa48...
centrality/digraphs/betweenness_centrality 16 ± 0.47 ms 16 ± 0.52 ms 0.999 ± 0.043
centrality/digraphs/closeness_centrality 11.1 ± 0.41 ms 11.2 ± 0.44 ms 0.99 ± 0.053
centrality/digraphs/degree_centrality 1.96 ± 0.15 μs 1.93 ± 0.15 μs 1.02 ± 0.11
centrality/digraphs/katz_centrality 0.857 ± 0.054 ms 0.849 ± 0.053 ms 1.01 ± 0.09
centrality/digraphs/pagerank 0.036 ± 0.00065 ms 0.0362 ± 0.00057 ms 0.995 ± 0.024
centrality/graphs/betweenness_centrality 28.8 ± 1.2 ms 28.8 ± 1.2 ms 0.999 ± 0.06
centrality/graphs/closeness_centrality 21 ± 0.19 ms 21.1 ± 0.22 ms 0.995 ± 0.014
centrality/graphs/degree_centrality 1.48 ± 0.17 μs 1.48 ± 0.17 μs 1 ± 0.16
centrality/graphs/katz_centrality 1.01 ± 0.051 ms 1.01 ± 0.05 ms 1 ± 0.071
connectivity/digraphs/strongly_connected_components 0.0429 ± 0.001 ms 0.0425 ± 0.00095 ms 1.01 ± 0.033
connectivity/graphs/connected_components 24.1 ± 0.74 μs 22.4 ± 0.66 μs 1.08 ± 0.046
core/edges/digraphs 7.04 ± 0.02 μs 7.11 ± 0.7 μs 0.99 ± 0.098
core/edges/graphs 16.9 ± 0.051 μs 15.8 ± 0.11 μs 1.07 ± 0.0081
core/has_edge/digraphs 5.11 ± 0.34 μs 5.12 ± 0.34 μs 0.998 ± 0.094
core/has_edge/graphs 5.6 ± 0.35 μs 5.56 ± 0.36 μs 1.01 ± 0.091
core/nv/digraphs 0.371 ± 0.01 μs 0.36 ± 0.01 μs 1.03 ± 0.04
core/nv/graphs 0.381 ± 0.01 μs 0.381 ± 0.01 μs 1 ± 0.037
edges/fille 7.83 ± 0.92 μs 7.88 ± 0.97 μs 0.992 ± 0.17
edges/fillp 4.79 ± 3.7 μs 4.51 ± 3.8 μs 1.06 ± 1.2
edges/tsume 2.54 ± 0.03 μs 2.72 ± 0.03 μs 0.933 ± 0.015
edges/tsump 2.48 ± 0.029 μs 2.5 ± 0.031 μs 0.992 ± 0.017
insertions/SG(n,e) Generation 24.4 ± 3.6 ms 24.7 ± 3.3 ms 0.987 ± 0.19
parallel/egonet/twohop 0.291 ± 0.0097 s 0.292 ± 0.0042 s 0.996 ± 0.036
parallel/egonet/vertexfunction 2.26 ± 0.12 ms 2.28 ± 0.13 ms 0.99 ± 0.077
serial/egonet/twohop 0.292 ± 0.0051 s 0.292 ± 0.0032 s 1 ± 0.021
serial/egonet/vertexfunction 2.21 ± 0.12 ms 2.28 ± 0.16 ms 0.969 ± 0.086
traversals/digraphs/bfs_tree 0.0493 ± 0.002 ms 0.0497 ± 0.0022 ms 0.993 ± 0.061
traversals/digraphs/dfs_tree 0.0635 ± 0.0022 ms 0.0634 ± 0.0025 ms 1 ± 0.053
traversals/graphs/bfs_tree 0.0531 ± 0.0016 ms 0.0531 ± 0.0019 ms 0.999 ± 0.046
traversals/graphs/dfs_tree 0.0656 ± 0.0019 ms 0.0653 ± 0.0021 ms 1 ± 0.044
time_to_load 0.527 ± 0.004 s 0.527 ± 0.0023 s 1 ± 0.0088
Memory benchmarks
master 051aa48... master / 051aa48...
centrality/digraphs/betweenness_centrality 0.29 M allocs: 24 MB 0.29 M allocs: 24 MB 1
centrality/digraphs/closeness_centrality 18.6 k allocs: 14.5 MB 18.6 k allocs: 14.5 MB 1
centrality/digraphs/degree_centrality 8 allocs: 5.01 kB 8 allocs: 5.01 kB 1
centrality/digraphs/katz_centrality 2.63 k allocs: 2.83 MB 2.63 k allocs: 2.83 MB 1
centrality/digraphs/pagerank 21 allocs: 14.9 kB 21 allocs: 14.9 kB 1
centrality/graphs/betweenness_centrality 0.545 M allocs: 0.0313 GB 0.545 M allocs: 0.0313 GB 1
centrality/graphs/closeness_centrality 19.3 k allocs: 14 MB 19.3 k allocs: 14 MB 1
centrality/graphs/degree_centrality 10 allocs: 5.43 kB 10 allocs: 5.43 kB 1
centrality/graphs/katz_centrality 2.96 k allocs: 3.1 MB 2.96 k allocs: 3.1 MB 1
connectivity/digraphs/strongly_connected_components 1.05 k allocs: 0.075 MB 1.05 k allocs: 0.075 MB 1
connectivity/graphs/connected_components 0.061 k allocs: 22.5 kB 0.061 k allocs: 22.5 kB 1
core/edges/digraphs 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
core/edges/graphs 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
core/has_edge/digraphs 20 allocs: 12.6 kB 20 allocs: 12.6 kB 1
core/has_edge/graphs 28 allocs: 13.8 kB 28 allocs: 13.8 kB 1
core/nv/digraphs 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
core/nv/graphs 3 allocs: 0.0938 kB 3 allocs: 0.0938 kB 1
edges/fille 3 allocs: 0.153 MB 3 allocs: 0.153 MB 1
edges/fillp 3 allocs: 0.153 MB 3 allocs: 0.153 MB 1
edges/tsume 0 allocs: 0 B 0 allocs: 0 B
edges/tsump 0 allocs: 0 B 0 allocs: 0 B
insertions/SG(n,e) Generation 0.0465 M allocs: 10.9 MB 0.0465 M allocs: 10.9 MB 1
parallel/egonet/twohop 10 allocs: 0.0768 MB 10 allocs: 0.0768 MB 1
parallel/egonet/vertexfunction 10 allocs: 0.0768 MB 10 allocs: 0.0768 MB 1
serial/egonet/twohop 3 allocs: 0.0764 MB 3 allocs: 0.0764 MB 1
serial/egonet/vertexfunction 3 allocs: 0.0764 MB 3 allocs: 0.0764 MB 1
traversals/digraphs/bfs_tree 2.34 k allocs: 0.113 MB 2.34 k allocs: 0.113 MB 1
traversals/digraphs/dfs_tree 2.44 k allocs: 0.118 MB 2.44 k allocs: 0.118 MB 1
traversals/graphs/bfs_tree 2.52 k allocs: 0.121 MB 2.52 k allocs: 0.121 MB 1
traversals/graphs/dfs_tree 2.63 k allocs: 0.127 MB 2.63 k allocs: 0.127 MB 1
time_to_load 0.145 k allocs: 11 kB 0.145 k allocs: 11 kB 1

@mcognetta
Copy link
Contributor Author

@Krastanov Hi, thanks for bringing this to my attention.

I addressed most of the comments. I am not sure of the right way to handle the potential case where we use the inplace modification version but don't have enough memory to store the result. I'm happy to update this if there is a good way to handle it, but for now it will just crash.

I updated the tests, the doc string, signature, etc. and I also split it into a mycielski! and mycielski pair. And I addressed the implementation optimization (not materializing the whole vertex/neighbor list at each iteration), but now that I am writing this comment, it occurs to me that this assumes that the graph nodes are densely labeled (like you must have the nodes be 1,2,3,...n).

I also implemented this function in networkx and there was a similar problem, which is why we just dumped the entire vertex set into a list at the start of each iteration, so we could rely on that for the vertex numbering and not the implicit index numbers.

@mcognetta mcognetta marked this pull request as ready for review February 22, 2026 10:23
@henrik-wolf
Copy link
Contributor

I am not sure of the right way to handle the potential case where we use the inplace modification version but don't have enough memory to store the result. I'm happy to update this if there is a good way to handle it, but for now it will just crash.

I think the problem is not with memory (as in physical memory on your computer) but with the type of the node ids in the given graph. If you have a SimpleGraph{UInt8} it can only have 255 nodes. To me it looks like the final graph will have 2^iterations * (nv(g)+1) - 1 nodes. So you could do

@assert 2^iterations * (nv(g)+1) - 1 <= typemax(eltype(g)) "eltype too small (or a better error message)"

Edge 10 => 11
```
"""
function mycielski(g; iterations=1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function mycielski(g; iterations=1)
function mycielski(g::SimpleGraph; iterations=1)

```
"""
function mycielski(g; iterations=1)
has_self_loops(g) && return g
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "doing nothing" the right thing for the mycielski operator on graphs with self loops? Or is the operator not defined on graphs with self loops?

Suggested change
has_self_loops(g) && return g
@assert !has_self_loops(g) "The graph can not have self loops"

@test nv(m) == 5
@test ne(m) == 2
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple test for insufficient eltype could look like:

@testset "insufficient eltype" begin
g = complete_graph(UInt8(2))
@test_throws AssertionError mycielski!(g, iterations=10)
end

```
"""
function mycielski(g; iterations=1)
has_self_loops(g) && return g
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@assert 2^iterations * (nv(g)+1) - 1 < typemax(eltype(g)) "eltype too small (or a good error message)"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants