Skip to content

Commit 9ff6d4a

Browse files
committed
feat : reliable idiomatic wrapper for the C library igraphs
1 parent ca5cbc3 commit 9ff6d4a

11 files changed

Lines changed: 469 additions & 3 deletions

File tree

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1515
[weakdeps]
1616
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
1717
SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383"
18+
IGraphs = "647e90d3-2106-487c-adb4-c91fc07b96ea"
1819

1920
[extensions]
2021
GraphsSharedArraysExt = "SharedArrays"
22+
IGraphsExt = "IGraphs"
2123

2224
[compat]
2325
ArnoldiMethod = "0.4"

ext/IGraphsExt.jl

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
module IGraphsExt
2+
3+
using Graphs
4+
using IGraphs
5+
using LinearAlgebra
6+
using IGraphs: LibIGraph
7+
8+
import Graphs: igraph, AbstractIGraph, IGraphAlgorithm
9+
import Graphs: sir_model, layout_kamada_kawai, layout_fruchterman_reingold, community_leiden, modularity_matrix
10+
import Graphs: betweenness_centrality, pagerank
11+
import Graphs: nv, ne, is_directed, vertices, edges, has_vertex, has_edge, inneighbors, outneighbors, edgetype, eltype
12+
13+
# Note: IGraphs wrappers (IGraph, IGVectorInt, etc.) store the underlying C struct
14+
# in an `objref` field which is a Ref{LibIGraph.ctype}.
15+
16+
function _check_ret(ret, funcname)
17+
if ret != LibIGraph.IGRAPH_SUCCESS
18+
error("$funcname failed with error code $ret")
19+
end
20+
end
21+
22+
"""
23+
igraph(g::AbstractSimpleGraph)
24+
25+
Fast conversion from `Graphs.SimpleGraph` to `IGraphs.IGraph`.
26+
Uses `igraph_add_edges` for high performance.
27+
"""
28+
function igraph(g::Graphs.AbstractSimpleGraph)
29+
n = Graphs.nv(g)
30+
ig = IGraphs.IGraph(n; directed=Graphs.is_directed(g))
31+
m = Graphs.ne(g)
32+
edges_vec = Vector{Int64}(undef, 2*m)
33+
for (i, e) in enumerate(Graphs.edges(g))
34+
edges_vec[2*i-1] = Graphs.src(e) - 1
35+
edges_vec[2*i] = Graphs.dst(e) - 1
36+
end
37+
# Create the IGVectorInt wrapper
38+
v_edges = IGraphs.IGVectorInt(edges_vec)
39+
# Add edges in bulk using .objref to pass the underlying Ref to ccall
40+
ret = LibIGraph.igraph_add_edges(ig.objref, v_edges.objref, IGraphs.IGNull().objref)
41+
_check_ret(ret, "igraph_add_edges")
42+
return ig
43+
end
44+
45+
# --- Graph API Implementation for AbstractIGraph ---
46+
47+
Graphs.nv(g::AbstractIGraph) = Int(LibIGraph.igraph_vcount(g.objref))
48+
Graphs.ne(g::AbstractIGraph) = Int(LibIGraph.igraph_ecount(g.objref))
49+
Graphs.is_directed(g::AbstractIGraph) = Bool(LibIGraph.igraph_is_directed(g.objref))
50+
51+
Graphs.vertices(g::AbstractIGraph) = 1:nv(g)
52+
53+
function Graphs.edges(g::AbstractIGraph)
54+
55+
m = ne(g)
56+
v_edges = IGraphs.IGVectorInt(; _uninitialized=Val(true))
57+
LibIGraph.igraph_vector_int_init(v_edges.objref, 2 * m)
58+
LibIGraph.igraph_get_edgelist(g.objref, v_edges.objref, false)
59+
edge_list = Vector(v_edges)
60+
ET = edgetype(g)
61+
return [ET(edge_list[2*i-1]+1, edge_list[2*i]+1) for i in 1:m]
62+
end
63+
64+
Graphs.has_vertex(g::AbstractIGraph, v::Integer) = 1 <= v <= nv(g)
65+
66+
function Graphs.has_edge(g::AbstractIGraph, u::Integer, v::Integer)
67+
eid = Ref{LibIGraph.igraph_integer_t}(-1)
68+
ret = LibIGraph.igraph_get_eid(g.objref, eid, u-1, v-1, is_directed(g), false)
69+
return eid[] != -1
70+
end
71+
72+
function Graphs.outneighbors(g::AbstractIGraph, v::Integer)
73+
res = IGraphs.IGVectorInt(; _uninitialized=Val(true))
74+
LibIGraph.igraph_vector_int_init(res.objref, 0)
75+
# IGRAPH_OUT = 1
76+
ret = LibIGraph.igraph_neighbors(g.objref, res.objref, v-1, 1)
77+
_check_ret(ret, "igraph_neighbors (out)")
78+
return [Int(x) + 1 for x in Vector(res)]
79+
end
80+
81+
function Graphs.inneighbors(g::AbstractIGraph, v::Integer)
82+
res = IGraphs.IGVectorInt(; _uninitialized=Val(true))
83+
LibIGraph.igraph_vector_int_init(res.objref, 0)
84+
# IGRAPH_IN = 2
85+
ret = LibIGraph.igraph_neighbors(g.objref, res.objref, v-1, 2)
86+
_check_ret(ret, "igraph_neighbors (in)")
87+
return [Int(x) + 1 for x in Vector(res)]
88+
end
89+
90+
Graphs.neighbors(g::AbstractIGraph, v::Integer) = outneighbors(g, v)
91+
92+
Graphs.edgetype(g::AbstractIGraph) = Graphs.SimpleGraphs.SimpleEdge{Int}
93+
Graphs.eltype(g::AbstractIGraph) = Int
94+
95+
# Fallback for other graph types
96+
igraph(g::AbstractGraph) = IGraphs.IGraph(g)
97+
98+
# Helper to handle weights
99+
function _handle_weights(weights)
100+
if weights === nothing
101+
return IGraphs.IGNull()
102+
else
103+
return IGraphs.IGVectorFloat(weights)
104+
end
105+
end
106+
107+
# --- Algorithm Implementations ---
108+
109+
function sir_model(g::AbstractGraph{U}, ::IGraphAlgorithm; beta=0.1, gamma=0.1, no_sim=100) where U<:Integer
110+
ig = igraph(g)
111+
112+
# result is an igraph_vector_ptr_t
113+
# We use a Ref to a pointer to hold the vector_ptr_t if it's not wrapped
114+
# or we can try to use a raw ccall if IGraphs doesn't provide it.
115+
116+
# Let's assume LibIGraph has the type igraph_vector_ptr_t
117+
# Based on typical igraph wrappers, we might need to handle memory manually here.
118+
119+
ptr_vec = Ref{LibIGraph.igraph_vector_ptr_t}()
120+
LibIGraph.igraph_vector_ptr_init(ptr_vec, 0)
121+
122+
try
123+
ret = LibIGraph.igraph_sir(ig.objref, beta, gamma, no_sim, ptr_vec)
124+
_check_ret(ret, "igraph_sir")
125+
126+
n_sim = LibIGraph.igraph_vector_ptr_size(ptr_vec)
127+
results = Vector{Vector{Float64}}(undef, n_sim)
128+
129+
for i in 1:n_sim
130+
# Each element is an igraph_vector_t*
131+
v_ptr = LibIGraph.igraph_vector_ptr_get(ptr_vec, i-1)
132+
# We can wrap this in a temporary IGVectorFloat if we know it doesn't own the memory
133+
# or just copy it.
134+
# Assuming LibIGraph.igraph_vector_size(v_ptr) works
135+
v_size = LibIGraph.igraph_vector_size(v_ptr)
136+
res_v = Vector{Float64}(undef, v_size)
137+
for j in 1:v_size
138+
res_v[j] = LibIGraph.igraph_vector_get(v_ptr, j-1)
139+
end
140+
results[i] = res_v
141+
end
142+
return results
143+
finally
144+
# Clean up: destroy all vectors inside and the ptr vector itself
145+
# igraph_vector_ptr_destroy_all calls igraph_vector_destroy and then igraph_free on each element
146+
LibIGraph.igraph_vector_ptr_destroy_all(ptr_vec)
147+
end
148+
end
149+
150+
function modularity_matrix(g::AbstractGraph{U}, ::IGraphAlgorithm; kwargs...) where U<:Integer
151+
ig = igraph(g)
152+
return IGraphs.modularity_matrix(ig; kwargs...)
153+
end
154+
155+
function community_leiden(g::AbstractGraph{U}, ::IGraphAlgorithm; resolution=1.0, beta=0.01, n_iterations=10, kwargs...) where U<:Integer
156+
ig = igraph(g)
157+
membership = IGraphs.IGVectorInt(; _uninitialized=Val(true))
158+
LibIGraph.igraph_vector_int_init(membership.objref, 0)
159+
160+
nb_clusters = Ref{LibIGraph.igraph_int_t}(0)
161+
quality = Ref{LibIGraph.igraph_real_t}(0.0)
162+
163+
ret = LibIGraph.igraph_community_leiden_simple(ig.objref, IGraphs.IGNull().objref, 0, resolution, beta, IGraphs.IGNull().objref, n_iterations, membership.objref, nb_clusters, quality)
164+
_check_ret(ret, "igraph_community_leiden_simple")
165+
return Vector(membership)
166+
end
167+
168+
function layout_kamada_kawai(g::AbstractGraph{U}, ::IGraphAlgorithm; maxiter=100, epsilon=0.0, kkconst=0.0, kwargs...) where U<:Integer
169+
ig = igraph(g)
170+
n = nv(g)
171+
res = IGraphs.IGMatrixFloat(; _uninitialized=Val(true))
172+
LibIGraph.igraph_matrix_init(res.objref, n, 2)
173+
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)
174+
_check_ret(ret, "igraph_layout_kamada_kawai")
175+
return Matrix(res)
176+
end
177+
178+
function layout_fruchterman_reingold(g::AbstractGraph{U}, ::IGraphAlgorithm; niter=500, kwargs...) where U<:Integer
179+
ig = igraph(g)
180+
n = nv(g)
181+
res = IGraphs.IGMatrixFloat(; _uninitialized=Val(true))
182+
LibIGraph.igraph_matrix_init(res.objref, n, 2)
183+
ret = LibIGraph.igraph_layout_fruchterman_reingold(ig.objref, res.objref, false, niter, IGraphs.IGNull().objref, -100.0, 100.0, -100.0, 100.0)
184+
_check_ret(ret, "igraph_layout_fruchterman_reingold")
185+
return Matrix(res)
186+
end
187+
188+
function Graphs.modularity(g::AbstractGraph{U}, membership::Vector{Int}, ::IGraphAlgorithm; weights=nothing, resolution=1.0, kwargs...) where U<:Integer
189+
ig = igraph(g)
190+
m_vec = IGraphs.IGVectorInt(membership .- 1) # igraph uses 0-indexed membership
191+
w = _handle_weights(weights)
192+
res = Ref{LibIGraph.igraph_real_t}(0.0)
193+
ret = LibIGraph.igraph_modularity(ig.objref, m_vec.objref, w.objref, resolution, is_directed(g), res)
194+
_check_ret(ret, "igraph_modularity")
195+
return res[]
196+
end
197+
198+
# --- Dispatch Overrides for Core Algorithms ---
199+
200+
function betweenness_centrality(g::AbstractGraph{U}, ::IGraphAlgorithm; weights=nothing, normalized=true, kwargs...) where U<:Integer
201+
ig = igraph(g)
202+
n = nv(g)
203+
res = IGraphs.IGVectorFloat(; _uninitialized=Val(true))
204+
LibIGraph.igraph_vector_init(res.objref, n)
205+
w = _handle_weights(weights)
206+
207+
ret = LibIGraph.igraph_betweenness(ig.objref, w.objref, res.objref, LibIGraph.igraph_vss_all(), is_directed(g), normalized)
208+
_check_ret(ret, "igraph_betweenness")
209+
210+
return Vector(res)
211+
end
212+
213+
function pagerank(g::AbstractGraph{U}, ::IGraphAlgorithm; damping=0.85, weights=nothing, kwargs...) where U<:Integer
214+
ig = igraph(g)
215+
n = nv(g)
216+
res = IGraphs.IGVectorFloat(; _uninitialized=Val(true))
217+
LibIGraph.igraph_vector_init(res.objref, n)
218+
w = _handle_weights(weights)
219+
220+
val = Ref{LibIGraph.igraph_real_t}(0.0)
221+
# Default behavior: try PRPACK first as it is more robust for small/disconnected graphs
222+
ret = LibIGraph.igraph_pagerank(ig.objref, w.objref, res.objref, val, damping, is_directed(g), LibIGraph.igraph_vss_all(), LibIGraph.IGRAPH_PAGERANK_ALGO_PRPACK, IGraphs.IGNull().objref)
223+
_check_ret(ret, "igraph_pagerank")
224+
return Vector(res)
225+
end
226+
227+
# --- Specialized dispatches for AbstractIGraph ---
228+
# These ensure that IGraph types use C implementations automatically.
229+
230+
Graphs.sir_model(g::AbstractIGraph; kwargs...) = sir_model(g, IGraphAlgorithm(); kwargs...)
231+
Graphs.modularity_matrix(g::AbstractIGraph; kwargs...) = modularity_matrix(g, IGraphAlgorithm(); kwargs...)
232+
Graphs.community_leiden(g::AbstractIGraph; kwargs...) = community_leiden(g, IGraphAlgorithm(); kwargs...)
233+
Graphs.layout_kamada_kawai(g::AbstractIGraph; kwargs...) = layout_kamada_kawai(g, IGraphAlgorithm(); kwargs...)
234+
Graphs.layout_fruchterman_reingold(g::AbstractIGraph; kwargs...) = layout_fruchterman_reingold(g, IGraphAlgorithm(); kwargs...)
235+
Graphs.betweenness_centrality(g::AbstractIGraph; kwargs...) = betweenness_centrality(g, IGraphAlgorithm(); kwargs...)
236+
Graphs.pagerank(g::AbstractIGraph; kwargs...) = pagerank(g, IGraphAlgorithm(); kwargs...)
237+
238+
end # module

