diff --git a/.github/workflows/multidocs.yml b/.github/workflows/multidocs.yml index 140c184fc..46418813d 100644 --- a/.github/workflows/multidocs.yml +++ b/.github/workflows/multidocs.yml @@ -61,16 +61,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} - # Build tutorials - - name: Install dependencies for tutorials - run: julia --project=tutorials/docs/ -e ' - using Pkg; - pkg"dev ./GraphNeuralNetworks ./GNNlib ./GNNGraphs"; - Pkg.instantiate(); - include("tutorials/docs/make.jl")' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # Build and deploy multidocs - name: Install dependencies for multidocs diff --git a/GNNGraphs/docs/make.jl b/GNNGraphs/docs/make.jl index 4b61dcd82..94cecf6bf 100644 --- a/GNNGraphs/docs/make.jl +++ b/GNNGraphs/docs/make.jl @@ -1,6 +1,7 @@ using Documenter using DocumenterInterLinks using GNNGraphs +using MLUtils # this is needed by setdocmeta! import Graphs using Graphs: induced_subgraph diff --git a/GNNGraphs/docs/src/api/gnngraph.md b/GNNGraphs/docs/src/api/gnngraph.md index 088d059a4..47b4040a2 100644 --- a/GNNGraphs/docs/src/api/gnngraph.md +++ b/GNNGraphs/docs/src/api/gnngraph.md @@ -1,5 +1,6 @@ ```@meta CurrentModule = GNNGraphs +CollapsedDocStrings = true ``` # GNNGraph @@ -10,12 +11,6 @@ Besides the methods documented here, one can rely on the large set of functional given by [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl) thanks to the fact that `GNNGraph` inherits from `Graphs.AbstractGraph`. -## Index - -```@index -Order = [:type, :function] -Pages = ["gnngraph.md"] -``` ## GNNGraph type @@ -36,7 +31,7 @@ Private = false ```@autodocs Modules = [GNNGraphs] -Pages = ["query.jl"] +Pages = ["src/query.jl"] Private = false ``` @@ -48,7 +43,7 @@ Graphs.neighbors(::GNNGraph, ::Integer) ```@autodocs Modules = [GNNGraphs] -Pages = ["transform.jl"] +Pages = ["src/transform.jl"] Private = false ``` @@ -63,17 +58,16 @@ GNNGraphs.color_refinement ```@autodocs Modules = [GNNGraphs] -Pages = ["generate.jl"] +Pages = ["src/generate.jl"] Private = false Filter = t -> typeof(t) <: Function && t!=rand_temporal_radius_graph && t!=rand_temporal_hyperbolic_graph - ``` ## Operators ```@autodocs Modules = [GNNGraphs] -Pages = ["operators.jl"] +Pages = ["src/operators.jl"] Private = false ``` @@ -85,7 +79,7 @@ Base.intersect ```@autodocs Modules = [GNNGraphs] -Pages = ["sampling.jl"] +Pages = ["src/sampling.jl"] Private = false ``` diff --git a/GNNGraphs/docs/src/api/heterograph.md b/GNNGraphs/docs/src/api/heterograph.md index 3734d757b..18a5562e2 100644 --- a/GNNGraphs/docs/src/api/heterograph.md +++ b/GNNGraphs/docs/src/api/heterograph.md @@ -1,17 +1,40 @@ +```@meta +CurrentModule = GNNGraphs +CollapsedDocStrings = true +``` + # Heterogeneous Graphs ## GNNHeteroGraph Documentation page for the type `GNNHeteroGraph` representing heterogeneous graphs, where nodes and edges can have different types. - ```@autodocs Modules = [GNNGraphs] Pages = ["gnnheterograph.jl"] Private = false ``` -```@docs -Graphs.has_edge(::GNNHeteroGraph, ::Tuple{Symbol, Symbol, Symbol}, ::Integer, ::Integer) +## Query + +```@autodocs +Modules = [GNNGraphs] +Pages = ["gnnheterograph/query.jl"] +Private = false +``` + +## Transform + +```@autodocs +Modules = [GNNGraphs] +Pages = ["gnnheterograph/transform.jl"] +Private = false ``` +## Generate + +```@autodocs +Modules = [GNNGraphs] +Pages = ["gnnheterograph/generate.jl"] +Private = false +``` diff --git a/GNNGraphs/docs/src/api/samplers.md b/GNNGraphs/docs/src/api/samplers.md index 0f3a7e56b..a98637433 100644 --- a/GNNGraphs/docs/src/api/samplers.md +++ b/GNNGraphs/docs/src/api/samplers.md @@ -1,12 +1,10 @@ ```@meta CurrentModule = GNNGraphs +CollapsedDocStrings = true ``` # Samplers - -## Docs - ```@autodocs Modules = [GNNGraphs] Pages = ["samplers.jl"] diff --git a/GNNGraphs/docs/src/api/temporalgraph.md b/GNNGraphs/docs/src/api/temporalgraph.md index c9992e7c4..dbe5c4899 100644 --- a/GNNGraphs/docs/src/api/temporalgraph.md +++ b/GNNGraphs/docs/src/api/temporalgraph.md @@ -1,3 +1,8 @@ +```@meta +CurrentModule = GNNGraphs +CollapsedDocStrings = true +``` + # Temporal Graphs ## TemporalSnapshotsGNNGraph @@ -10,7 +15,7 @@ Pages = ["temporalsnapshotsgnngraph.jl"] Private = false ``` -## TemporalSnapshotsGNNGraph random generators +## Random Generators ```@docs rand_temporal_radius_graph diff --git a/GNNGraphs/docs/src/guides/heterograph.md b/GNNGraphs/docs/src/guides/heterograph.md index 7999e2a48..525a64474 100644 --- a/GNNGraphs/docs/src/guides/heterograph.md +++ b/GNNGraphs/docs/src/guides/heterograph.md @@ -1,3 +1,7 @@ +```@meta +CurrentModule = GNNGraphs +``` + # Heterogeneous Graphs Heterogeneous graphs (also called heterographs), are graphs where each node has a type, @@ -67,18 +71,15 @@ julia> g.num_edges Dict{Tuple{Symbol, Symbol, Symbol}, Int64} with 1 entry: (:user, :rate, :movie) => 4 -# source and target node for a given relation -julia> edge_index(g, (:user, :rate, :movie)) +julia> edge_index(g, (:user, :rate, :movie)) # source and target node for a given relation ([1, 1, 2, 3], [7, 13, 5, 7]) -# node types -julia> g.ntypes +julia> g.ntypes # node types 2-element Vector{Symbol}: :user :movie -# edge types -julia> g.etypes +julia> g.etypes # edge types 1-element Vector{Tuple{Symbol, Symbol, Symbol}}: (:user, :rate, :movie) ``` @@ -120,8 +121,8 @@ GNNHeteroGraph: Batching is automatically performed by the [`DataLoader`](https://fluxml.ai/Flux.jl/stable/data/mlutils/#MLUtils.DataLoader) iterator when the `collate` option is set to `true`. -```jldoctest hetero -using Flux: DataLoader +```julia +using MLUtils: DataLoader data = [rand_bipartite_heterograph((5, 10), 20, ndata=Dict(:A=>rand(Float32, 3, 5))) diff --git a/GNNGraphs/docs/src/guides/temporalgraph.md b/GNNGraphs/docs/src/guides/temporalgraph.md index 40d817cdf..74202041f 100644 --- a/GNNGraphs/docs/src/guides/temporalgraph.md +++ b/GNNGraphs/docs/src/guides/temporalgraph.md @@ -1,3 +1,7 @@ +```@meta +CurrentModule = GNNGraphs +``` + # Temporal Graphs Temporal Graphs are graphs with time varying topologies and features. In GNNGraphs.jl, temporal graphs with fixed number of nodes over time are supported by the [`TemporalSnapshotsGNNGraph`](@ref) type. @@ -45,7 +49,7 @@ TemporalSnapshotsGNNGraph: See [`rand_temporal_radius_graph`](@ref) and [`rand_temporal_hyperbolic_graph`](@ref) for generating random temporal graphs. -```jldoctest temporal +```julia julia> tg = rand_temporal_radius_graph(10, 3, 0.1, 0.5) TemporalSnapshotsGNNGraph: num_nodes: [10, 10, 10] @@ -97,11 +101,13 @@ A temporal graph can store global feature for the entire time series in the `tgd Also, each snapshot can store node, edge, and graph features in the `ndata`, `edata`, and `gdata` fields, respectively. ```jldoctest temporal -julia> snapshots = [rand_graph(10,20; ndata = rand(3,10)), rand_graph(10,14; ndata = rand(4,10)), rand_graph(10,22; ndata = rand(5,10))]; # node features at construction time +julia> snapshots = [rand_graph(10, 20; ndata = rand(Float32, 3, 10)), + rand_graph(10, 14; ndata = rand(Float32, 4, 10)), + rand_graph(10, 22; ndata = rand(Float32, 5, 10))]; # node features at construction time julia> tg = TemporalSnapshotsGNNGraph(snapshots); -julia> tg.tgdata.y = rand(3,1); # add global features after construction +julia> tg.tgdata.y = rand(Float32, 3, 1); # add global features after construction julia> tg TemporalSnapshotsGNNGraph: @@ -109,16 +115,16 @@ TemporalSnapshotsGNNGraph: num_edges: [20, 14, 22] num_snapshots: 3 tgdata: - y = 3×1 Matrix{Float64} + y = 3×1 Matrix{Float32} julia> tg.ndata # vector of DataStore containing node features for each snapshot 3-element Vector{DataStore}: DataStore(10) with 1 element: - x = 3×10 Matrix{Float64} + x = 3×10 Matrix{Float32} DataStore(10) with 1 element: - x = 4×10 Matrix{Float64} + x = 4×10 Matrix{Float32} DataStore(10) with 1 element: - x = 5×10 Matrix{Float64} + x = 5×10 Matrix{Float32} julia> [ds.x for ds in tg.ndata]; # vector containing the x feature of each snapshot diff --git a/GNNGraphs/src/GNNGraphs.jl b/GNNGraphs/src/GNNGraphs.jl index 3054a9ab8..e6a79407f 100644 --- a/GNNGraphs/src/GNNGraphs.jl +++ b/GNNGraphs/src/GNNGraphs.jl @@ -4,7 +4,7 @@ using SparseArrays using Functors: @functor import Graphs using Graphs: AbstractGraph, outneighbors, inneighbors, adjacency_matrix, degree, - has_self_loops, is_directed, induced_subgraph + has_self_loops, is_directed, induced_subgraph, has_edge import NearestNeighbors import NNlib import StatsBase @@ -30,7 +30,7 @@ export GNNGraph, edge_features, graph_features -include("gnnheterograph.jl") +include("gnnheterograph/gnnheterograph.jl") export GNNHeteroGraph, num_edge_types, num_node_types, @@ -44,6 +44,7 @@ export TemporalSnapshotsGNNGraph, # remove_snapshot! include("query.jl") +include("gnnheterograph/query.jl") export adjacency_list, edge_index, get_edge_weight, @@ -58,13 +59,15 @@ export adjacency_list, # from Graphs adjacency_matrix, degree, - has_self_loops, + has_edge, has_isolated_nodes, + has_self_loops, inneighbors, outneighbors, khop_adj include("transform.jl") +include("gnnheterograph/transform.jl") export add_nodes, add_edges, add_self_loops, @@ -88,6 +91,7 @@ export add_nodes, blockdiag include("generate.jl") +include("gnnheterograph/generate.jl") export rand_graph, rand_heterograph, rand_bipartite_heterograph, @@ -104,6 +108,7 @@ include("operators.jl") include("convert.jl") include("utils.jl") +include("gnnheterograph/utils.jl") export sort_edge_index, color_refinement include("gatherscatter.jl") diff --git a/GNNGraphs/src/datastore.jl b/GNNGraphs/src/datastore.jl index 1bfa37e00..7bc1bd29b 100644 --- a/GNNGraphs/src/datastore.jl +++ b/GNNGraphs/src/datastore.jl @@ -15,29 +15,12 @@ DataStore(3) with 2 elements: x = 2×3 Matrix{Float32} julia> ds = DataStore(3, Dict(:x => rand(Float32, 2, 3), :y => rand(Float32, 3))); # equivalent to above - -julia> ds = DataStore(3, (x = rand(Float32, 2, 3), y = rand(Float32, 30))) -ERROR: AssertionError: DataStore: data[y] has 30 observations, but n = 3 -Stacktrace: - [1] DataStore(n::Int64, data::Dict{Symbol, Any}) - @ GNNGraphs ~/.julia/dev/GNNGraphs/datastore.jl:54 - [2] DataStore(n::Int64, data::NamedTuple{(:x, :y), Tuple{Matrix{Float32}, Vector{Float32}}}) - @ GNNGraphs ~/.julia/dev/GNNGraphs/datastore.jl:73 - [3] top-level scope - @ REPL[13]:1 - -julia> ds = DataStore(x = randFloat32, 2, 3), y = rand(Float32, 30)) # no checks -DataStore() with 2 elements: - y = 30-element Vector{Float32} - x = 2×3 Matrix{Float32} - y = 30-element Vector{Float64} - x = 2×3 Matrix{Float64} ``` The `DataStore` has an interface similar to both dictionaries and named tuples. Arrays can be accessed and added using either the indexing or the property syntax: -```jldoctest datastore +```jldoctest docstr_datastore julia> ds = DataStore(x = ones(Float32, 2, 3), y = zeros(Float32, 3)) DataStore() with 2 elements: y = 3-element Vector{Float32} @@ -59,14 +42,16 @@ The `DataStore` can be iterated over, and the keys and values can be accessed using `keys(ds)` and `values(ds)`. `map(f, ds)` applies the function `f` to each feature array: -```jldoctest datastore +```jldoctest docstr_datastore julia> ds2 = map(x -> x .+ 1, ds) -DataStore() with 2 elements: - a = 2-element Vector{Float64} - b = 2-element Vector{Float64} +DataStore() with 3 elements: + y = 3-element Vector{Float32} + z = 3-element Vector{Float32} + x = 2×3 Matrix{Float32} -julia> ds2.a -2-element Vector{Float64}: +julia> ds2.z +3-element Vector{Float32}: + 1.0 1.0 1.0 ``` diff --git a/GNNGraphs/src/generate.jl b/GNNGraphs/src/generate.jl index 6005ac023..819f2b519 100644 --- a/GNNGraphs/src/generate.jl +++ b/GNNGraphs/src/generate.jl @@ -16,26 +16,26 @@ Additional keyword arguments will be passed to the [`GNNGraph`](@ref) constructo # Examples -```jldoctest +```julia julia> g = rand_graph(5, 4, bidirected=false) GNNGraph: - num_nodes = 5 - num_edges = 4 + num_nodes: 5 + num_edges: 4 julia> edge_index(g) -([1, 3, 3, 4], [5, 4, 5, 2]) +([4, 3, 2, 1], [5, 4, 3, 2]) # In the bidirected case, edge data will be duplicated on the reverse edges if needed. julia> g = rand_graph(5, 4, edata=rand(Float32, 16, 2)) GNNGraph: - num_nodes = 5 - num_edges = 4 - edata: - e => (16, 4) + num_nodes: 5 + num_edges: 4 + edata: + e = 16×4 Matrix{Float32} # Each edge has a reverse julia> edge_index(g) -([1, 3, 3, 4], [3, 4, 1, 3]) +([1, 1, 5, 3], [5, 3, 1, 1]) ``` """ function rand_graph(n::Integer, m::Integer; seed=-1, kws...) @@ -64,130 +64,6 @@ function rand_graph(rng::AbstractRNG, n::Integer, m::Integer; return GNNGraph((s, t, edge_weight); num_nodes=n, kws...) end -""" - rand_heterograph([rng,] n, m; bidirected=false, kws...) - -Construct an [`GNNHeteroGraph`](@ref) with random edges and with number of nodes and edges -specified by `n` and `m` respectively. `n` and `m` can be any iterable of pairs -specifing node/edge types and their numbers. - -Pass a random number generator as a first argument to make the generation reproducible. - -Setting `bidirected=true` will generate a bidirected graph, i.e. each edge will have a reverse edge. -Therefore, for each edge type `(:A, :rel, :B)` a corresponding reverse edge type `(:B, :rel, :A)` -will be generated. - -Additional keyword arguments will be passed to the [`GNNHeteroGraph`](@ref) constructor. - -# Examples - -```jldoctest -julia> g = rand_heterograph((:user => 10, :movie => 20), - (:user, :rate, :movie) => 30) -GNNHeteroGraph: - num_nodes: (:user => 10, :movie => 20) - num_edges: ((:user, :rate, :movie) => 30,) -``` -""" -function rand_heterograph end - -# for generic iterators of pairs -rand_heterograph(n, m; kws...) = rand_heterograph(Dict(n), Dict(m); kws...) -rand_heterograph(rng::AbstractRNG, n, m; kws...) = rand_heterograph(rng, Dict(n), Dict(m); kws...) - -function rand_heterograph(n::NDict, m::EDict; seed=-1, kws...) - if seed != -1 - Base.depwarn("Keyword argument `seed` is deprecated, pass an rng as first argument instead.", :rand_heterograph) - rng = MersenneTwister(seed) - else - rng = Random.default_rng() - end - return rand_heterograph(rng, n, m; kws...) -end - -function rand_heterograph(rng::AbstractRNG, n::NDict, m::EDict; bidirected::Bool = false, kws...) - if bidirected - return _rand_bidirected_heterograph(rng, n, m; kws...) - end - graphs = Dict(k => _rand_edges(rng, (n[k[1]], n[k[3]]), m[k]) for k in keys(m)) - return GNNHeteroGraph(graphs; num_nodes = n, kws...) -end - -function _rand_bidirected_heterograph(rng::AbstractRNG, n::NDict, m::EDict; kws...) - for k in keys(m) - if reverse(k) ∈ keys(m) - @assert m[k] == m[reverse(k)] "Number of edges must be the same in reverse edge types for bidirected graphs." - else - m[reverse(k)] = m[k] - end - end - graphs = Dict{EType, Tuple{Vector{Int}, Vector{Int}, Nothing}}() - for k in keys(m) - reverse(k) ∈ keys(graphs) && continue - s, t, val = _rand_edges(rng, (n[k[1]], n[k[3]]), m[k]) - graphs[k] = s, t, val - graphs[reverse(k)] = t, s, val - end - return GNNHeteroGraph(graphs; num_nodes = n, kws...) -end - - -""" - rand_bipartite_heterograph([rng,] - (n1, n2), (m12, m21); - bidirected = true, - node_t = (:A, :B), - edge_t = :to, - kws...) - -Construct an [`GNNHeteroGraph`](@ref) with random edges representing a bipartite graph. -The graph will have two types of nodes, and edges will only connect nodes of different types. - -The first argument is a tuple `(n1, n2)` specifying the number of nodes of each type. -The second argument is a tuple `(m12, m21)` specifying the number of edges connecting nodes of type `1` to nodes of type `2` -and vice versa. - -The type of nodes and edges can be specified with the `node_t` and `edge_t` keyword arguments, -which default to `(:A, :B)` and `:to` respectively. - -If `bidirected=true` (default), the reverse edge of each edge will be present. In this case -`m12 == m21` is required. - -A random number generator can be passed as the first argument to make the generation reproducible. - -Additional keyword arguments will be passed to the [`GNNHeteroGraph`](@ref) constructor. - -See [`rand_heterograph`](@ref) for a more general version. - -# Examples - -```julia-repl -julia> g = rand_bipartite_heterograph((10, 15), 20) -GNNHeteroGraph: - num_nodes: (:A => 10, :B => 15) - num_edges: ((:A, :to, :B) => 20, (:B, :to, :A) => 20) - -julia> g = rand_bipartite_heterograph((10, 15), (20, 0), node_t=(:user, :item), edge_t=:-, bidirected=false) -GNNHeteroGraph: - num_nodes: Dict(:item => 15, :user => 10) - num_edges: Dict((:item, :-, :user) => 0, (:user, :-, :item) => 20) -``` -""" -rand_bipartite_heterograph(n, m; kws...) = rand_bipartite_heterograph(Random.default_rng(), n, m; kws...) - -function rand_bipartite_heterograph(rng::AbstractRNG, (n1, n2)::NTuple{2,Int}, m; bidirected=true, - node_t = (:A, :B), edge_t::Symbol = :to, kws...) - if m isa Integer - m12 = m21 = m - else - m12, m21 = m - end - - return rand_heterograph(rng, Dict(node_t[1] => n1, node_t[2] => n2), - Dict((node_t[1], edge_t, node_t[2]) => m12, (node_t[2], edge_t, node_t[1]) => m21); - bidirected, kws...) -end - """ knn_graph(points::AbstractMatrix, k::Int; @@ -214,7 +90,7 @@ to its `k` closest `points`. # Examples -```jldoctest +```julia julia> n, k = 10, 3; julia> x = rand(Float32, 3, n); @@ -231,7 +107,6 @@ GNNGraph: num_nodes = 10 num_edges = 30 num_graphs = 2 - ``` """ function knn_graph(points::AbstractMatrix, k::Int; @@ -295,7 +170,7 @@ to its neighbors within a given distance `r`. # Examples -```jldoctest +```julia julia> n, r = 10, 0.75; julia> x = rand(Float32, 3, n); @@ -312,9 +187,10 @@ GNNGraph: num_nodes = 10 num_edges = 20 num_graphs = 2 - ``` + # References + Section B paragraphs 1 and 2 of the paper [Dynamic Hidden-Variable Network Models](https://arxiv.org/pdf/2101.00414.pdf) """ function radius_graph(points::AbstractMatrix, r::AbstractFloat; @@ -447,7 +323,7 @@ First, the positions of the nodes are generated with a quasi-uniform distributio # Example -```jldoctest +```julia julia> n, snaps, α, R, speed, ζ = 10, 5, 1.0, 4.0, 0.1, 1.0; julia> thg = rand_temporal_hyperbolic_graph(n, snaps; α, R, speed, ζ) diff --git a/GNNGraphs/src/gnngraph.jl b/GNNGraphs/src/gnngraph.jl index 6ebdee8f7..c19483332 100644 --- a/GNNGraphs/src/gnngraph.jl +++ b/GNNGraphs/src/gnngraph.jl @@ -291,19 +291,19 @@ function Base.show(io::IO, ::MIME"text/plain", g::GNNGraph) if !isempty(g.ndata) print(io, "\n ndata:") for k in keys(g.ndata) - print(io, "\n\t$k = $(shortsummary(g.ndata[k]))") + print(io, "\n $k = $(shortsummary(g.ndata[k]))") end end if !isempty(g.edata) print(io, "\n edata:") for k in keys(g.edata) - print(io, "\n\t$k = $(shortsummary(g.edata[k]))") + print(io, "\n $k = $(shortsummary(g.edata[k]))") end end if !isempty(g.gdata) print(io, "\n gdata:") for k in keys(g.gdata) - print(io, "\n\t$k = $(shortsummary(g.gdata[k]))") + print(io, "\n $k = $(shortsummary(g.gdata[k]))") end end end diff --git a/GNNGraphs/src/gnnheterograph/generate.jl b/GNNGraphs/src/gnnheterograph/generate.jl new file mode 100644 index 000000000..de828675d --- /dev/null +++ b/GNNGraphs/src/gnnheterograph/generate.jl @@ -0,0 +1,124 @@ +""" + rand_heterograph([rng,] n, m; bidirected=false, kws...) + +Construct an [`GNNHeteroGraph`](@ref) with random edges and with number of nodes and edges +specified by `n` and `m` respectively. `n` and `m` can be any iterable of pairs +specifing node/edge types and their numbers. + +Pass a random number generator as a first argument to make the generation reproducible. + +Setting `bidirected=true` will generate a bidirected graph, i.e. each edge will have a reverse edge. +Therefore, for each edge type `(:A, :rel, :B)` a corresponding reverse edge type `(:B, :rel, :A)` +will be generated. + +Additional keyword arguments will be passed to the [`GNNHeteroGraph`](@ref) constructor. + +# Examples + +```jldoctest +julia> g = rand_heterograph((:user => 10, :movie => 20), + (:user, :rate, :movie) => 30) +GNNHeteroGraph: + num_nodes: Dict(:movie => 20, :user => 10) + num_edges: Dict((:user, :rate, :movie) => 30) +``` +""" +function rand_heterograph end + +# for generic iterators of pairs +rand_heterograph(n, m; kws...) = rand_heterograph(Dict(n), Dict(m); kws...) +rand_heterograph(rng::AbstractRNG, n, m; kws...) = rand_heterograph(rng, Dict(n), Dict(m); kws...) + +function rand_heterograph(n::NDict, m::EDict; seed=-1, kws...) + if seed != -1 + Base.depwarn("Keyword argument `seed` is deprecated, pass an rng as first argument instead.", :rand_heterograph) + rng = MersenneTwister(seed) + else + rng = Random.default_rng() + end + return rand_heterograph(rng, n, m; kws...) +end + +function rand_heterograph(rng::AbstractRNG, n::NDict, m::EDict; bidirected::Bool = false, kws...) + if bidirected + return _rand_bidirected_heterograph(rng, n, m; kws...) + end + graphs = Dict(k => _rand_edges(rng, (n[k[1]], n[k[3]]), m[k]) for k in keys(m)) + return GNNHeteroGraph(graphs; num_nodes = n, kws...) +end + +function _rand_bidirected_heterograph(rng::AbstractRNG, n::NDict, m::EDict; kws...) + for k in keys(m) + if reverse(k) ∈ keys(m) + @assert m[k] == m[reverse(k)] "Number of edges must be the same in reverse edge types for bidirected graphs." + else + m[reverse(k)] = m[k] + end + end + graphs = Dict{EType, Tuple{Vector{Int}, Vector{Int}, Nothing}}() + for k in keys(m) + reverse(k) ∈ keys(graphs) && continue + s, t, val = _rand_edges(rng, (n[k[1]], n[k[3]]), m[k]) + graphs[k] = s, t, val + graphs[reverse(k)] = t, s, val + end + return GNNHeteroGraph(graphs; num_nodes = n, kws...) +end + + +""" + rand_bipartite_heterograph([rng,] + (n1, n2), (m12, m21); + bidirected = true, + node_t = (:A, :B), + edge_t = :to, + kws...) + +Construct an [`GNNHeteroGraph`](@ref) with random edges representing a bipartite graph. +The graph will have two types of nodes, and edges will only connect nodes of different types. + +The first argument is a tuple `(n1, n2)` specifying the number of nodes of each type. +The second argument is a tuple `(m12, m21)` specifying the number of edges connecting nodes of type `1` to nodes of type `2` +and vice versa. + +The type of nodes and edges can be specified with the `node_t` and `edge_t` keyword arguments, +which default to `(:A, :B)` and `:to` respectively. + +If `bidirected=true` (default), the reverse edge of each edge will be present. In this case +`m12 == m21` is required. + +A random number generator can be passed as the first argument to make the generation reproducible. + +Additional keyword arguments will be passed to the [`GNNHeteroGraph`](@ref) constructor. + +See [`rand_heterograph`](@ref) for a more general version. + +# Examples + +```julia-repl +julia> g = rand_bipartite_heterograph((10, 15), 20) +GNNHeteroGraph: + num_nodes: (:A => 10, :B => 15) + num_edges: ((:A, :to, :B) => 20, (:B, :to, :A) => 20) + +julia> g = rand_bipartite_heterograph((10, 15), (20, 0), node_t=(:user, :item), edge_t=:-, bidirected=false) +GNNHeteroGraph: + num_nodes: Dict(:item => 15, :user => 10) + num_edges: Dict((:item, :-, :user) => 0, (:user, :-, :item) => 20) +``` +""" +rand_bipartite_heterograph(n, m; kws...) = rand_bipartite_heterograph(Random.default_rng(), n, m; kws...) + +function rand_bipartite_heterograph(rng::AbstractRNG, (n1, n2)::NTuple{2,Int}, m; bidirected=true, + node_t = (:A, :B), edge_t::Symbol = :to, kws...) + if m isa Integer + m12 = m21 = m + else + m12, m21 = m + end + + return rand_heterograph(rng, Dict(node_t[1] => n1, node_t[2] => n2), + Dict((node_t[1], edge_t, node_t[2]) => m12, (node_t[2], edge_t, node_t[1]) => m21); + bidirected, kws...) +end + diff --git a/GNNGraphs/src/gnnheterograph.jl b/GNNGraphs/src/gnnheterograph/gnnheterograph.jl similarity index 100% rename from GNNGraphs/src/gnnheterograph.jl rename to GNNGraphs/src/gnnheterograph/gnnheterograph.jl diff --git a/GNNGraphs/src/gnnheterograph/query.jl b/GNNGraphs/src/gnnheterograph/query.jl new file mode 100644 index 000000000..831e654eb --- /dev/null +++ b/GNNGraphs/src/gnnheterograph/query.jl @@ -0,0 +1,91 @@ +""" + edge_index(g::GNNHeteroGraph, [edge_t]) + +Return a tuple containing two vectors, respectively storing the source and target nodes +for each edges in `g` of type `edge_t = (src_t, rel_t, trg_t)`. + +If `edge_t` is not provided, it will error if `g` has more than one edge type. +""" +edge_index(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) = g.graph[edge_t][1:2] +edge_index(g::GNNHeteroGraph{<:COO_T}) = only(g.graph)[2][1:2] + +get_edge_weight(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) = g.graph[edge_t][3] + +""" + has_edge(g::GNNHeteroGraph, edge_t, i, j) + +Return `true` if there is an edge of type `edge_t` from node `i` to node `j` in `g`. + +# Examples + +```jldoctest +julia> g = rand_bipartite_heterograph((2, 2), (4, 0), bidirected=false) +GNNHeteroGraph: + num_nodes: Dict(:A => 2, :B => 2) + num_edges: Dict((:A, :to, :B) => 4, (:B, :to, :A) => 0) + +julia> has_edge(g, (:A,:to,:B), 1, 1) +true + +julia> has_edge(g, (:B,:to,:A), 1, 1) +false +``` +""" +function Graphs.has_edge(g::GNNHeteroGraph, edge_t::EType, i::Integer, j::Integer) + s, t = edge_index(g, edge_t) + return any((s .== i) .& (t .== j)) +end + + +""" + degree(g::GNNHeteroGraph, edge_type::EType; dir = :in) + +Return a vector containing the degrees of the nodes in `g` GNNHeteroGraph +given `edge_type`. + +# Arguments + +- `g`: A graph. +- `edge_type`: A tuple of symbols `(source_t, edge_t, target_t)` representing the edge type. +- `T`: Element type of the returned vector. If `nothing`, is + chosen based on the graph type. Default `nothing`. +- `dir`: For `dir = :out` the degree of a node is counted based on the outgoing edges. + For `dir = :in`, the ingoing edges are used. If `dir = :both` we have the sum of the two. + Default `dir = :out`. + +""" +function Graphs.degree(g::GNNHeteroGraph, edge::EType, + T::TT = nothing; dir = :out) where { + TT <: Union{Nothing, Type{<:Number}}} + + s, t = edge_index(g, edge) + + T = isnothing(T) ? eltype(s) : T + + n_type = dir == :in ? g.ntypes[2] : g.ntypes[1] + + return _degree((s, t), T, dir, nothing, g.num_nodes[n_type]) +end + +""" + graph_indicator(g::GNNHeteroGraph, [node_t]) + +Return a Dict of vectors containing the graph membership +(an integer from `1` to `g.num_graphs`) of each node in the graph for each node type. +If `node_t` is provided, return the graph membership of each node of type `node_t` instead. + +See also [`batch`](@ref). +""" +function graph_indicator(g::GNNHeteroGraph) + return g.graph_indicator +end + +function graph_indicator(g::GNNHeteroGraph, node_t::Symbol) + @assert node_t ∈ g.ntypes + if isnothing(g.graph_indicator) + gi = ones_like(edge_index(g, first(g.etypes))[1], Int, g.num_nodes[node_t]) + else + gi = g.graph_indicator[node_t] + end + return gi +end diff --git a/GNNGraphs/src/gnnheterograph/transform.jl b/GNNGraphs/src/gnnheterograph/transform.jl new file mode 100644 index 000000000..6e6c8f277 --- /dev/null +++ b/GNNGraphs/src/gnnheterograph/transform.jl @@ -0,0 +1,230 @@ +""" + add_self_loops(g::GNNHeteroGraph, edge_t::EType) + add_self_loops(g::GNNHeteroGraph) + +If the source node type is the same as the destination node type in `edge_t`, +return a graph with the same features as `g` but also add self-loops +of the specified type, `edge_t`. Otherwise, it returns `g` unchanged. + +Nodes with already existing self-loops of type `edge_t` will obtain +a second set of self-loops of the same type. + +If the graph has edge weights for edges of type `edge_t`, the new edges will have weight 1. + +If no edges of type `edge_t` exist, or all existing edges have no weight, +then all new self loops will have no weight. + +If `edge_t` is not passed as argument, for the entire graph self-loop is added to each node for every edge type in the graph where the source and destination node types are the same. +This iterates over all edge types present in the graph, applying the self-loop addition logic to each applicable edge type. +""" +function add_self_loops(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) + + function get_edge_weight_nullable(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) + get(g.graph, edge_t, (nothing, nothing, nothing))[3] + end + + src_t, _, tgt_t = edge_t + (src_t === tgt_t) || + return g + + n = get(g.num_nodes, src_t, 0) + + if haskey(g.graph, edge_t) + s, t = g.graph[edge_t][1:2] + nodes = convert(typeof(s), [1:n;]) + s = [s; nodes] + t = [t; nodes] + else + if !isempty(g.graph) + T = typeof(first(values(g.graph))[1]) + nodes = convert(T, [1:n;]) + else + nodes = [1:n;] + end + s = nodes + t = nodes + end + + graph = g.graph |> copy + ew = get(g.graph, edge_t, (nothing, nothing, nothing))[3] + + if ew !== nothing + ew = [ew; fill!(similar(ew, n), 1)] + end + + graph[edge_t] = (s, t, ew) + edata = g.edata |> copy + ndata = g.ndata |> copy + ntypes = g.ntypes |> copy + etypes = g.etypes |> copy + num_nodes = g.num_nodes |> copy + num_edges = g.num_edges |> copy + num_edges[edge_t] = length(get(graph, edge_t, ([],[]))[1]) + + return GNNHeteroGraph(graph, + num_nodes, num_edges, g.num_graphs, + g.graph_indicator, + ndata, edata, g.gdata, + ntypes, etypes) +end + +function add_self_loops(g::GNNHeteroGraph) + for edge_t in keys(g.graph) + g = add_self_loops(g, edge_t) + end + return g +end + +""" + add_edges(g::GNNHeteroGraph, edge_t, s, t; [edata, num_nodes]) + add_edges(g::GNNHeteroGraph, edge_t => (s, t); [edata, num_nodes]) + add_edges(g::GNNHeteroGraph, edge_t => (s, t, w); [edata, num_nodes]) + +Add to heterograph `g` edges of type `edge_t` with source node vector `s` and target node vector `t`. +Optionally, pass the edge weights `w` or the features `edata` for the new edges. +`edge_t` is a triplet of symbols `(src_t, rel_t, dst_t)`. + +If the edge type is not already present in the graph, it is added. +If it involves new node types, they are added to the graph as well. +In this case, a dictionary or named tuple of `num_nodes` can be passed to specify the number of nodes of the new types, +otherwise the number of nodes is inferred from the maximum node id in `s` and `t`. +""" +add_edges(g::GNNHeteroGraph{<:COO_T}, edge_t::EType, snew::AbstractVector, tnew::AbstractVector; kws...) = add_edges(g, edge_t => (snew, tnew, nothing); kws...) +add_edges(g::GNNHeteroGraph{<:COO_T}, data::Pair{EType, <:Tuple{<:AbstractVector, <:AbstractVector}}; kws...) = add_edges(g, data.first => (data.second..., nothing); kws...) + +function add_edges(g::GNNHeteroGraph{<:COO_T}, + data::Pair{EType, <:COO_T}; + edata = nothing, + num_nodes = Dict{Symbol,Int}()) + edge_t, (snew, tnew, wnew) = data + @assert length(snew) == length(tnew) + if length(snew) == 0 + return g + end + @assert minimum(snew) >= 1 + @assert minimum(tnew) >= 1 + + is_existing_rel = haskey(g.graph, edge_t) + + edata = normalize_graphdata(edata, default_name = :e, n = length(snew)) + _edata = g.edata |> copy + if haskey(_edata, edge_t) + _edata[edge_t] = cat_features(g.edata[edge_t], edata) + else + _edata[edge_t] = edata + end + + graph = g.graph |> copy + etypes = g.etypes |> copy + ntypes = g.ntypes |> copy + _num_nodes = g.num_nodes |> copy + ndata = g.ndata |> copy + if !is_existing_rel + for (node_t, st) in [(edge_t[1], snew), (edge_t[3], tnew)] + if node_t ∉ ntypes + push!(ntypes, node_t) + if haskey(num_nodes, node_t) + _num_nodes[node_t] = num_nodes[node_t] + else + _num_nodes[node_t] = maximum(st) + end + ndata[node_t] = DataStore(_num_nodes[node_t]) + end + end + push!(etypes, edge_t) + else + s, t = edge_index(g, edge_t) + snew = [s; snew] + tnew = [t; tnew] + w = get_edge_weight(g, edge_t) + wnew = cat_features(w, wnew, length(s), length(snew)) + end + + if maximum(snew) > _num_nodes[edge_t[1]] + ndata_new = normalize_graphdata((;), default_name = :x, n = maximum(snew) - _num_nodes[edge_t[1]]) + ndata[edge_t[1]] = cat_features(ndata[edge_t[1]], ndata_new) + _num_nodes[edge_t[1]] = maximum(snew) + end + if maximum(tnew) > _num_nodes[edge_t[3]] + ndata_new = normalize_graphdata((;), default_name = :x, n = maximum(tnew) - _num_nodes[edge_t[3]]) + ndata[edge_t[3]] = cat_features(ndata[edge_t[3]], ndata_new) + _num_nodes[edge_t[3]] = maximum(tnew) + end + + graph[edge_t] = (snew, tnew, wnew) + num_edges = g.num_edges |> copy + num_edges[edge_t] = length(graph[edge_t][1]) + + return GNNHeteroGraph(graph, + _num_nodes, num_edges, g.num_graphs, + g.graph_indicator, + ndata, _edata, g.gdata, + ntypes, etypes) +end + +function MLUtils.batch(gs::AbstractVector{<:GNNHeteroGraph}) + function edge_index_nullable(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) + if haskey(g.graph, edge_t) + g.graph[edge_t][1:2] + else + nothing + end + end + + function get_edge_weight_nullable(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) + get(g.graph, edge_t, (nothing, nothing, nothing))[3] + end + + @assert length(gs) > 0 + ntypes = union([g.ntypes for g in gs]...) + etypes = union([g.etypes for g in gs]...) + + v_num_nodes = Dict(node_t => [get(g.num_nodes, node_t, 0) for g in gs] for node_t in ntypes) + num_nodes = Dict(node_t => sum(v_num_nodes[node_t]) for node_t in ntypes) + num_edges = Dict(edge_t => sum(get(g.num_edges, edge_t, 0) for g in gs) for edge_t in etypes) + edge_indices = edge_indices = Dict(edge_t => [edge_index_nullable(g, edge_t) for g in gs] for edge_t in etypes) + nodesum = Dict(node_t => cumsum([0; v_num_nodes[node_t]])[1:(end - 1)] for node_t in ntypes) + graphs = [] + for edge_t in etypes + src_t, _, dst_t = edge_t + # @show edge_t edge_indices[edge_t] first(edge_indices[edge_t]) + # for ei in edge_indices[edge_t] + # @show ei[1] + # end + # # [ei[1] for (ii, ei) in enumerate(edge_indices[edge_t])] + s = cat_features([ei[1] .+ nodesum[src_t][ii] for (ii, ei) in enumerate(edge_indices[edge_t]) if ei !== nothing]) + t = cat_features([ei[2] .+ nodesum[dst_t][ii] for (ii, ei) in enumerate(edge_indices[edge_t]) if ei !== nothing]) + w = cat_features(filter(x -> x !== nothing, [get_edge_weight_nullable(g, edge_t) for g in gs])) + push!(graphs, edge_t => (s, t, w)) + end + graph = Dict(graphs...) + + #TODO relax this restriction + @assert all(g -> g.num_graphs == 1, gs) + + s = edge_index(gs[1], gs[1].etypes[1])[1] # grab any source vector + + function materialize_graph_indicator(g, node_t) + n = get(g.num_nodes, node_t, 0) + return ones_like(s, n) + end + v_gi = Dict(node_t => [materialize_graph_indicator(g, node_t) for g in gs] for node_t in ntypes) + v_num_graphs = [g.num_graphs for g in gs] + graphsum = cumsum([0; v_num_graphs])[1:(end - 1)] + v_gi = Dict(node_t => [ng .+ gi for (ng, gi) in zip(graphsum, v_gi[node_t])] for node_t in ntypes) + graph_indicator = Dict(node_t => cat_features(v_gi[node_t]) for node_t in ntypes) + + function data_or_else(data, types) + Dict(type => get(data, type, DataStore(0)) for type in types) + end + + return GNNHeteroGraph(graph, + num_nodes, + num_edges, + sum(v_num_graphs), + graph_indicator, + cat_features([data_or_else(g.ndata, ntypes) for g in gs]), + cat_features([data_or_else(g.edata, etypes) for g in gs]), + cat_features([g.gdata for g in gs]), + ntypes, etypes) +end diff --git a/GNNGraphs/src/gnnheterograph/utils.jl b/GNNGraphs/src/gnnheterograph/utils.jl new file mode 100644 index 000000000..1559dd9ee --- /dev/null +++ b/GNNGraphs/src/gnnheterograph/utils.jl @@ -0,0 +1,18 @@ +function check_num_nodes(g::GNNHeteroGraph, x::Tuple) + @assert length(x) == 2 + @assert length(g.etypes) == 1 + nt1, _, nt2 = only(g.etypes) + if x[1] isa AbstractArray + @assert size(x[1], ndims(x[1])) == g.num_nodes[nt1] + end + if x[2] isa AbstractArray + @assert size(x[2], ndims(x[2])) == g.num_nodes[nt2] + end + return true +end + +function check_num_edges(g::GNNHeteroGraph, e::AbstractArray) + num_edgs = only(g.num_edges)[2] + @assert only(num_edgs)==size(e, ndims(e)) "Got $(size(e, ndims(e))) as last dimension size instead of num_edges=$(num_edgs)" + return true +end diff --git a/GNNGraphs/src/mldatasets.jl b/GNNGraphs/src/mldatasets.jl index df072ca9a..40568a6c7 100644 --- a/GNNGraphs/src/mldatasets.jl +++ b/GNNGraphs/src/mldatasets.jl @@ -12,14 +12,14 @@ julia> using MLDatasets, GNNGraphs julia> mldataset2gnngraph(Cora()) GNNGraph: - num_nodes = 2708 - num_edges = 10556 - ndata: - features => 1433×2708 Matrix{Float32} - targets => 2708-element Vector{Int64} - train_mask => 2708-element BitVector - val_mask => 2708-element BitVector - test_mask => 2708-element BitVector + num_nodes: 2708 + num_edges: 10556 + ndata: + val_mask = 2708-element BitVector + targets = 2708-element Vector{Int64} + test_mask = 2708-element BitVector + features = 1433×2708 Matrix{Float32} + train_mask = 2708-element BitVector ``` """ function mldataset2gnngraph(dataset::D) where {D} diff --git a/GNNGraphs/src/operators.jl b/GNNGraphs/src/operators.jl index 1faa4adcb..9ad8fbb07 100644 --- a/GNNGraphs/src/operators.jl +++ b/GNNGraphs/src/operators.jl @@ -1,5 +1,5 @@ # 2 or more args graph operators -"""" +""" intersect(g1::GNNGraph, g2::GNNGraph) Intersect two graphs by keeping only the common edges. diff --git a/GNNGraphs/src/query.jl b/GNNGraphs/src/query.jl index a11eb564c..719cbfd17 100644 --- a/GNNGraphs/src/query.jl +++ b/GNNGraphs/src/query.jl @@ -13,23 +13,10 @@ edge_index(g::GNNGraph{<:COO_T}) = g.graph[1:2] edge_index(g::GNNGraph{<:ADJMAT_T}) = to_coo(g.graph, num_nodes = g.num_nodes)[1][1:2] -""" - edge_index(g::GNNHeteroGraph, [edge_t]) - -Return a tuple containing two vectors, respectively storing the source and target nodes -for each edges in `g` of type `edge_t = (src_t, rel_t, trg_t)`. - -If `edge_t` is not provided, it will error if `g` has more than one edge type. -""" -edge_index(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) = g.graph[edge_t][1:2] -edge_index(g::GNNHeteroGraph{<:COO_T}) = only(g.graph)[2][1:2] - get_edge_weight(g::GNNGraph{<:COO_T}) = g.graph[3] get_edge_weight(g::GNNGraph{<:ADJMAT_T}) = to_coo(g.graph, num_nodes = g.num_nodes)[1][3] -get_edge_weight(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) = g.graph[edge_t][3] - Graphs.edges(g::GNNGraph) = Graphs.Edge.(edge_index(g)...) Graphs.edgetype(g::GNNGraph) = Graphs.Edge{eltype(g)} @@ -55,31 +42,6 @@ end Graphs.has_edge(g::GNNGraph{<:ADJMAT_T}, i::Integer, j::Integer) = g.graph[i, j] != 0 -""" - has_edge(g::GNNHeteroGraph, edge_t, i, j) - -Return `true` if there is an edge of type `edge_t` from node `i` to node `j` in `g`. - -# Examples - -```jldoctest -julia> g = rand_bipartite_heterograph((2, 2), (4, 0), bidirected=false) -GNNHeteroGraph: - num_nodes: (:A => 2, :B => 2) - num_edges: ((:A, :to, :B) => 4, (:B, :to, :A) => 0) - -julia> has_edge(g, (:A,:to,:B), 1, 1) -true - -julia> has_edge(g, (:B,:to,:A), 1, 1) -false -``` -""" -function Graphs.has_edge(g::GNNHeteroGraph, edge_t::EType, i::Integer, j::Integer) - s, t = edge_index(g, edge_t) - return any((s .== i) .& (t .== j)) -end - """ get_graph_type(g::GNNGraph) @@ -390,36 +352,6 @@ function Graphs.degree(g::GNNGraph{<:ADJMAT_T}, T::TT = nothing; dir = :out, return _degree(A, T, dir, edge_weight, g.num_nodes) end -""" - degree(g::GNNHeteroGraph, edge_type::EType; dir = :in) - -Return a vector containing the degrees of the nodes in `g` GNNHeteroGraph -given `edge_type`. - -# Arguments - -- `g`: A graph. -- `edge_type`: A tuple of symbols `(source_t, edge_t, target_t)` representing the edge type. -- `T`: Element type of the returned vector. If `nothing`, is - chosen based on the graph type. Default `nothing`. -- `dir`: For `dir = :out` the degree of a node is counted based on the outgoing edges. - For `dir = :in`, the ingoing edges are used. If `dir = :both` we have the sum of the two. - Default `dir = :out`. - -""" -function Graphs.degree(g::GNNHeteroGraph, edge::EType, - T::TT = nothing; dir = :out) where { - TT <: Union{Nothing, Type{<:Number}}} - - s, t = edge_index(g, edge) - - T = isnothing(T) ? eltype(s) : T - - n_type = dir == :in ? g.ntypes[2] : g.ntypes[1] - - return _degree((s, t), T, dir, nothing, g.num_nodes[n_type]) -end - function _degree((s, t)::Tuple, T::Type, dir::Symbol, edge_weight::Nothing, num_nodes::Int) _degree((s, t), T, dir, ones_like(s, T), num_nodes) end @@ -579,28 +511,7 @@ function graph_indicator(g::GNNGraph; edges = false) end end -""" - graph_indicator(g::GNNHeteroGraph, [node_t]) - -Return a Dict of vectors containing the graph membership -(an integer from `1` to `g.num_graphs`) of each node in the graph for each node type. -If `node_t` is provided, return the graph membership of each node of type `node_t` instead. -See also [`batch`](@ref). -""" -function graph_indicator(g::GNNHeteroGraph) - return g.graph_indicator -end - -function graph_indicator(g::GNNHeteroGraph, node_t::Symbol) - @assert node_t ∈ g.ntypes - if isnothing(g.graph_indicator) - gi = ones_like(edge_index(g, first(g.etypes))[1], Int, g.num_nodes[node_t]) - else - gi = g.graph_indicator[node_t] - end - return gi -end function node_features(g::GNNGraph) if isempty(g.ndata) diff --git a/GNNGraphs/src/samplers.jl b/GNNGraphs/src/samplers.jl index 44bbe1480..050c950e5 100644 --- a/GNNGraphs/src/samplers.jl +++ b/GNNGraphs/src/samplers.jl @@ -12,11 +12,13 @@ originally introduced in ["Inductive Representation Learning on Large Graphs"}(h - `num_layers::Int`: The number of layers for neighborhood expansion (how far to sample neighbors). - `batch_size::Union{Int, Nothing}`: The size of the batch. If not specified, it defaults to the number of `input_nodes`. -# Usage -```jldoctest -julia> loader = NeighborLoader(graph; num_neighbors=[10, 5], input_nodes=[1, 2, 3], num_layers=2) +# Examples + +```julia +julia> loader = NeighborLoader(graph; num_neighbors=[10, 5], input_nodes=[1, 2, 3], num_layers=2) julia> batch_counter = 0 + julia> for mini_batch_gnn in loader batch_counter += 1 println("Batch ", batch_counter, ": Nodes in mini-batch graph: ", nv(mini_batch_gnn)) diff --git a/GNNGraphs/src/transform.jl b/GNNGraphs/src/transform.jl index b2a88daab..dd7ef52f2 100644 --- a/GNNGraphs/src/transform.jl +++ b/GNNGraphs/src/transform.jl @@ -38,83 +38,6 @@ function add_self_loops(g::GNNGraph{<:ADJMAT_T}) g.ndata, g.edata, g.gdata) end -""" - add_self_loops(g::GNNHeteroGraph, edge_t::EType) - add_self_loops(g::GNNHeteroGraph) - -If the source node type is the same as the destination node type in `edge_t`, -return a graph with the same features as `g` but also add self-loops -of the specified type, `edge_t`. Otherwise, it returns `g` unchanged. - -Nodes with already existing self-loops of type `edge_t` will obtain -a second set of self-loops of the same type. - -If the graph has edge weights for edges of type `edge_t`, the new edges will have weight 1. - -If no edges of type `edge_t` exist, or all existing edges have no weight, -then all new self loops will have no weight. - -If `edge_t` is not passed as argument, for the entire graph self-loop is added to each node for every edge type in the graph where the source and destination node types are the same. -This iterates over all edge types present in the graph, applying the self-loop addition logic to each applicable edge type. -""" -function add_self_loops(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) - - function get_edge_weight_nullable(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) - get(g.graph, edge_t, (nothing, nothing, nothing))[3] - end - - src_t, _, tgt_t = edge_t - (src_t === tgt_t) || - return g - - n = get(g.num_nodes, src_t, 0) - - if haskey(g.graph, edge_t) - s, t = g.graph[edge_t][1:2] - nodes = convert(typeof(s), [1:n;]) - s = [s; nodes] - t = [t; nodes] - else - if !isempty(g.graph) - T = typeof(first(values(g.graph))[1]) - nodes = convert(T, [1:n;]) - else - nodes = [1:n;] - end - s = nodes - t = nodes - end - - graph = g.graph |> copy - ew = get(g.graph, edge_t, (nothing, nothing, nothing))[3] - - if ew !== nothing - ew = [ew; fill!(similar(ew, n), 1)] - end - - graph[edge_t] = (s, t, ew) - edata = g.edata |> copy - ndata = g.ndata |> copy - ntypes = g.ntypes |> copy - etypes = g.etypes |> copy - num_nodes = g.num_nodes |> copy - num_edges = g.num_edges |> copy - num_edges[edge_t] = length(get(graph, edge_t, ([],[]))[1]) - - return GNNHeteroGraph(graph, - num_nodes, num_edges, g.num_graphs, - g.graph_indicator, - ndata, edata, g.gdata, - ntypes, etypes) -end - -function add_self_loops(g::GNNHeteroGraph) - for edge_t in keys(g.graph) - g = add_self_loops(g, edge_t) - end - return g -end - """ remove_self_loops(g::GNNGraph) @@ -384,13 +307,13 @@ GNNGraph: ```jldoctest julia> g = GNNGraph() GNNGraph: - num_nodes: 0 - num_edges: 0 + num_nodes: 0 + num_edges: 0 julia> add_edges(g, [1,2], [2,3]) GNNGraph: - num_nodes: 3 - num_edges: 2 + num_nodes: 3 + num_edges: 2 ``` """ add_edges(g::GNNGraph{<:COO_T}, snew::AbstractVector, tnew::AbstractVector; kws...) = add_edges(g, (snew, tnew, nothing); kws...) @@ -429,94 +352,6 @@ function add_edges(g::GNNGraph{<:COO_T}, data::COO_T; edata = nothing) ndata, edata, g.gdata) end -""" - add_edges(g::GNNHeteroGraph, edge_t, s, t; [edata, num_nodes]) - add_edges(g::GNNHeteroGraph, edge_t => (s, t); [edata, num_nodes]) - add_edges(g::GNNHeteroGraph, edge_t => (s, t, w); [edata, num_nodes]) - -Add to heterograph `g` edges of type `edge_t` with source node vector `s` and target node vector `t`. -Optionally, pass the edge weights `w` or the features `edata` for the new edges. -`edge_t` is a triplet of symbols `(src_t, rel_t, dst_t)`. - -If the edge type is not already present in the graph, it is added. -If it involves new node types, they are added to the graph as well. -In this case, a dictionary or named tuple of `num_nodes` can be passed to specify the number of nodes of the new types, -otherwise the number of nodes is inferred from the maximum node id in `s` and `t`. -""" -add_edges(g::GNNHeteroGraph{<:COO_T}, edge_t::EType, snew::AbstractVector, tnew::AbstractVector; kws...) = add_edges(g, edge_t => (snew, tnew, nothing); kws...) -add_edges(g::GNNHeteroGraph{<:COO_T}, data::Pair{EType, <:Tuple{<:AbstractVector, <:AbstractVector}}; kws...) = add_edges(g, data.first => (data.second..., nothing); kws...) - -function add_edges(g::GNNHeteroGraph{<:COO_T}, - data::Pair{EType, <:COO_T}; - edata = nothing, - num_nodes = Dict{Symbol,Int}()) - edge_t, (snew, tnew, wnew) = data - @assert length(snew) == length(tnew) - if length(snew) == 0 - return g - end - @assert minimum(snew) >= 1 - @assert minimum(tnew) >= 1 - - is_existing_rel = haskey(g.graph, edge_t) - - edata = normalize_graphdata(edata, default_name = :e, n = length(snew)) - _edata = g.edata |> copy - if haskey(_edata, edge_t) - _edata[edge_t] = cat_features(g.edata[edge_t], edata) - else - _edata[edge_t] = edata - end - - graph = g.graph |> copy - etypes = g.etypes |> copy - ntypes = g.ntypes |> copy - _num_nodes = g.num_nodes |> copy - ndata = g.ndata |> copy - if !is_existing_rel - for (node_t, st) in [(edge_t[1], snew), (edge_t[3], tnew)] - if node_t ∉ ntypes - push!(ntypes, node_t) - if haskey(num_nodes, node_t) - _num_nodes[node_t] = num_nodes[node_t] - else - _num_nodes[node_t] = maximum(st) - end - ndata[node_t] = DataStore(_num_nodes[node_t]) - end - end - push!(etypes, edge_t) - else - s, t = edge_index(g, edge_t) - snew = [s; snew] - tnew = [t; tnew] - w = get_edge_weight(g, edge_t) - wnew = cat_features(w, wnew, length(s), length(snew)) - end - - if maximum(snew) > _num_nodes[edge_t[1]] - ndata_new = normalize_graphdata((;), default_name = :x, n = maximum(snew) - _num_nodes[edge_t[1]]) - ndata[edge_t[1]] = cat_features(ndata[edge_t[1]], ndata_new) - _num_nodes[edge_t[1]] = maximum(snew) - end - if maximum(tnew) > _num_nodes[edge_t[3]] - ndata_new = normalize_graphdata((;), default_name = :x, n = maximum(tnew) - _num_nodes[edge_t[3]]) - ndata[edge_t[3]] = cat_features(ndata[edge_t[3]], ndata_new) - _num_nodes[edge_t[3]] = maximum(tnew) - end - - graph[edge_t] = (snew, tnew, wnew) - num_edges = g.num_edges |> copy - num_edges[edge_t] = length(graph[edge_t][1]) - - return GNNHeteroGraph(graph, - _num_nodes, num_edges, g.num_graphs, - g.graph_indicator, - ndata, _edata, g.gdata, - ntypes, etypes) -end - - """ perturb_edges([rng], g::GNNGraph, perturb_ratio) @@ -804,38 +639,33 @@ See also [`MLUtils.unbatch`](@ref). # Examples ```jldoctest -julia> g1 = rand_graph(4, 6, ndata=ones(8, 4)) +julia> g1 = rand_graph(4, 4, ndata=ones(Float32, 3, 4)) GNNGraph: - num_nodes = 4 - num_edges = 6 - ndata: - x => (8, 4) + num_nodes: 4 + num_edges: 4 + ndata: + x = 3×4 Matrix{Float32} -julia> g2 = rand_graph(7, 4, ndata=zeros(8, 7)) +julia> g2 = rand_graph(5, 4, ndata=zeros(Float32, 3, 5)) GNNGraph: - num_nodes = 7 - num_edges = 4 - ndata: - x => (8, 7) + num_nodes: 5 + num_edges: 4 + ndata: + x = 3×5 Matrix{Float32} julia> g12 = MLUtils.batch([g1, g2]) GNNGraph: - num_nodes = 11 - num_edges = 10 - num_graphs = 2 - ndata: - x => (8, 11) + num_nodes: 9 + num_edges: 8 + num_graphs: 2 + ndata: + x = 3×9 Matrix{Float32} julia> g12.ndata.x -8×11 Matrix{Float64}: - 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +3×9 Matrix{Float32}: + 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 + 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 + 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 ``` """ function MLUtils.batch(gs::AbstractVector{<:GNNGraph}) @@ -882,74 +712,6 @@ function MLUtils.batch(g::GNNGraph) throw(ArgumentError("Cannot batch a `GNNGraph` (containing $(g.num_graphs) graphs). Pass a vector of `GNNGraph`s instead.")) end - -function MLUtils.batch(gs::AbstractVector{<:GNNHeteroGraph}) - function edge_index_nullable(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) - if haskey(g.graph, edge_t) - g.graph[edge_t][1:2] - else - nothing - end - end - - function get_edge_weight_nullable(g::GNNHeteroGraph{<:COO_T}, edge_t::EType) - get(g.graph, edge_t, (nothing, nothing, nothing))[3] - end - - @assert length(gs) > 0 - ntypes = union([g.ntypes for g in gs]...) - etypes = union([g.etypes for g in gs]...) - - v_num_nodes = Dict(node_t => [get(g.num_nodes, node_t, 0) for g in gs] for node_t in ntypes) - num_nodes = Dict(node_t => sum(v_num_nodes[node_t]) for node_t in ntypes) - num_edges = Dict(edge_t => sum(get(g.num_edges, edge_t, 0) for g in gs) for edge_t in etypes) - edge_indices = edge_indices = Dict(edge_t => [edge_index_nullable(g, edge_t) for g in gs] for edge_t in etypes) - nodesum = Dict(node_t => cumsum([0; v_num_nodes[node_t]])[1:(end - 1)] for node_t in ntypes) - graphs = [] - for edge_t in etypes - src_t, _, dst_t = edge_t - # @show edge_t edge_indices[edge_t] first(edge_indices[edge_t]) - # for ei in edge_indices[edge_t] - # @show ei[1] - # end - # # [ei[1] for (ii, ei) in enumerate(edge_indices[edge_t])] - s = cat_features([ei[1] .+ nodesum[src_t][ii] for (ii, ei) in enumerate(edge_indices[edge_t]) if ei !== nothing]) - t = cat_features([ei[2] .+ nodesum[dst_t][ii] for (ii, ei) in enumerate(edge_indices[edge_t]) if ei !== nothing]) - w = cat_features(filter(x -> x !== nothing, [get_edge_weight_nullable(g, edge_t) for g in gs])) - push!(graphs, edge_t => (s, t, w)) - end - graph = Dict(graphs...) - - #TODO relax this restriction - @assert all(g -> g.num_graphs == 1, gs) - - s = edge_index(gs[1], gs[1].etypes[1])[1] # grab any source vector - - function materialize_graph_indicator(g, node_t) - n = get(g.num_nodes, node_t, 0) - return ones_like(s, n) - end - v_gi = Dict(node_t => [materialize_graph_indicator(g, node_t) for g in gs] for node_t in ntypes) - v_num_graphs = [g.num_graphs for g in gs] - graphsum = cumsum([0; v_num_graphs])[1:(end - 1)] - v_gi = Dict(node_t => [ng .+ gi for (ng, gi) in zip(graphsum, v_gi[node_t])] for node_t in ntypes) - graph_indicator = Dict(node_t => cat_features(v_gi[node_t]) for node_t in ntypes) - - function data_or_else(data, types) - Dict(type => get(data, type, DataStore(0)) for type in types) - end - - return GNNHeteroGraph(graph, - num_nodes, - num_edges, - sum(v_num_graphs), - graph_indicator, - cat_features([data_or_else(g.ndata, ntypes) for g in gs]), - cat_features([data_or_else(g.edata, etypes) for g in gs]), - cat_features([g.gdata for g in gs]), - ntypes, etypes) -end - """ unbatch(g::GNNGraph) @@ -961,25 +723,19 @@ See also [`MLUtils.batch`](@ref) and [`getgraph`](@ref). # Examples ```jldoctest +julia> using MLUtils + julia> gbatched = MLUtils.batch([rand_graph(5, 6), rand_graph(10, 8), rand_graph(4,2)]) GNNGraph: - num_nodes = 19 - num_edges = 16 - num_graphs = 3 + num_nodes: 19 + num_edges: 16 + num_graphs: 3 julia> MLUtils.unbatch(gbatched) 3-element Vector{GNNGraph{Tuple{Vector{Int64}, Vector{Int64}, Nothing}}}: - GNNGraph: - num_nodes = 5 - num_edges = 6 - - GNNGraph: - num_nodes = 10 - num_edges = 8 - - GNNGraph: - num_nodes = 4 - num_edges = 2 + GNNGraph(5, 6) with no data + GNNGraph(10, 8) with no data + GNNGraph(4, 2) with no data ``` """ function MLUtils.unbatch(g::GNNGraph{T}) where {T <: COO_T} diff --git a/GNNGraphs/src/utils.jl b/GNNGraphs/src/utils.jl index 7cdc3e543..a5a8bae67 100644 --- a/GNNGraphs/src/utils.jl +++ b/GNNGraphs/src/utils.jl @@ -16,20 +16,6 @@ function check_num_nodes(g::GNNGraph, x::Tuple) return true end -# x = (Xsrc, Xdst) = (Xj, Xi) -function check_num_nodes(g::GNNHeteroGraph, x::Tuple) - @assert length(x) == 2 - @assert length(g.etypes) == 1 - nt1, _, nt2 = only(g.etypes) - if x[1] isa AbstractArray - @assert size(x[1], ndims(x[1])) == g.num_nodes[nt1] - end - if x[2] isa AbstractArray - @assert size(x[2], ndims(x[2])) == g.num_nodes[nt2] - end - return true -end - function check_num_edges(g::GNNGraph, e::AbstractArray) @assert g.num_edges==size(e, ndims(e)) "Got $(size(e, ndims(e))) as last dimension size instead of num_edges=$(g.num_edges)" return true @@ -41,12 +27,6 @@ end check_num_edges(::AbstractGNNGraph, ::Nothing) = true -function check_num_edges(g::GNNHeteroGraph, e::AbstractArray) - num_edgs = only(g.num_edges)[2] - @assert only(num_edgs)==size(e, ndims(e)) "Got $(size(e, ndims(e))) as last dimension size instead of num_edges=$(num_edgs)" - return true -end - sort_edge_index(eindex::Tuple) = sort_edge_index(eindex...) """ diff --git a/GNNLux/docs/src/api/basic.md b/GNNLux/docs/src/api/basic.md index 18f943f5e..f61419095 100644 --- a/GNNLux/docs/src/api/basic.md +++ b/GNNLux/docs/src/api/basic.md @@ -1,5 +1,6 @@ ```@meta CurrentModule = GNNLux +CollapsedDocStrings = true ``` ## Basic Layers diff --git a/GNNLux/docs/src/api/conv.md b/GNNLux/docs/src/api/conv.md index 44f70334e..73c8df069 100644 --- a/GNNLux/docs/src/api/conv.md +++ b/GNNLux/docs/src/api/conv.md @@ -1,5 +1,6 @@ ```@meta CurrentModule = GNNLux +CollapsedDocStrings = true ``` # Convolutional Layers diff --git a/GNNLux/docs/src/api/temporalconv.md b/GNNLux/docs/src/api/temporalconv.md index f6d565e8a..d1b9fbb1c 100644 --- a/GNNLux/docs/src/api/temporalconv.md +++ b/GNNLux/docs/src/api/temporalconv.md @@ -1,5 +1,6 @@ ```@meta CurrentModule = GNNLux +CollapsedDocStrings = true ``` # Temporal Graph-Convolutional Layers diff --git a/GNNlib/docs/src/api/messagepassing.md b/GNNlib/docs/src/api/messagepassing.md index 03b50914e..fc8654ad1 100644 --- a/GNNlib/docs/src/api/messagepassing.md +++ b/GNNlib/docs/src/api/messagepassing.md @@ -1,16 +1,10 @@ ```@meta CurrentModule = GNNlib +CollapsedDocStrings = true ``` # Message Passing -## Index - -```@index -Order = [:type, :function] -Pages = ["messagepassing.md"] -``` - ## Interface ```@docs diff --git a/GNNlib/docs/src/api/utils.md b/GNNlib/docs/src/api/utils.md index a3ae827a0..7db4baa60 100644 --- a/GNNlib/docs/src/api/utils.md +++ b/GNNlib/docs/src/api/utils.md @@ -1,20 +1,11 @@ ```@meta CurrentModule = GNNlib +CollapsedDocStrings = true ``` # Utility Functions -## Index - -```@index -Order = [:type, :function] -Pages = ["utils.md"] -``` - -## Docs - - -### Graph-wise operations +## Graph-wise operations ```@docs reduce_nodes @@ -25,13 +16,13 @@ broadcast_nodes broadcast_edges ``` -### Neighborhood operations +## Neighborhood operations ```@docs softmax_edge_neighbors ``` -### NNlib's gather and scatter functions +## NNlib's gather and scatter functions Primitive functions for message passing implemented in [NNlib.jl](https://fluxml.ai/NNlib.jl/stable/reference/#Gather-and-Scatter): diff --git a/GraphNeuralNetworks/docs/Project.toml b/GraphNeuralNetworks/docs/Project.toml index ae13fe2db..f317ed97f 100644 --- a/GraphNeuralNetworks/docs/Project.toml +++ b/GraphNeuralNetworks/docs/Project.toml @@ -1,12 +1,21 @@ [deps] +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c" GNNGraphs = "aed8fd31-079b-4b5a-b342-a13352159b8c" GNNlib = "a6a84749-d869-43f8-aacc-be26a1996e48" +GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" GraphNeuralNetworks = "cffab07f-9bc2-4db1-8861-388f63bf7694" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MLDatasets = "eb30cadb-4394-5ae3-aed4-317e484a6458" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +PlutoStaticHTML = "359b1769-a58e-495b-9770-312e911026ad" +PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +TSne = "24678dba-d5e9-5843-a4c6-250288b04835" +cuDNN = "02a925ec-e4fe-4b08-9a7e-0d78e3d38ccd" diff --git a/GraphNeuralNetworks/docs/make.jl b/GraphNeuralNetworks/docs/make.jl index d3dfb947c..1b9dad02c 100644 --- a/GraphNeuralNetworks/docs/make.jl +++ b/GraphNeuralNetworks/docs/make.jl @@ -36,7 +36,7 @@ makedocs(; prettyurls = get(ENV, "CI", nothing) == "true", assets = [], size_threshold=nothing, - size_threshold_warn=200000), + size_threshold_warn=2000000), sitename = "GraphNeuralNetworks.jl", pages = [ @@ -51,15 +51,38 @@ makedocs(; "Datasets" => "GNNGraphs/guides/datasets.md", ], + "Tutorials" => [ + "Introductory tutorials" => [ + "Hands on" => "tutorials/gnn_intro_pluto.md", + "Node classification" => "tutorials/node_classification_pluto.md", + "Graph classification" => "tutorials/graph_classification_pluto.md" + ], + "Temporal graph neural networks" =>[ + "Node autoregression" => "tutorials/traffic_prediction.md", + "Temporal graph classification" => "tutorials/temporal_graph_classification_pluto.md" + ], + ], + "API Reference" => [ + "Graphs (GNNGraphs.jl)" => [ + "GNNGraph" => "GNNGraphs/api/gnngraph.md", + "GNNHeteroGraph" => "GNNGraphs/api/heterograph.md", + "TemporalSnapshotsGNNGraph" => "GNNGraphs/api/temporalgraph.md", + "Samplers" => "GNNGraphs/api/samplers.md", + ] + + "Message Passing (GNNlib.jl)" => [ "Message Passing" => "GNNlib/api/messagepassing.md", - "Utils" => "GNNlib/api/utils.md", - "Basic" => "api/basic.md", + "Other Operators" => "GNNlib/api/utils.md", + ] + + "Layers" => [ + "Basic layers" => "api/basic.md", "Convolutional layers" => "api/conv.md", "Pooling layers" => "api/pool.md", "Temporal Convolutional layers" => "api/temporalconv.md", "Hetero Convolutional layers" => "api/heteroconv.md", - + ] ], "Developer guide" => "dev.md", diff --git a/GraphNeuralNetworks/docs/make_tutorials.jl b/GraphNeuralNetworks/docs/make_tutorials.jl new file mode 100644 index 000000000..5091a696f --- /dev/null +++ b/GraphNeuralNetworks/docs/make_tutorials.jl @@ -0,0 +1,35 @@ +using PlutoStaticHTML + +function move_tutorials(source, dest) + files = readdir(source) + + for file in files + if endswith(file, ".md") + mv(joinpath(source, file), joinpath(dest, file); force = true) + end + end +end + +# Build intro tutorials +bopt = BuildOptions("src_tutorials/introductory_tutorials"; + output_format = documenter_output, use_distributed = false) + +build_notebooks(bopt, + ["gnn_intro_pluto.jl", "node_classification_pluto.jl", "graph_classification_pluto.jl"], + OutputOptions() +) + +move_tutorials("src_tutorials/introductory_tutorials/", "src/tutorials/") + +# Build temporal tutorials +bopt_temp = BuildOptions("src_tutorials/temporalconv_tutorials/"; + output_format = documenter_output, use_distributed = false) + +build_notebooks( + BuildOptions(bopt_temp; + output_format = documenter_output), + ["temporal_graph_classification_pluto.jl", "traffic_prediction.jl"], + OutputOptions() +) + +move_tutorials("src_tutorials/temporalconv_tutorials/", "src/tutorials/") \ No newline at end of file diff --git a/GraphNeuralNetworks/docs/src/api/basic.md b/GraphNeuralNetworks/docs/src/api/basic.md index 28ec14d0f..2fd05ed4f 100644 --- a/GraphNeuralNetworks/docs/src/api/basic.md +++ b/GraphNeuralNetworks/docs/src/api/basic.md @@ -1,16 +1,10 @@ ```@meta CurrentModule = GraphNeuralNetworks +CollapsedDocStrings = true ``` # Basic Layers -## Index - -```@index -Modules = [GraphNeuralNetworks] -Pages = ["basic.md"] -``` - ## Docs ```@autodocs diff --git a/GraphNeuralNetworks/docs/src/api/conv.md b/GraphNeuralNetworks/docs/src/api/conv.md index 8715f1e06..560cadec8 100644 --- a/GraphNeuralNetworks/docs/src/api/conv.md +++ b/GraphNeuralNetworks/docs/src/api/conv.md @@ -1,5 +1,6 @@ ```@meta CurrentModule = GraphNeuralNetworks +CollapsedDocStrings = true ``` # Convolutional Layers diff --git a/GraphNeuralNetworks/docs/src/api/heteroconv.md b/GraphNeuralNetworks/docs/src/api/heteroconv.md index 969fbde71..5248ee9da 100644 --- a/GraphNeuralNetworks/docs/src/api/heteroconv.md +++ b/GraphNeuralNetworks/docs/src/api/heteroconv.md @@ -1,5 +1,6 @@ ```@meta CurrentModule = GraphNeuralNetworks +CollapsedDocStrings = true ``` # Hetero Graph-Convolutional Layers diff --git a/GraphNeuralNetworks/docs/src/api/pool.md b/GraphNeuralNetworks/docs/src/api/pool.md index bee2c5951..ac9116094 100644 --- a/GraphNeuralNetworks/docs/src/api/pool.md +++ b/GraphNeuralNetworks/docs/src/api/pool.md @@ -1,5 +1,6 @@ ```@meta CurrentModule = GraphNeuralNetworks +CollapsedDocStrings = true ``` # Pooling Layers diff --git a/GraphNeuralNetworks/docs/src/api/temporalconv.md b/GraphNeuralNetworks/docs/src/api/temporalconv.md index 12d51767d..f7b1e3672 100644 --- a/GraphNeuralNetworks/docs/src/api/temporalconv.md +++ b/GraphNeuralNetworks/docs/src/api/temporalconv.md @@ -1,5 +1,6 @@ ```@meta CurrentModule = GraphNeuralNetworks +CollapsedDocStrings = true ``` # Temporal Graph-Convolutional Layers diff --git a/GraphNeuralNetworks/docs/src/index.md b/GraphNeuralNetworks/docs/src/index.md index d0500d3e1..b810d99e9 100644 --- a/GraphNeuralNetworks/docs/src/index.md +++ b/GraphNeuralNetworks/docs/src/index.md @@ -16,7 +16,7 @@ Among its features: The package is part of a larger ecosystem of packages that includes [GNNlib.jl](https://juliagraphs.org/GraphNeuralNetworks.jl/gnnlib), [GNNGraphs.jl](https://juliagraphs.org/GraphNeuralNetworks.jl/gnngraphs), and [GNNLux.jl](https://juliagraphs.org/GraphNeuralNetworks.jl/gnnlux). -GraphNeuralNetworks.jl is the fronted package for Flux.jl users. [Lux.jl](https://lux.csail.mit.edu/stable/) users instead, can relyi on GNNLux.jl (still in development). +GraphNeuralNetworks.jl is the fronted package for Flux.jl users. [Lux.jl](https://lux.csail.mit.edu/stable/) users instead, can rely on GNNLux.jl (still in development). ## Installation @@ -52,11 +52,11 @@ end ### Model building -We concisely define our model as a [`GraphNeuralNetworks.GNNChain`](@ref) containing two graph convolutional layers. If CUDA is available, our model will live on the gpu. +We concisely define our model as a [`GNNChain`](@ref) containing two graph convolutional layers. If CUDA is available, our model will live on the gpu. ```julia -device = CUDA.functional() ? Flux.gpu : Flux.cpu; +device = gpu_device() model = GNNChain(GCNConv(16 => 64), BatchNorm(64), # Apply batch normalization on node features (nodes dimension is batch dimension) diff --git a/tutorials/docs/src/pluto_output/gnn_intro_pluto.md b/GraphNeuralNetworks/docs/src/tutorials/gnn_intro_pluto.md similarity index 100% rename from tutorials/docs/src/pluto_output/gnn_intro_pluto.md rename to GraphNeuralNetworks/docs/src/tutorials/gnn_intro_pluto.md diff --git a/tutorials/docs/src/pluto_output/graph_classification_pluto.md b/GraphNeuralNetworks/docs/src/tutorials/graph_classification_pluto.md similarity index 100% rename from tutorials/docs/src/pluto_output/graph_classification_pluto.md rename to GraphNeuralNetworks/docs/src/tutorials/graph_classification_pluto.md diff --git a/tutorials/docs/src/pluto_output/node_classification_pluto.md b/GraphNeuralNetworks/docs/src/tutorials/node_classification_pluto.md similarity index 100% rename from tutorials/docs/src/pluto_output/node_classification_pluto.md rename to GraphNeuralNetworks/docs/src/tutorials/node_classification_pluto.md diff --git a/tutorials/docs/src/pluto_output/temporal_graph_classification_pluto.md b/GraphNeuralNetworks/docs/src/tutorials/temporal_graph_classification_pluto.md similarity index 100% rename from tutorials/docs/src/pluto_output/temporal_graph_classification_pluto.md rename to GraphNeuralNetworks/docs/src/tutorials/temporal_graph_classification_pluto.md diff --git a/tutorials/docs/src/pluto_output/traffic_prediction.md b/GraphNeuralNetworks/docs/src/tutorials/traffic_prediction.md similarity index 100% rename from tutorials/docs/src/pluto_output/traffic_prediction.md rename to GraphNeuralNetworks/docs/src/tutorials/traffic_prediction.md diff --git a/tutorials/tutorials/config.json b/GraphNeuralNetworks/docs/src_tutorials/config.json similarity index 100% rename from tutorials/tutorials/config.json rename to GraphNeuralNetworks/docs/src_tutorials/config.json diff --git a/tutorials/tutorials/index.md b/GraphNeuralNetworks/docs/src_tutorials/index.md similarity index 100% rename from tutorials/tutorials/index.md rename to GraphNeuralNetworks/docs/src_tutorials/index.md diff --git a/tutorials/tutorials/introductory_tutorials/assets/brain_gnn.gif b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/brain_gnn.gif similarity index 100% rename from tutorials/tutorials/introductory_tutorials/assets/brain_gnn.gif rename to GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/brain_gnn.gif diff --git a/tutorials/tutorials/introductory_tutorials/assets/graph_classification.gif b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/graph_classification.gif similarity index 100% rename from tutorials/tutorials/introductory_tutorials/assets/graph_classification.gif rename to GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/graph_classification.gif diff --git a/tutorials/tutorials/introductory_tutorials/assets/intro_1.png b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/intro_1.png similarity index 100% rename from tutorials/tutorials/introductory_tutorials/assets/intro_1.png rename to GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/intro_1.png diff --git a/tutorials/tutorials/introductory_tutorials/assets/node_classsification.gif b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/node_classsification.gif similarity index 100% rename from tutorials/tutorials/introductory_tutorials/assets/node_classsification.gif rename to GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/node_classsification.gif diff --git a/tutorials/tutorials/introductory_tutorials/assets/traffic.gif b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/traffic.gif similarity index 100% rename from tutorials/tutorials/introductory_tutorials/assets/traffic.gif rename to GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/assets/traffic.gif diff --git a/tutorials/tutorials/introductory_tutorials/gnn_intro_pluto.jl b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/gnn_intro_pluto.jl similarity index 100% rename from tutorials/tutorials/introductory_tutorials/gnn_intro_pluto.jl rename to GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/gnn_intro_pluto.jl diff --git a/tutorials/tutorials/introductory_tutorials/graph_classification_pluto.jl b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/graph_classification_pluto.jl similarity index 100% rename from tutorials/tutorials/introductory_tutorials/graph_classification_pluto.jl rename to GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/graph_classification_pluto.jl diff --git a/tutorials/tutorials/introductory_tutorials/node_classification_pluto.jl b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/node_classification_pluto.jl similarity index 100% rename from tutorials/tutorials/introductory_tutorials/node_classification_pluto.jl rename to GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/node_classification_pluto.jl diff --git a/tutorials/tutorials/temporalconv_tutorials/temporal_graph_classification_pluto.jl b/GraphNeuralNetworks/docs/src_tutorials/temporalconv_tutorials/temporal_graph_classification_pluto.jl similarity index 100% rename from tutorials/tutorials/temporalconv_tutorials/temporal_graph_classification_pluto.jl rename to GraphNeuralNetworks/docs/src_tutorials/temporalconv_tutorials/temporal_graph_classification_pluto.jl diff --git a/tutorials/tutorials/temporalconv_tutorials/traffic_prediction.jl b/GraphNeuralNetworks/docs/src_tutorials/temporalconv_tutorials/traffic_prediction.jl similarity index 100% rename from tutorials/tutorials/temporalconv_tutorials/traffic_prediction.jl rename to GraphNeuralNetworks/docs/src_tutorials/temporalconv_tutorials/traffic_prediction.jl diff --git a/README.md b/README.md index 50e88ffad..06d7151c8 100644 --- a/README.md +++ b/README.md @@ -7,44 +7,44 @@ ![](https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/actions/workflows/ci.yml/badge.svg) [![codecov](https://codecov.io/gh/JuliaGraphs/GraphNeuralNetworks.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaGraphs/GraphNeuralNetworks.jl) -Libraries for deep learning on graphs in Julia, using either [Flux.jl](https://fluxml.ai/Flux.jl/stable/) or [Lux.jl](https://lux.csail.mit.edu/stable/) as backend framework. + +**Libraries for deep learning on graphs in Julia**, using either [Flux.jl](https://fluxml.ai/Flux.jl/stable/) or [Lux.jl](https://lux.csail.mit.edu/stable/) as backend frameworks. This repository contains the following packages: -- **GraphNeuralNetworks.jl**: Graph convolutional layers based on the deep learning framework [Flux.jl](https://fluxml.ai/Flux.jl/stable/). This is the fronted package for Flux users. +- **GraphNeuralNetworks.jl**: Provides graph convolutional layers based on the deep learning framework [Flux.jl](https://fluxml.ai/Flux.jl/stable/). This is the frontend package for Flux users. -- **GNNLux.jl**: Graph convolutional layers based on the deep learning framework [Lux.jl](https://lux.csail.mit.edu/stable/). This is the fronted package for Lux users. This package is still under development and not yet registered. +- **GNNLux.jl**: Offers graph convolutional layers based on the deep learning framework [Lux.jl](https://lux.csail.mit.edu/stable/). This is the frontend package for Lux users. Note: This package is still under development and not yet registered. -- **GNNlib.jl**: Contains the message-passing framework based on the gather/scatter mechanism or on - sparse matrix multiplication. It also contains the shared implementation for the layers of the two fronted packages. This package is not meant to be used directly by the user, but its functionalities - are used and re-exported by the fronted packages. +- **GNNGraphs.jl**: Provides graph data structures and helper functions for working with graph data. This package is re-exported by the frontend packages. -- **GNNGraphs.jl**: Package that contains the graph data structures and helper functions for working with graph data. +- **GNNlib.jl**: Implements the message-passing framework based on the gather/scatter mechanism or sparse matrix multiplication. It also includes shared implementations for the layers used by the two frontend packages. This package is not intended for direct use by end-users but is re-exported by the frontend packages. +### Features -Both GraphNeuralNetworks.jl and GNNLux.jl display several features: +Both **GraphNeuralNetworks.jl** and **GNNLux.jl** support the following features: -* Implement common graph convolutional layers. -* Support computations on batched graphs. -* Easy to define custom layers. -* CUDA and AMDGPU support. -* Integration with [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl). -* [Examples](https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/tree/master/GraphNeuralNetworks/examples) of node, edge, and graph level machine learning tasks. -* Heterogeneous and temporal graphs support. +- Implementation of common graph convolutional layers. +- Computation on batched graphs. +- Custom layer definitions. +- Support for CUDA and AMDGPU. +- Integration with [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl). +- [Examples](https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/tree/master/GraphNeuralNetworks/examples) of node, edge, and graph-level machine learning tasks. +- Support for heterogeneous and temporal graphs. ## Installation -GraphNeuralNetworks.jl, GNNlib.jl and GNNGraphs.jl are registered Julia packages. You can easily install any of them through the package manager : +**GraphNeuralNetworks.jl**, **GNNlib.jl**, and **GNNGraphs.jl** are registered Julia packages. You can install them easily through the package manager: ```julia pkg> add GraphNeuralNetworks ``` - - ## Usage -Usage examples can be found in the [examples](https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/tree/master/GraphNeuralNetworks/examples) and in the [notebooks](https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/tree/master/GraphNeuralNetworks/notebooks) folder. Also, make sure to read the [documentation](https://juliagraphs.org/GraphNeuralNetworks.jl/graphneuralnetworks/) and the [tutorials](https://juliagraphs.org/GraphNeuralNetworks.jl/tutorials/) for a comprehensive introduction to the library. +Usage examples can be found in the [examples folder](https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/tree/master/GraphNeuralNetworks/examples) and the [notebooks folder](https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/tree/master/GraphNeuralNetworks/notebooks). + +For a comprehensive introduction to the library, refer to the [Documentation](https://juliagraphs.org/GraphNeuralNetworks.jl/graphneuralnetworks/) and the [Tutorials](https://juliagraphs.org/GraphNeuralNetworks.jl/tutorials/) ## Citing diff --git a/docs/make-multi.jl b/docs/make-multi.jl index 43a9ff626..2c6c2b573 100644 --- a/docs/make-multi.jl +++ b/docs/make-multi.jl @@ -17,6 +17,11 @@ docs = [ path = "graphneuralnetworks", name = "GraphNeuralNetworks.jl", fix_canonical_url = false), + MultiDocumenter.MultiDocRef( + upstream = joinpath(dirname(@__DIR__), "GNNLux", "docs", "build"), + path = "gnnlux", + name = "GNNLux.jl", + fix_canonical_url = false), MultiDocumenter.MultiDocRef( upstream = joinpath(dirname(@__DIR__), "GNNGraphs", "docs", "build"), path = "gnngraphs", @@ -26,17 +31,7 @@ docs = [ upstream = joinpath(dirname(@__DIR__), "GNNlib", "docs", "build"), path = "gnnlib", name = "GNNlib.jl", - fix_canonical_url = false), - MultiDocumenter.MultiDocRef( - upstream = joinpath(dirname(@__DIR__), "GNNLux", "docs", "build"), - path = "gnnlux", - name = "GNNLux", - fix_canonical_url = false), - MultiDocumenter.MultiDocRef( - upstream = joinpath(dirname(@__DIR__), "tutorials", "docs", "build"), - path = "tutorials", - name = "tutorials", - fix_canonical_url = false), + fix_canonical_url = false), ] outpath = joinpath(@__DIR__, "build") @@ -44,10 +39,11 @@ outpath = joinpath(@__DIR__, "build") MultiDocumenter.make( outpath, docs; - search_engine = MultiDocumenter.SearchConfig( - index_versions = ["stable"], - engine = MultiDocumenter.FlexSearch - ), + search_engine = MultiDocumenter.SearchConfig(), + # search_engine = MultiDocumenter.SearchConfig( + # index_versions = ["stable"], + # engine = MultiDocumenter.FlexSearch + # ), brand_image = MultiDocumenter.BrandImage("", "logo.svg"), rootpath = "/GraphNeuralNetworks.jl/" ) diff --git a/tutorials/docs/Project.toml b/tutorials/docs/Project.toml deleted file mode 100644 index 8e1472137..000000000 --- a/tutorials/docs/Project.toml +++ /dev/null @@ -1,4 +0,0 @@ -[deps] -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Pluto = "c3e4b0f8-55cb-11ea-2926-15256bba5781" -PlutoStaticHTML = "359b1769-a58e-495b-9770-312e911026ad" diff --git a/tutorials/docs/make.jl b/tutorials/docs/make.jl deleted file mode 100644 index 9399daec2..000000000 --- a/tutorials/docs/make.jl +++ /dev/null @@ -1,32 +0,0 @@ -using Documenter - - -assets = [] -prettyurls = get(ENV, "CI", nothing) == "true" -mathengine = MathJax3() - -# interlinks = InterLinks( -# "NNlib" => "https://fluxml.ai/NNlib.jl/stable/", -# "GNNGraphs" => ("https://carlolucibello.github.io/GraphNeuralNetworks.jl/GNNGraphs/", joinpath(dirname(dirname(@__DIR__)), "GNNGraphs", "docs", "build", "objects.inv")), -# "GraphNeuralNetworks" => ("https://carlolucibello.github.io/GraphNeuralNetworks.jl/GraphNeuralNetworks/", joinpath(dirname(dirname(@__DIR__)), "docs", "build", "objects.inv")),) - -makedocs(; - doctest = false, - clean = true, - format = Documenter.HTML(; - mathengine, prettyurls, assets = assets, size_threshold = nothing), - sitename = "Tutorials", - pages = ["Home" => "index.md", - "Introductory tutorials" => [ - "Hands on" => "pluto_output/gnn_intro_pluto.md", - "Node classification" => "pluto_output/node_classification_pluto.md", - "Graph classification" => "pluto_output/graph_classification_pluto.md" - ], - "Temporal graph neural networks" =>[ - "Node autoregression" => "pluto_output/traffic_prediction.md", - "Temporal graph classification" => "pluto_output/temporal_graph_classification_pluto.md" - - ]]) - - -deploydocs(;repo = "github.com/JuliaGraphs/GraphNeuralNetworks.jl.git", devbranch = "master", dirname = "tutorials") \ No newline at end of file diff --git a/tutorials/docs/src/index.md b/tutorials/docs/src/index.md deleted file mode 100644 index af1b57997..000000000 --- a/tutorials/docs/src/index.md +++ /dev/null @@ -1,24 +0,0 @@ -# Tutorials - -## Introductory tutorials - - -Here are some introductory tutorials to get you started: - -- [Hands-on introduction to Graph Neural Networks](pluto_output/gnn_intro_pluto.md) -- [Node classification with GraphNeuralNetworks.jl](pluto_output/node_classification_pluto.md) -- [Graph classification with GraphNeuralNetworks.jl](pluto_output/graph_classification_pluto.md) - - - -## Temporal graph neural networks tutorials - -Here some tutorials on temporal graph neural networks: - -- [Traffic Prediction using recurrent Temporal Graph Convolutional Network](pluto_output/traffic_prediction.md) - -- [Temporal Graph classification with GraphNeuralNetworks.jl](pluto_output/temporal_graph_classification_pluto.md) - -## Contributions - -If you have a suggestion on adding new tutorials, feel free to create a new issue [here](https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/issues/new). Users are invited to contribute demonstrations of their own. If you want to contribute new tutorials and looking for inspiration, checkout these tutorials from [PyTorch Geometric](https://pytorch-geometric.readthedocs.io/en/latest/notes/colabs.html). Please check out existing tutorials for more details. \ No newline at end of file