Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[default.extend-words]
# igraph C library identifiers
neis = "neis"
eid = "eid"

[files]
extend-exclude = ["src/LibIGraph.jl"]
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# News

## v1.1.0 - 2026-03-27

- Refactored `IGraph` into a parametric type `IGraph{Directed}` to ensure `is_directed(typeof(g)) == is_directed(g)`.
- Full compliance with `Graphs.jl`'s `AbstractGraph` interface, verified via `GraphsInterfaceChecker.jl`.
- Fixed `MethodError` in `connected_components` and `strongly_connected_components` by using correct `LibIGraph` enums.
- Added `IGVectorPtr` support for low-level C bindings.
- Improved internal constructor to handle uninitialized state more robustly.

## v1.0.0 - 2025-09-25

- Update the underlying igraph C library to v1.0.0.
Expand All @@ -12,7 +20,8 @@

## v0.10.17 - 2025-06-29

- `IGNull` is introduced as a convenient placehold argument for when the low-level C function expects a `NULL` as a default.
- `IGNull` is introduced as a convenient placeholder argument for when the low-level C function expects a `NULL` as a default.


## v0.10.16 - 2025-04-21

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ By default, all of these types are initialized, but empty. If you want to create

### Alternatives to Graphs.jl algorithms

Some Graphs.jl functions have new methods defined here, which provide an alternative implementation for the given algorithm. E.g. `Graphs.diameter(graph)` runs a Julia-native implementation of that algorithm from `Graphs.jl`. Here we add the method `diamater(graph, ::IGraphAlg)` which converts `graph` to an `IGraph` type and runs the corresponding algorithm from the `igraph` C library.
Some Graphs.jl functions have new methods defined here, which provide an alternative implementation for the given algorithm. E.g. `Graphs.diameter(graph)` runs a Julia-native implementation of that algorithm from `Graphs.jl`. Here we add the method `diameter(graph, ::IGraphAlg)` which converts `graph` to an `IGraph` type and runs the corresponding algorithm from the `igraph` C library.


Dispatch to these new methods happens by adding an instance of the `IGraphAlg` type.

Expand Down
12 changes: 12 additions & 0 deletions benchmark/benchmarks.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using BenchmarkTools
using IGraphs
using Graphs

const SUITE = BenchmarkGroup()

SUITE["construction"] = BenchmarkGroup()
SUITE["construction"]["undirected_100"] = @benchmarkable IGraph(100, false)
SUITE["construction"]["directed_100"] = @benchmarkable IGraph(100, true)

SUITE["conversion"] = BenchmarkGroup()
SUITE["conversion"]["SimpleGraph_to_IGraph"] = @benchmarkable IGraph($g) setup=(g = Graphs.cycle_graph(100))
3 changes: 3 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
IGraphs = "647e90d3-2106-487c-adb4-c91fc07b96ea"
11 changes: 11 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Documenter
using IGraphs

makedocs(
sitename = "IGraphs.jl",
modules = [IGraphs],
warnonly = true,
pages = [
"Home" => "index.md",
],
)
29 changes: 29 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# IGraphs.jl

