diff --git a/CHANGELOG.md b/CHANGELOG.md index 30638257c..c2e7c7f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ We follow SemVer as most of the Julia ecosystem. Below you might see the "breaki ## unreleased - `is_articulation(g, v)` for checking whether a single vertex is an articulation point +- Integration with the `igraph` C library via `IGraphs.jl` for high-performance algorithm implementations ## v1.14.0 - 2026-02-26 diff --git a/Project.toml b/Project.toml index 1ff85172b..1c408fcdd 100644 --- a/Project.toml +++ b/Project.toml @@ -15,9 +15,11 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [weakdeps] Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383" +IGraphs = "647e90d3-2106-487c-adb4-c91fc07b96ea" [extensions] GraphsSharedArraysExt = "SharedArrays" +IGraphsExt = "IGraphs" [compat] ArnoldiMethod = "0.4" diff --git a/docs/make.jl b/docs/make.jl index 30797f0f3..18a922b11 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -46,6 +46,7 @@ pages_files = [ "ecosystem/graphtypes.md", "ecosystem/graphalgorithms.md", "ecosystem/interface.md", + "ecosystem/igraphs.md", ], "Core API" => [ "core_functions/core.md", diff --git a/docs/src/ecosystem/igraphs.md b/docs/src/ecosystem/igraphs.md new file mode 100644 index 000000000..cc213c68b --- /dev/null +++ b/docs/src/ecosystem/igraphs.md @@ -0,0 +1,36 @@ +# IGraphs integration + +_Graphs.jl_ provides an integration with the [`igraph`](https://github.com/igraph/igraph) C library via the [IGraphs.jl](https://github.com/JuliaGraphs/IGraphs.jl) package. This integration allows you to use high-performance implementations of various graph algorithms directly on `Graphs.jl` graph types, or use `IGraph` objects as first-class `AbstractGraph` types. + +## Usage + +To use the `igraph` integration, you must load both `Graphs.jl` and `IGraphs.jl`: + +```julia +using Graphs +using IGraphs +``` + +When `IGraphs.jl` is loaded, specialized dispatches for several algorithms become available. You can either call them on an `IGraph` object, or pass `IGraphAlgorithm()` as an argument to existing `Graphs.jl` functions to use the `igraph` implementation. + +## Interface and Traits + +```@docs +Graphs.AbstractIGraph +Graphs.IGraphAlgorithm +Graphs.igraph +``` + +## Algorithms + +The following algorithms have specialized implementations via `igraph`: + +```@docs +Graphs.sir_model +Graphs.modularity_matrix +Graphs.community_leiden +Graphs.betweenness_centrality +Graphs.pagerank +Graphs.layout_kamada_kawai +Graphs.layout_fruchterman_reingold +``` diff --git a/ext/IGraphsExt.jl b/ext/IGraphsExt.jl new file mode 100644 index 000000000..34ec54ebd --- /dev/null +++ b/ext/IGraphsExt.jl @@ -0,0 +1,282 @@ +module IGraphsExt + +using Graphs +using IGraphs +using LinearAlgebra +using IGraphs: LibIGraph + +import Graphs: igraph, IGraphAlgorithm +import Graphs: + sir_model, + layout_kamada_kawai, + layout_fruchterman_reingold, + community_leiden, + modularity_matrix + +function _check_ret(ret, funcname) + if ret != LibIGraph.IGRAPH_SUCCESS + error("$funcname failed with error code $ret") + end +end + +# --- Conversion --- + +""" + igraph(g::AbstractSimpleGraph) + +Fast conversion from `Graphs.SimpleGraph`/`SimpleDiGraph` to `IGraphs.IGraph`. +Uses `igraph_add_edges` for high performance. +""" +function igraph(g::Graphs.AbstractSimpleGraph) + n = Graphs.nv(g) + ig = IGraphs.IGraph(; _uninitialized=Val(true)) + _check_ret(LibIGraph.igraph_empty(ig.objref, n, Graphs.is_directed(g)), "igraph_empty") + m = Graphs.ne(g) + if m > 0 + edges_vec = Vector{Int64}(undef, 2*m) + for (i, e) in enumerate(Graphs.edges(g)) + edges_vec[2 * i - 1] = Graphs.src(e) - 1 + edges_vec[2 * i] = Graphs.dst(e) - 1 + end + v_edges = IGraphs.IGVectorInt(edges_vec) + ret = LibIGraph.igraph_add_edges(ig.objref, v_edges.objref, IGraphs.IGNull().objref) + _check_ret(ret, "igraph_add_edges") + end + return ig +end + +# Identity conversion +igraph(g::IGraphs.IGraph) = g + +# Fallback for other AbstractGraph types +igraph(g::Graphs.AbstractGraph) = IGraphs.IGraph(g) + +# --- Missing Graphs.jl interface methods for IGraph --- +# IGraphs.jl already provides: nv, ne, has_edge, has_vertex, vertices, edgetype, eltype +# We add the missing ones: edges, inneighbors, outneighbors, is_directed (instance method) + +function Graphs.edges(g::IGraphs.IGraph) + m = Graphs.ne(g) + if m == 0 + return Graphs.SimpleGraphs.SimpleEdge{eltype(g)}[] + end + v_edges = IGraphs.IGVectorInt(; _uninitialized=Val(true)) + LibIGraph.igraph_vector_int_init(v_edges.objref, 2 * m) + LibIGraph.igraph_get_edgelist(g.objref, v_edges.objref, false) + edge_list = Vector(v_edges) + ET = Graphs.edgetype(g) + return [ET(edge_list[2 * i - 1]+1, edge_list[2 * i]+1) for i in 1:m] +end + +# Instance method for is_directed (IGraphs only defines the Type method) +Graphs.is_directed(g::IGraphs.IGraph) = Bool(LibIGraph.igraph_is_directed(g.objref)) + +function Graphs.outneighbors(g::IGraphs.IGraph, v::Integer) + res = IGraphs.IGVectorInt(; _uninitialized=Val(true)) + LibIGraph.igraph_vector_int_init(res.objref, 0) + mode = Graphs.is_directed(g) ? LibIGraph.IGRAPH_OUT : LibIGraph.IGRAPH_ALL + ret = LibIGraph.igraph_neighbors( + g.objref, + res.objref, + v-1, + mode, + LibIGraph.IGRAPH_NO_LOOPS, + LibIGraph.IGRAPH_NO_MULTIPLE, + ) + _check_ret(ret, "igraph_neighbors (out)") + return sort!([Int(x) + 1 for x in Vector(res)]) +end + +function Graphs.inneighbors(g::IGraphs.IGraph, v::Integer) + res = IGraphs.IGVectorInt(; _uninitialized=Val(true)) + LibIGraph.igraph_vector_int_init(res.objref, 0) + mode = Graphs.is_directed(g) ? LibIGraph.IGRAPH_IN : LibIGraph.IGRAPH_ALL + ret = LibIGraph.igraph_neighbors( + g.objref, + res.objref, + v-1, + mode, + LibIGraph.IGRAPH_NO_LOOPS, + LibIGraph.IGRAPH_NO_MULTIPLE, + ) + _check_ret(ret, "igraph_neighbors (in)") + return sort!([Int(x) + 1 for x in Vector(res)]) +end + +# Helper to handle weights +function _handle_weights(weights) + if weights === nothing + return IGraphs.IGNull() + else + return IGraphs.IGVectorFloat(weights) + end +end + +# --- Algorithm Implementations --- + +function sir_model( + g::Graphs.AbstractGraph, ::IGraphAlgorithm; beta=0.1, gamma=0.1, no_sim=100 +) + ig = igraph(g) + + ptr_vec = Ref{LibIGraph.igraph_vector_ptr_t}() + LibIGraph.igraph_vector_ptr_init(ptr_vec, 0) + + try + ret = LibIGraph.igraph_sir(ig.objref, beta, gamma, no_sim, ptr_vec) + _check_ret(ret, "igraph_sir") + + n_sim = LibIGraph.igraph_vector_ptr_size(ptr_vec) + results = Vector{Vector{Float64}}(undef, n_sim) + + for i in 1:n_sim + v_ptr = LibIGraph.igraph_vector_ptr_get(ptr_vec, i-1) + v_ptr_typed = reinterpret(Ptr{LibIGraph.igraph_vector_t}, v_ptr) + v_size = Int(LibIGraph.igraph_vector_size(v_ptr_typed)) + res_v = Vector{Float64}(undef, v_size) + for j in 1:v_size + res_v[j] = Float64(LibIGraph.igraph_vector_get(v_ptr_typed, j-1)) + end + results[i] = res_v + end + return results + finally + LibIGraph.igraph_vector_ptr_destroy_all(ptr_vec) + end +end + +function modularity_matrix(g::Graphs.AbstractGraph, ::IGraphAlgorithm; kwargs...) + ig = igraph(g) + return IGraphs.modularity_matrix(ig; kwargs...) +end + +function community_leiden( + g::Graphs.AbstractGraph, + ::IGraphAlgorithm; + resolution=1.0, + beta=0.01, + n_iterations=10, + kwargs..., +) + ig = igraph(g) + membership = IGraphs.IGVectorInt(; _uninitialized=Val(true)) + LibIGraph.igraph_vector_int_init(membership.objref, 0) + + nb_clusters = Ref{LibIGraph.igraph_int_t}(0) + quality = Ref{LibIGraph.igraph_real_t}(0.0) + + ret = LibIGraph.igraph_community_leiden_simple( + ig.objref, + IGraphs.IGNull().objref, + 0, + resolution, + beta, + IGraphs.IGNull().objref, + n_iterations, + membership.objref, + nb_clusters, + quality, + ) + _check_ret(ret, "igraph_community_leiden_simple") + return Vector(membership) +end + +function layout_kamada_kawai( + g::Graphs.AbstractGraph, + ::IGraphAlgorithm; + maxiter=100, + epsilon=0.0, + kkconst=0.0, + kwargs..., +) + ig = igraph(g) + n = Graphs.nv(g) + res = IGraphs.IGMatrixFloat(; _uninitialized=Val(true)) + LibIGraph.igraph_matrix_init(res.objref, n, 2) + ret = LibIGraph.igraph_layout_kamada_kawai( + ig.objref, + res.objref, + false, + maxiter, + epsilon, + kkconst, + IGraphs.IGNull().objref, + -100.0, + 100.0, + -100.0, + 100.0, + ) + _check_ret(ret, "igraph_layout_kamada_kawai") + return Matrix(res) +end + +function layout_fruchterman_reingold( + g::Graphs.AbstractGraph, ::IGraphAlgorithm; niter=500, kwargs... +) + ig = igraph(g) + n = Graphs.nv(g) + res = IGraphs.IGMatrixFloat(; _uninitialized=Val(true)) + LibIGraph.igraph_matrix_init(res.objref, n, 2) + ret = LibIGraph.igraph_layout_fruchterman_reingold( + ig.objref, + res.objref, + false, + niter, + IGraphs.IGNull().objref, + -100.0, + 100.0, + -100.0, + 100.0, + ) + _check_ret(ret, "igraph_layout_fruchterman_reingold") + return Matrix(res) +end + +function Graphs.betweenness_centrality( + g::Graphs.AbstractGraph, ::IGraphAlgorithm; weights=nothing, normalized=true, kwargs... +) + ig = igraph(g) + n = Graphs.nv(g) + res = IGraphs.IGVectorFloat(; _uninitialized=Val(true)) + LibIGraph.igraph_vector_init(res.objref, n) + w = _handle_weights(weights) + + ret = LibIGraph.igraph_betweenness( + ig.objref, + w.objref, + res.objref, + LibIGraph.igraph_vss_all(), + Graphs.is_directed(g), + normalized, + ) + _check_ret(ret, "igraph_betweenness") + + return Vector(res) +end + +function Graphs.pagerank( + g::Graphs.AbstractGraph{U}, ::IGraphAlgorithm; damping=0.85, weights=nothing, kwargs... +) where {U<:Integer} + ig = igraph(g) + n = Graphs.nv(g) + res = IGraphs.IGVectorFloat(; _uninitialized=Val(true)) + LibIGraph.igraph_vector_init(res.objref, n) + w = _handle_weights(weights) + + val = Ref{LibIGraph.igraph_real_t}(0.0) + ret = LibIGraph.igraph_pagerank( + ig.objref, + w.objref, + res.objref, + val, + damping, + Graphs.is_directed(g), + LibIGraph.igraph_vss_all(), + LibIGraph.IGRAPH_PAGERANK_ALGO_PRPACK, + IGraphs.IGNull().objref, + ) + _check_ret(ret, "igraph_pagerank") + return Vector(res) +end + +end # module diff --git a/src/Experimental/ShortestPaths/ShortestPaths.jl b/src/Experimental/ShortestPaths/ShortestPaths.jl index ece5232ff..b9c579f0d 100644 --- a/src/Experimental/ShortestPaths/ShortestPaths.jl +++ b/src/Experimental/ShortestPaths/ShortestPaths.jl @@ -16,8 +16,7 @@ import Graphs.Experimental.Traversals: # LGEnvironment() = new(false, false) # end -abstract type AbstractGraphResult end -abstract type AbstractGraphAlgorithm end +# Redefinitions removed, now inherited from Graphs """ ShortestPathResult <: AbstractGraphResult diff --git a/src/Graphs.jl b/src/Graphs.jl index 5380f88f2..a5c8c4f57 100644 --- a/src/Graphs.jl +++ b/src/Graphs.jl @@ -71,6 +71,8 @@ export AbstractGraph, AbstractEdge, AbstractEdgeIter, + AbstractGraphAlgorithm, + AbstractGraphResult, Edge, Graph, SimpleGraph, @@ -454,7 +456,17 @@ export # planarity is_planar, - planar_maximally_filtered_graph + planar_maximally_filtered_graph, + + # igraphs + IGraphAlgorithm, + AbstractIGraph, + igraph, + sir_model, + layout_kamada_kawai, + layout_fruchterman_reingold, + community_leiden, + modularity_matrix """ Graphs @@ -478,6 +490,7 @@ and tutorials are available at the """ Graphs include("interface.jl") +include("igraphs.jl") include("utils.jl") include("frozenvector.jl") include("deprecations.jl") @@ -582,4 +595,13 @@ include("planarity.jl") include("spanningtrees/planar_maximally_filtered_graph.jl") using .LinAlg + +function __init__() + Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs + if exc.f in _IGRAPH_REQUIRED_FUNCTIONS + print(io, "\n\nThis function requires the IGraphs.jl package to be loaded.") + end + end +end + end # module diff --git a/src/igraphs.jl b/src/igraphs.jl new file mode 100644 index 000000000..4e3d42676 --- /dev/null +++ b/src/igraphs.jl @@ -0,0 +1,89 @@ +""" + IGraphAlgorithm <: AbstractGraphAlgorithm + +A trait to specify that an algorithm is implemented via the `IGraphs.jl` package. +""" +struct IGraphAlgorithm <: AbstractGraphAlgorithm end + +# This list will hold functions that are defined in Graphs.jl but require +# IGraphs.jl for their implementation. +const _IGRAPH_REQUIRED_FUNCTIONS = Function[] + +macro igraph_declare(name, doc) + return quote + @doc $doc + function $(esc(name)) end + push!(_IGRAPH_REQUIRED_FUNCTIONS, $(esc(name))) + end +end + +@igraph_declare sir_model """ + sir_model(g, beta, gamma, no_steps) + +Simulate an SIR (Susceptible-Infected-Recovered) model on graph `g`. +This function requires the `IGraphs.jl` package to be loaded. +""" + +@igraph_declare layout_kamada_kawai """ + layout_kamada_kawai(g) + +Calculate the Kamada-Kawai layout for graph `g`. +This function requires the `IGraphs.jl` package to be loaded. +""" + +@igraph_declare layout_fruchterman_reingold """ + layout_fruchterman_reingold(g) + +Calculate the Fruchterman-Reingold layout for graph `g`. +This function requires the `IGraphs.jl` package to be loaded. +""" + +@igraph_declare community_leiden """ + community_leiden(g) + +Calculate communities in graph `g` using the Leiden algorithm. +This function requires the `IGraphs.jl` package to be loaded. +""" + +@igraph_declare modularity_matrix """ + modularity_matrix(g) + +Calculate the modularity matrix for graph `g`. +This function requires the `IGraphs.jl` package to be loaded. +""" + +# --- Conversion and Interface Support --- + +""" + AbstractIGraph{T} <: AbstractGraph{T} + +Abstract type for graphs that are backed by the `igraph` C library. +Implementations should live in `IGraphs.jl`. +""" +abstract type AbstractIGraph{T} <: AbstractGraph{T} end + +""" + igraph(g::AbstractGraph) + +Convert a `Graphs.jl` graph to an `igraph` representation. +The specific implementation should be provided by `IGraphs.jl`. +""" +function igraph end + +# --- Dispatch Overrides for Existing Algorithms --- + +""" + betweenness_centrality(g, ::IGraphAlgorithm; kwargs...) + +Dispatch `betweenness_centrality` to the `igraph` implementation. +This requires `IGraphs.jl` to be loaded. +""" +function betweenness_centrality end + +""" + pagerank(g, ::IGraphAlgorithm; kwargs...) + +Dispatch `pagerank` to the `igraph` implementation. +This requires `IGraphs.jl` to be loaded. +""" +function pagerank end diff --git a/src/interface.jl b/src/interface.jl index 2a9c15fb0..cf04e7822 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -16,6 +16,10 @@ function Base.showerror(io::IO, ie::NotImplementedError) end _NI(m) = throw(NotImplementedError(m)) +abstract type AbstractGraphResult end +abstract type AbstractGraphAlgorithm end + +export AbstractGraphResult, AbstractGraphAlgorithm """ AbstractEdge diff --git a/test/Project.toml b/test/Project.toml index e386e377c..823a10e8d 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -6,9 +6,11 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" IGraphs = "647e90d3-2106-487c-adb4-c91fc07b96ea" Inflate = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" NautyGraphs = "7509a0a4-015a-4167-b44b-0799a1a2605e" diff --git a/test/igraphs.jl b/test/igraphs.jl new file mode 100644 index 000000000..f5bbce652 --- /dev/null +++ b/test/igraphs.jl @@ -0,0 +1,40 @@ +@testset "igraphs" begin + @test IGraphAlgorithm() isa AbstractGraphAlgorithm + + # Test error hint for sir_model + try + sir_model(SimpleGraph(3), 0.1, 0.1, 10) + @test false # Should not reach here + catch e + @test e isa MethodError + @test e.f === sir_model + + msg = sprint(showerror, e) + @test contains(msg, "This function requires the IGraphs.jl package to be loaded") + end + + # Test community_leiden + try + community_leiden(SimpleGraph(3)) + @test false + catch e + @test e isa MethodError + msg = sprint(showerror, e) + @test contains(msg, "This function requires the IGraphs.jl package to be loaded") + end + + # Test modularity_matrix + try + modularity_matrix(SimpleGraph(3)) + @test false + catch e + @test e isa MethodError + msg = sprint(showerror, e) + @test contains(msg, "This function requires the IGraphs.jl package to be loaded") + end +end + +if !isnothing(Base.find_package("IGraphs")) + include("igraphs_ext.jl") + include("igraphs_interface.jl") +end diff --git a/test/igraphs_ext.jl b/test/igraphs_ext.jl new file mode 100644 index 000000000..c2a324550 --- /dev/null +++ b/test/igraphs_ext.jl @@ -0,0 +1,43 @@ +using Graphs +using IGraphs +using Test + +@testset "IGraphs Extension" begin + g = SimpleGraph(3) + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + + # Test conversion + ig = Graphs.igraph(g) + @test ig isa IGraphs.IGraph + @test Graphs.nv(ig) == 3 + @test Graphs.ne(ig) == 2 + + # Test real dispatch (Pagerank) + # This should call the implementation in ext/IGraphsExt.jl + pr = Graphs.pagerank(g, Graphs.IGraphAlgorithm()) + @test pr isa Vector{Float64} + @test length(pr) == 3 + @test sum(pr) ≈ 1.0 + + # Test real dispatch (Betweenness) + bc = Graphs.betweenness_centrality(g, Graphs.IGraphAlgorithm()) + @test bc isa Vector{Float64} + @test length(bc) == 3 + # For 1-2-3 graph, 2 has betweenness 1.0 (normalized) or 2.0 (unnormalized) + # igraph usually returns unnormalized by default unless specified + @test bc[2] > 0 + + # Test real dispatch (SIR Model) + # Using a slightly larger graph for better SIR simulation results + g_sir = SimpleGraph(10) + for i in 1:9 + add_edge!(g_sir, i, i+1) + end + res = Graphs.sir_model(g_sir, Graphs.IGraphAlgorithm(); beta=0.5, gamma=0.1, no_sim=10) + @test res isa Vector{Vector{Float64}} + @test length(res) == 10 + for sim_res in res + @test !isempty(sim_res) + end +end diff --git a/test/igraphs_interface.jl b/test/igraphs_interface.jl new file mode 100644 index 000000000..0f05cda3a --- /dev/null +++ b/test/igraphs_interface.jl @@ -0,0 +1,60 @@ +using Graphs +using IGraphs +using Test + +@testset "IGraphs Interface Compliance" begin + # Test with undirected graph + @testset "Undirected Graph" begin + g_orig = path_graph(5) + ig = igraph(g_orig) + + @testset "Basic Properties" begin + @test nv(ig) == nv(g_orig) + @test ne(ig) == ne(g_orig) + @test is_directed(ig) == false + end + + @testset "Vertices and Edges" begin + @test collect(vertices(ig)) == collect(vertices(g_orig)) + ig_edges = edges(ig) + @test length(ig_edges) == ne(ig) + + if ne(ig) > 0 + e = first(ig_edges) + @test has_edge(ig, src(e), dst(e)) + end + end + + @testset "Neighbors" begin + for v in vertices(ig) + @test sort(outneighbors(ig, v)) == sort(outneighbors(g_orig, v)) + @test sort(inneighbors(ig, v)) == sort(inneighbors(g_orig, v)) + end + end + end + + # Test with directed graph + @testset "Directed Graph" begin + g_orig = path_digraph(5) + ig = igraph(g_orig) + + @testset "Basic Properties" begin + @test nv(ig) == nv(g_orig) + @test ne(ig) == ne(g_orig) + @test is_directed(ig) == true + end + + @testset "Vertices and Edges" begin + @test collect(vertices(ig)) == collect(vertices(g_orig)) + ig_edges = edges(ig) + @test length(ig_edges) == ne(ig) + end + + @testset "Neighbors" begin + for v in vertices(ig) + @test sort(outneighbors(ig, v)) == sort(outneighbors(g_orig, v)) + @test sort(inneighbors(ig, v)) == sort(inneighbors(g_orig, v)) + end + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index c4da7ef47..b15650a2b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -153,6 +153,7 @@ tests = [ "vertexcover/degree_vertex_cover", "vertexcover/random_vertex_cover", "trees/prufer", + "igraphs", "experimental/experimental", "planarity", ]