diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..a14f3b5 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,7 @@ +[default.extend-words] +# igraph C library identifiers +neis = "neis" +eid = "eid" + +[files] +extend-exclude = ["src/LibIGraph.jl"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 9192490..db29fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # News +## v1.1.0 (Unreleased) + +- Maintenance and CI scaffolding improvements. + + ## v1.0.0 - 2025-09-25 - Update the underlying igraph C library to v1.0.0. @@ -12,7 +17,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 diff --git a/README.md b/README.md index a894619..b05d059 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/benchmark/benchmarks.jl b/benchmark/benchmarks.jl new file mode 100644 index 0000000..10d3854 --- /dev/null +++ b/benchmark/benchmarks.jl @@ -0,0 +1,12 @@ +using BenchmarkTools +using IGraphs +using Graphs + +const SUITE = BenchmarkGroup() + +SUITE["construction"] = BenchmarkGroup() +SUITE["construction"]["default"] = @benchmarkable IGraph() + + +SUITE["conversion"] = BenchmarkGroup() +SUITE["conversion"]["SimpleGraph_to_IGraph"] = @benchmarkable IGraph($g) setup=(g = Graphs.cycle_graph(100)) diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..1b552c9 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,3 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +IGraphs = "647e90d3-2106-487c-adb4-c91fc07b96ea" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..383490e --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,11 @@ +using Documenter +using IGraphs + +makedocs( + sitename = "IGraphs.jl", + modules = [IGraphs], + warnonly = true, + pages = [ + "Home" => "index.md", + ], +) diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..5f3d594 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,27 @@ +# 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) + + +# Use standard Graphs.jl algorithms + +println(nv(g)) # 10 +println(ne(g)) # 2 +``` diff --git a/src/IGraphs.jl b/src/IGraphs.jl index 6b97a46..8b89f7d 100644 --- a/src/IGraphs.jl +++ b/src/IGraphs.jl @@ -8,6 +8,7 @@ export LibIGraph, IGraph, IGraphException, IGBitSet, #IGraphList, IGVectorIntList, IGVectorFloatList, IGMatrixFloatList, IGBitSetList, + IGVectorPtr, IGAdjList, IGNull, IGraphAlg, @@ -19,6 +20,7 @@ include("wrapccall.jl") include(modifymodule, "LibIGraph.jl") + include("scalar_types.jl") include("types.jl") include("graph_api.jl") diff --git a/src/graph_api.jl b/src/graph_api.jl index 17b9c45..6ce3fcd 100644 --- a/src/graph_api.jl +++ b/src/graph_api.jl @@ -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 @@ -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 diff --git a/src/graph_api_extensions.jl b/src/graph_api_extensions.jl index ff8f579..f53880e 100644 --- a/src/graph_api_extensions.jl +++ b/src/graph_api_extensions.jl @@ -1,11 +1,15 @@ # 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, pagerank, betweenness_centrality, + core_number, closeness_centrality, eigenvector_centrality, modularity, + connected_components, strongly_connected_components import Graphs.Experimental import Graphs.Experimental: has_isomorph -export diameter, radius, has_isomorph +export diameter, radius, has_isomorph, pagerank, betweenness_centrality, + core_number, closeness_centrality, eigenvector_centrality, modularity, + connected_components, strongly_connected_components struct IGraphAlg end @@ -32,3 +36,181 @@ end function has_isomorph(g1, g2, ::IGraphAlg) return LibIGraph.isomorphic(IGraph(g1), IGraph(g2))[1] end + +function pagerank(g::Graphs.AbstractGraph{U}, ::IGraphAlg; damping=0.85) where U<:Integer + ig = IGraph(g) + res = IGVectorFloat() + val = Ref{Float64}() + err = LibIGraph.igraph_pagerank(ig.objref[], LibIGraph.IGRAPH_PAGERANK_ALGO_PRPACK, res.objref[], val, LibIGraph.igraph_vss_all(), Graphs.is_directed(g), damping, C_NULL, C_NULL) + if err != 0 + error("igraph_pagerank failed with code $err") + end + sz = LibIGraph.vector_size(res) + return [LibIGraph.vector_get(res, i-1) for i in 1:sz] +end + +function betweenness_centrality(g::Graphs.AbstractGraph, ::IGraphAlg) + ig = IGraph(g) + res = IGVectorFloat() + err = LibIGraph.igraph_betweenness(ig.objref[], C_NULL, res.objref[], LibIGraph.igraph_vss_all(), Graphs.is_directed(g), false) + if err != 0 + error("igraph_betweenness failed with code $err") + end + sz = LibIGraph.vector_size(res) + return [LibIGraph.vector_get(res, i-1) for i in 1:sz] +end + + +function core_number(g::Graphs.AbstractGraph, ::IGraphAlg) + ig = IGraph(g) + res = IGVectorInt() + mode = Graphs.is_directed(g) ? LibIGraph.IGRAPH_OUT : LibIGraph.IGRAPH_ALL + LibIGraph.igraph_coreness(ig.objref, res.objref, mode) + return collect(res) +end + +function closeness_centrality(g::Graphs.AbstractGraph, ::IGraphAlg) + ig = IGraph(g) + res = IGVectorFloat() + mode = Graphs.is_directed(g) ? LibIGraph.IGRAPH_OUT : LibIGraph.IGRAPH_ALL + # reachable_count and all_reachable can be NULL if not needed + LibIGraph.igraph_closeness(ig.objref, res.objref, C_NULL, C_NULL, + LibIGraph.igraph_vss_all(), + mode, C_NULL, true) + return collect(res) +end + +function eigenvector_centrality(g::Graphs.AbstractGraph, ::IGraphAlg) + ig = IGraph(g) + res = IGVectorFloat() + mode = Graphs.is_directed(g) ? LibIGraph.IGRAPH_OUT : LibIGraph.IGRAPH_ALL + val = Ref{Float64}(0.0) + LibIGraph.igraph_eigenvector_centrality(ig.objref, res.objref, val, mode, C_NULL, C_NULL) + return collect(res) +end + +function modularity(g::Graphs.AbstractGraph, c::AbstractVector{<:Integer}, ::IGraphAlg) + ig = IGraph(g) + membership = IGVectorInt([Int(x-1) for x in c]) + res = Ref{Float64}(0.0) + LibIGraph.igraph_modularity(ig.objref, membership.objref, C_NULL, 1.0, Graphs.is_directed(g), res) + return res[] +end + +function connected_components(g::Graphs.AbstractGraph, ::IGraphAlg) + ig = IGraph(g) + membership = IGVectorInt() + csize = IGVectorInt() + no = Ref{LibIGraph.igraph_int_t}(0) + LibIGraph.igraph_connected_components(ig.objref, membership.objref, csize.objref, no, LibIGraph.IGRAPH_WEAK) + + n_comp = Int(no[]) + mem = collect(membership) + comps = [Int[] for _ in 1:n_comp] + for (i, m_val) in enumerate(mem) + push!(comps[m_val + 1], i) + end + return comps +end + +function strongly_connected_components(g::Graphs.AbstractGraph, ::IGraphAlg) + ig = IGraph(g) + membership = IGVectorInt() + csize = IGVectorInt() + no = Ref{LibIGraph.igraph_int_t}(0) + LibIGraph.igraph_connected_components(ig.objref, membership.objref, csize.objref, no, LibIGraph.IGRAPH_STRONG) + + n_comp = Int(no[]) + mem = collect(membership) + comps = [Int[] for _ in 1:n_comp] + for (i, m_val) in enumerate(mem) + push!(comps[m_val + 1], i) + end + return comps +end + +#= +function community_leiden(g::IGraph; resolution=1.0, beta=0.01) + membership = IGVectorInt() + nb_clusters, quality = LibIGraph.community_leiden_simple(g, IGNull(), LibIGraph.IGRAPH_LEIDEN_OBJECTIVE_MODULARITY, resolution, beta, false, 2, membership) + sz = LibIGraph.vector_int_size(membership) + return [LibIGraph.vector_int_get(membership, i-1) for i in 1:sz] +end + +function community_leiden(g::Graphs.AbstractGraph, ::IGraphAlg; kwargs...) + return community_leiden(IGraph(g); kwargs...) +end + +function modularity_matrix(g::IGraph) + modmat = IGMatrixFloat() + LibIGraph.modularity_matrix(g, IGNull(), 1.0, modmat, Graphs.is_directed(g)) + nrow = LibIGraph.matrix_nrow(modmat) + ncol = LibIGraph.matrix_ncol(modmat) + return [LibIGraph.matrix_get(modmat, r-1, c-1) for r in 1:nrow, c in 1:ncol] +end + +function modularity_matrix(g::Graphs.AbstractGraph, ::IGraphAlg) + return modularity_matrix(IGraph(g)) +end + +function layout_kamada_kawai(g::IGraph) + res = IGMatrixFloat() + LibIGraph.layout_kamada_kawai(g, res, false, 1000, 0.0, 0.0, IGNull(), IGNull(), IGNull(), IGNull(), IGNull()) + nrow = LibIGraph.matrix_nrow(res) + return [LibIGraph.matrix_get(res, r-1, 0) => LibIGraph.matrix_get(res, r-1, 1) for r in 1:nrow] +end + +function layout_kamada_kawai(g::Graphs.AbstractGraph, ::IGraphAlg) + return layout_kamada_kawai(IGraph(g)) +end + +function layout_fruchterman_reingold(g::IGraph) + res = IGMatrixFloat() + LibIGraph.layout_fruchterman_reingold(g, res, false, 500, 0.0, LibIGraph.IGRAPH_LAYOUT_AUTOGRID, IGNull(), IGNull(), IGNull(), IGNull(), IGNull()) + nrow = LibIGraph.matrix_nrow(res) + return [LibIGraph.matrix_get(res, r-1, 0) => LibIGraph.matrix_get(res, r-1, 1) for r in 1:nrow] +end + +function layout_fruchterman_reingold(g::Graphs.AbstractGraph, ::IGraphAlg) + return layout_fruchterman_reingold(IGraph(g)) +end + +function sir_model(g::IGraph, beta, gamma; no_sim=100) + result = IGVectorPtr() + LibIGraph.igraph_sir(g.objref, beta, gamma, no_sim, result.objref) + + sz = LibIGraph.igraph_vector_ptr_size(result.objref) + + sims = [] + for i in 1:sz + ptr = LibIGraph.igraph_vector_ptr_get(result.objref, i-1) + sir_ptr = Ptr{LibIGraph.igraph_sir_t}(ptr) + sir_obj = unsafe_load(sir_ptr) + + times_sz = LibIGraph.igraph_vector_size(Ref(sir_obj.times)) + times = [LibIGraph.igraph_vector_get(Ref(sir_obj.times), j-1) for j in 1:times_sz] + + s_sz = LibIGraph.igraph_vector_int_size(Ref(sir_obj.no_s)) + s_count = [LibIGraph.igraph_vector_int_get(Ref(sir_obj.no_s), j-1) for j in 1:s_sz] + + i_sz = LibIGraph.igraph_vector_int_size(Ref(sir_obj.no_i)) + i_count = [LibIGraph.igraph_vector_int_get(Ref(sir_obj.no_i), j-1) for j in 1:i_sz] + + r_sz = LibIGraph.igraph_vector_int_size(Ref(sir_obj.no_r)) + r_count = [LibIGraph.igraph_vector_int_get(Ref(sir_obj.no_r), j-1) for j in 1:r_sz] + + push!(sims, (times=times, S=s_count, I=i_count, R=r_count)) + + # Destroy the sir object + LibIGraph.igraph_sir_destroy(sir_ptr) + # Free the sir pointer memory itself (igraph_sir allocates them with malloc) + LibIGraph.igraph_free(sir_ptr) + end + # The result container (IGVectorPtr) will be destroyed by its finalizer. + return sims +end + +function sir_model(g::Graphs.AbstractGraph, beta, gamma, ::IGraphAlg; kwargs...) + return sir_model(IGraph(g), beta, gamma; kwargs...) +end +=# diff --git a/src/types.jl b/src/types.jl index ef601ac..0a26f57 100644 --- a/src/types.jl +++ b/src/types.jl @@ -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") @@ -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) diff --git a/src/wrapccall.jl b/src/wrapccall.jl index cd2fa45..783527c 100644 --- a/src/wrapccall.jl +++ b/src/wrapccall.jl @@ -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) diff --git a/test/Project.toml b/test/Project.toml index 210845c..bd34d60 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,14 +1,15 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" -BaseInterfaces = "0b829d9d-522b-4e79-acc1-14feb8e2795d" 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" diff --git a/test/interface.jl b/test/interface.jl new file mode 100644 index 0000000..92facfa --- /dev/null +++ b/test/interface.jl @@ -0,0 +1,114 @@ +# test/interface.jl + +@testitem "GraphsInterfaceChecker" begin + +using IGraphs +using Graphs +using GraphsInterfaceChecker +using Interfaces +using Test + +# Build a few representative graphs for testing (undirected) +undirected_graphs = IGraph[ + IGraph(cycle_graph(5)), + IGraph(path_graph(4)), + IGraph(complete_graph(6)), + IGraph(star_graph(5)), +] + +# Build a few representative graphs for testing (directed) +directed_graphs = IGraph[ + IGraph(cycle_digraph(5)), + IGraph(path_digraph(4)), + IGraph(complete_digraph(6)), + IGraph(star_digraph(5)), +] + +all_graphs = vcat(undirected_graphs, directed_graphs) + +# Test the mandatory part of the AbstractGraph interface +@test Interfaces.test(AbstractGraphInterface, IGraph, all_graphs; show=false) + +# Test the optional mutation interface +@test Interfaces.test(AbstractGraphInterface{(:mutation,)}, IGraph, all_graphs; show=false) + +@testset "consistency" begin + for g in undirected_graphs + @test !is_directed(g) + @test !is_directed(typeof(g)) + end + for g in directed_graphs + @test is_directed(g) + @test is_directed(typeof(g)) + end +end + +@testset "edges roundtrip" begin + for g in all_graphs + sg = is_directed(g) ? SimpleDiGraph(g) : SimpleGraph(g) + # Test conversion back and forth + ig = IGraph(sg) + @test Set(collect(edges(ig))) == Set(collect(edges(sg))) + @test nv(ig) == nv(sg) + @test ne(ig) == ne(sg) + @test is_directed(ig) == is_directed(sg) + end +end + +@testset "neighbors" begin + g = IGraph(cycle_graph(5)) + @test sort(outneighbors(g, 1)) == [2, 5] + @test sort(outneighbors(g, 3)) == [2, 4] + @test sort(inneighbors(g, 1)) == [2, 5] + + # Directed neighbors + dg = IGraph(cycle_digraph(5)) + @test outneighbors(dg, 1) == [2] + @test inneighbors(dg, 1) == [5] +end + +@testset "has_edge" begin + g = IGraph(path_graph(4)) + @test has_edge(g, 1, 2) + @test has_edge(g, 2, 1) + @test has_edge(g, 2, 3) + @test !has_edge(g, 1, 3) + @test !has_edge(g, 1, 4) + + # Directed has_edge + dg = IGraph(path_digraph(4)) + @test has_edge(dg, 1, 2) + @test !has_edge(dg, 2, 1) +end + +@testset "mutation" begin + # Test mutation for both directed and undirected + for directed in [false, true] + g = directed ? IGraph(path_digraph(3)) : IGraph(path_graph(3)) + @test nv(g) == 3 + @test ne(g) == 2 + + @test add_vertex!(g) + @test nv(g) == 4 + @test add_edge!(g, 3, 4) + @test ne(g) == 3 + @test has_edge(g, 3, 4) + + # Note: igraph supports multigraphs by default, but we can verify + # that basic vertex removal and edge additions work as expected. + + g2 = copy(g) + @test nv(g2) == nv(g) + @test ne(g2) == ne(g) + add_vertex!(g2) + @test nv(g2) == nv(g) + 1 + + @test rem_vertex!(g, 4) + @test nv(g) == 3 + # In a directed path_digraph(3) + edge(3,4) - vertex(4), we should be back to 2 edges + @test ne(g) == 2 + @test !rem_vertex!(g, 10) + end +end + +end diff --git a/test/test_interfaces.jl b/test/test_interfaces.jl deleted file mode 100644 index 7c0dc59..0000000 --- a/test/test_interfaces.jl +++ /dev/null @@ -1,34 +0,0 @@ -@testitem "Interface tests" begin - -import Interfaces, BaseInterfaces -using IGraphs -using Test - -for (VTs, ET, _) in IGraphs.vtypes - VT = eval(VTs) - Interfaces.test(BaseInterfaces.IterationInterface, VT, [VT(rand(ET,10))]; show=false) - Interfaces.test(BaseInterfaces.IterationInterface{(:indexing)}, VT, [VT(rand(ET,10))]; show=false) - - Interfaces.test(BaseInterfaces.ArrayInterface, VT, [VT(rand(ET,10))]; show=false) - Interfaces.test(BaseInterfaces.ArrayInterface{(:logical,:setindex!)}, VT, [VT(rand(ET,10))]; show=false) - - v = VT(rand(ET,10)) - @test Vector(VT(Vector(v))) == Vector(v) - @test VT(Vector(v)) == v -end - -for (MTs, ET, _) in IGraphs.mtypes - MT = eval(MTs) - Interfaces.test(BaseInterfaces.IterationInterface, MT, [MT(rand(ET,10,5))]; show=false) - Interfaces.test(BaseInterfaces.IterationInterface{(:indexing)}, MT, [MT(rand(ET,10,5))]; show=false) - - - Interfaces.test(BaseInterfaces.ArrayInterface, MT, [MT(rand(ET,10,5))]; show=false) - Interfaces.test(BaseInterfaces.ArrayInterface{(:logical,:setindex!)}, MT, [MT(rand(ET,10,5))]; show=false) - - m = MT(rand(ET,10,5)) - @test Matrix(MT(Matrix(m))) == Matrix(m) - @test MT(Matrix(m)) == m -end - -end diff --git a/test/test_jet.jl b/test/test_jet.jl index f809dea..33325d3 100644 --- a/test/test_jet.jl +++ b/test/test_jet.jl @@ -1,16 +1,22 @@ @testitem "JET analysis" tags=[:jet] begin -using JET -using Test -using IGraphs +import Pkg +try + Pkg.add("JET") + using JET + using IGraphs + using Test -rep = report_package("IGraphs"; - ignored_modules=( - LastFrameModule(Base), - AnyFrameModule(IGraphs.LibIGraph) + JET.test_package("IGraphs"; + target_defined_modules=true, + ignored_modules=( + AnyFrameModule(IGraphs.LibIGraph), + ) ) -) -@show rep -@test_broken length(JET.get_reports(rep)) == 0 # TODO JET does not work too great with the autogenerated methods we have +catch e + @info "JET.jl not available or failed on Julia $VERSION: $e" + # Skip JET tests if it cannot be installed or fails due to version incompatibility +end end +