src/Experimental/ShortestPaths/ShortestPaths.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import Graphs.Experimental.Traversals:
1616
# LGEnvironment() = new(false, false)
1717
# end
1818

19-
abstract type AbstractGraphResult end
20-
abstract type AbstractGraphAlgorithm end
19+
# Redefinitions removed, now inherited from Graphs
2120

2221
"""
2322
ShortestPathResult <: AbstractGraphResult

src/Graphs.jl

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export
7171
AbstractGraph,
7272
AbstractEdge,
7373
AbstractEdgeIter,
74+
AbstractGraphAlgorithm,
75+
AbstractGraphResult,
7476
Edge,
7577
Graph,
7678
SimpleGraph,
@@ -454,7 +456,17 @@ export
454456

455457
# planarity
456458
is_planar,
457-
planar_maximally_filtered_graph
459+
planar_maximally_filtered_graph,
460+
461+
# igraphs
462+
IGraphAlgorithm,
463+
AbstractIGraph,
464+
igraph,
465+
sir_model,
466+
layout_kamada_kawai,
467+
layout_fruchterman_reingold,
468+
community_leiden,
469+
modularity_matrix
458470
"""
459471
Graphs
460472
@@ -478,6 +490,7 @@ and tutorials are available at the
478490
"""
479491
Graphs
480492
include("interface.jl")
493+
include("igraphs.jl")
481494
include("utils.jl")
482495
include("frozenvector.jl")
483496
include("deprecations.jl")
@@ -582,4 +595,13 @@ include("planarity.jl")
582595
include("spanningtrees/planar_maximally_filtered_graph.jl")
583596

584597
using .LinAlg
598+
599+
function __init__()
600+
Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs
601+
if exc.f in _IGRAPH_REQUIRED_FUNCTIONS
602+
print(io, "\n\nThis function requires the IGraphs.jl package to be loaded.")
603+
end
604+
end
605+
end
606+
585607
end # module

0 commit comments

Comments
 (0)