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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,18 @@ _LightGraphs.jl_ and _Graphs.jl_ are functionally identical, still there are som
- There was also an older package also called _Graphs.jl_ (git tags `v0.2.5` through `v0.10.3`), but the current code base here is a fork of _LightGraphs.jl_ v1.3.5.
- All older _LightGraphs.jl_ versions are tagged using the naming scheme `lg-vX.Y.Z` rather than plain `vX.Y.Z`, which is used for old _Graphs.jl_ versions (≤ v0.10) and newer versions derived from _LightGraphs.jl_ but released with the _Graphs.jl_ name (≥ v1.4).
- If you are using a version of Julia prior to 1.x, then you should use _LightGraphs.jl_ at `lg-v.12.*` or _Graphs.jl_ at `v0.10.3`

## Graphs.jl + igraph support

Graphs.jl now works with the igraph C library through the IGraphs.jl package.
Just load the compatibility layer:

```julia
using Graphs, IGraphs
using IGraphs.GraphsCompat # enable Graphs.jl interface for igraph
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not believe that a separate interface module is necessary. Julia is a multiple dispatch language. You just need to add a new method to the existing functions, nothing more. We already have an IGraph type (that might need to be improved), but new graph types should not be needed.


g = Graphs.grid([5,5]) # Graphs.jl graph
ig = GraphsCompat.IGSimpleGraph(g) # igraph-backed graph

Graphs.nv(ig) # number of vertices
Graphs.connected_components(ig) # runs igraph’s fast implementation
2 changes: 1 addition & 1 deletion src/Graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ include("interface.jl")
include("utils.jl")
include("deprecations.jl")
include("core.jl")

include("igraph_stubs.jl")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these should not be igraph specific. There are many other libraries that implement interesting algorithms and we are implementing extensions for them. If an algorithm does not exist in Graphs, a function should be declared in the appropriate location for that algorithm (without any methods attached to that function).

E.g. if we want coreness, the function declaration should be added to the appropriate place for the concept of coreness, not to an igraph specific file, as another library might also implement it.

Let's not export any of these though, so they are not available by default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on further consideration, I see the point of organizing things the way you did, with everything in a single file if it does not have implementation outside of igraph.

Maybe we can structure it in a simpler way that automates how the hints are added too. Something like this:

"""
These are algorithms available in external wrapper libraries (like `igraph`/`LEMON`/`networkx`) but not available in any Julia-native libraries in the JuliaGraphs ecosystem.

They are all listed in this single dictionary as a convenient way to automatically add nice error hints directing users to the appropriate wrapper library.

We are encouraging Julia-native implementations and would be happy to help any new contributors implement Julia versions of these algorithms -- simply go ahead and submit a PR to Graphs.jl.
When such a contribution is made, we can remove the contributed algorithm from this dict.
"""
const EXTERNAL_STUBS = Dict(
:function_name => (
docstring=""" yada yada yada

yada yada
""",
libs=[:IGraphs]
),
...
)

And then we can just have an eval that declares the needed functions.

include("SimpleGraphs/SimpleGraphs.jl")
using .SimpleGraphs
"""
Expand Down
332 changes: 332 additions & 0 deletions src/igraph_stubs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
#### Optional APIs backed by external packages (e.g., IGraphs.jl)
#### These functions are PUBLIC names that Graphs.jl exposes, but by default
#### they throw a helpful error until a backend (IGraphs.jl) provides methods.

# Friendly message builder
_igraph_backend_msg(fname) = """
`$(fname)` is not implemented in base Graphs.jl.
It is available via the **IGraphs.jl** backend.

Enable it and re-run:

using IGraphs, IGraphs.GraphsCompat
$(fname)(g, IGraphAlgorithm())

"""

# -----------------------
# Assortativity
# -----------------------
@static if !isdefined(@__MODULE__, :assortativity)
"""
assortativity(g::AbstractGraph, attr; normalized::Bool=true)

Degree- or attribute-based assortativity coefficient.

!!! note "Availability"
Backend-only: use **IGraphs.jl** (see message if not loaded).
"""
function assortativity end
assortativity(g::AbstractGraph, attr; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:assortativity)))
end

@static if !isdefined(@__MODULE__, :assortativity_degree)
"""
assortativity_degree(g::AbstractGraph; normalized::Bool=true)

Degree assortativity (scalar coefficient).
"""
function assortativity_degree end
assortativity_degree(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:assortativity_degree)))
end

@static if !isdefined(@__MODULE__, :assortativity_nominal)
"""
assortativity_nominal(g::AbstractGraph, attr)

Nominal/categorical assortativity based on a vertex attribute.
"""
function assortativity_nominal end
assortativity_nominal(g::AbstractGraph, attr; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:assortativity_nominal)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably should just throw the default MethodError, not have a custom error defined for each method. A big drawback of the current approach you have here is the significant extra compilation and then invalidation on importing other packages.

In julia it is idiomatic to rely on MethodErrors to mark unimplemented APIs. Then you can add error hints to be more helpful to the user. All of these should probably be converted to error hints, but let's leave that to another PR -- for the moment we can just focus on removing all these default methods.

end

# -----------------------
# Coreness / k-core number
# -----------------------
@static if !isdefined(@__MODULE__, :coreness)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand the point of these if isdefined.

"""
coreness(g::AbstractGraph)

Return the k-core number (coreness) per vertex.
"""
function coreness end
coreness(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:coreness)))
end

# -----------------------
# Connectivity & disjoint paths
# -----------------------
@static if !isdefined(@__MODULE__, :edge_connectivity)
"""
edge_connectivity(g::AbstractGraph)

Global edge connectivity (minimum edge cut size).
"""
function edge_connectivity end
edge_connectivity(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:edge_connectivity)))
end

@static if !isdefined(@__MODULE__, :vertex_connectivity)
"""
vertex_connectivity(g::AbstractGraph)

Global vertex connectivity (minimum vertex cut size).
"""
function vertex_connectivity end
vertex_connectivity(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:vertex_connectivity)))
end

@static if !isdefined(@__MODULE__, :edge_disjoint_paths)
"""
edge_disjoint_paths(g::AbstractGraph, s::Integer, t::Integer)

Maximum number of edge-disjoint s–t paths.
"""
function edge_disjoint_paths end
edge_disjoint_paths(g::AbstractGraph, s::Integer, t::Integer; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:edge_disjoint_paths)))
end

@static if !isdefined(@__MODULE__, :vertex_disjoint_paths)
"""
vertex_disjoint_paths(g::AbstractGraph, s::Integer, t::Integer)

Maximum number of vertex-disjoint s–t paths.
"""
function vertex_disjoint_paths end
vertex_disjoint_paths(g::AbstractGraph, s::Integer, t::Integer; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:vertex_disjoint_paths)))
end

# -----------------------
# Cuts & cut trees
# -----------------------
@static if !isdefined(@__MODULE__, :gomory_hu_tree)
"""
gomory_hu_tree(g::AbstractGraph; capacity=:weight)

Compute the Gomory–Hu cut tree (all-pairs min-cuts).
"""
function gomory_hu_tree end
gomory_hu_tree(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:gomory_hu_tree)))
end

@static if !isdefined(@__MODULE__, :minimum_size_separators)
"""
minimum_size_separators(g::AbstractGraph)

Enumerate minimum cardinality vertex separators.
"""
function minimum_size_separators end
minimum_size_separators(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:minimum_size_separators)))
end

# -----------------------
# Isomorphism, VF2, automorphisms
# -----------------------
@static if !isdefined(@__MODULE__, :isomorphic)
"""
isomorphic(g1::AbstractGraph, g2::AbstractGraph)

Graph isomorphism test (e.g., VF2 backend).
"""
function isomorphic end
isomorphic(g1::AbstractGraph, g2::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:isomorphic)))
end

@static if !isdefined(@__MODULE__, :subgraph_isomorphisms_vf2)
"""
subgraph_isomorphisms_vf2(pat::AbstractGraph, g::AbstractGraph)

Find (all) VF2 subgraph isomorphisms of `pat` in `g`.
"""
function subgraph_isomorphisms_vf2 end
subgraph_isomorphisms_vf2(pat::AbstractGraph, g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:subgraph_isomorphisms_vf2)))
end

@static if !isdefined(@__MODULE__, :count_automorphisms)
"""
count_automorphisms(g::AbstractGraph)

Count graph automorphisms (backend dependent).
"""
function count_automorphisms end
count_automorphisms(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:count_automorphisms)))
end

# -----------------------
# Graphlets & motifs
# -----------------------
@static if !isdefined(@__MODULE__, :graphlets)
"""
graphlets(g::AbstractGraph; k=5)

Graphlet counts / signatures up to size `k`.
"""
function graphlets end
graphlets(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:graphlets)))
end

@static if !isdefined(@__MODULE__, :motifs)
"""
motifs(g::AbstractGraph; size)

Motif (small subgraph) counts for a given size.
"""
function motifs end
motifs(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:motifs)))
end

# -----------------------
# Line graph, spectral embeddings, modularity
# -----------------------
@static if !isdefined(@__MODULE__, :linegraph)
"""
linegraph(g::AbstractGraph)

Construct the line graph of `g`.
"""
function linegraph end
linegraph(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:linegraph)))
end

@static if !isdefined(@__MODULE__, :adjacency_spectral_embedding)
"""
adjacency_spectral_embedding(g::AbstractGraph; nev, which=:LM)

Compute an adjacency spectral embedding (ASE).
"""
function adjacency_spectral_embedding end
adjacency_spectral_embedding(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:adjacency_spectral_embedding)))
end

@static if !isdefined(@__MODULE__, :modularity_matrix)
"""
modularity_matrix(g::AbstractGraph; weights=:weight)

Return the modularity matrix (backend dependent).
"""
function modularity_matrix end
modularity_matrix(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:modularity_matrix)))
end

# -----------------------
# Clustering (transitivity) & strength
# -----------------------
@static if !isdefined(@__MODULE__, :transitivity_avglocal_undirected)
"""
transitivity_avglocal_undirected(g::AbstractGraph)

Average local clustering coefficient (undirected).
"""
function transitivity_avglocal_undirected end
transitivity_avglocal_undirected(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:transitivity_avglocal_undirected)))
end

@static if !isdefined(@__MODULE__, :transitivity_local_undirected)
"""
transitivity_local_undirected(g::AbstractGraph)

Local clustering coefficients per vertex (undirected).
"""
function transitivity_local_undirected end
transitivity_local_undirected(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:transitivity_local_undirected)))
end

@static if !isdefined(@__MODULE__, :strength)
"""
strength(g::AbstractGraph; mode=:out, weights=:weight)

Weighted degree ("strength") per vertex.
"""
function strength end
strength(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:strength)))
end

# -----------------------
# Independent sets, neighborhood graphs
# -----------------------
@static if !isdefined(@__MODULE__, :independence_number)
"""
independence_number(g::AbstractGraph)

Size of a maximum independent set.
"""
function independence_number end
independence_number(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:independence_number)))
end

@static if !isdefined(@__MODULE__, :independent_vertex_sets)
"""
independent_vertex_sets(g::AbstractGraph)

Enumerate maximal (or all) independent sets (backend dependent).
"""
function independent_vertex_sets end
independent_vertex_sets(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:independent_vertex_sets)))
end

@static if !isdefined(@__MODULE__, :neighborhood_graphs)
"""
neighborhood_graphs(g::AbstractGraph, vset; order=1, mode=:all)

Induced neighborhood graphs around `vset`.
"""
function neighborhood_graphs end
neighborhood_graphs(g::AbstractGraph, vset; kwargs...) =
throw(ArgumentError(_igraph_backend_msg(:neighborhood_graphs)))
end

# -----------------------
# Layouts (visualization helpers)
# (Backends often provide: circle, kk, fr, drl, graphopt, grid, lgl, mds, random, reingold_tilford, sugiyama)
# -----------------------
for fname in (
:layout_circle, :layout_kamada_kawai, :layout_fruchterman_reingold, :layout_drl,
:layout_graphopt, :layout_grid, :layout_lgl, :layout_mds,
:layout_random, :layout_reingold_tilford, :layout_sugiyama
)
@eval begin
@static if !isdefined(@__MODULE__, $fname)
"""
$(Symbol($fname))(g::AbstractGraph; kwargs...)

Compute a 2D/3D layout embedding (backend dependent).
"""
function $(Symbol(fname)) end
$(Symbol(fname))(g::AbstractGraph; kwargs...) =
throw(ArgumentError(_igraph_backend_msg($fname)))
end
end
end