diff --git a/CHANGELOG.md b/CHANGELOG.md index 9192490..6a6cde8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # News +## Unreleased + +- Add `IGraphAlg` dispatch for `connected_components`, `is_connected`, `articulation`, and `bridges`. + ## v1.0.0 - 2025-09-25 - Update the underlying igraph C library to v1.0.0. diff --git a/src/graph_api_extensions.jl b/src/graph_api_extensions.jl index ec21b95..2306aa7 100644 --- a/src/graph_api_extensions.jl +++ b/src/graph_api_extensions.jl @@ -1,11 +1,12 @@ # Explicit import/export of the functions # that are getting new methods, # so that `igraphalg_methods` can pick them up. -import Graphs: diameter, radius +import Graphs: diameter, radius, connected_components, is_connected, articulation, bridges import Graphs.Experimental import Graphs.Experimental: has_isomorph -export diameter, radius, has_isomorph +export diameter, radius, has_isomorph, + connected_components, is_connected, articulation, bridges struct IGraphAlg end @@ -31,3 +32,39 @@ end function has_isomorph(g1, g2, ::IGraphAlg) return LibIGraph.isomorphic(IGraph(g1), IGraph(g2))[1] end + +function connected_components(g, ::IGraphAlg) + ig = IGraph(g) + membership = IGVectorInt() + csize = IGVectorInt() + ncomp = LibIGraph.connected_components(ig, membership, csize, LibIGraph.IGRAPH_WEAK)[1] + # Convert igraph 0-based component IDs to Graphs.jl format: + # Vector of vectors, each containing 1-based vertex IDs + components = [Int[] for _ in 1:ncomp] + for (v, c) in enumerate(membership) + push!(components[c+1], v) + end + return components +end + +function is_connected(g, ::IGraphAlg) + return LibIGraph.is_connected(IGraph(g), LibIGraph.IGRAPH_WEAK)[1] +end + +function articulation(g, ::IGraphAlg) + ig = IGraph(g) + res = IGVectorInt() + LibIGraph.articulation_points(ig, res) + return sort!([v + 1 for v in res]) +end + +function bridges(g, ::IGraphAlg) + ig = IGraph(g) + res = IGVectorInt() + LibIGraph.bridges(ig, res) + # Convert edge IDs to SimpleEdge pairs + return [begin + (from, to) = LibIGraph.edge(ig, eid) + Graphs.SimpleGraphs.SimpleEdge(from+1, to+1) + end for eid in res] +end diff --git a/test/test_graph_api_extensions.jl b/test/test_graph_api_extensions.jl index ea5d600..6fd2ea3 100644 --- a/test/test_graph_api_extensions.jl +++ b/test/test_graph_api_extensions.jl @@ -20,4 +20,57 @@ using Random end end +@testset "connected_components" begin + # Connected graph + g = cycle_graph(5) + cc = connected_components(g, IGraphAlg()) + @test length(cc) == 1 + @test sort(cc[1]) == [1, 2, 3, 4, 5] + + # Disconnected graph: path(3) + isolated vertices + g2 = Graph(5) + add_edge!(g2, 1, 2) + add_edge!(g2, 2, 3) + add_edge!(g2, 4, 5) + cc2 = connected_components(g2, IGraphAlg()) + @test length(cc2) == 2 + cc2_sorted = sort(cc2; by=first) + @test sort(cc2_sorted[1]) == [1, 2, 3] + @test sort(cc2_sorted[2]) == [4, 5] + + # Match Graphs.jl result (same component grouping) + cc_ref = connected_components(g2) + @test length(cc2) == length(cc_ref) + @test Set(Set.(cc2)) == Set(Set.(cc_ref)) +end + +@testset "is_connected" begin + @test is_connected(cycle_graph(5), IGraphAlg()) == true + g = Graph(4) + add_edge!(g, 1, 2) + add_edge!(g, 3, 4) + @test is_connected(g, IGraphAlg()) == false + @test is_connected(g, IGraphAlg()) == is_connected(g) +end + +@testset "articulation" begin + g = path_graph(5) + art = articulation(g, IGraphAlg()) + art_ref = sort(articulation(g)) + @test art == art_ref + + g2 = cycle_graph(5) + @test isempty(articulation(g2, IGraphAlg())) +end + +@testset "bridges" begin + g = path_graph(5) + br = bridges(g, IGraphAlg()) + br_ref = bridges(g) + @test Set(br) == Set(br_ref) + + g2 = cycle_graph(5) + @test isempty(bridges(g2, IGraphAlg())) +end + end