A Julia wrapper for the [igraph](https://igraph.org/) C library, providing high-performance graph algorithms through the [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl) interface.

## Installation

```julia
using Pkg
Pkg.add("IGraphs")
```

## Quick Start

```julia
using IGraphs, Graphs

# Create an undirected graph
g = IGraph(10)
add_edge!(g, 1, 2)
add_edge!(g, 2, 3)

# Create a directed graph
dg = IGraph(10, true)
add_edge!(dg, 1, 2)

# Use standard Graphs.jl algorithms
println(nv(g)) # 10
println(ne(g)) # 2
```
10 changes: 9 additions & 1 deletion src/IGraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ const last_thrown_error_ref = Ref{Any}() # TODO make this thread safe

include("wrapccall.jl")

include(modifymodule, "LibIGraph.jl")
# NOTE: We avoid `include(modifymodule, "LibIGraph.jl")` because Revise.jl
# (used by JET.jl) does not support the two-argument `include(mapexpr, path)` form
# and throws a "Bad include call" error during static analysis.
let _path = joinpath(@__DIR__, "LibIGraph.jl")
_code = read(_path, String)
_expr = Meta.parse("begin $(_code) end")
_mod_expr = modifymodule(_expr.args[2]) # The module expression is inside the begin-end block
Core.eval(@__MODULE__, _mod_expr)
end

include("scalar_types.jl")
include("types.jl")
Expand Down
139 changes: 127 additions & 12 deletions src/graph_api.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
function IGraph(n::Integer)
g = IGraph(_uninitialized=Val(true))
LibIGraph.empty(g,n,false)
function IGraph(n::Integer, directed::Bool=false)
g = IGraph(_uninitialized=Val(true), directed=directed)
LibIGraph.empty(g, n, directed)
return g
end

Expand All @@ -23,25 +23,140 @@ end

function IGraph(g::Graphs.AbstractSimpleGraph)
n = Graphs.nv(g)
ig = IGraph(n)
for (;src,dst) in Graphs.edges(g)
LibIGraph.add_edge(ig, src-1, dst-1)
directed = Graphs.is_directed(g)
edges_vec = LibIGraph.igraph_int_t[]
sizehint!(edges_vec, 2 * Graphs.ne(g))
for e in Graphs.edges(g)
push!(edges_vec, Graphs.src(e) - 1)
push!(edges_vec, Graphs.dst(e) - 1)
end
vint = IGVectorInt(edges_vec)
ig = IGraph(_uninitialized=Val(true), directed=directed)
LibIGraph.igraph_create(ig.objref, vint.objref, n, directed)
return ig
end


Base.eltype(::IGraph) = LibIGraph.igraph_int_t
Base.zero(::Type{IGraph}) = IGraph(0)
# Graphs.edges # TODO
struct IGraphEdgeIterator
g::IGraph
end
Base.length(it::IGraphEdgeIterator) = Graphs.ne(it.g)
Base.eltype(::Type{IGraphEdgeIterator}) = Graphs.SimpleGraphs.SimpleEdge{Int}
function Base.iterate(it::IGraphEdgeIterator, state=0)
state >= Graphs.ne(it.g) && return nothing
from, to = LibIGraph.edge(it.g, state)
return (Graphs.SimpleGraphs.SimpleEdge(from + 1, to + 1), state + 1)
end
Graphs.edges(g::IGraph) = IGraphEdgeIterator(g)
Graphs.edgetype(g::IGraph) = Graphs.SimpleGraphs.SimpleEdge{eltype(g)} # TODO maybe expose the edge id information from IGraph
Graphs.has_edge(g::IGraph,s,d) = LibIGraph.get_eid(g,s,d,false,false)[1]!=-1
function Graphs.has_edge(g::IGraph, s::Integer, d::Integer)
(s < 1 || s > Graphs.nv(g) || d < 1 || d > Graphs.nv(g)) && return false
eid = Ref{Int}(-1)
LibIGraph.igraph_get_eid(g.objref, eid, s-1, d-1, Graphs.is_directed(g), false)
return eid[] != -1
end
Graphs.has_vertex(g::IGraph,n::Integer) = 1≤n≤Graphs.nv(g)
# Graphs.inneighbors # TODO
Graphs.is_directed(::Type{IGraph}) = false # TODO support directed graphs
function Graphs.inneighbors(g::IGraph, v::Integer)
neis = IGVectorInt()
LibIGraph.neighbors(g, neis, v-1, LibIGraph.IGRAPH_IN, LibIGraph.IGRAPH_LOOPS, true)
return [LibIGraph.vector_int_get(neis, i-1) + 1 for i in 1:LibIGraph.vector_int_size(neis)]
end

Graphs.is_directed(g::IGraph{Directed}) where Directed = Directed
Graphs.is_directed(::Type{<:IGraph{Directed}}) where Directed = Directed

Graphs.ne(g::IGraph) = LibIGraph.ecount(g)
Graphs.nv(g::IGraph) = LibIGraph.vcount(g)
# Graphs.outneighbors # TODO

function Graphs.outneighbors(g::IGraph, v::Integer)
neis = IGVectorInt()
LibIGraph.neighbors(g, neis, v-1, LibIGraph.IGRAPH_OUT, LibIGraph.IGRAPH_LOOPS, true)
return [LibIGraph.vector_int_get(neis, i-1) + 1 for i in 1:LibIGraph.vector_int_size(neis)]
end
Graphs.vertices(g::IGraph) = 1:Graphs.nv(g)

Graphs.add_edge!(g::IGraph, e::Graphs.SimpleGraphEdge) = LibIGraph.add_edge(g,e.src-1,e.dst-1)
Graphs.add_edge!(g::IGraph, e::Graphs.AbstractEdge) = Graphs.add_edge!(g, Graphs.src(e), Graphs.dst(e))
Graphs.add_edge!(g::IGraph, s::Integer, d::Integer) = (LibIGraph.igraph_add_edge(g.objref, s-1, d-1) == 0)

function Graphs.rem_edge!(g::IGraph, s::Integer, d::Integer)
eid = Ref{Int}(-1)
LibIGraph.igraph_get_eid(g.objref, eid, s-1, d-1, Graphs.is_directed(g), false)
eid[] == -1 && return false
es = Ref{LibIGraph.igraph_es_t}()
LibIGraph.igraph_es_1(es, eid[])
return LibIGraph.igraph_delete_edges(g.objref, es[]) == 0
end
Graphs.rem_edge!(g::IGraph, e::Graphs.AbstractEdge) = Graphs.rem_edge!(g, Graphs.src(e), Graphs.dst(e))

function Graphs.add_vertex!(g::IGraph)
LibIGraph.igraph_add_vertices(g.objref, 1, C_NULL)
return true
end

function Graphs.add_vertices!(g::IGraph, n::Integer)
LibIGraph.igraph_add_vertices(g.objref, n, C_NULL)
return n
end

function Graphs.rem_vertex!(g::IGraph, v::Integer)
(v < 1 || v > Graphs.nv(g)) && return false
vs = Ref{LibIGraph.igraph_vs_t}()
LibIGraph.igraph_vs_1(vs, v-1)
return LibIGraph.igraph_delete_vertices(g.objref, vs[]) == 0
end

function Graphs.rem_vertices!(g::IGraph, vs::AbstractVector)
# Convert Julia 1-based indices to 0-based
vint = IGVectorInt([Int(v-1) for v in vs])
igraph_vs = Ref{LibIGraph.igraph_vs_t}()
LibIGraph.igraph_vs_vector(igraph_vs, vint.objref)
res = LibIGraph.igraph_delete_vertices(g.objref, igraph_vs[])
return res == 0
end

Graphs.neighbors(g::IGraph, v::Integer) = Graphs.outneighbors(g, v)

function Graphs.all_neighbors(g::IGraph, v::Integer)
neis = IGVectorInt()
LibIGraph.neighbors(g, neis, v-1, LibIGraph.IGRAPH_ALL, LibIGraph.IGRAPH_LOOPS, true)
return [LibIGraph.vector_int_get(neis, i-1) + 1 for i in 1:LibIGraph.vector_int_size(neis)]
end

function Graphs.degree(g::IGraph, v::Integer)
return LibIGraph.degree(g, v-1, LibIGraph.IGRAPH_ALL, LibIGraph.IGRAPH_LOOPS)[1]
end

function Graphs.indegree(g::IGraph, v::Integer)
return LibIGraph.degree(g, v-1, LibIGraph.IGRAPH_IN, LibIGraph.IGRAPH_LOOPS)[1]
end

function Graphs.outdegree(g::IGraph, v::Integer)
return LibIGraph.degree(g, v-1, LibIGraph.IGRAPH_OUT, LibIGraph.IGRAPH_LOOPS)[1]
end

function Graphs.has_self_loops(g::IGraph)
res = Ref{LibIGraph.igraph_bool_t}()
LibIGraph.igraph_has_loop(g.objref, res)
return Bool(res[])
end

function Graphs.num_self_loops(g::IGraph)
count = 0
# This is slow, but correct for now. igraph might have a better way but it's not obvious.
for e in Graphs.edges(g)
if Graphs.src(e) == Graphs.dst(e)
count += 1
end
end
return count
end

function Base.copy(g::IGraph{Directed}) where Directed
# igraph_copy is not wrapped in LibIGraph.jl in a high-level way usually,
# but we can use the raw one and wrap it.
new_g = IGraph(_uninitialized=Val(true), directed=Directed)
LibIGraph.igraph_copy(new_g.objref, g.objref)
return new_g
end
43 changes: 32 additions & 11 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ function initializer(ctype)
:(LibIGraph.$(Symbol(sname[1:end-1],"init"))(cinstance, 0))
elseif startswith(sname, "igraph_adjlist")
:(LibIGraph.$(Symbol(sname[1:end-1],"init_empty"))(cinstance, 0))
elseif startswith(sname, "igraph_vector_ptr")
:(LibIGraph.$(Symbol(sname[1:end-1],"init"))(cinstance, 0))
elseif startswith(sname, "igraph_vector")
:(LibIGraph.$(Symbol(sname[1:end-1],"init"))(cinstance, 0))
elseif startswith(sname, "igraph_matrix")
Expand Down Expand Up @@ -44,20 +46,39 @@ const parent_types = Dict(
for (ptr_ctype, jtype) in pairs(wrappedtypes)
ctype = ptr_ctype.args[2]
ptype = get(parent_types, jtype, Any)
expr = quote
struct $jtype <: $ptype
objref::Ref{LibIGraph.$ctype}
if jtype == :IGraph
expr = quote
struct $jtype{Directed} <: $ptype
objref::Ref{LibIGraph.$ctype}
end
function $jtype(;_uninitialized::Val{B}=Val(false), directed::Bool=false) where {B}
cinstance = Ref{LibIGraph.$ctype}()
finalizer(cinstance) do cinstance
LibIGraph.$(Symbol(string(ctype)[1:end-1],"destroy"))(cinstance)
cinstance
end
if !B
LibIGraph.igraph_empty(cinstance, 0, directed)
end
return $jtype{directed}(cinstance)
end
end
function $jtype(;_uninitialized::Val{B}=Val(false)) where {B}
cinstance = Ref{LibIGraph.$ctype}()
finalizer(cinstance) do cinstance
LibIGraph.$(Symbol(string(ctype)[1:end-1],"destroy"))(cinstance)
cinstance
else
expr = quote
struct $jtype <: $ptype
objref::Ref{LibIGraph.$ctype}
end
if !B
$(initializer(ctype))
function $jtype(;_uninitialized::Val{B}=Val(false)) where {B}
cinstance = Ref{LibIGraph.$ctype}()
finalizer(cinstance) do cinstance
LibIGraph.$(Symbol(string(ctype)[1:end-1],"destroy"))(cinstance)
cinstance
end
if !B
$(initializer(ctype))
end
return $jtype(cinstance)
end
return $jtype(cinstance)
end
end
eval(expr)
Expand Down
1 change: 1 addition & 0 deletions src/wrapccall.jl
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const wrappedtypes = Dict(
:(Ptr{igraph_vector_list_t})=>:IGVectorFloatList,
:(Ptr{igraph_matrix_list_t})=>:IGMatrixFloatList,
:(Ptr{igraph_bitset_list_t})=>:IGBitSetList,
:(Ptr{igraph_vector_ptr_t})=>:IGVectorPtr,
:(Ptr{igraph_adjlist_t})=>:IGAdjList,
)
const permittedinputtypes = nativetypes ∪ keys(wrappedtypes)
Expand Down
3 changes: 2 additions & 1 deletion test/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
GraphsInterfaceChecker = "3bef136c-15ff-4091-acbb-1a4aafe67608"
IGraphs = "647e90d3-2106-487c-adb4-c91fc07b96ea"
Interfaces = "85a1e053-f937-4924-92a5-1367d23b7b87"
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Expand Down
19 changes: 19 additions & 0 deletions test/interface.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@testitem "GraphsInterfaceChecker" begin
using Graphs
using IGraphs
using Interfaces
using GraphsInterfaceChecker

g = IGraph(5)
Graphs.add_edge!(g, 1, 2)
Graphs.add_edge!(g, 2, 3)

@test Interfaces.test(AbstractGraphInterface, IGraph, [g])

# Directed graph test
dg = IGraph(5, true)
Graphs.add_edge!(dg, 1, 2)
Graphs.add_edge!(dg, 2, 3)
@test Graphs.is_directed(dg)
@test Interfaces.test(AbstractGraphInterface, IGraph, [dg])
end
Loading
Loading