diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 49127a403..913c455aa 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -100,3 +100,54 @@ steps: GNN_TEST_AMDGPU: "true" GNN_TEST_CPU: "false" timeout_in_minutes: 60 + + - label: "GNNGraphs CUDA" + plugins: + - JuliaCI/julia#v1: + version: "1" + - JuliaCI/julia-coverage#v1: + dirs: + - GNNGraphs/src + command: | + julia --color=yes --depwarn=yes --project=GNNGraphs/test -e ' + import Pkg + dev_pkgs = Pkg.PackageSpec[] + for pkg in ("GNNGraphs",) + push!(dev_pkgs, Pkg.PackageSpec(path=pkg)); + end + Pkg.develop(dev_pkgs) + Pkg.add(["CUDA", "cuDNN"]) + Pkg.test("GNNGraphs")' + agents: + queue: "juliagpu" + cuda: "*" + env: + GNN_TEST_CUDA: "true" + GNN_TEST_CPU: "false" + timeout_in_minutes: 60 + + - label: "GNNGraphs AMDGPU" + plugins: + - JuliaCI/julia#v1: + version: "1" + - JuliaCI/julia-coverage#v1: + dirs: + - GNNGraphs/src + command: | + julia --color=yes --depwarn=yes --project=GNNGraphs/test -e ' + import Pkg + dev_pkgs = Pkg.PackageSpec[] + for pkg in ("GNNGraphs",) + push!(dev_pkgs, Pkg.PackageSpec(path=pkg)); + end + Pkg.develop(dev_pkgs) + Pkg.add(["AMDGPU"]) + Pkg.test("GNNGraphs")' + agents: + queue: "juliagpu" + rocm: "*" + rocmgpu: "*" + env: + GNN_TEST_AMDGPU: "true" + GNN_TEST_CPU: "false" + timeout_in_minutes: 60 diff --git a/.github/workflows/test_GNNGraphs.yml b/.github/workflows/test_GNNGraphs.yml index 6b9e52ddd..e9d996be4 100644 --- a/.github/workflows/test_GNNGraphs.yml +++ b/.github/workflows/test_GNNGraphs.yml @@ -8,7 +8,7 @@ on: - master jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + name: Julia ${{ matrix.version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/.github/workflows/test_GNNLux.yml b/.github/workflows/test_GNNLux.yml index 9b7dc9b6e..5b5ef2ccb 100644 --- a/.github/workflows/test_GNNLux.yml +++ b/.github/workflows/test_GNNLux.yml @@ -8,7 +8,7 @@ on: - master jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + name: Julia ${{ matrix.version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/.github/workflows/test_GNNlib.yml b/.github/workflows/test_GNNlib.yml index d858bb8d7..744811fab 100644 --- a/.github/workflows/test_GNNlib.yml +++ b/.github/workflows/test_GNNlib.yml @@ -8,7 +8,7 @@ on: - master jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + name: Julia ${{ matrix.version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/.github/workflows/test_GraphNeuralNetworks.yml b/.github/workflows/test_GraphNeuralNetworks.yml index faf26a43f..f2dc9264d 100644 --- a/.github/workflows/test_GraphNeuralNetworks.yml +++ b/.github/workflows/test_GraphNeuralNetworks.yml @@ -8,7 +8,7 @@ on: - master jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + name: Julia ${{ matrix.version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false diff --git a/GNNGraphs/Project.toml b/GNNGraphs/Project.toml index 0db318a93..91db933b0 100644 --- a/GNNGraphs/Project.toml +++ b/GNNGraphs/Project.toml @@ -4,13 +4,13 @@ authors = ["Carlo Lucibello and contributors"] version = "1.4.1" [deps] -Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" Functors = "d9f16b24-f501-4c13-a1f2-28368ffc5196" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" KrylovKit = "0b1a1467-8014-51b9-945f-bf0ae24f4b77" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MLDataDevices = "7e8f7934-dd98-4c1a-8fe8-92b47a384d40" +MLDatasets = "eb30cadb-4394-5ae3-aed4-317e484a6458" MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54" NNlib = "872c559c-99b0-510c-b3b7-b6c96a88d5cd" NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce" @@ -28,7 +28,6 @@ GNNGraphsCUDAExt = "CUDA" GNNGraphsSimpleWeightedGraphsExt = "SimpleWeightedGraphs" [compat] -Adapt = "4" CUDA = "5" ChainRulesCore = "1" Functors = "0.5" @@ -36,7 +35,7 @@ Graphs = "1.4" KrylovKit = "0.8" LinearAlgebra = "1" MLDataDevices = "1.0" -MLDatasets = "0.7" +MLDatasets = "0.7.18" MLUtils = "0.4" NNlib = "0.9" NearestNeighbors = "0.4" @@ -45,21 +44,4 @@ SimpleWeightedGraphs = "1.4.0" SparseArrays = "1" Statistics = "1" StatsBase = "0.34" -cuDNN = "1" julia = "1.10" - -[extras] -Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" -ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" -DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" -FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" -InlineStrings = "842dd82b-1e85-43dc-bf29-5d0ee9dffc48" -MLDatasets = "eb30cadb-4394-5ae3-aed4-317e484a6458" -SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" -cuDNN = "02a925ec-e4fe-4b08-9a7e-0d78e3d38ccd" - -[targets] -test = ["Test", "Adapt", "DataFrames", "InlineStrings", "SimpleWeightedGraphs", "Zygote", "FiniteDifferences", "ChainRulesTestUtils", "MLDatasets", "CUDA", "cuDNN"] diff --git a/GNNGraphs/docs/make.jl b/GNNGraphs/docs/make.jl index 30a3a2f56..97ccb2060 100644 --- a/GNNGraphs/docs/make.jl +++ b/GNNGraphs/docs/make.jl @@ -45,7 +45,6 @@ makedocs(; "GNNGraph" => "api/gnngraph.md", "GNNHeteroGraph" => "api/heterograph.md", "TemporalSnapshotsGNNGraph" => "api/temporalgraph.md", - "Samplers" => "api/samplers.md", "Datasets" => "api/datasets.md", ], ] diff --git a/GNNGraphs/docs/src/api/gnngraph.md b/GNNGraphs/docs/src/api/gnngraph.md index bd344fec4..e8a6ceffc 100644 --- a/GNNGraphs/docs/src/api/gnngraph.md +++ b/GNNGraphs/docs/src/api/gnngraph.md @@ -77,12 +77,8 @@ Base.intersect ## Sampling -```@autodocs; canonical = true -Modules = [GNNGraphs] -Pages = ["src/sampling.jl"] -Private = false -``` - -```@docs; canonical = true +```@docs +NeighborLoader +sample_neighbors Graphs.induced_subgraph(::GNNGraph, ::Vector{Int}) -``` \ No newline at end of file +``` diff --git a/GNNGraphs/docs/src/api/samplers.md b/GNNGraphs/docs/src/api/samplers.md deleted file mode 100644 index a98637433..000000000 --- a/GNNGraphs/docs/src/api/samplers.md +++ /dev/null @@ -1,12 +0,0 @@ -```@meta -CurrentModule = GNNGraphs -CollapsedDocStrings = true -``` - -# Samplers - -```@autodocs -Modules = [GNNGraphs] -Pages = ["samplers.jl"] -Private = false -``` diff --git a/GNNGraphs/src/GNNGraphs.jl b/GNNGraphs/src/GNNGraphs.jl index 736af601a..af451918c 100644 --- a/GNNGraphs/src/GNNGraphs.jl +++ b/GNNGraphs/src/GNNGraphs.jl @@ -13,6 +13,7 @@ using LinearAlgebra, Random, Statistics import MLUtils using MLUtils: getobs, numobs, ones_like, zeros_like, chunk, batch, rand_like using MLDataDevices: get_device, cpu_device, CPUDevice +using Functors: @functor include("chainrules.jl") # hacks for differentiability @@ -54,7 +55,7 @@ export adjacency_list, normalized_laplacian, scaled_laplacian, laplacian_lambda_max, -# from Graphs +# from Graphs.jl adjacency_matrix, degree, has_edge, @@ -82,7 +83,7 @@ export add_nodes, perturb_edges, remove_nodes, ppr_diffusion, -# from MLUtils +# from MLUtils.jl batch, unbatch, # from SparseArrays @@ -98,9 +99,6 @@ export rand_graph, rand_temporal_radius_graph, rand_temporal_hyperbolic_graph -include("sampling.jl") -export sample_neighbors - include("operators.jl") # Base.intersect @@ -117,7 +115,8 @@ export mldataset2gnngraph include("deprecations.jl") -include("samplers.jl") -export NeighborLoader +include("sampling.jl") +export NeighborLoader, sample_neighbors, + induced_subgraph # from Graphs.jl end #module diff --git a/GNNGraphs/src/samplers.jl b/GNNGraphs/src/samplers.jl deleted file mode 100644 index 050c950e5..000000000 --- a/GNNGraphs/src/samplers.jl +++ /dev/null @@ -1,105 +0,0 @@ -""" - NeighborLoader(graph; num_neighbors, input_nodes, num_layers, [batch_size]) - -A data structure for sampling neighbors from a graph for training Graph Neural Networks (GNNs). -It supports multi-layer sampling of neighbors for a batch of input nodes, useful for mini-batch training -originally introduced in ["Inductive Representation Learning on Large Graphs"}(https://arxiv.org/abs/1706.02216) paper. - -# Fields -- `graph::GNNGraph`: The input graph. -- `num_neighbors::Vector{Int}`: A vector specifying the number of neighbors to sample per node at each GNN layer. -- `input_nodes::Vector{Int}`: A vector containing the starting nodes for neighbor sampling. -- `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`. - -# 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)) - end -``` -""" -struct NeighborLoader - graph::GNNGraph # The input GNNGraph (graph + features from GraphNeuralNetworks.jl) - num_neighbors::Vector{Int} # Number of neighbors to sample per node, for each layer - input_nodes::Vector{Int} # Set of input nodes (starting nodes for sampling) - num_layers::Int # Number of layers for neighborhood expansion - batch_size::Union{Int, Nothing} # Optional batch size, defaults to the length of input_nodes if not given - neighbors_cache::Dict{Int, Vector{Int}} # Cache neighbors to avoid recomputation -end - -function NeighborLoader(graph::GNNGraph; num_neighbors::Vector{Int}, input_nodes::Vector{Int}=nothing, - num_layers::Int, batch_size::Union{Int, Nothing}=nothing) - return NeighborLoader(graph, num_neighbors, input_nodes === nothing ? collect(1:graph.num_nodes) : input_nodes, num_layers, - batch_size === nothing ? length(input_nodes) : batch_size, Dict{Int, Vector{Int}}()) -end - -# Function to get cached neighbors or compute them -function get_neighbors(loader::NeighborLoader, node::Int) - if haskey(loader.neighbors_cache, node) - return loader.neighbors_cache[node] - else - neighbors = Graphs.neighbors(loader.graph, node, dir = :in) # Get neighbors from graph - loader.neighbors_cache[node] = neighbors - return neighbors - end -end - -# Function to sample neighbors for a given node at a specific layer -function sample_nbrs(loader::NeighborLoader, node::Int, layer::Int) - neighbors = get_neighbors(loader, node) - if isempty(neighbors) - return Int[] - else - num_samples = min(loader.num_neighbors[layer], length(neighbors)) # Limit to required samples for this layer - return rand(neighbors, num_samples) # Randomly sample neighbors - end -end - -# Iterator protocol for NeighborLoader with lazy batch loading -function Base.iterate(loader::NeighborLoader, state=1) - if state > length(loader.input_nodes) - return nothing # End of iteration if batches are exhausted (state larger than amount of input nodes or current batch no >= batch number) - end - - # Determine the size of the current batch - batch_size = min(loader.batch_size, length(loader.input_nodes) - state + 1) # Conditional in case there is not enough nodes to fill the last batch - batch_nodes = loader.input_nodes[state:state + batch_size - 1] # Each mini-batch uses different set of input nodes - - # Set for tracking the subgraph nodes - subgraph_nodes = Set(batch_nodes) - - for node in batch_nodes - # Initialize current layer of nodes (starting with the node itself) - sampled_neighbors = Set([node]) - - # For each GNN layer, sample the neighborhood - for layer in 1:loader.num_layers - new_neighbors = Set{Int}() - for n in sampled_neighbors - neighbors = sample_nbrs(loader, n, layer) # Sample neighbors of the node for this layer - new_neighbors = union(new_neighbors, neighbors) # Avoid duplicates in the neighbor set - end - sampled_neighbors = new_neighbors - subgraph_nodes = union(subgraph_nodes, sampled_neighbors) # Expand the subgraph with the new neighbors - end - end - - # Collect subgraph nodes and their features - subgraph_node_list = collect(subgraph_nodes) - - if isempty(subgraph_node_list) - return GNNGraph(), state + batch_size - end - - mini_batch_gnn = Graphs.induced_subgraph(loader.graph, subgraph_node_list) # Create a subgraph of the nodes - - # Continue iteration for the next batch - return mini_batch_gnn, state + batch_size -end diff --git a/GNNGraphs/src/sampling.jl b/GNNGraphs/src/sampling.jl index 6e38730f0..27e084320 100644 --- a/GNNGraphs/src/sampling.jl +++ b/GNNGraphs/src/sampling.jl @@ -1,3 +1,110 @@ +""" + NeighborLoader(graph; num_neighbors, input_nodes, num_layers, [batch_size]) + +A data structure for sampling neighbors from a graph for training Graph Neural Networks (GNNs). +It supports multi-layer sampling of neighbors for a batch of input nodes, useful for mini-batch training +originally introduced in ["Inductive Representation Learning on Large Graphs"}(https://arxiv.org/abs/1706.02216) paper. + +# Fields +- `graph::GNNGraph`: The input graph. +- `num_neighbors::Vector{Int}`: A vector specifying the number of neighbors to sample per node at each GNN layer. +- `input_nodes::Vector{Int}`: A vector containing the starting nodes for neighbor sampling. +- `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`. + +# 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)) + end +``` +""" +struct NeighborLoader + graph::GNNGraph # The input GNNGraph (graph + features from GraphNeuralNetworks.jl) + num_neighbors::Vector{Int} # Number of neighbors to sample per node, for each layer + input_nodes::Vector{Int} # Set of input nodes (starting nodes for sampling) + num_layers::Int # Number of layers for neighborhood expansion + batch_size::Union{Int, Nothing} # Optional batch size, defaults to the length of input_nodes if not given + neighbors_cache::Dict{Int, Vector{Int}} # Cache neighbors to avoid recomputation +end + +function NeighborLoader(graph::GNNGraph; num_neighbors::Vector{Int}, input_nodes::Vector{Int}=nothing, + num_layers::Int, batch_size::Union{Int, Nothing}=nothing) + return NeighborLoader(graph, num_neighbors, input_nodes === nothing ? collect(1:graph.num_nodes) : input_nodes, num_layers, + batch_size === nothing ? length(input_nodes) : batch_size, Dict{Int, Vector{Int}}()) +end + +# Function to get cached neighbors or compute them +function get_neighbors(loader::NeighborLoader, node::Int) + if haskey(loader.neighbors_cache, node) + return loader.neighbors_cache[node] + else + neighbors = Graphs.neighbors(loader.graph, node, dir = :in) # Get neighbors from graph + loader.neighbors_cache[node] = neighbors + return neighbors + end +end + +# Function to sample neighbors for a given node at a specific layer +function sample_nbrs(loader::NeighborLoader, node::Int, layer::Int) + neighbors = get_neighbors(loader, node) + if isempty(neighbors) + return Int[] + else + num_samples = min(loader.num_neighbors[layer], length(neighbors)) # Limit to required samples for this layer + return rand(neighbors, num_samples) # Randomly sample neighbors + end +end + +# Iterator protocol for NeighborLoader with lazy batch loading +function Base.iterate(loader::NeighborLoader, state=1) + if state > length(loader.input_nodes) + return nothing # End of iteration if batches are exhausted (state larger than amount of input nodes or current batch no >= batch number) + end + + # Determine the size of the current batch + batch_size = min(loader.batch_size, length(loader.input_nodes) - state + 1) # Conditional in case there is not enough nodes to fill the last batch + batch_nodes = loader.input_nodes[state:state + batch_size - 1] # Each mini-batch uses different set of input nodes + + # Set for tracking the subgraph nodes + subgraph_nodes = Set(batch_nodes) + + for node in batch_nodes + # Initialize current layer of nodes (starting with the node itself) + sampled_neighbors = Set([node]) + + # For each GNN layer, sample the neighborhood + for layer in 1:loader.num_layers + new_neighbors = Set{Int}() + for n in sampled_neighbors + neighbors = sample_nbrs(loader, n, layer) # Sample neighbors of the node for this layer + new_neighbors = union(new_neighbors, neighbors) # Avoid duplicates in the neighbor set + end + sampled_neighbors = new_neighbors + subgraph_nodes = union(subgraph_nodes, sampled_neighbors) # Expand the subgraph with the new neighbors + end + end + + # Collect subgraph nodes and their features + subgraph_node_list = collect(subgraph_nodes) + + if isempty(subgraph_node_list) + return GNNGraph(), state + batch_size + end + + mini_batch_gnn = Graphs.induced_subgraph(loader.graph, subgraph_node_list) # Create a subgraph of the nodes + + # Continue iteration for the next batch + return mini_batch_gnn, state + batch_size +end + + """ sample_neighbors(g, nodes, K=-1; dir=:in, replace=false, dropnodes=false) diff --git a/GNNGraphs/src/temporalsnapshotsgnngraph.jl b/GNNGraphs/src/temporalsnapshotsgnngraph.jl index 641162e58..a769892d7 100644 --- a/GNNGraphs/src/temporalsnapshotsgnngraph.jl +++ b/GNNGraphs/src/temporalsnapshotsgnngraph.jl @@ -61,6 +61,9 @@ struct TemporalSnapshotsGNNGraph{G<:GNNGraph, D<:DataStore} tgdata::D end +# do not move to gpu num_nodes and num_edges +@functor TemporalSnapshotsGNNGraph (snapshots, tgdata) + function TemporalSnapshotsGNNGraph(snapshots) snapshots = collect(snapshots) return TemporalSnapshotsGNNGraph( diff --git a/GNNGraphs/test/Project.toml b/GNNGraphs/test/Project.toml new file mode 100644 index 000000000..d104c951a --- /dev/null +++ b/GNNGraphs/test/Project.toml @@ -0,0 +1,26 @@ +[deps] +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" +Functors = "d9f16b24-f501-4c13-a1f2-28368ffc5196" +GNNGraphs = "aed8fd31-079b-4b5a-b342-a13352159b8c" +GPUArraysCore = "46192b85-c4d5-4398-a991-12ede77f4527" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +MLDataDevices = "7e8f7934-dd98-4c1a-8fe8-92b47a384d40" +MLDatasets = "eb30cadb-4394-5ae3-aed4-317e484a6458" +MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" +TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" +cuDNN = "02a925ec-e4fe-4b08-9a7e-0d78e3d38ccd" + +[compat] +GPUArraysCore = "0.1" diff --git a/GNNGraphs/test/chainrules.jl b/GNNGraphs/test/chainrules.jl index f0df6b6ca..42898d172 100644 --- a/GNNGraphs/test/chainrules.jl +++ b/GNNGraphs/test/chainrules.jl @@ -1,4 +1,5 @@ -@testset "dict constructor" begin +@testitem "dict constructor" setup=[GraphsTestModule] begin + using .GraphsTestModule grad = gradient(1.) do x d = Dict([:x => x, :y => 5]...) return sum(d[:x].^2) @@ -6,19 +7,17 @@ @test grad == 2 - ## BROKEN Constructors - # grad = gradient(1.) do x - # d = Dict([(:x => x), (:y => 5)]) - # return sum(d[:x].^2) - # end[1] - - # @test grad == 2 + grad = gradient(1.) do x + d = Dict([:x => x, :y => 5]) + return sum(d[:x].^2) + end[1] + @test grad == 2 - # grad = gradient(1.) do x - # d = Dict([(:x => x), (:y => 5)]) - # return sum(d[:x].^2) - # end[1] + grad = gradient(1.) do x + d = Dict(:x => x, :y => 5) + return sum(d[:x].^2) + end[1] - # @test grad == 2 + @test grad == 2 end diff --git a/GNNGraphs/test/convert.jl b/GNNGraphs/test/convert.jl index 898a8d771..6f4911577 100644 --- a/GNNGraphs/test/convert.jl +++ b/GNNGraphs/test/convert.jl @@ -1,20 +1,24 @@ -if TEST_GPU - @testset "to_coo(dense) on gpu" begin - get_st(A) = GNNGraphs.to_coo(A)[1][1:2] - get_val(A) = GNNGraphs.to_coo(A)[1][3] +@testitem "to_coo(dense) on gpu" setup=[GraphsTestModule] tags=[:gpu] begin + using .GraphsTestModule + get_st(A) = GNNGraphs.to_coo(A)[1][1:2] + get_val(A) = GNNGraphs.to_coo(A)[1][3] + gpu = gpu_device(force=true) + A = gpu([0 2 2; 2.0 0 2; 2 2 0]) - A = cu([0 2 2; 2.0 0 2; 2 2 0]) + y = get_val(A) + @test y isa AbstractVector{Float32} + @test get_device(y) == get_device(A) + @test Array(y) ≈ [2, 2, 2, 2, 2, 2] - y = get_val(A) - @test y isa CuVector{Float32} - @test Array(y) ≈ [2, 2, 2, 2, 2, 2] + s, t = get_st(A) + @test s isa AbstractVector{<:Integer} + @test t isa AbstractVector{<:Integer} + @test get_device(s) == get_device(A) + @test get_device(t) == get_device(A) + @test Array(s) == [2, 3, 1, 3, 1, 2] + @test Array(t) == [1, 1, 2, 2, 3, 3] - s, t = get_st(A) - @test s isa CuVector{<:Integer} - @test t isa CuVector{<:Integer} - @test Array(s) == [2, 3, 1, 3, 1, 2] - @test Array(t) == [1, 1, 2, 2, 3, 3] - - @test gradient(A -> sum(get_val(A)), A)[1] isa CuMatrix{Float32} - end + grad = gradient(A -> sum(get_val(A)), A)[1] + @test grad isa AbstractMatrix{Float32} + @test get_device(grad) == get_device(A) end diff --git a/GNNGraphs/test/datastore.jl b/GNNGraphs/test/datastore.jl index 404136bdf..5e067a83c 100644 --- a/GNNGraphs/test/datastore.jl +++ b/GNNGraphs/test/datastore.jl @@ -1,5 +1,5 @@ -@testset "constructor" begin +@testitem "constructor" begin @test_throws AssertionError DataStore(10, (:x => rand(10), :y => rand(2, 4))) @testset "keyword args" begin @@ -13,7 +13,7 @@ end end -@testset "getproperty / setproperty!" begin +@testitem "getproperty / setproperty!" begin x = rand(10) ds = DataStore(10, (:x => x, :y => rand(2, 10))) @test ds.x == ds[:x] == x @@ -25,49 +25,50 @@ end @test fill(DataStore(), 3) isa Vector end -@testset "setindex!" begin +@testitem "setindex!" begin ds = DataStore(10) x = rand(10) @test (ds[:x] = x) == x # Tests setindex! @test ds.x == ds[:x] == x end -@testset "map" begin +@testitem "map" begin ds = DataStore(10, (:x => rand(10), :y => rand(2, 10))) ds2 = map(x -> x .+ 1, ds) @test ds2.x == ds.x .+ 1 @test ds2.y == ds.y .+ 1 - @test_throws AssertionError ds2=map(x -> [x; x], ds) + @test_throws AssertionError ds2 == map(x -> [x; x], ds) end -@testset "getdata / getn" begin +@testitem "getdata / getn" begin ds = DataStore(10, (:x => rand(10), :y => rand(2, 10))) - @test getdata(ds) == getfield(ds, :_data) + @test GNNGraphs.getdata(ds) == getfield(ds, :_data) @test_throws KeyError ds.data - @test getn(ds) == getfield(ds, :_n) + @test GNNGraphs.getn(ds) == getfield(ds, :_n) @test_throws KeyError ds.n end -@testset "cat empty" begin +@testitem "cat empty" begin ds1 = DataStore(2, (:x => rand(2))) ds2 = DataStore(1, (:x => rand(1))) dsempty = DataStore(0, (:x => rand(0))) ds = GNNGraphs.cat_features(ds1, ds2) - @test getn(ds) == 3 + @test GNNGraphs.getn(ds) == 3 ds = GNNGraphs.cat_features(ds1, dsempty) - @test getn(ds) == 2 + @test GNNGraphs.getn(ds) == 2 # issue #280 g = GNNGraph([1], [2]) h = add_edges(g, Int[], Int[]) # adds no edges - @test getn(g.edata) == 1 - @test getn(h.edata) == 1 + @test GNNGraphs.getn(g.edata) == 1 + @test GNNGraphs.getn(h.edata) == 1 end -@testset "gradient" begin +@testitem "gradient" setup=[GraphsTestModule] begin + using .GraphsTestModule ds = DataStore(10, (:x => rand(10), :y => rand(2, 10))) f1(ds) = sum(ds.x) @@ -80,11 +81,12 @@ end @test grad == exp.(x) end -@testset "functor" begin +@testitem "functor" begin + using Functors ds = DataStore(10, (:x => zeros(10), :y => ones(2, 10))) p, re = Functors.functor(ds) - @test p[1] === getn(ds) - @test p[2] === getdata(ds) + @test p[1] === GNNGraphs.getn(ds) + @test p[2] === GNNGraphs.getdata(ds) @test ds == re(p) ds2 = Functors.fmap(ds) do x diff --git a/GNNGraphs/test/ext/SimpleWeightedGraphs.jl b/GNNGraphs/test/ext/SimpleWeightedGraphs.jl index 254498999..7271ad9ca 100644 --- a/GNNGraphs/test/ext/SimpleWeightedGraphs.jl +++ b/GNNGraphs/test/ext/SimpleWeightedGraphs.jl @@ -1,4 +1,5 @@ -@testset "simple_weighted_graph" begin +@testitem "simple_weighted_graph" begin + using SimpleWeightedGraphs srcs = [1, 2, 1] dsts = [2, 3, 3] wts = [0.5, 0.8, 2.0] diff --git a/GNNGraphs/test/generate.jl b/GNNGraphs/test/generate.jl index c26b651c3..63cea373e 100644 --- a/GNNGraphs/test/generate.jl +++ b/GNNGraphs/test/generate.jl @@ -1,86 +1,94 @@ -@testset "rand_graph" begin +@testitem "rand_graph" setup=[GraphsTestModule] begin + using .GraphsTestModule n, m = 10, 20 m2 = m ÷ 2 x = rand(3, n) e = rand(4, m2) + for GRAPH_T in GRAPH_TYPES + g = rand_graph(n, m, ndata = x, edata = e, graph_type = GRAPH_T) + @test g.num_nodes == n + @test g.num_edges == m + @test g.ndata.x === x + if GRAPH_T == :coo + s, t = edge_index(g) + @test s[1:m2] == t[(m2 + 1):end] + @test t[1:m2] == s[(m2 + 1):end] + @test g.edata.e[:, 1:m2] == e + @test g.edata.e[:, (m2 + 1):end] == e + end - g = rand_graph(n, m, ndata = x, edata = e, graph_type = GRAPH_T) - @test g.num_nodes == n - @test g.num_edges == m - @test g.ndata.x === x - if GRAPH_T == :coo - s, t = edge_index(g) - @test s[1:m2] == t[(m2 + 1):end] - @test t[1:m2] == s[(m2 + 1):end] - @test g.edata.e[:, 1:m2] == e - @test g.edata.e[:, (m2 + 1):end] == e - end - - rng = MersenneTwister(17) - g = rand_graph(rng, n, m, bidirected = false, graph_type = GRAPH_T) - @test g.num_nodes == n - @test g.num_edges == m + rng = MersenneTwister(17) + g = rand_graph(rng, n, m, bidirected = false, graph_type = GRAPH_T) + @test g.num_nodes == n + @test g.num_edges == m - rng = MersenneTwister(17) - g2 = rand_graph(rng, n, m, bidirected = false, graph_type = GRAPH_T) - @test edge_index(g2) == edge_index(g) + rng = MersenneTwister(17) + g2 = rand_graph(rng, n, m, bidirected = false, graph_type = GRAPH_T) + @test edge_index(g2) == edge_index(g) - ew = rand(m2) - rng = MersenneTwister(17) - g = rand_graph(rng, n, m, bidirected = true, graph_type = GRAPH_T, edge_weight = ew) - @test get_edge_weight(g) == [ew; ew] broken=(GRAPH_T != :coo) - - ew = rand(m) - rng = MersenneTwister(17) - g = rand_graph(n, m, bidirected = false, graph_type = GRAPH_T, edge_weight = ew) - @test get_edge_weight(g) == ew broken=(GRAPH_T != :coo) + ew = rand(m2) + rng = MersenneTwister(17) + g = rand_graph(rng, n, m, bidirected = true, graph_type = GRAPH_T, edge_weight = ew) + @test get_edge_weight(g) == [ew; ew] broken=(GRAPH_T != :coo) + + ew = rand(m) + rng = MersenneTwister(17) + g = rand_graph(n, m, bidirected = false, graph_type = GRAPH_T, edge_weight = ew) + @test get_edge_weight(g) == ew broken=(GRAPH_T != :coo) + end end -@testset "knn_graph" begin - n, k = 10, 3 - x = rand(3, n) - g = knn_graph(x, k; graph_type = GRAPH_T) - @test g.num_nodes == 10 - @test g.num_edges == n * k - @test degree(g, dir = :in) == fill(k, n) - @test has_self_loops(g) == false +@testitem "knn_graph" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + n, k = 10, 3 + x = rand(3, n) + g = knn_graph(x, k; graph_type = GRAPH_T) + @test g.num_nodes == 10 + @test g.num_edges == n * k + @test degree(g, dir = :in) == fill(k, n) + @test has_self_loops(g) == false - g = knn_graph(x, k; dir = :out, self_loops = true, graph_type = GRAPH_T) - @test g.num_nodes == 10 - @test g.num_edges == n * k - @test degree(g, dir = :out) == fill(k, n) - @test has_self_loops(g) == true + g = knn_graph(x, k; dir = :out, self_loops = true, graph_type = GRAPH_T) + @test g.num_nodes == 10 + @test g.num_edges == n * k + @test degree(g, dir = :out) == fill(k, n) + @test has_self_loops(g) == true - graph_indicator = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2] - g = knn_graph(x, k; graph_indicator, graph_type = GRAPH_T) - @test g.num_graphs == 2 - s, t = edge_index(g) - ne = n * k ÷ 2 - @test all(1 .<= s[1:ne] .<= 5) - @test all(1 .<= t[1:ne] .<= 5) - @test all(6 .<= s[(ne + 1):end] .<= 10) - @test all(6 .<= t[(ne + 1):end] .<= 10) + graph_indicator = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2] + g = knn_graph(x, k; graph_indicator, graph_type = GRAPH_T) + @test g.num_graphs == 2 + s, t = edge_index(g) + ne = n * k ÷ 2 + @test all(1 .<= s[1:ne] .<= 5) + @test all(1 .<= t[1:ne] .<= 5) + @test all(6 .<= s[(ne + 1):end] .<= 10) + @test all(6 .<= t[(ne + 1):end] .<= 10) + end end -@testset "radius_graph" begin - n, r = 10, 0.5 - x = rand(3, n) - g = radius_graph(x, r; graph_type = GRAPH_T) - @test g.num_nodes == 10 - @test has_self_loops(g) == false +@testitem "radius_graph" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + n, r = 10, 0.5 + x = rand(3, n) + g = radius_graph(x, r; graph_type = GRAPH_T) + @test g.num_nodes == 10 + @test has_self_loops(g) == false - g = radius_graph(x, r; dir = :out, self_loops = true, graph_type = GRAPH_T) - @test g.num_nodes == 10 - @test has_self_loops(g) == true + g = radius_graph(x, r; dir = :out, self_loops = true, graph_type = GRAPH_T) + @test g.num_nodes == 10 + @test has_self_loops(g) == true - graph_indicator = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2] - g = radius_graph(x, r; graph_indicator, graph_type = GRAPH_T) - @test g.num_graphs == 2 - s, t = edge_index(g) - @test (s .> 5) == (t .> 5) + graph_indicator = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2] + g = radius_graph(x, r; graph_indicator, graph_type = GRAPH_T) + @test g.num_graphs == 2 + s, t = edge_index(g) + @test (s .> 5) == (t .> 5) + end end -@testset "rand_bipartite_heterograph" begin +@testitem "rand_bipartite_heterograph" begin g = rand_bipartite_heterograph((10, 15), (20, 20)) @test g.num_nodes == Dict(:A => 10, :B => 15) @test g.num_edges == Dict((:A, :to, :B) => 20, (:B, :to, :A) => 20) @@ -97,7 +105,8 @@ end @test !has_edge(g, (:B,:to,:A), 1, 1) end -@testset "rand_temporal_radius_graph" begin +@testitem "rand_temporal_radius_graph" begin + using Statistics number_nodes = 30 number_snapshots = 5 r = 0.1 @@ -110,7 +119,8 @@ end @test mean(mean(degree.(tg.snapshots)))<=mean(mean(degree.(tg2.snapshots))) end -@testset "rand_temporal_hyperbolic_graph" begin +@testitem "rand_temporal_hyperbolic_graph" begin + using Statistics @test GNNGraphs._hyperbolic_distance([1.0,1.0],[1.0,1.0];ζ=1)==0 @test GNNGraphs._hyperbolic_distance([0.23,0.11],[0.98,0.55];ζ=1) == GNNGraphs._hyperbolic_distance([0.98,0.55],[0.23,0.11];ζ=1) number_nodes = 30 diff --git a/GNNGraphs/test/gnngraph.jl b/GNNGraphs/test/gnngraph.jl index abe9518cb..1e78349dc 100644 --- a/GNNGraphs/test/gnngraph.jl +++ b/GNNGraphs/test/gnngraph.jl @@ -1,132 +1,150 @@ # TODO test that the graph type is preserved # when constructing a GNNGraph from another -@testset "Constructor: adjacency matrix" begin - A = sprand(10, 10, 0.5) - sA, tA, vA = findnz(A) - - g = GNNGraph(A, graph_type = GRAPH_T) - s, t = edge_index(g) - v = get_edge_weight(g) - @test s == sA - @test t == tA - @test v == vA - - g = GNNGraph(Matrix(A), graph_type = GRAPH_T) - s, t = edge_index(g) - v = get_edge_weight(g) - @test s == sA - @test t == tA - @test v == vA - - g = GNNGraph([0 0 0 - 0 0 1 - 0 1 0], graph_type = GRAPH_T) - @test g.num_nodes == 3 - @test g.num_edges == 2 - - g = GNNGraph([0 1 0 - 1 0 0 - 0 0 0], graph_type = GRAPH_T) - @test g.num_nodes == 3 - @test g.num_edges == 2 -end - -@testset "Constructor: integer" begin - g = GNNGraph(10, graph_type = GRAPH_T) - @test g.num_nodes == 10 - @test g.num_edges == 0 - - g2 = rand_graph(10, 30, graph_type = GRAPH_T) - G = typeof(g2) - g = G(10) - @test g.num_nodes == 10 - @test g.num_edges == 0 - - g = GNNGraph(graph_type = GRAPH_T) - @test g.num_nodes == 0 +@testitem "Constructor: adjacency matrix" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + A = sprand(10, 10, 0.5) + sA, tA, vA = findnz(A) + + g = GNNGraph(A, graph_type = GRAPH_T) + s, t = edge_index(g) + v = get_edge_weight(g) + @test s == sA + @test t == tA + @test v == vA + + g = GNNGraph(Matrix(A), graph_type = GRAPH_T) + s, t = edge_index(g) + v = get_edge_weight(g) + @test s == sA + @test t == tA + @test v == vA + + g = GNNGraph([0 0 0 + 0 0 1 + 0 1 0], graph_type = GRAPH_T) + @test g.num_nodes == 3 + @test g.num_edges == 2 + + g = GNNGraph([0 1 0 + 1 0 0 + 0 0 0], graph_type = GRAPH_T) + @test g.num_nodes == 3 + @test g.num_edges == 2 + end end -@testset "symmetric graph" begin - s = [1, 1, 2, 2, 3, 3, 4, 4] - t = [2, 4, 1, 3, 2, 4, 1, 3] - adj_mat = [0 1 0 1 - 1 0 1 0 - 0 1 0 1 - 1 0 1 0] - adj_list_out = [[2, 4], [1, 3], [2, 4], [1, 3]] - adj_list_in = [[2, 4], [1, 3], [2, 4], [1, 3]] - - # core functionality - g = GNNGraph(s, t; graph_type = GRAPH_T) - if TEST_GPU - dev = CUDADevice() - g_gpu = g |> dev +@testitem "Constructor: integer" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = GNNGraph(10, graph_type = GRAPH_T) + @test g.num_nodes == 10 + @test g.num_edges == 0 + + g2 = rand_graph(10, 30, graph_type = GRAPH_T) + G = typeof(g2) + g = G(10) + @test g.num_nodes == 10 + @test g.num_edges == 0 + + g = GNNGraph(graph_type = GRAPH_T) + @test g.num_nodes == 0 end +end - @test g.num_edges == 8 - @test g.num_nodes == 4 - @test nv(g) == g.num_nodes - @test ne(g) == g.num_edges - @test Tuple.(collect(edges(g))) |> sort == collect(zip(s, t)) |> sort - @test sort(outneighbors(g, 1)) == [2, 4] - @test sort(inneighbors(g, 1)) == [2, 4] - @test is_directed(g) == true - s1, t1 = sort_edge_index(edge_index(g)) - @test s1 == s - @test t1 == t - @test vertices(g) == 1:(g.num_nodes) - - @test sort.(adjacency_list(g; dir = :in)) == adj_list_in - @test sort.(adjacency_list(g; dir = :out)) == adj_list_out - - @testset "adjacency_matrix" begin - @test adjacency_matrix(g) == adj_mat - @test adjacency_matrix(g; dir = :in) == adj_mat - @test adjacency_matrix(g; dir = :out) == adj_mat - +@testitem "symmetric graph" setup=[GraphsTestModule] tags=[:gpu] begin + using .GraphsTestModule + dev = gpu_device() + TEST_GPU = dev != cpu_device() + for GRAPH_T in GRAPH_TYPES + s = [1, 1, 2, 2, 3, 3, 4, 4] + t = [2, 4, 1, 3, 2, 4, 1, 3] + adj_mat = [0 1 0 1 + 1 0 1 0 + 0 1 0 1 + 1 0 1 0] + adj_list_out = [[2, 4], [1, 3], [2, 4], [1, 3]] + adj_list_in = [[2, 4], [1, 3], [2, 4], [1, 3]] + + # core functionality + g = GNNGraph(s, t; graph_type = GRAPH_T) if TEST_GPU - # See https://github.com/JuliaGPU/CUDA.jl/pull/1093 - mat_gpu = adjacency_matrix(g_gpu) - @test mat_gpu isa ACUMatrix{Int} - @test Array(mat_gpu) == adj_mat + g_gpu = g |> dev end - end - @testset "normalized_laplacian" begin - mat = normalized_laplacian(g) - if TEST_GPU - mat_gpu = normalized_laplacian(g_gpu) - @test mat_gpu isa ACUMatrix{Float32} - @test Array(mat_gpu) == mat + @test g.num_edges == 8 + @test g.num_nodes == 4 + @test nv(g) == g.num_nodes + @test ne(g) == g.num_edges + @test Tuple.(collect(edges(g))) |> sort == collect(zip(s, t)) |> sort + @test sort(outneighbors(g, 1)) == [2, 4] + @test sort(inneighbors(g, 1)) == [2, 4] + @test is_directed(g) == true + s1, t1 = sort_edge_index(edge_index(g)) + @test s1 == s + @test t1 == t + @test vertices(g) == 1:(g.num_nodes) + + @test sort.(adjacency_list(g; dir = :in)) == adj_list_in + @test sort.(adjacency_list(g; dir = :out)) == adj_list_out + + @testset "adjacency_matrix" begin + @test adjacency_matrix(g) == adj_mat + @test adjacency_matrix(g; dir = :in) == adj_mat + @test adjacency_matrix(g; dir = :out) == adj_mat + + if TEST_GPU && !(dev isa MetalDevice) && GRAPH_T != :sparse + # See https://github.com/JuliaGPU/CUDA.jl/pull/1093 + mat_gpu = adjacency_matrix(g_gpu) + @test mat_gpu isa AbstractMatrix{Int} + @test get_device(mat_gpu) isa AbstractGPUDevice + @test Array(mat_gpu) == adj_mat + end end - end - @testset "scaled_laplacian" begin if TEST_GPU - mat = scaled_laplacian(g) - mat_gpu = scaled_laplacian(g_gpu) - @test mat_gpu isa ACUMatrix{Float32} - @test Array(mat_gpu) ≈ mat - end end + @testset "normalized_laplacian" begin + mat = normalized_laplacian(g) + if TEST_GPU && !(dev isa MetalDevice) && GRAPH_T != :sparse + mat_gpu = normalized_laplacian(g_gpu) + @test mat_gpu isa AbstractMatrix{Float32} + @test get_device(mat_gpu)isa AbstractGPUDevice + @test Array(mat_gpu) == mat + end + end - @testset "constructors" begin - adjacency_matrix(g; dir = :out) == adj_mat - adjacency_matrix(g; dir = :in) == adj_mat - end + @testset "scaled_laplacian" begin + if TEST_GPU && !(dev isa MetalDevice) && GRAPH_T != :sparse + mat = scaled_laplacian(g) + mat_gpu = scaled_laplacian(g_gpu) + @test mat_gpu isa AbstractMatrix{Float32} + @test get_device(mat_gpu) isa AbstractGPUDevice + @test Array(mat_gpu) ≈ mat + end + end + + @testset "constructors" begin + adjacency_matrix(g; dir = :out) == adj_mat + adjacency_matrix(g; dir = :in) == adj_mat + end - if TEST_GPU - @testset "functor" begin - s_cpu, t_cpu = edge_index(g) - s_gpu, t_gpu = edge_index(g_gpu) - @test s_gpu isa CuVector{Int} - @test Array(s_gpu) == s_cpu - @test t_gpu isa CuVector{Int} - @test Array(t_gpu) == t_cpu + if TEST_GPU + @testset "functor" begin + s_cpu, t_cpu = edge_index(g) + s_gpu, t_gpu = edge_index(g_gpu) + @test s_gpu isa AbstractVector{<:Integer} + @test get_device(s_gpu) isa AbstractGPUDevice + @test Array(s_gpu) == s_cpu + @test t_gpu isa AbstractVector{<:Integer} + @test get_device(t_gpu) isa AbstractGPUDevice + @test Array(t_gpu) == t_cpu + end end end end -@testset "asymmetric graph" begin +@testitem "asymmetric graph" setup=[GraphsTestModule] begin + using .GraphsTestModule s = [1, 2, 3, 4] t = [2, 3, 4, 1] adj_mat_out = [0 1 0 0 @@ -141,210 +159,231 @@ end 0 0 1 0] adj_list_in = [[4], [1], [2], [3]] - # core functionality - g = GNNGraph(s, t; graph_type = GRAPH_T) - if TEST_GPU - dev = CUDADevice() #TODO replace with `gpu_device()` - g_gpu = g |> dev + for GRAPH_T in GRAPH_TYPES + g = GNNGraph(s, t; graph_type = GRAPH_T) + + @test g.num_edges == 4 + @test g.num_nodes == 4 + @test length(edges(g)) == 4 + @test sort(outneighbors(g, 1)) == [2] + @test sort(inneighbors(g, 1)) == [4] + @test is_directed(g) == true + @test is_directed(typeof(g)) == true + s1, t1 = sort_edge_index(edge_index(g)) + @test s1 == s + @test t1 == t + + # adjacency + @test adjacency_matrix(g) == adj_mat_out + @test adjacency_list(g) == adj_list_out + @test adjacency_matrix(g, dir = :out) == adj_mat_out + @test adjacency_list(g, dir = :out) == adj_list_out + @test adjacency_matrix(g, dir = :in) == adj_mat_in + @test adjacency_list(g, dir = :in) == adj_list_in end - - @test g.num_edges == 4 - @test g.num_nodes == 4 - @test length(edges(g)) == 4 - @test sort(outneighbors(g, 1)) == [2] - @test sort(inneighbors(g, 1)) == [4] - @test is_directed(g) == true - @test is_directed(typeof(g)) == true - s1, t1 = sort_edge_index(edge_index(g)) - @test s1 == s - @test t1 == t - - # adjacency - @test adjacency_matrix(g) == adj_mat_out - @test adjacency_list(g) == adj_list_out - @test adjacency_matrix(g, dir = :out) == adj_mat_out - @test adjacency_list(g, dir = :out) == adj_list_out - @test adjacency_matrix(g, dir = :in) == adj_mat_in - @test adjacency_list(g, dir = :in) == adj_list_in end -@testset "zero" begin - g = rand_graph(4, 6, graph_type = GRAPH_T) - G = typeof(g) - @test zero(G) == G(0) +@testitem "zero" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = rand_graph(4, 6, graph_type = GRAPH_T) + G = typeof(g) + @test zero(G) == G(0) + end end -@testset "Graphs.jl constructor" begin - lg = random_regular_graph(10, 4) - @test !Graphs.is_directed(lg) - g = GNNGraph(lg) - @test g.num_edges == 2 * ne(lg) # g in undirected - @test Graphs.is_directed(g) - for e in Graphs.edges(lg) - i, j = src(e), dst(e) - @test has_edge(g, i, j) - @test has_edge(g, j, i) - end +@testitem "Graphs.jl constructor" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + lg = random_regular_graph(10, 4) + @test !Graphs.is_directed(lg) + g = GNNGraph(lg, graph_type = GRAPH_T) + @test g.num_edges == 2 * ne(lg) # g in undirected + @test Graphs.is_directed(g) + for e in Graphs.edges(lg) + i, j = src(e), dst(e) + @test has_edge(g, i, j) + @test has_edge(g, j, i) + end - @testset "SimpleGraph{Int32}" begin - g = GNNGraph(SimpleGraph{Int32}(6), graph_type = GRAPH_T) - @test g.num_nodes == 6 + @testset "SimpleGraph{Int32}" begin + g = GNNGraph(SimpleGraph{Int32}(6), graph_type = GRAPH_T) + @test g.num_nodes == 6 + end end end -@testset "Features" begin - g = GNNGraph(sprand(10, 10, 0.3), graph_type = GRAPH_T) - - # default names - X = rand(10, g.num_nodes) - E = rand(10, g.num_edges) - U = rand(10, g.num_graphs) - - g = GNNGraph(g, ndata = X, edata = E, gdata = U) - @test g.ndata.x === X - @test g.edata.e === E - @test g.gdata.u === U - @test g.x === g.ndata.x - @test g.e === g.edata.e - @test g.u === g.gdata.u - - # Check no args - g = GNNGraph(g) - @test g.ndata.x === X - @test g.edata.e === E - @test g.gdata.u === U - - # multiple features names - g = GNNGraph(g, ndata = (x2 = 2X, g.ndata...), edata = (e2 = 2E, g.edata...), - gdata = (u2 = 2U, g.gdata...)) - @test g.ndata.x === X - @test g.edata.e === E - @test g.gdata.u === U - @test g.ndata.x2 ≈ 2X - @test g.edata.e2 ≈ 2E - @test g.gdata.u2 ≈ 2U - @test g.x === g.ndata.x - @test g.e === g.edata.e - @test g.u === g.gdata.u - @test g.x2 === g.ndata.x2 - @test g.e2 === g.edata.e2 - @test g.u2 === g.gdata.u2 - - # Dimension checks - @test_throws AssertionError GNNGraph(erdos_renyi(10, 30), edata = rand(29), - graph_type = GRAPH_T) - @test_throws AssertionError GNNGraph(erdos_renyi(10, 30), edata = rand(2, 29), - graph_type = GRAPH_T) - @test_throws AssertionError GNNGraph(erdos_renyi(10, 30), - edata = (; x = rand(30), y = rand(29)), - graph_type = GRAPH_T) - - # Copy features on reverse edge - e = rand(30) - g = GNNGraph(erdos_renyi(10, 30), edata = e, graph_type = GRAPH_T) - @test g.edata.e == [e; e] - - # non-array global - g = rand_graph(10, 30, gdata = "ciao", graph_type = GRAPH_T) - @test g.gdata.u == "ciao" - - # vectors stays vectors - g = rand_graph(10, 30, ndata = rand(10), - edata = rand(30), - gdata = (u = rand(2), z = rand(1), q = 1), - graph_type = GRAPH_T) - @test size(g.ndata.x) == (10,) - @test size(g.edata.e) == (30,) - @test size(g.gdata.u) == (2, 1) - @test size(g.gdata.z) == (1,) - @test g.gdata.q === 1 - - # Error for non-array ndata - @test_throws AssertionError rand_graph(10, 30, ndata = "ciao", graph_type = GRAPH_T) - @test_throws AssertionError rand_graph(10, 30, ndata = 1, graph_type = GRAPH_T) - - # Error for Ambiguous getproperty - g = rand_graph(10, 20, ndata = rand(2, 10), edata = (; x = rand(3, 20)), - graph_type = GRAPH_T) - @test size(g.ndata.x) == (2, 10) - @test size(g.edata.x) == (3, 20) - @test_throws ArgumentError g.x +@testitem "Features" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = GNNGraph(sprand(10, 10, 0.3), graph_type = GRAPH_T) + + # default names + X = rand(10, g.num_nodes) + E = rand(10, g.num_edges) + U = rand(10, g.num_graphs) + + g = GNNGraph(g, ndata = X, edata = E, gdata = U) + @test g.ndata.x === X + @test g.edata.e === E + @test g.gdata.u === U + @test g.x === g.ndata.x + @test g.e === g.edata.e + @test g.u === g.gdata.u + + # Check no args + g = GNNGraph(g) + @test g.ndata.x === X + @test g.edata.e === E + @test g.gdata.u === U + + # multiple features names + g = GNNGraph(g, ndata = (x2 = 2X, g.ndata...), edata = (e2 = 2E, g.edata...), + gdata = (u2 = 2U, g.gdata...)) + @test g.ndata.x === X + @test g.edata.e === E + @test g.gdata.u === U + @test g.ndata.x2 ≈ 2X + @test g.edata.e2 ≈ 2E + @test g.gdata.u2 ≈ 2U + @test g.x === g.ndata.x + @test g.e === g.edata.e + @test g.u === g.gdata.u + @test g.x2 === g.ndata.x2 + @test g.e2 === g.edata.e2 + @test g.u2 === g.gdata.u2 + + # Dimension checks + @test_throws AssertionError GNNGraph(erdos_renyi(10, 30), edata = rand(29), + graph_type = GRAPH_T) + @test_throws AssertionError GNNGraph(erdos_renyi(10, 30), edata = rand(2, 29), + graph_type = GRAPH_T) + @test_throws AssertionError GNNGraph(erdos_renyi(10, 30), + edata = (; x = rand(30), y = rand(29)), + graph_type = GRAPH_T) + + # Copy features on reverse edge + e = rand(30) + g = GNNGraph(erdos_renyi(10, 30), edata = e, graph_type = GRAPH_T) + @test g.edata.e == [e; e] + + # non-array global + g = rand_graph(10, 30, gdata = "ciao", graph_type = GRAPH_T) + @test g.gdata.u == "ciao" + + # vectors stays vectors + g = rand_graph(10, 30, ndata = rand(10), + edata = rand(30), + gdata = (u = rand(2), z = rand(1), q = 1), + graph_type = GRAPH_T) + @test size(g.ndata.x) == (10,) + @test size(g.edata.e) == (30,) + @test size(g.gdata.u) == (2, 1) + @test size(g.gdata.z) == (1,) + @test g.gdata.q === 1 + + # Error for non-array ndata + @test_throws AssertionError rand_graph(10, 30, ndata = "ciao", graph_type = GRAPH_T) + @test_throws AssertionError rand_graph(10, 30, ndata = 1, graph_type = GRAPH_T) + + # Error for Ambiguous getproperty + g = rand_graph(10, 20, ndata = rand(2, 10), edata = (; x = rand(3, 20)), + graph_type = GRAPH_T) + @test size(g.ndata.x) == (2, 10) + @test size(g.edata.x) == (3, 20) + @test_throws ArgumentError g.x + end end -@testset "MLUtils and DataLoader compat" begin - n, m, num_graphs = 10, 30, 50 - X = rand(10, n) - E = rand(10, m) - U = rand(10, 1) - data = [rand_graph(n, m, ndata = X, edata = E, gdata = U, graph_type = GRAPH_T) - for _ in 1:num_graphs] - g = MLUtils.batch(data) - - @testset "batch then pass to dataloader" begin - @test MLUtils.getobs(g, 3) == getgraph(g, 3) - @test MLUtils.getobs(g, 3:5) == getgraph(g, 3:5) - @test MLUtils.numobs(g) == g.num_graphs - - d = MLUtils.DataLoader(g, batchsize = 2, shuffle = false) - @test first(d) == getgraph(g, 1:2) - end +@testitem "MLUtils and DataLoader compat" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + n, m, num_graphs = 10, 30, 50 + X = rand(10, n) + E = rand(10, m) + U = rand(10, 1) + data = [rand_graph(n, m, ndata = X, edata = E, gdata = U, graph_type = GRAPH_T) + for _ in 1:num_graphs] + g = MLUtils.batch(data) + + @testset "batch then pass to dataloader" begin + @test MLUtils.getobs(g, 3) == getgraph(g, 3) + @test MLUtils.getobs(g, 3:5) == getgraph(g, 3:5) + @test MLUtils.numobs(g) == g.num_graphs + + d = MLUtils.DataLoader(g, batchsize = 2, shuffle = false) + @test first(d) == getgraph(g, 1:2) + end - @testset "pass to dataloader and no automatic collation" begin - @test MLUtils.getobs(data, 3) == data[3] - @test MLUtils.getobs(data, 3:5) isa Vector{<:GNNGraph} - @test MLUtils.getobs(data, 3:5) == [data[3], data[4], data[5]] - @test MLUtils.numobs(data) == g.num_graphs + @testset "pass to dataloader and no automatic collation" begin + @test MLUtils.getobs(data, 3) == data[3] + @test MLUtils.getobs(data, 3:5) isa Vector{<:GNNGraph} + @test MLUtils.getobs(data, 3:5) == [data[3], data[4], data[5]] + @test MLUtils.numobs(data) == g.num_graphs - d = MLUtils.DataLoader(data, batchsize = 2, shuffle = false) - @test first(d) == [data[1], data[2]] + d = MLUtils.DataLoader(data, batchsize = 2, shuffle = false) + @test first(d) == [data[1], data[2]] + end end end -@testset "Graphs.jl integration" begin - g = GNNGraph(erdos_renyi(10, 20), graph_type = GRAPH_T) - @test g isa Graphs.AbstractGraph +@testitem "Graphs.jl integration" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = GNNGraph(erdos_renyi(10, 20), graph_type = GRAPH_T) + @test g isa Graphs.AbstractGraph + end end -@testset "==" begin - g1 = rand_graph(5, 6, ndata = rand(5), edata = rand(6), graph_type = GRAPH_T) - @test g1 == g1 - @test g1 == deepcopy(g1) - @test g1 !== deepcopy(g1) - - g2 = GNNGraph(g1, graph_type = GRAPH_T) - @test g1 == g2 - @test g1 === g2 # this is true since GNNGraph is immutable - - g2 = GNNGraph(g1, ndata = rand(5), graph_type = GRAPH_T) - @test g1 != g2 - @test g1 !== g2 - - g2 = GNNGraph(g1, edata = rand(6), graph_type = GRAPH_T) - @test g1 != g2 - @test g1 !== g2 +@testitem "==" setup = [GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g1 = rand_graph(5, 6, ndata = rand(5), edata = rand(6), graph_type = GRAPH_T) + @test g1 == g1 + @test g1 == deepcopy(g1) + @test g1 !== deepcopy(g1) + + g2 = GNNGraph(g1, graph_type = GRAPH_T) + @test g1 == g2 + @test g1 === g2 # this is true since GNNGraph is immutable + + g2 = GNNGraph(g1, ndata = rand(5), graph_type = GRAPH_T) + @test g1 != g2 + @test g1 !== g2 + + g2 = GNNGraph(g1, edata = rand(6), graph_type = GRAPH_T) + @test g1 != g2 + @test g1 !== g2 + end end -@testset "hash" begin - g1 = rand_graph(5, 6, ndata = rand(5), edata = rand(6), graph_type = GRAPH_T) - @test hash(g1) == hash(g1) - @test hash(g1) == hash(deepcopy(g1)) - @test hash(g1) == hash(GNNGraph(g1, ndata = g1.ndata, graph_type = GRAPH_T)) - @test hash(g1) == hash(GNNGraph(g1, ndata = g1.ndata, graph_type = GRAPH_T)) - @test hash(g1) != hash(GNNGraph(g1, ndata = rand(5), graph_type = GRAPH_T)) - @test hash(g1) != hash(GNNGraph(g1, edata = rand(6), graph_type = GRAPH_T)) +@testitem "hash" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g1 = rand_graph(5, 6, ndata = rand(5), edata = rand(6), graph_type = GRAPH_T) + @test hash(g1) == hash(g1) + @test hash(g1) == hash(deepcopy(g1)) + @test hash(g1) == hash(GNNGraph(g1, ndata = g1.ndata, graph_type = GRAPH_T)) + @test hash(g1) == hash(GNNGraph(g1, ndata = g1.ndata, graph_type = GRAPH_T)) + @test hash(g1) != hash(GNNGraph(g1, ndata = rand(5), graph_type = GRAPH_T)) + @test hash(g1) != hash(GNNGraph(g1, edata = rand(6), graph_type = GRAPH_T)) + end end -@testset "copy" begin - g1 = rand_graph(10, 4, ndata = rand(2, 10), graph_type = GRAPH_T) - g2 = copy(g1) - @test g1 === g2 # shallow copies are identical for immutable objects +@testitem "copy" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g1 = rand_graph(10, 4, ndata = rand(2, 10), graph_type = GRAPH_T) + g2 = copy(g1) + @test g1 === g2 # shallow copies are identical for immutable objects - g2 = copy(g1, deep = true) - @test g1 == g2 - @test g1 !== g2 + g2 = copy(g1, deep = true) + @test g1 == g2 + @test g1 !== g2 + end end -@testset "show" begin +@testitem "show" begin # no throw when global data is not an array g = rand_graph(10, 20, gdata = "ciao") @test sprint(show, g) == "GNNGraph(10, 20) with u: string(4) data" diff --git a/GNNGraphs/test/gnnheterograph.jl b/GNNGraphs/test/gnnheterograph.jl index f92bbad0f..7b389c207 100644 --- a/GNNGraphs/test/gnnheterograph.jl +++ b/GNNGraphs/test/gnnheterograph.jl @@ -1,6 +1,6 @@ -@testset "Empty constructor" begin +@testitem "Empty constructor" begin g = GNNHeteroGraph() @test isempty(g.num_nodes) g = add_edges(g, (:user, :like, :actor) => ([1,2,3,3,3], [3,5,1,9,4])) @@ -9,7 +9,7 @@ @test g.num_edges[(:user, :like, :actor)] == 5 end -@testset "Constructor from pairs" begin +@testitem "Constructor from pairs" begin hg = GNNHeteroGraph((:A, :e1, :B) => ([1,2,3,4], [3,2,1,5])) @test hg.num_nodes == Dict(:A => 4, :B => 5) @test hg.num_edges == Dict((:A, :e1, :B) => 4) @@ -20,7 +20,7 @@ end @test hg.num_edges == Dict((:A, :e1, :B) => 3, (:A, :e2, :C) => 3) end -@testset "Generation" begin +@testitem "Generation" begin hg = rand_heterograph(Dict(:A => 10, :B => 20), Dict((:A, :rel1, :B) => 30, (:B, :rel2, :A) => 10)) @@ -36,7 +36,7 @@ end @test sort(hg.etypes) == [(:A, :rel1, :B), (:B, :rel2, :A)] end -@testset "features" begin +@testitem "features" begin hg = rand_heterograph(Dict(:A => 10, :B => 20), Dict((:A, :rel1, :B) => 30, (:B, :rel2, :A) => 10), ndata = Dict(:A => rand(2, 10), @@ -52,7 +52,7 @@ end end -@testset "indexing syntax" begin +@testitem "indexing syntax" begin g = GNNHeteroGraph((:user, :rate, :movie) => ([1,1,2,3], [7,13,5,7])) g[:movie].z = rand(Float32, 64, 13); g[:user, :rate, :movie].e = rand(Float32, 64, 4); @@ -63,7 +63,7 @@ end end -@testset "simplified constructor" begin +@testitem "simplified constructor" begin hg = rand_heterograph((:A => 10, :B => 20), ((:A, :rel1, :B) => 30, (:B, :rel2, :A) => 10), ndata = (:A => rand(2, 10), @@ -96,7 +96,7 @@ end @test hg.num_edges == Dict((:A, :rel1, :B) => 20, (:B, :rel2, :A) => 30) end -@testset "num_edge_types / num_node_types" begin +@testitem "num_edge_types / num_node_types" begin hg = rand_heterograph((:A => 10, :B => 20), ((:A, :rel1, :B) => 30, (:B, :rel2, :A) => 10), ndata = (:A => rand(2, 10), @@ -111,7 +111,8 @@ end @test num_node_types(g) == 1 end -@testset "numobs" begin +@testitem "numobs" begin + using MLUtils hg = rand_heterograph((:A => 10, :B => 20), ((:A, :rel1, :B) => 30, (:B, :rel2, :A) => 10), ndata = (:A => rand(2, 10), @@ -121,7 +122,7 @@ end @test MLUtils.numobs(hg) == 1 end -@testset "get/set node features" begin +@testitem "get/set node features" begin d, n = 3, 5 g = rand_bipartite_heterograph((n, 2*n), 15) g[:A].x = rand(Float32, d, n) @@ -131,7 +132,7 @@ end @test size(g[:B].y) == (d, 2*n) end -@testset "add_edges" begin +@testitem "add_edges" begin d, n = 3, 5 g = rand_bipartite_heterograph((n, 2 * n), 15) s, t = [1, 2, 3], [3, 2, 1] @@ -177,7 +178,7 @@ end @test g3.num_nodes[:C] == 10 end -@testset "add self loops" begin +@testitem "add self loops" begin g1 = GNNHeteroGraph((:A, :to, :B) => ([1,2,3,4], [3,2,1,5])) g2 = add_self_loops(g1, (:A, :to, :B)) @test g2.num_edges[(:A, :to, :B)] === g1.num_edges[(:A, :to, :B)] diff --git a/GNNGraphs/test/mldatasets.jl b/GNNGraphs/test/mldatasets.jl index 21fa7dbcf..b7fc58466 100644 --- a/GNNGraphs/test/mldatasets.jl +++ b/GNNGraphs/test/mldatasets.jl @@ -1,8 +1,12 @@ -dataset = Cora() -classes = dataset.metadata["classes"] -gml = dataset[1] -g = mldataset2gnngraph(dataset) -@test g isa GNNGraph -@test g.num_nodes == gml.num_nodes -@test g.num_edges == gml.num_edges -@test edge_index(g) === gml.edge_index +@testitem "mldataset2gnngraph" begin + using MLDatasets: Cora + ENV["DATADEPS_ALWAYS_ACCEPT"] = true # for MLDatasets + dataset = Cora() + classes = dataset.metadata["classes"] + gml = dataset[1] + g = mldataset2gnngraph(dataset) + @test g isa GNNGraph + @test g.num_nodes == gml.num_nodes + @test g.num_edges == gml.num_edges + @test edge_index(g) === gml.edge_index +end diff --git a/GNNGraphs/test/operators.jl b/GNNGraphs/test/operators.jl index 9ba65ae91..85a0d8111 100644 --- a/GNNGraphs/test/operators.jl +++ b/GNNGraphs/test/operators.jl @@ -1,4 +1,7 @@ -@testset "intersect" begin - g = rand_graph(10, 20, graph_type = GRAPH_T) - @test intersect(g, g).num_edges == 20 +@testitem "intersect" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = rand_graph(10, 20, graph_type = GRAPH_T) + @test intersect(g, g).num_edges == 20 + end end diff --git a/GNNGraphs/test/query.jl b/GNNGraphs/test/query.jl index a345ae779..54aab409b 100644 --- a/GNNGraphs/test/query.jl +++ b/GNNGraphs/test/query.jl @@ -1,187 +1,231 @@ -@testset "is_bidirected" begin - g = rand_graph(10, 20, bidirected = true, graph_type = GRAPH_T) - @test is_bidirected(g) - - g = rand_graph(10, 20, bidirected = false, graph_type = GRAPH_T) - @test !is_bidirected(g) +@testitem "is_bidirected" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = rand_graph(10, 20, bidirected = true, graph_type = GRAPH_T) + @test is_bidirected(g) + + g = rand_graph(10, 20, bidirected = false, graph_type = GRAPH_T) + @test !is_bidirected(g) + end end -@testset "has_multi_edges" begin if GRAPH_T == :coo - s = [1, 1, 2, 3] - t = [2, 2, 2, 4] - g = GNNGraph(s, t, graph_type = GRAPH_T) - @test has_multi_edges(g) - - s = [1, 2, 2, 3] - t = [2, 1, 2, 4] - g = GNNGraph(s, t, graph_type = GRAPH_T) - @test !has_multi_edges(g) -end end - -@testset "edges" begin - g = rand_graph(4, 10, graph_type = GRAPH_T) - @test edgetype(g) <: Graphs.Edge - for e in edges(g) - @test e isa Graphs.Edge +@testitem "has_multi_edges" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + s = [1, 1, 2, 3] + t = [2, 2, 2, 4] + g = GNNGraph(s, t, graph_type = GRAPH_T) + @test has_multi_edges(g) + + s = [1, 2, 2, 3] + t = [2, 1, 2, 4] + g = GNNGraph(s, t, graph_type = GRAPH_T) + @test !has_multi_edges(g) + end end end -@testset "has_isolated_nodes" begin - s = [1, 2, 3] - t = [2, 3, 2] - g = GNNGraph(s, t, graph_type = GRAPH_T) - @test has_isolated_nodes(g) == false - @test has_isolated_nodes(g, dir = :in) == true +@testitem "edges" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = rand_graph(4, 10, graph_type = GRAPH_T) + @test edgetype(g) <: Graphs.Edge + for e in edges(g) + @test e isa Graphs.Edge + end + end end -@testset "has_self_loops" begin - s = [1, 1, 2, 3] - t = [2, 2, 2, 4] - g = GNNGraph(s, t, graph_type = GRAPH_T) - @test has_self_loops(g) - - s = [1, 1, 2, 3] - t = [2, 2, 3, 4] - g = GNNGraph(s, t, graph_type = GRAPH_T) - @test !has_self_loops(g) +@testitem "has_isolated_nodes" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + s = [1, 2, 3] + t = [2, 3, 2] + g = GNNGraph(s, t, graph_type = GRAPH_T) + @test has_isolated_nodes(g) == false + @test has_isolated_nodes(g, dir = :in) == true + end end -@testset "degree" begin - @testset "unweighted" begin +@testitem "has_self_loops" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES s = [1, 1, 2, 3] t = [2, 2, 2, 4] g = GNNGraph(s, t, graph_type = GRAPH_T) + @test has_self_loops(g) - @test degree(g) isa Vector{Int} - @test degree(g) == degree(g; dir = :out) == [2, 1, 1, 0] # default is outdegree - @test degree(g; dir = :in) == [0, 3, 0, 1] - @test degree(g; dir = :both) == [2, 4, 1, 1] - @test eltype(degree(g, Float32)) == Float32 - - if TEST_GPU - dev = CUDADevice() #TODO replace with `gpu_device()` - g_gpu = g |> dev - d = degree(g) - d_gpu = degree(g_gpu) - @test d_gpu isa CuVector{Int} - @test Array(d_gpu) == d - end + s = [1, 1, 2, 3] + t = [2, 2, 3, 4] + g = GNNGraph(s, t, graph_type = GRAPH_T) + @test !has_self_loops(g) end +end - @testset "weighted" begin - # weighted degree - s = [1, 1, 2, 3] - t = [2, 2, 2, 4] - eweight = Float32[0.1, 2.1, 1.2, 1] - g = GNNGraph((s, t, eweight), graph_type = GRAPH_T) - @test degree(g) ≈ [2.2, 1.2, 1.0, 0.0] - d = degree(g, edge_weight = false) - if GRAPH_T == :coo - @test d == [2, 1, 1, 0] - else - # Adjacency matrix representation cannot disambiguate multiple edges - # and edge weights - @test d == [1, 1, 1, 0] +@testitem "degree" setup=[GraphsTestModule] begin + using .GraphsTestModule + using SparseArrays: AbstractSparseMatrix + + for GRAPH_T in GRAPH_TYPES + @testset "unweighted" begin + s = [1, 1, 2, 3] + t = [2, 2, 2, 4] + g = GNNGraph(s, t, graph_type = GRAPH_T) + + @test degree(g) isa Vector{Int} + @test degree(g) == degree(g; dir = :out) == [2, 1, 1, 0] # default is outdegree + @test degree(g; dir = :in) == [0, 3, 0, 1] + @test degree(g; dir = :both) == [2, 4, 1, 1] + @test eltype(degree(g, Float32)) == Float32 end - @test eltype(d) <: Integer - @test degree(g, edge_weight = 2 * eweight) ≈ [4.4, 2.4, 2.0, 0.0] broken = (GRAPH_T != :coo) - - if TEST_GPU - dev = CUDADevice() #TODO replace with `gpu_device()` - g_gpu = g |> dev - d = degree(g) - d_gpu = degree(g_gpu) - @test d_gpu isa CuVector{Float32} - @test Array(d_gpu) ≈ d - end - @testset "gradient" begin - gw = gradient(eweight) do w - g = GNNGraph((s, t, w), graph_type = GRAPH_T) - sum(degree(g, edge_weight = false)) - end[1] - - @test gw === nothing - - gw = gradient(eweight) do w - g = GNNGraph((s, t, w), graph_type = GRAPH_T) - sum(degree(g, edge_weight = true)) - end[1] - - @test gw isa AbstractVector{Float32} - @test gw isa Vector{Float32} broken = (GRAPH_T == :sparse) - @test gw ≈ ones(Float32, length(gw)) - gw = gradient(eweight) do w - g = GNNGraph((s, t, w), graph_type = GRAPH_T) - sum(degree(g, dir=:both, edge_weight=true)) - end[1] - - @test gw isa AbstractVector{Float32} - @test gw isa Vector{Float32} broken = (GRAPH_T == :sparse) - @test gw ≈ 2 * ones(Float32, length(gw)) - - grad = gradient(g) do g - sum(degree(g, edge_weight=false)) - end[1] - @test grad === nothing - - grad = gradient(g) do g - sum(degree(g, edge_weight=true)) - end[1] - + @testset "weighted" begin + # weighted degree + s = [1, 1, 2, 3] + t = [2, 2, 2, 4] + eweight = Float32[0.1, 2.1, 1.2, 1] + g = GNNGraph((s, t, eweight), graph_type = GRAPH_T) + @test degree(g) ≈ [2.2, 1.2, 1.0, 0.0] + d = degree(g, edge_weight = false) if GRAPH_T == :coo - @test grad.graph[3] isa Vector{Float32} - @test grad.graph[3] ≈ ones(Float32, length(gw)) + @test d == [2, 1, 1, 0] else - if GRAPH_T == :sparse - @test grad.graph isa AbstractSparseMatrix{Float32} - end - @test grad.graph isa AbstractMatrix{Float32} - - @test grad.graph ≈ [0.0 1.0 0.0 0.0 - 0.0 1.0 0.0 0.0 - 0.0 0.0 0.0 1.0 - 0.0 0.0 0.0 0.0] + # Adjacency matrix representation cannot disambiguate multiple edges + # and edge weights + @test d == [1, 1, 1, 0] end + @test eltype(d) <: Integer + @test degree(g, edge_weight = 2 * eweight) ≈ [4.4, 2.4, 2.0, 0.0] broken = (GRAPH_T != :coo) + + @testset "gradient" begin + gw = gradient(eweight) do w + g = GNNGraph((s, t, w), graph_type = GRAPH_T) + sum(degree(g, edge_weight = false)) + end[1] - @testset "directed, degree dir=$dir" for dir in [:in, :out, :both] - g = rand_graph(10, 30, bidirected=false) - w = rand(Float32, 30) - s, t = edge_index(g) - - grad = gradient(w) do w + @test gw === nothing + + gw = gradient(eweight) do w g = GNNGraph((s, t, w), graph_type = GRAPH_T) - sum(tanh.(degree(g; dir, edge_weight=true))) + sum(degree(g, edge_weight = true)) end[1] - ngrad = ngradient(w) do w + @test gw isa AbstractVector{Float32} + @test gw isa Vector{Float32} broken = (GRAPH_T == :sparse) + @test gw ≈ ones(Float32, length(gw)) + + gw = gradient(eweight) do w g = GNNGraph((s, t, w), graph_type = GRAPH_T) - sum(tanh.(degree(g; dir, edge_weight=true))) + sum(degree(g, dir=:both, edge_weight=true)) end[1] - @test grad ≈ ngrad - end + @test gw isa AbstractVector{Float32} + @test gw isa Vector{Float32} broken = (GRAPH_T == :sparse) + @test gw ≈ 2 * ones(Float32, length(gw)) + + grad = gradient(g) do g + sum(degree(g, edge_weight=false)) + end[1] + @test grad === nothing + + grad = gradient(g) do g + sum(degree(g, edge_weight=true)) + end[1] + + if GRAPH_T == :coo + @test grad.graph[3] isa Vector{Float32} + @test grad.graph[3] ≈ ones(Float32, length(gw)) + else + if GRAPH_T == :sparse + @test grad.graph isa AbstractSparseMatrix{Float32} + end + @test grad.graph isa AbstractMatrix{Float32} + + @test grad.graph ≈ [0.0 1.0 0.0 0.0 + 0.0 1.0 0.0 0.0 + 0.0 0.0 0.0 1.0 + 0.0 0.0 0.0 0.0] + end - @testset "heterognn, degree" begin - g = GNNHeteroGraph((:A, :to, :B) => ([1,1,2,3], [7,13,5,7])) - @test degree(g, (:A, :to, :B), dir = :out) == [2, 1, 1] - @test degree(g, (:A, :to, :B), dir = :in) == [0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1] - @test degree(g, (:A, :to, :B)) == [2, 1, 1] + @testset "directed, degree dir=$dir" for dir in [:in, :out, :both] + g = rand_graph(10, 30, bidirected=false) + w = rand(Float32, 30) + s, t = edge_index(g) + + grad = gradient(w) do w + g = GNNGraph((s, t, w), graph_type = GRAPH_T) + sum(tanh.(degree(g; dir, edge_weight=true))) + end[1] + + ngrad = ngradient(w) do w + g = GNNGraph((s, t, w), graph_type = GRAPH_T) + sum(tanh.(degree(g; dir, edge_weight=true))) + end[1] + + @test grad ≈ ngrad + end end end end end -@testset "laplacian_matrix" begin - g = rand_graph(10, 30, graph_type = GRAPH_T) - A = adjacency_matrix(g) - D = Diagonal(vec(sum(A, dims = 2))) - L = laplacian_matrix(g) - @test eltype(L) == eltype(g) - @test L ≈ D - A + +@testitem "degree GPU" setup=[GraphsTestModule] tags=[:gpu] begin + using .GraphsTestModule + dev = gpu_device(force=true) + for GRAPH_T in GRAPH_TYPES + dev isa MetalDevice && continue # TODO: remove when gather/scatter is implemented for Metal + @testset "unweighted" begin + s = [1, 1, 2, 3] + t = [2, 2, 2, 4] + g = GNNGraph(s, t, graph_type = GRAPH_T) + + g_gpu = g |> dev + d = degree(g) + d_gpu = degree(g_gpu) + @test d_gpu isa AbstractVector{Int} + @test get_device(d_gpu) isa AbstractGPUDevice + @test Array(d_gpu) == d + end + + @testset "weighted" begin + # weighted degree + s = [1, 1, 2, 3] + t = [2, 2, 2, 4] + eweight = Float32[0.1, 2.1, 1.2, 1] + g = GNNGraph((s, t, eweight), graph_type = GRAPH_T) + g_gpu = g |> dev + d = degree(g) + d_gpu = degree(g_gpu) + @test d_gpu isa AbstractArray{Float32} + @test get_device(d_gpu) isa AbstractGPUDevice + @test Array(d_gpu) ≈ d + end + end end -@testset "laplacian_lambda_max" begin +@testitem "heterognn, degree" begin + g = GNNHeteroGraph((:A, :to, :B) => ([1,1,2,3], [7,13,5,7])) + @test degree(g, (:A, :to, :B), dir = :out) == [2, 1, 1] + @test degree(g, (:A, :to, :B), dir = :in) == [0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0, 1] + @test degree(g, (:A, :to, :B)) == [2, 1, 1] +end + +@testitem "laplacian_matrix" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = rand_graph(10, 30, graph_type = GRAPH_T) + A = adjacency_matrix(g) + D = Diagonal(vec(sum(A, dims = 2))) + L = laplacian_matrix(g) + @test eltype(L) == eltype(g) + @test L ≈ D - A + end +end + +@testitem "laplacian_lambda_max" setup=[GraphsTestModule] begin + using .GraphsTestModule s = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5] t = [2, 3, 4, 5, 1, 5, 1, 2, 3, 4] g = GNNGraph(s, t) @@ -194,37 +238,40 @@ end @test length(laplacian_lambda_max(gall2, add_self_loops=true)) == 3 end -@testset "adjacency_matrix" begin - a = sprand(5, 5, 0.5) - abin = map(x -> x > 0 ? 1 : 0, a) +@testitem "adjacency_matrix" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + a = sprand(5, 5, 0.5) + abin = map(x -> x > 0 ? 1 : 0, a) - g = GNNGraph(a, graph_type = GRAPH_T) - A = adjacency_matrix(g, Float32) - @test A ≈ a - @test eltype(A) == Float32 + g = GNNGraph(a, graph_type = GRAPH_T) + A = adjacency_matrix(g, Float32) + @test A ≈ a + @test eltype(A) == Float32 - Abin = adjacency_matrix(g, Float32, weighted = false) - @test Abin ≈ abin - @test eltype(Abin) == Float32 + Abin = adjacency_matrix(g, Float32, weighted = false) + @test Abin ≈ abin + @test eltype(Abin) == Float32 - @testset "gradient" begin - s = [1, 2, 3] - t = [2, 3, 1] - w = [0.1, 0.1, 0.2] - gw = gradient(w) do w - g = GNNGraph(s, t, w, graph_type = GRAPH_T) - A = adjacency_matrix(g, weighted = false) - sum(A) - end[1] - @test gw === nothing - - gw = gradient(w) do w - g = GNNGraph(s, t, w, graph_type = GRAPH_T) - A = adjacency_matrix(g, weighted = true) - sum(A) - end[1] - - @test gw == [1, 1, 1] + @testset "gradient" begin + s = [1, 2, 3] + t = [2, 3, 1] + w = [0.1, 0.1, 0.2] + gw = gradient(w) do w + g = GNNGraph(s, t, w, graph_type = GRAPH_T) + A = adjacency_matrix(g, weighted = false) + sum(A) + end[1] + @test gw === nothing + + gw = gradient(w) do w + g = GNNGraph(s, t, w, graph_type = GRAPH_T) + A = adjacency_matrix(g, weighted = true) + sum(A) + end[1] + + @test gw == [1, 1, 1] + end end @testset "khop_adj" begin @@ -242,35 +289,37 @@ end end end -if GRAPH_T == :coo - @testset "HeteroGraph" begin - @testset "graph_indicator" begin - gs = [rand_heterograph(Dict(:user => 10, :movie => 20, :actor => 30), - Dict((:user,:like,:movie) => 10, - (:actor,:rate,:movie)=>20)) for _ in 1:3] - g = MLUtils.batch(gs) - @test graph_indicator(g) == Dict(:user => [repeat([1], 10); repeat([2], 10); repeat([3], 10)], - :movie => [repeat([1], 20); repeat([2], 20); repeat([3], 20)], - :actor => [repeat([1], 30); repeat([2], 30); repeat([3], 30)]) - @test graph_indicator(g, :movie) == [repeat([1], 20); repeat([2], 20); repeat([3], 20)] - end - end -end +@testitem "HeteroGraph" setup=[GraphsTestModule] begin + using .GraphsTestModule + @testset "graph_indicator" begin + gs = [rand_heterograph(Dict(:user => 10, :movie => 20, :actor => 30), + Dict((:user,:like,:movie) => 10, + (:actor,:rate,:movie)=>20)) for _ in 1:3] + g = MLUtils.batch(gs) + @test graph_indicator(g) == Dict(:user => [repeat([1], 10); repeat([2], 10); repeat([3], 10)], + :movie => [repeat([1], 20); repeat([2], 20); repeat([3], 20)], + :actor => [repeat([1], 30); repeat([2], 30); repeat([3], 30)]) + @test graph_indicator(g, :movie) == [repeat([1], 20); repeat([2], 20); repeat([3], 20)] + end +end -@testset "get_graph_type" begin - g = rand_graph(10, 20, graph_type = GRAPH_T) - @test get_graph_type(g) == GRAPH_T +@testitem "get_graph_type" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = rand_graph(10, 20, graph_type = GRAPH_T) + @test get_graph_type(g) == GRAPH_T - gsparse = GNNGraph(g, graph_type=:sparse) - @test get_graph_type(gsparse) == :sparse - @test gsparse.graph isa SparseMatrixCSC + gsparse = GNNGraph(g, graph_type=:sparse) + @test get_graph_type(gsparse) == :sparse + @test gsparse.graph isa SparseMatrixCSC - gcoo = GNNGraph(g, graph_type=:coo) - @test get_graph_type(gcoo) == :coo - @test gcoo.graph[1:2] isa Tuple{Vector{Int}, Vector{Int}} + gcoo = GNNGraph(g, graph_type=:coo) + @test get_graph_type(gcoo) == :coo + @test gcoo.graph[1:2] isa Tuple{Vector{Int}, Vector{Int}} - gdense = GNNGraph(g, graph_type=:dense) - @test get_graph_type(gdense) == :dense - @test gdense.graph isa Matrix{Int} + gdense = GNNGraph(g, graph_type=:dense) + @test get_graph_type(gdense) == :dense + @test gdense.graph isa Matrix{Int} + end end diff --git a/GNNGraphs/test/runtests.jl b/GNNGraphs/test/runtests.jl index 1dbb84bc2..aa1f58496 100644 --- a/GNNGraphs/test/runtests.jl +++ b/GNNGraphs/test/runtests.jl @@ -1,56 +1,34 @@ -using CUDA, cuDNN -using GNNGraphs -using GNNGraphs: getn, getdata -using Functors: Functors -using LinearAlgebra, Statistics, Random -using NNlib -import MLUtils -import StatsBase -using SparseArrays -using Graphs -using Zygote -using Test -using MLDatasets -using InlineStrings # not used but with the import we test #98 and #104 -using SimpleWeightedGraphs -using MLDataDevices: gpu_device, cpu_device, get_device -using MLDataDevices: CUDADevice - -CUDA.allowscalar(false) - -const ACUMatrix{T} = Union{CuMatrix{T}, CUDA.CUSPARSE.CuSparseMatrix{T}} - -ENV["DATADEPS_ALWAYS_ACCEPT"] = true # for MLDatasets - -include("test_utils.jl") - -tests = [ - "chainrules", - "datastore", - "gnngraph", - "convert", - "transform", - "operators", - "generate", - "query", - "sampling", - "gnnheterograph", - "temporalsnapshotsgnngraph", - "mldatasets", - "ext/SimpleWeightedGraphs", - "samplers" -] - -!CUDA.functional() && @warn("CUDA unavailable, not testing GPU support") - -for graph_type in (:coo, :dense, :sparse) - @info "Testing graph format :$graph_type" - global GRAPH_T = graph_type - global TEST_GPU = CUDA.functional() && (GRAPH_T != :sparse) - # global GRAPH_T = :sparse - # global TEST_GPU = false - - @testset "$t" for t in tests - include("$t.jl") - end +## The test environment is instantiated as follows: +# using Pkg +# Pkg.activate(@__DIR__) +# Pkg.develop(path=joinpath(@__DIR__, "..")) +# Pkg.instantiate() + +using TestItemRunner + +## See https://www.julia-vscode.org/docs/stable/userguide/testitems/ +## for how to run the tests within VS Code. +## See test_module.jl for the test infrastructure. + +## Uncomment below and in test_module.jl to change the default test settings +# ENV["GNN_TEST_CPU"] = "false" +# ENV["GNN_TEST_CUDA"] = "true" +# ENV["GNN_TEST_AMDGPU"] = "true" +# ENV["GNN_TEST_Metal"] = "true" + +# The only available tag at the moment is :gpu +# Tests not tagged with :gpu are considered to be CPU tests +# Tests tagged with :gpu should run on all GPU backends + +if get(ENV, "GNN_TEST_CPU", "true") == "true" + @run_package_tests filter = ti -> :gpu ∉ ti.tags +end +if get(ENV, "GNN_TEST_CUDA", "false") == "true" + @run_package_tests filter = ti -> :gpu ∈ ti.tags +end +if get(ENV, "GNN_TEST_AMDGPU", "false") == "true" + @run_package_tests filter = ti -> :gpu ∈ ti.tags +end +if get(ENV, "GNN_TEST_Metal", "false") == "true" + @run_package_tests filter = ti -> :gpu ∈ ti.tags end diff --git a/GNNGraphs/test/samplers.jl b/GNNGraphs/test/samplers.jl deleted file mode 100644 index 649cea70a..000000000 --- a/GNNGraphs/test/samplers.jl +++ /dev/null @@ -1,126 +0,0 @@ -#TODO reactivate test -# @testitem "NeighborLoader" setup=[TestModule] begin -# using .TestModule -# # Helper function to create a simple graph with node features using GNNGraph -# function create_test_graph() -# source = [1, 2, 3, 4] # Define source nodes of edges -# target = [2, 3, 4, 5] # Define target nodes of edges -# node_features = rand(Float32, 5, 5) # Create random node features (5 features for 5 nodes) - -# return GNNGraph(source, target, ndata = node_features) # Create a GNNGraph with edges and features -# end - - -# # 1. Basic functionality: Check neighbor sampling and subgraph creation -# @testset "Basic functionality" begin -# g = create_test_graph() - -# # Define NeighborLoader with 2 neighbors per layer, 2 layers, batch size 2 -# loader = NeighborLoader(g; num_neighbors=[2, 2], input_nodes=[1, 2], num_layers=2, batch_size=2) - -# mini_batch_gnn, next_state = iterate(loader) - -# # Test if the mini-batch graph is not empty -# @test !isempty(mini_batch_gnn.graph) - -# num_sampled_nodes = mini_batch_gnn.num_nodes -# println("Number of nodes in mini-batch: ", num_sampled_nodes) - -# @test num_sampled_nodes == 2 - -# # Test if there are edges in the subgraph -# @test mini_batch_gnn.num_edges > 0 -# end - -# # 2. Edge case: Single node with no neighbors -# @testset "Single node with no neighbors" begin -# g = SimpleDiGraph(1) # A graph with a single node and no edges -# node_features = rand(Float32, 5, 1) -# graph = GNNGraph(g, ndata = node_features) - -# loader = NeighborLoader(graph; num_neighbors=[2], input_nodes=[1], num_layers=1) - -# mini_batch_gnn, next_state = iterate(loader) - -# # Test if the mini-batch graph contains only one node -# @test size(mini_batch_gnn.x, 2) == 1 -# end - -# # 3. Edge case: A node with no outgoing edges (isolated node) -# @testset "Node with no outgoing edges" begin -# g = SimpleDiGraph(2) # Graph with 2 nodes, no edges -# node_features = rand(Float32, 5, 2) -# graph = GNNGraph(g, ndata = node_features) - -# loader = NeighborLoader(graph; num_neighbors=[1], input_nodes=[1, 2], num_layers=1) - -# mini_batch_gnn, next_state = iterate(loader) - -# # Test if the mini-batch graph contains the input nodes only (as no neighbors can be sampled) -# @test size(mini_batch_gnn.x, 2) == 2 # Only two isolated nodes -# end - -# # 4. Edge case: A fully connected graph -# @testset "Fully connected graph" begin -# g = SimpleDiGraph(3) -# add_edge!(g, 1, 2) -# add_edge!(g, 2, 3) -# add_edge!(g, 3, 1) -# node_features = rand(Float32, 5, 3) -# graph = GNNGraph(g, ndata = node_features) - -# loader = NeighborLoader(graph; num_neighbors=[2, 2], input_nodes=[1], num_layers=2) - -# mini_batch_gnn, next_state = iterate(loader) - -# # Test if all nodes are included in the mini-batch since it's fully connected -# @test size(mini_batch_gnn.x, 2) == 3 # All nodes should be included -# end - -# # 5. Edge case: More layers than the number of neighbors -# @testset "More layers than available neighbors" begin -# g = SimpleDiGraph(3) -# add_edge!(g, 1, 2) -# add_edge!(g, 2, 3) -# node_features = rand(Float32, 5, 3) -# graph = GNNGraph(g, ndata = node_features) - -# # Test with 3 layers but only enough connections for 2 layers -# loader = NeighborLoader(graph; num_neighbors=[1, 1, 1], input_nodes=[1], num_layers=3) - -# mini_batch_gnn, next_state = iterate(loader) - -# # Test if the mini-batch graph contains all available nodes -# @test size(mini_batch_gnn.x, 2) == 1 -# end - -# # 6. Edge case: Large batch size greater than the number of input nodes -# @testset "Large batch size" begin -# g = create_test_graph() - -# # Define NeighborLoader with a larger batch size than input nodes -# loader = NeighborLoader(g; num_neighbors=[2], input_nodes=[1, 2], num_layers=1, batch_size=10) - -# mini_batch_gnn, next_state = iterate(loader) - -# # Test if the mini-batch graph is not empty -# @test !isempty(mini_batch_gnn.graph) - -# # Test if the correct number of nodes are sampled -# @test size(mini_batch_gnn.x, 2) == length(unique([1, 2])) # Nodes [1, 2] are expected -# end - -# # 7. Edge case: No neighbors sampled (num_neighbors = [0]) and 1 layer -# @testset "No neighbors sampled" begin -# g = create_test_graph() - -# # Define NeighborLoader with 0 neighbors per layer, 1 layer, batch size 2 -# loader = NeighborLoader(g; num_neighbors=[0], input_nodes=[1, 2], num_layers=1, batch_size=2) - -# mini_batch_gnn, next_state = iterate(loader) - -# # Test if the mini-batch graph contains only the input nodes -# @test size(mini_batch_gnn.x, 2) == 2 # No neighbors should be sampled, only nodes 1 and 2 should be in the graph -# end - -# end \ No newline at end of file diff --git a/GNNGraphs/test/sampling.jl b/GNNGraphs/test/sampling.jl index 9b4cd5b16..557a8ae89 100644 --- a/GNNGraphs/test/sampling.jl +++ b/GNNGraphs/test/sampling.jl @@ -1,6 +1,8 @@ -if GRAPH_T == :coo - @testset "sample_neighbors" begin - # replace = false +@testitem "sample_neighbors" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + GRAPH_T != :coo && continue # TODO + # replace = false dir = :in nodes = 2:3 g = rand_graph(10, 40, bidirected = false, graph_type = GRAPH_T) @@ -45,36 +47,162 @@ if GRAPH_T == :coo @test sg.ndata.x1 == g.ndata.x1[sg.ndata.NID] @test length(union(sg.ndata.NID)) == length(sg.ndata.NID) end +end - @testset "induced_subgraph" begin - s = [1, 2] - t = [2, 3] - - graph = GNNGraph((s, t), ndata = (; x=rand(Float32, 32, 3), y=rand(Float32, 3)), edata = rand(Float32, 2)) - - nodes = [1, 2, 3] - subgraph = Graphs.induced_subgraph(graph, nodes) - - @test subgraph.num_nodes == 3 - @test subgraph.num_edges == 2 - @test subgraph.ndata.x == graph.ndata.x - @test subgraph.ndata.y == graph.ndata.y - @test subgraph.edata == graph.edata - - nodes = [1, 2] - subgraph = Graphs.induced_subgraph(graph, nodes) - - @test subgraph.num_nodes == 2 - @test subgraph.num_edges == 1 - @test subgraph.ndata == getobs(graph.ndata, [1, 2]) - @test isapprox(getobs(subgraph.edata.e, 1), getobs(graph.edata.e, 1); atol=1e-6) - - graph = GNNGraph(2) - graph = add_edges(graph, ([2], [1])) - nodes = [1] - subgraph = Graphs.induced_subgraph(graph, nodes) - - @test subgraph.num_nodes == 1 - @test subgraph.num_edges == 0 +@testitem "induced_subgraph" setup=[GraphsTestModule] begin + using .GraphsTestModule + using MLUtils: getobs + s = [1, 2] + t = [2, 3] + + graph = GNNGraph((s, t), ndata = (; x=rand(Float32, 32, 3), y=rand(Float32, 3)), edata = rand(Float32, 2)) + + nodes = [1, 2, 3] + subgraph = Graphs.induced_subgraph(graph, nodes) + + @test subgraph.num_nodes == 3 + @test subgraph.num_edges == 2 + @test subgraph.ndata.x == graph.ndata.x + @test subgraph.ndata.y == graph.ndata.y + @test subgraph.edata == graph.edata + + nodes = [1, 2] + subgraph = Graphs.induced_subgraph(graph, nodes) + + @test subgraph.num_nodes == 2 + @test subgraph.num_edges == 1 + @test subgraph.ndata == getobs(graph.ndata, [1, 2]) + @test isapprox(getobs(subgraph.edata.e, 1), getobs(graph.edata.e, 1); atol=1e-6) + + graph = GNNGraph(2) + graph = add_edges(graph, ([2], [1])) + nodes = [1] + subgraph = Graphs.induced_subgraph(graph, nodes) + + @test subgraph.num_nodes == 1 + @test subgraph.num_edges == 0 +end + +@testitem "NeighborLoader" setup=[GraphsTestModule] begin + using .GraphsTestModule + # Helper function to create a simple graph with node features using GNNGraph + function create_test_graph() + source = [1, 2, 3, 4] # Define source nodes of edges + target = [2, 3, 4, 5] # Define target nodes of edges + node_features = rand(Float32, 5, 5) # Create random node features (5 features for 5 nodes) + + return GNNGraph(source, target, ndata = node_features) # Create a GNNGraph with edges and features + end + + + # 1. Basic functionality: Check neighbor sampling and subgraph creation + @testset "Basic functionality" begin + g = create_test_graph() + + # Define NeighborLoader with 2 neighbors per layer, 2 layers, batch size 2 + loader = NeighborLoader(g; num_neighbors=[2, 2], input_nodes=[1, 2], num_layers=2, batch_size=2) + + mini_batch_gnn, next_state = iterate(loader) + + # Test if the mini-batch graph is not empty + @test !isempty(mini_batch_gnn.graph) + + num_sampled_nodes = mini_batch_gnn.num_nodes + + @test num_sampled_nodes == 2 + + # Test if there are edges in the subgraph + @test mini_batch_gnn.num_edges > 0 + end + + # 2. Edge case: Single node with no neighbors + @testset "Single node with no neighbors" begin + g = SimpleDiGraph(1) # A graph with a single node and no edges + node_features = rand(Float32, 5, 1) + graph = GNNGraph(g, ndata = node_features) + + loader = NeighborLoader(graph; num_neighbors=[2], input_nodes=[1], num_layers=1) + + mini_batch_gnn, next_state = iterate(loader) + + # Test if the mini-batch graph contains only one node + @test size(mini_batch_gnn.x, 2) == 1 + end + + # 3. Edge case: A node with no outgoing edges (isolated node) + @testset "Node with no outgoing edges" begin + g = SimpleDiGraph(2) # Graph with 2 nodes, no edges + node_features = rand(Float32, 5, 2) + graph = GNNGraph(g, ndata = node_features) + + loader = NeighborLoader(graph; num_neighbors=[1], input_nodes=[1, 2], num_layers=1) + + mini_batch_gnn, next_state = iterate(loader) + + # Test if the mini-batch graph contains the input nodes only (as no neighbors can be sampled) + @test size(mini_batch_gnn.x, 2) == 2 # Only two isolated nodes + end + + # 4. Edge case: A fully connected graph + @testset "Fully connected graph" begin + g = SimpleDiGraph(3) + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + add_edge!(g, 3, 1) + node_features = rand(Float32, 5, 3) + graph = GNNGraph(g, ndata = node_features) + + loader = NeighborLoader(graph; num_neighbors=[2, 2], input_nodes=[1], num_layers=2) + + mini_batch_gnn, next_state = iterate(loader) + + # Test if all nodes are included in the mini-batch since it's fully connected + @test size(mini_batch_gnn.x, 2) == 3 # All nodes should be included + end + + # 5. Edge case: More layers than the number of neighbors + @testset "More layers than available neighbors" begin + g = SimpleDiGraph(3) + add_edge!(g, 1, 2) + add_edge!(g, 2, 3) + node_features = rand(Float32, 5, 3) + graph = GNNGraph(g, ndata = node_features) + + # Test with 3 layers but only enough connections for 2 layers + loader = NeighborLoader(graph; num_neighbors=[1, 1, 1], input_nodes=[1], num_layers=3) + + mini_batch_gnn, next_state = iterate(loader) + + # Test if the mini-batch graph contains all available nodes + @test size(mini_batch_gnn.x, 2) == 1 + end + + # 6. Edge case: Large batch size greater than the number of input nodes + @testset "Large batch size" begin + g = create_test_graph() + + # Define NeighborLoader with a larger batch size than input nodes + loader = NeighborLoader(g; num_neighbors=[2], input_nodes=[1, 2], num_layers=1, batch_size=10) + + mini_batch_gnn, next_state = iterate(loader) + + # Test if the mini-batch graph is not empty + @test !isempty(mini_batch_gnn.graph) + + # Test if the correct number of nodes are sampled + @test size(mini_batch_gnn.x, 2) == length(unique([1, 2])) # Nodes [1, 2] are expected + end + + # 7. Edge case: No neighbors sampled (num_neighbors = [0]) and 1 layer + @testset "No neighbors sampled" begin + g = create_test_graph() + + # Define NeighborLoader with 0 neighbors per layer, 1 layer, batch size 2 + loader = NeighborLoader(g; num_neighbors=[0], input_nodes=[1, 2], num_layers=1, batch_size=2) + + mini_batch_gnn, next_state = iterate(loader) + + # Test if the mini-batch graph contains only the input nodes + @test size(mini_batch_gnn.x, 2) == 2 # No neighbors should be sampled, only nodes 1 and 2 should be in the graph end -end \ No newline at end of file +end diff --git a/GNNGraphs/test/temporalsnapshotsgnngraph.jl b/GNNGraphs/test/temporalsnapshotsgnngraph.jl index 352dbbedd..4a2db194b 100644 --- a/GNNGraphs/test/temporalsnapshotsgnngraph.jl +++ b/GNNGraphs/test/temporalsnapshotsgnngraph.jl @@ -1,21 +1,24 @@ #TODO add graph_type = GRAPH_TYPE to all constructor calls -@testset "Constructor array TemporalSnapshotsGNNGraph" begin - snapshots = [rand_graph(10, 20) for i in 1:5] - tg = TemporalSnapshotsGNNGraph(snapshots) - @test tg.num_nodes == [10 for i in 1:5] - @test tg.num_edges == [20 for i in 1:5] - @test tg.num_snapshots == 5 - - snapshots = [rand_graph(i, 2*i) for i in 10:10:50] - tg = TemporalSnapshotsGNNGraph(snapshots) - @test tg.num_nodes == [i for i in 10:10:50] - @test tg.num_edges == [2*i for i in 10:10:50] - @test tg.num_snapshots == 5 +@testitem "Constructor array TemporalSnapshotsGNNGraph" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + snapshots = [rand_graph(10, 20, graph_type=GRAPH_T) for i in 1:5] + tg = TemporalSnapshotsGNNGraph(snapshots) + @test tg.num_nodes == [10 for i in 1:5] + @test tg.num_edges == [20 for i in 1:5] + @test tg.num_snapshots == 5 + + snapshots = [rand_graph(i, 2*i, graph_type=GRAPH_T) for i in 10:10:50] + tg = TemporalSnapshotsGNNGraph(snapshots) + @test tg.num_nodes == [i for i in 10:10:50] + @test tg.num_edges == [2*i for i in 10:10:50] + @test tg.num_snapshots == 5 + end end -@testset "==" begin +@testitem "==" begin snapshots = [rand_graph(10, 20) for i in 1:5] tsg1 = TemporalSnapshotsGNNGraph(snapshots) tsg2 = TemporalSnapshotsGNNGraph(snapshots) @@ -25,14 +28,14 @@ end @test tsg1 !== tsg3 end -@testset "getindex" begin +@testitem "getindex" begin snapshots = [rand_graph(10, 20) for i in 1:5] tsg = TemporalSnapshotsGNNGraph(snapshots) @test tsg[3] == snapshots[3] @test tsg[[1,2]] == TemporalSnapshotsGNNGraph([10,10], [20,20], 2, snapshots[1:2], tsg.tgdata) end -@testset "setindex!" begin +@testitem "setindex!" begin snapshots = [rand_graph(10, 20) for i in 1:5] tsg = TemporalSnapshotsGNNGraph(snapshots) g = rand_graph(20, 40) @@ -43,7 +46,7 @@ end @test_throws MethodError tsg[3:4] = g end -@testset "getproperty" begin +@testitem "getproperty" begin x = rand(Float32, 10) snapshots = [rand_graph(10, 20, ndata = x) for i in 1:5] tsg = TemporalSnapshotsGNNGraph(snapshots) @@ -53,7 +56,7 @@ end @test_throws ArgumentError tsg.w end -@testset "add/remove_snapshot" begin +@testitem "add/remove_snapshot" begin snapshots = [rand_graph(10, 20) for i in 1:5] tsg = TemporalSnapshotsGNNGraph(snapshots) g = rand_graph(10, 20) @@ -67,7 +70,7 @@ end @test tsg.snapshots == snapshots end -@testset "add/remove_snapshot" begin +@testitem "add/remove_snapshot" begin snapshots = [rand_graph(10, 20) for i in 1:5] tsg = TemporalSnapshotsGNNGraph(snapshots) g = rand_graph(10, 20) @@ -93,7 +96,7 @@ end end -# @testset "add/remove_snapshot!" begin +# @testitem "add/remove_snapshot!" begin # snapshots = [rand_graph(10, 20) for i in 1:5] # tsg = TemporalSnapshotsGNNGraph(snapshots) # g = rand_graph(10, 20) @@ -111,7 +114,7 @@ end # @test tsg3 === tsg # end -@testset "show" begin +@testitem "show" begin snapshots = [rand_graph(10, 20) for i in 1:5] tsg = TemporalSnapshotsGNNGraph(snapshots) @test sprint(show,tsg) == "TemporalSnapshotsGNNGraph(5)" @@ -121,30 +124,33 @@ end @test sprint(show,tsg) == "TemporalSnapshotsGNNGraph(5)" end -@testset "broadcastable" begin +@testitem "broadcastable" begin snapshots = [rand_graph(10, 20) for i in 1:5] tsg = TemporalSnapshotsGNNGraph(snapshots) f(g) = g isa GNNGraph @test f.(tsg) == trues(5) end -@testset "iterate" begin +@testitem "iterate" begin snapshots = [rand_graph(10, 20) for i in 1:5] tsg = TemporalSnapshotsGNNGraph(snapshots) @test [g for g in tsg] isa Vector{<:GNNGraph} end -if TEST_GPU - @testset "gpu" begin - snapshots = [rand_graph(10, 20; ndata = rand(Float32, 5,10)) for i in 1:5] - tsg = TemporalSnapshotsGNNGraph(snapshots) - tsg.tgdata.x = rand(Float32, 5) - dev = CUDADevice() #TODO replace with `gpu_device()` - tsg = tsg |> dev - @test tsg.snapshots[1].ndata.x isa CuArray - @test tsg.snapshots[end].ndata.x isa CuArray - @test tsg.tgdata.x isa CuArray - @test tsg.num_nodes isa CuArray - @test tsg.num_edges isa CuArray - end +@testitem "gpu" setup=[GraphsTestModule] tags=[:gpu] begin + using .GraphsTestModule + dev = gpu_device(force=true) + snapshots = [rand_graph(10, 20; ndata = rand(Float32, 5,10)) for i in 1:5] + tsg = TemporalSnapshotsGNNGraph(snapshots) + tsg.tgdata.x = rand(Float32, 5) + tsg = tsg |> dev + @test tsg.snapshots[1].ndata.x isa AbstractMatrix{Float32} + @test get_device(tsg.snapshots[1].ndata.x) isa AbstractGPUDevice + @test tsg.snapshots[end].ndata.x isa AbstractMatrix{Float32} + @test get_device(tsg.snapshots[end].ndata.x) isa AbstractGPUDevice + @test tsg.tgdata.x isa AbstractVector{Float32} + @test get_device(tsg.tgdata.x) isa AbstractGPUDevice + # num_nodes and num_edges are not copied to the device + @test tsg.num_nodes isa Vector{Int} + @test tsg.num_edges isa Vector{Int} end diff --git a/GNNGraphs/test/test_module.jl b/GNNGraphs/test/test_module.jl new file mode 100644 index 000000000..e0dfea248 --- /dev/null +++ b/GNNGraphs/test/test_module.jl @@ -0,0 +1,57 @@ +@testmodule GraphsTestModule begin + +using Pkg + +## Uncomment below to change the default test settings +# ENV["GNN_TEST_CUDA"] = "true" +# ENV["GNN_TEST_AMDGPU"] = "true" +# ENV["GNN_TEST_Metal"] = "true" + +to_test(backend) = get(ENV, "GNN_TEST_$(backend)", "false") == "true" +has_dependecies(pkgs) = all(pkg -> haskey(Pkg.project().dependencies, pkg), pkgs) +deps_dict = Dict(:CUDA => ["CUDA", "cuDNN"], :AMDGPU => ["AMDGPU"], :Metal => ["Metal"]) + +for (backend, deps) in deps_dict + if to_test(backend) + if !has_dependecies(deps) + Pkg.add(deps) + end + @eval using $backend + if backend == :CUDA + @eval using cuDNN + end + @eval $backend.allowscalar(false) + end +end + +using FiniteDifferences: FiniteDifferences +using Reexport: @reexport +using MLUtils: MLUtils +using Zygote: gradient +using MLDataDevices: AbstractGPUDevice +@reexport using SparseArrays +@reexport using MLDataDevices +@reexport using Random +@reexport using Statistics +@reexport using LinearAlgebra +@reexport using GNNGraphs +@reexport using Test +@reexport using Graphs +export MLUtils, gradient, AbstractGPUDevice +export ngradient, GRAPH_TYPES + + +# Using this until https://github.com/JuliaDiff/FiniteDifferences.jl/issues/188 is fixed +function FiniteDifferences.to_vec(x::Integer) + Integer_from_vec(v) = x + return Int[x], Integer_from_vec +end + +function ngradient(f, x...) + fdm = FiniteDifferences.central_fdm(5, 1) + return FiniteDifferences.grad(fdm, f, x...) +end + +const GRAPH_TYPES = [:coo, :dense, :sparse] + +end # module diff --git a/GNNGraphs/test/test_utils.jl b/GNNGraphs/test/test_utils.jl deleted file mode 100644 index 4f076bf56..000000000 --- a/GNNGraphs/test/test_utils.jl +++ /dev/null @@ -1,13 +0,0 @@ -using ChainRulesTestUtils, FiniteDifferences, Zygote, Adapt, CUDA -CUDA.allowscalar(false) - -# Using this until https://github.com/JuliaDiff/FiniteDifferences.jl/issues/188 is fixed -function FiniteDifferences.to_vec(x::Integer) - Integer_from_vec(v) = x - return Int[x], Integer_from_vec -end - -function ngradient(f, x...) - fdm = central_fdm(5, 1) - return FiniteDifferences.grad(fdm, f, x...) -end diff --git a/GNNGraphs/test/transform.jl b/GNNGraphs/test/transform.jl index 05413fd4f..275efe006 100644 --- a/GNNGraphs/test/transform.jl +++ b/GNNGraphs/test/transform.jl @@ -1,4 +1,6 @@ -@testset "add self-loops" begin +@testitem "add self-loops" setup=[GraphsTestModule] begin + using .GraphsTestModule + A = [1 1 0 0 0 0 1 0 0 0 0 1 @@ -8,438 +10,516 @@ 0 0 1 1 1 0 0 1] - g = GNNGraph(A; graph_type = GRAPH_T) - fg2 = add_self_loops(g) - @test adjacency_matrix(g) == A - @test g.num_edges == sum(A) - @test adjacency_matrix(fg2) == A2 - @test fg2.num_edges == sum(A2) + for GRAPH_T in GRAPH_TYPES + g = GNNGraph(A; graph_type = GRAPH_T) + fg2 = add_self_loops(g) + @test adjacency_matrix(g) == A + @test g.num_edges == sum(A) + @test adjacency_matrix(fg2) == A2 + @test fg2.num_edges == sum(A2) + end end -@testset "batch" begin - g1 = GNNGraph(random_regular_graph(10, 2), ndata = rand(16, 10), - graph_type = GRAPH_T) - g2 = GNNGraph(random_regular_graph(4, 2), ndata = rand(16, 4), graph_type = GRAPH_T) - g3 = GNNGraph(random_regular_graph(7, 2), ndata = rand(16, 7), graph_type = GRAPH_T) - - g12 = MLUtils.batch([g1, g2]) - g12b = blockdiag(g1, g2) - @test g12 == g12b - - g123 = MLUtils.batch([g1, g2, g3]) - @test g123.graph_indicator == [fill(1, 10); fill(2, 4); fill(3, 7)] - - # Allow wider eltype - g123 = MLUtils.batch(GNNGraph[g1, g2, g3]) - @test g123.graph_indicator == [fill(1, 10); fill(2, 4); fill(3, 7)] - - - s, t = edge_index(g123) - @test s == [edge_index(g1)[1]; 10 .+ edge_index(g2)[1]; 14 .+ edge_index(g3)[1]] - @test t == [edge_index(g1)[2]; 10 .+ edge_index(g2)[2]; 14 .+ edge_index(g3)[2]] - @test node_features(g123)[:, 11:14] ≈ node_features(g2) - - # scalar graph features - g1 = GNNGraph(g1, gdata = rand()) - g2 = GNNGraph(g2, gdata = rand()) - g3 = GNNGraph(g3, gdata = rand()) - g123 = MLUtils.batch([g1, g2, g3]) - @test g123.gdata.u == [g1.gdata.u, g2.gdata.u, g3.gdata.u] - - # Batch of batches - g123123 = MLUtils.batch([g123, g123]) - @test g123123.graph_indicator == - [fill(1, 10); fill(2, 4); fill(3, 7); fill(4, 10); fill(5, 4); fill(6, 7)] - @test g123123.num_graphs == 6 +@testitem "batch" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g1 = GNNGraph(random_regular_graph(10, 2), ndata = rand(16, 10), + graph_type = GRAPH_T) + g2 = GNNGraph(random_regular_graph(4, 2), ndata = rand(16, 4), graph_type = GRAPH_T) + g3 = GNNGraph(random_regular_graph(7, 2), ndata = rand(16, 7), graph_type = GRAPH_T) + + g12 = MLUtils.batch([g1, g2]) + g12b = blockdiag(g1, g2) + @test g12 == g12b + + g123 = MLUtils.batch([g1, g2, g3]) + @test g123.graph_indicator == [fill(1, 10); fill(2, 4); fill(3, 7)] + + # Allow wider eltype + g123 = MLUtils.batch(GNNGraph[g1, g2, g3]) + @test g123.graph_indicator == [fill(1, 10); fill(2, 4); fill(3, 7)] + + + s, t = edge_index(g123) + @test s == [edge_index(g1)[1]; 10 .+ edge_index(g2)[1]; 14 .+ edge_index(g3)[1]] + @test t == [edge_index(g1)[2]; 10 .+ edge_index(g2)[2]; 14 .+ edge_index(g3)[2]] + @test node_features(g123)[:, 11:14] ≈ node_features(g2) + + # scalar graph features + g1 = GNNGraph(g1, gdata = rand()) + g2 = GNNGraph(g2, gdata = rand()) + g3 = GNNGraph(g3, gdata = rand()) + g123 = MLUtils.batch([g1, g2, g3]) + @test g123.gdata.u == [g1.gdata.u, g2.gdata.u, g3.gdata.u] + + # Batch of batches + g123123 = MLUtils.batch([g123, g123]) + @test g123123.graph_indicator == + [fill(1, 10); fill(2, 4); fill(3, 7); fill(4, 10); fill(5, 4); fill(6, 7)] + @test g123123.num_graphs == 6 + end end -@testset "unbatch" begin - g1 = rand_graph(10, 20, graph_type = GRAPH_T) - g2 = rand_graph(5, 10, graph_type = GRAPH_T) - g12 = MLUtils.batch([g1, g2]) - gs = MLUtils.unbatch([g1, g2]) - @test length(gs) == 2 - @test gs[1].num_nodes == 10 - @test gs[1].num_edges == 20 - @test gs[1].num_graphs == 1 - @test gs[2].num_nodes == 5 - @test gs[2].num_edges == 10 - @test gs[2].num_graphs == 1 +@testitem "unbatch" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g1 = rand_graph(10, 20, graph_type = GRAPH_T) + g2 = rand_graph(5, 10, graph_type = GRAPH_T) + g12 = MLUtils.batch([g1, g2]) + gs = MLUtils.unbatch([g1, g2]) + @test length(gs) == 2 + @test gs[1].num_nodes == 10 + @test gs[1].num_edges == 20 + @test gs[1].num_graphs == 1 + @test gs[2].num_nodes == 5 + @test gs[2].num_edges == 10 + @test gs[2].num_graphs == 1 + end end -@testset "batch/unbatch roundtrip" begin - n = 20 - c = 3 - ngraphs = 10 - gs = [rand_graph(n, c * n, ndata = rand(2, n), edata = rand(3, c * n), - graph_type = GRAPH_T) - for _ in 1:ngraphs] - gall = MLUtils.batch(gs) - gs2 = MLUtils.unbatch(gall) - @test gs2[1] == gs[1] - @test gs2[end] == gs[end] +@testitem "batch/unbatch roundtrip" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + n = 20 + c = 3 + ngraphs = 10 + gs = [rand_graph(n, c * n, ndata = rand(2, n), edata = rand(3, c * n), + graph_type = GRAPH_T) + for _ in 1:ngraphs] + gall = MLUtils.batch(gs) + gs2 = MLUtils.unbatch(gall) + @test gs2[1] == gs[1] + @test gs2[end] == gs[end] + end end -@testset "getgraph" begin - g1 = GNNGraph(random_regular_graph(10, 2), ndata = rand(16, 10), - graph_type = GRAPH_T) - g2 = GNNGraph(random_regular_graph(4, 2), ndata = rand(16, 4), graph_type = GRAPH_T) - g3 = GNNGraph(random_regular_graph(7, 2), ndata = rand(16, 7), graph_type = GRAPH_T) - g = MLUtils.batch([g1, g2, g3]) - - g2b, nodemap = getgraph(g, 2, nmap = true) - s, t = edge_index(g2b) - @test s == edge_index(g2)[1] - @test t == edge_index(g2)[2] - @test node_features(g2b) ≈ node_features(g2) - - g2c = getgraph(g, 2) - @test g2c isa GNNGraph{typeof(g.graph)} - - g1b, nodemap = getgraph(g1, 1, nmap = true) - @test g1b === g1 - @test nodemap == 1:(g1.num_nodes) +@testitem "getgraph" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g1 = GNNGraph(random_regular_graph(10, 2), ndata = rand(16, 10), + graph_type = GRAPH_T) + g2 = GNNGraph(random_regular_graph(4, 2), ndata = rand(16, 4), graph_type = GRAPH_T) + g3 = GNNGraph(random_regular_graph(7, 2), ndata = rand(16, 7), graph_type = GRAPH_T) + g = MLUtils.batch([g1, g2, g3]) + + g2b, nodemap = getgraph(g, 2, nmap = true) + s, t = edge_index(g2b) + @test s == edge_index(g2)[1] + @test t == edge_index(g2)[2] + @test node_features(g2b) ≈ node_features(g2) + + g2c = getgraph(g, 2) + @test g2c isa GNNGraph{typeof(g.graph)} + + g1b, nodemap = getgraph(g1, 1, nmap = true) + @test g1b === g1 + @test nodemap == 1:(g1.num_nodes) + end end -@testset "remove_edges" begin - if GRAPH_T == :coo - s = [1, 1, 2, 3] - t = [2, 3, 4, 5] - w = [0.1, 0.2, 0.3, 0.4] - edata = ['a', 'b', 'c', 'd'] - g = GNNGraph(s, t, w, edata = edata, graph_type = GRAPH_T) - - # single edge removal - gnew = remove_edges(g, [1]) - new_s, new_t = edge_index(gnew) - @test gnew.num_edges == 3 - @test new_s == s[2:end] - @test new_t == t[2:end] - - # multiple edge removal - gnew = remove_edges(g, [1,2,4]) - new_s, new_t = edge_index(gnew) - new_w = get_edge_weight(gnew) - new_edata = gnew.edata.e - @test gnew.num_edges == 1 - @test new_s == [2] - @test new_t == [4] - @test new_w == [0.3] - @test new_edata == ['c'] - - # drop with probability - gnew = remove_edges(g, Float32(1.0)) - @test gnew.num_edges == 0 - - gnew = remove_edges(g, Float32(0.0)) - @test gnew.num_edges == g.num_edges +@testitem "remove_edges" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + s = [1, 1, 2, 3] + t = [2, 3, 4, 5] + w = [0.1, 0.2, 0.3, 0.4] + edata = ['a', 'b', 'c', 'd'] + g = GNNGraph(s, t, w, edata = edata, graph_type = GRAPH_T) + + # single edge removal + gnew = remove_edges(g, [1]) + new_s, new_t = edge_index(gnew) + @test gnew.num_edges == 3 + @test new_s == s[2:end] + @test new_t == t[2:end] + + # multiple edge removal + gnew = remove_edges(g, [1,2,4]) + new_s, new_t = edge_index(gnew) + new_w = get_edge_weight(gnew) + new_edata = gnew.edata.e + @test gnew.num_edges == 1 + @test new_s == [2] + @test new_t == [4] + @test new_w == [0.3] + @test new_edata == ['c'] + + # drop with probability + gnew = remove_edges(g, Float32(1.0)) + @test gnew.num_edges == 0 + + gnew = remove_edges(g, Float32(0.0)) + @test gnew.num_edges == g.num_edges + end end end -@testset "add_edges" begin - if GRAPH_T == :coo - s = [1, 1, 2, 3] - t = [2, 3, 4, 5] - g = GNNGraph(s, t, graph_type = GRAPH_T) - snew = [1] - tnew = [4] - gnew = add_edges(g, snew, tnew) - @test gnew.num_edges == 5 - @test sort(inneighbors(gnew, 4)) == [1, 2] - - gnew2 = add_edges(g, (snew, tnew)) - @test gnew2 == gnew - @test get_edge_weight(gnew2) === nothing - - g = GNNGraph(s, t, edata = (e1 = rand(2, 4), e2 = rand(3, 4)), graph_type = GRAPH_T) - # @test_throws ErrorException add_edges(g, snew, tnew) - gnew = add_edges(g, snew, tnew, edata = (e1 = ones(2, 1), e2 = zeros(3, 1))) - @test all(gnew.edata.e1[:, 5] .== 1) - @test all(gnew.edata.e2[:, 5] .== 0) - - @testset "adding new nodes" begin - g = GNNGraph() - g = add_edges(g, ([1,3], [2, 1])) - @test g.num_nodes == 3 - @test g.num_edges == 2 - @test sort(inneighbors(g, 1)) == [3] - @test sort(outneighbors(g, 1)) == [2] - end - @testset "also add weights" begin +@testitem "add_edges" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo s = [1, 1, 2, 3] t = [2, 3, 4, 5] - w = [1.0, 2.0, 3.0, 4.0] + g = GNNGraph(s, t, graph_type = GRAPH_T) snew = [1] tnew = [4] - wnew = [5.] + gnew = add_edges(g, snew, tnew) + @test gnew.num_edges == 5 + @test sort(inneighbors(gnew, 4)) == [1, 2] + + gnew2 = add_edges(g, (snew, tnew)) + @test gnew2 == gnew + @test get_edge_weight(gnew2) === nothing + + g = GNNGraph(s, t, edata = (e1 = rand(2, 4), e2 = rand(3, 4)), graph_type = GRAPH_T) + # @test_throws ErrorException add_edges(g, snew, tnew) + gnew = add_edges(g, snew, tnew, edata = (e1 = ones(2, 1), e2 = zeros(3, 1))) + @test all(gnew.edata.e1[:, 5] .== 1) + @test all(gnew.edata.e2[:, 5] .== 0) + + @testset "adding new nodes" begin + g = GNNGraph() + g = add_edges(g, ([1,3], [2, 1])) + @test g.num_nodes == 3 + @test g.num_edges == 2 + @test sort(inneighbors(g, 1)) == [3] + @test sort(outneighbors(g, 1)) == [2] + end + @testset "also add weights" begin + s = [1, 1, 2, 3] + t = [2, 3, 4, 5] + w = [1.0, 2.0, 3.0, 4.0] + snew = [1] + tnew = [4] + wnew = [5.] + + g = GNNGraph((s, t), graph_type = GRAPH_T) + gnew = add_edges(g, (snew, tnew, wnew)) + @test get_edge_weight(gnew) == [ones(length(s)); wnew] + + g = GNNGraph((s, t, w), graph_type = GRAPH_T) + gnew = add_edges(g, (snew, tnew, wnew)) + @test get_edge_weight(gnew) == [w; wnew] + end + end + end +end +@testitem "perturb_edges" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + s, t = [1, 2, 3, 4, 5], [2, 3, 4, 5, 1] g = GNNGraph((s, t), graph_type = GRAPH_T) - gnew = add_edges(g, (snew, tnew, wnew)) - @test get_edge_weight(gnew) == [ones(length(s)); wnew] + rng = MersenneTwister(42) + g_per = perturb_edges(rng, g, 0.5) + @test g_per.num_edges == 8 + end + end +end + +@testitem "remove_nodes" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + #single node + s = [1, 1, 2, 3] + t = [2, 3, 4, 5] + eweights = [0.1, 0.2, 0.3, 0.4] + ndata = [1.0, 2.0, 3.0, 4.0, 5.0] + edata = ['a', 'b', 'c', 'd'] + + g = GNNGraph(s, t, eweights, ndata = ndata, edata = edata, graph_type = GRAPH_T) + + gnew = remove_nodes(g, [1]) + + snew = [1, 2] + tnew = [3, 4] + eweights_new = [0.3, 0.4] + ndata_new = [2.0, 3.0, 4.0, 5.0] + edata_new = ['c', 'd'] + + stest, ttest = edge_index(gnew) + eweightstest = get_edge_weight(gnew) + ndatatest = gnew.ndata.x + edatatest = gnew.edata.e + + + @test gnew.num_edges == 2 + @test gnew.num_nodes == 4 + @test snew == stest + @test tnew == ttest + @test eweights_new == eweightstest + @test ndata_new == ndatatest + @test edata_new == edatatest + + # multiple nodes + s = [1, 5, 2, 3] + t = [2, 3, 4, 5] + eweights = [0.1, 0.2, 0.3, 0.4] + ndata = [1.0, 2.0, 3.0, 4.0, 5.0] + edata = ['a', 'b', 'c', 'd'] + + g = GNNGraph(s, t, eweights, ndata = ndata, edata = edata, graph_type = GRAPH_T) + + gnew = remove_nodes(g, [1,4]) + snew = [3,2] + tnew = [2,3] + eweights_new = [0.2,0.4] + ndata_new = [2.0,3.0,5.0] + edata_new = ['b','d'] + + stest, ttest = edge_index(gnew) + eweightstest = get_edge_weight(gnew) + ndatatest = gnew.ndata.x + edatatest = gnew.edata.e + + @test gnew.num_edges == 2 + @test gnew.num_nodes == 3 + @test snew == stest + @test tnew == ttest + @test eweights_new == eweightstest + @test ndata_new == ndatatest + @test edata_new == edatatest + end + end +end + +@testitem "remove_nodes(g, p)" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + Random.seed!(42) + s = [1, 1, 2, 3] + t = [2, 3, 4, 5] + g = GNNGraph(s, t, graph_type = GRAPH_T) - g = GNNGraph((s, t, w), graph_type = GRAPH_T) - gnew = add_edges(g, (snew, tnew, wnew)) - @test get_edge_weight(gnew) == [w; wnew] + gnew = remove_nodes(g, 0.5) + @test gnew.num_nodes == 3 + + gnew = remove_nodes(g, 1.0) + @test gnew.num_nodes == 0 + + gnew = remove_nodes(g, 0.0) + @test gnew.num_nodes == 5 + end + end +end + +@testitem "add_nodes" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + g = rand_graph(6, 4, ndata = rand(2, 6), graph_type = GRAPH_T) + gnew = add_nodes(g, 5, ndata = ones(2, 5)) + @test gnew.num_nodes == g.num_nodes + 5 + @test gnew.num_edges == g.num_edges + @test gnew.num_graphs == g.num_graphs + @test all(gnew.ndata.x[:, 7:11] .== 1) + end + end +end + +@testitem "remove_self_loops" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo # add_edges and set_edge_weight only implemented for coo + g = rand_graph(10, 20, graph_type = GRAPH_T) + g1 = add_edges(g, [1:5;], [1:5;]) + @test g1.num_edges == g.num_edges + 5 + g2 = remove_self_loops(g1) + @test g2.num_edges == g.num_edges + @test sort_edge_index(edge_index(g2)) == sort_edge_index(edge_index(g)) + + # with edge features and weights + g1 = GNNGraph(g1, edata = (e1 = ones(3, g1.num_edges), e2 = 2 * ones(g1.num_edges))) + g1 = set_edge_weight(g1, 3 * ones(g1.num_edges)) + g2 = remove_self_loops(g1) + @test g2.num_edges == g.num_edges + @test sort_edge_index(edge_index(g2)) == sort_edge_index(edge_index(g)) + @test size(get_edge_weight(g2)) == (g2.num_edges,) + @test size(g2.edata.e1) == (3, g2.num_edges) + @test size(g2.edata.e2) == (g2.num_edges,) + end + end +end + +@testitem "remove_multi_edges" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + g = rand_graph(10, 20, graph_type = GRAPH_T) + s, t = edge_index(g) + g1 = add_edges(g, s[1:5], t[1:5]) + @test g1.num_edges == g.num_edges + 5 + g2 = remove_multi_edges(g1, aggr = +) + @test g2.num_edges == g.num_edges + @test sort_edge_index(edge_index(g2)) == sort_edge_index(edge_index(g)) + + # Default aggregation is + + g1 = GNNGraph(g1, edata = (e1 = ones(3, g1.num_edges), e2 = 2 * ones(g1.num_edges))) + g1 = set_edge_weight(g1, 3 * ones(g1.num_edges)) + g2 = remove_multi_edges(g1) + @test g2.num_edges == g.num_edges + @test sort_edge_index(edge_index(g2)) == sort_edge_index(edge_index(g)) + @test count(g2.edata.e1[:, i] == 2 * ones(3) for i in 1:(g2.num_edges)) == 5 + @test count(g2.edata.e2[i] == 4 for i in 1:(g2.num_edges)) == 5 + w2 = get_edge_weight(g2) + @test count(w2[i] == 6 for i in 1:(g2.num_edges)) == 5 end end end -@testset "perturb_edges" begin if GRAPH_T == :coo - s, t = [1, 2, 3, 4, 5], [2, 3, 4, 5, 1] - g = GNNGraph((s, t)) - rng = MersenneTwister(42) - g_per = perturb_edges(rng, g, 0.5) - @test g_per.num_edges == 8 -end end - -@testset "remove_nodes" begin if GRAPH_T == :coo - #single node - s = [1, 1, 2, 3] - t = [2, 3, 4, 5] - eweights = [0.1, 0.2, 0.3, 0.4] - ndata = [1.0, 2.0, 3.0, 4.0, 5.0] - edata = ['a', 'b', 'c', 'd'] - - g = GNNGraph(s, t, eweights, ndata = ndata, edata = edata, graph_type = GRAPH_T) - - gnew = remove_nodes(g, [1]) - - snew = [1, 2] - tnew = [3, 4] - eweights_new = [0.3, 0.4] - ndata_new = [2.0, 3.0, 4.0, 5.0] - edata_new = ['c', 'd'] - - stest, ttest = edge_index(gnew) - eweightstest = get_edge_weight(gnew) - ndatatest = gnew.ndata.x - edatatest = gnew.edata.e - - - @test gnew.num_edges == 2 - @test gnew.num_nodes == 4 - @test snew == stest - @test tnew == ttest - @test eweights_new == eweightstest - @test ndata_new == ndatatest - @test edata_new == edatatest - - # multiple nodes - s = [1, 5, 2, 3] - t = [2, 3, 4, 5] - eweights = [0.1, 0.2, 0.3, 0.4] - ndata = [1.0, 2.0, 3.0, 4.0, 5.0] - edata = ['a', 'b', 'c', 'd'] - - g = GNNGraph(s, t, eweights, ndata = ndata, edata = edata, graph_type = GRAPH_T) - - gnew = remove_nodes(g, [1,4]) - snew = [3,2] - tnew = [2,3] - eweights_new = [0.2,0.4] - ndata_new = [2.0,3.0,5.0] - edata_new = ['b','d'] - - stest, ttest = edge_index(gnew) - eweightstest = get_edge_weight(gnew) - ndatatest = gnew.ndata.x - edatatest = gnew.edata.e - - @test gnew.num_edges == 2 - @test gnew.num_nodes == 3 - @test snew == stest - @test tnew == ttest - @test eweights_new == eweightstest - @test ndata_new == ndatatest - @test edata_new == edatatest -end end - -@testset "remove_nodes(g, p)" begin - if GRAPH_T == :coo - Random.seed!(42) - s = [1, 1, 2, 3] - t = [2, 3, 4, 5] - g = GNNGraph(s, t, graph_type = GRAPH_T) - - gnew = remove_nodes(g, 0.5) - @test gnew.num_nodes == 3 +@testitem "negative_sample" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + n, m = 10, 30 + g = rand_graph(n, m, bidirected = true, graph_type = GRAPH_T) + + # check bidirected=is_bidirected(g) default + gneg = negative_sample(g, num_neg_edges = 20) + @test gneg.num_nodes == g.num_nodes + @test gneg.num_edges == 20 + @test is_bidirected(gneg) + @test intersect(g, gneg).num_edges == 0 + end + end +end + +@testitem "rand_edge_split" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + n, m = 100, 300 + + g = rand_graph(n, m, bidirected = true, graph_type = GRAPH_T) + # check bidirected=is_bidirected(g) default + g1, g2 = rand_edge_split(g, 0.9) + @test is_bidirected(g1) + @test is_bidirected(g2) + @test intersect(g1, g2).num_edges == 0 + @test g1.num_edges + g2.num_edges == g.num_edges + @test g2.num_edges < 50 + + g = rand_graph(n, m, bidirected = false, graph_type = GRAPH_T) + # check bidirected=is_bidirected(g) default + g1, g2 = rand_edge_split(g, 0.9) + @test !is_bidirected(g1) + @test !is_bidirected(g2) + @test intersect(g1, g2).num_edges == 0 + @test g1.num_edges + g2.num_edges == g.num_edges + @test g2.num_edges < 50 + + g1, g2 = rand_edge_split(g, 0.9, bidirected = false) + @test !is_bidirected(g1) + @test !is_bidirected(g2) + @test intersect(g1, g2).num_edges == 0 + @test g1.num_edges + g2.num_edges == g.num_edges + @test g2.num_edges < 50 + end + end +end - gnew = remove_nodes(g, 1.0) - @test gnew.num_nodes == 0 +@testitem "set_edge_weight" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = rand_graph(10, 20, graph_type = GRAPH_T) + w = rand(20) + + gw = set_edge_weight(g, w) + @test get_edge_weight(gw) == w + + # now from weighted graph + s, t = edge_index(g) + g2 = GNNGraph(s, t, rand(20), graph_type = GRAPH_T) + gw2 = set_edge_weight(g2, w) + @test get_edge_weight(gw2) == w + end +end - gnew = remove_nodes(g, 0.0) - @test gnew.num_nodes == 5 +@testitem "to_bidirected" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + s, t = [1, 2, 3, 3, 4], [2, 3, 4, 4, 4] + w = [1.0, 2.0, 3.0, 4.0, 5.0] + e = [10.0, 20.0, 30.0, 40.0, 50.0] + g = GNNGraph(s, t, w, edata = e) + + g2 = to_bidirected(g) + @test g2.num_nodes == g.num_nodes + @test g2.num_edges == 7 + @test is_bidirected(g2) + @test !has_multi_edges(g2) + + s2, t2 = edge_index(g2) + w2 = get_edge_weight(g2) + @test s2 == [1, 2, 2, 3, 3, 4, 4] + @test t2 == [2, 1, 3, 2, 4, 3, 4] + @test w2 == [1, 1, 2, 2, 3.5, 3.5, 5] + @test g2.edata.e == [10.0, 10.0, 20.0, 20.0, 35.0, 35.0, 50.0] + end end end -@testset "add_nodes" begin if GRAPH_T == :coo - g = rand_graph(6, 4, ndata = rand(2, 6), graph_type = GRAPH_T) - gnew = add_nodes(g, 5, ndata = ones(2, 5)) - @test gnew.num_nodes == g.num_nodes + 5 - @test gnew.num_edges == g.num_edges - @test gnew.num_graphs == g.num_graphs - @test all(gnew.ndata.x[:, 7:11] .== 1) -end end - -@testset "remove_self_loops" begin if GRAPH_T == :coo # add_edges and set_edge_weight only implemented for coo - g = rand_graph(10, 20, graph_type = GRAPH_T) - g1 = add_edges(g, [1:5;], [1:5;]) - @test g1.num_edges == g.num_edges + 5 - g2 = remove_self_loops(g1) - @test g2.num_edges == g.num_edges - @test sort_edge_index(edge_index(g2)) == sort_edge_index(edge_index(g)) - - # with edge features and weights - g1 = GNNGraph(g1, edata = (e1 = ones(3, g1.num_edges), e2 = 2 * ones(g1.num_edges))) - g1 = set_edge_weight(g1, 3 * ones(g1.num_edges)) - g2 = remove_self_loops(g1) - @test g2.num_edges == g.num_edges - @test sort_edge_index(edge_index(g2)) == sort_edge_index(edge_index(g)) - @test size(get_edge_weight(g2)) == (g2.num_edges,) - @test size(g2.edata.e1) == (3, g2.num_edges) - @test size(g2.edata.e2) == (g2.num_edges,) -end end - -@testset "remove_multi_edges" begin if GRAPH_T == :coo - g = rand_graph(10, 20, graph_type = GRAPH_T) - s, t = edge_index(g) - g1 = add_edges(g, s[1:5], t[1:5]) - @test g1.num_edges == g.num_edges + 5 - g2 = remove_multi_edges(g1, aggr = +) - @test g2.num_edges == g.num_edges - @test sort_edge_index(edge_index(g2)) == sort_edge_index(edge_index(g)) - - # Default aggregation is + - g1 = GNNGraph(g1, edata = (e1 = ones(3, g1.num_edges), e2 = 2 * ones(g1.num_edges))) - g1 = set_edge_weight(g1, 3 * ones(g1.num_edges)) - g2 = remove_multi_edges(g1) - @test g2.num_edges == g.num_edges - @test sort_edge_index(edge_index(g2)) == sort_edge_index(edge_index(g)) - @test count(g2.edata.e1[:, i] == 2 * ones(3) for i in 1:(g2.num_edges)) == 5 - @test count(g2.edata.e2[i] == 4 for i in 1:(g2.num_edges)) == 5 - w2 = get_edge_weight(g2) - @test count(w2[i] == 6 for i in 1:(g2.num_edges)) == 5 -end end - -@testset "negative_sample" begin if GRAPH_T == :coo - n, m = 10, 30 - g = rand_graph(n, m, bidirected = true, graph_type = GRAPH_T) - - # check bidirected=is_bidirected(g) default - gneg = negative_sample(g, num_neg_edges = 20) - @test gneg.num_nodes == g.num_nodes - @test gneg.num_edges == 20 - @test is_bidirected(gneg) - @test intersect(g, gneg).num_edges == 0 -end end - -@testset "rand_edge_split" begin if GRAPH_T == :coo - n, m = 100, 300 - - g = rand_graph(n, m, bidirected = true, graph_type = GRAPH_T) - # check bidirected=is_bidirected(g) default - g1, g2 = rand_edge_split(g, 0.9) - @test is_bidirected(g1) - @test is_bidirected(g2) - @test intersect(g1, g2).num_edges == 0 - @test g1.num_edges + g2.num_edges == g.num_edges - @test g2.num_edges < 50 - - g = rand_graph(n, m, bidirected = false, graph_type = GRAPH_T) - # check bidirected=is_bidirected(g) default - g1, g2 = rand_edge_split(g, 0.9) - @test !is_bidirected(g1) - @test !is_bidirected(g2) - @test intersect(g1, g2).num_edges == 0 - @test g1.num_edges + g2.num_edges == g.num_edges - @test g2.num_edges < 50 - - g1, g2 = rand_edge_split(g, 0.9, bidirected = false) - @test !is_bidirected(g1) - @test !is_bidirected(g2) - @test intersect(g1, g2).num_edges == 0 - @test g1.num_edges + g2.num_edges == g.num_edges - @test g2.num_edges < 50 -end end - -@testset "set_edge_weight" begin - g = rand_graph(10, 20, graph_type = GRAPH_T) - w = rand(20) - - gw = set_edge_weight(g, w) - @test get_edge_weight(gw) == w - - # now from weighted graph - s, t = edge_index(g) - g2 = GNNGraph(s, t, rand(20), graph_type = GRAPH_T) - gw2 = set_edge_weight(g2, w) - @test get_edge_weight(gw2) == w +@testitem "to_unidirected" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + s = [1, 2, 3, 4, 4] + t = [2, 3, 4, 3, 4] + w = [1.0, 2.0, 3.0, 4.0, 5.0] + e = [10.0, 20.0, 30.0, 40.0, 50.0] + g = GNNGraph(s, t, w, edata = e) + + g2 = to_unidirected(g) + @test g2.num_nodes == g.num_nodes + @test g2.num_edges == 4 + @test !has_multi_edges(g2) + + s2, t2 = edge_index(g2) + w2 = get_edge_weight(g2) + @test s2 == [1, 2, 3, 4] + @test t2 == [2, 3, 4, 4] + @test w2 == [1, 2, 3.5, 5] + @test g2.edata.e == [10.0, 20.0, 35.0, 50.0] + end + end end -@testset "to_bidirected" begin if GRAPH_T == :coo - s, t = [1, 2, 3, 3, 4], [2, 3, 4, 4, 4] - w = [1.0, 2.0, 3.0, 4.0, 5.0] - e = [10.0, 20.0, 30.0, 40.0, 50.0] - g = GNNGraph(s, t, w, edata = e) - - g2 = to_bidirected(g) - @test g2.num_nodes == g.num_nodes - @test g2.num_edges == 7 - @test is_bidirected(g2) - @test !has_multi_edges(g2) - - s2, t2 = edge_index(g2) - w2 = get_edge_weight(g2) - @test s2 == [1, 2, 2, 3, 3, 4, 4] - @test t2 == [2, 1, 3, 2, 4, 3, 4] - @test w2 == [1, 1, 2, 2, 3.5, 3.5, 5] - @test g2.edata.e == [10.0, 10.0, 20.0, 20.0, 35.0, 35.0, 50.0] -end end - -@testset "to_unidirected" begin if GRAPH_T == :coo - s = [1, 2, 3, 4, 4] - t = [2, 3, 4, 3, 4] - w = [1.0, 2.0, 3.0, 4.0, 5.0] - e = [10.0, 20.0, 30.0, 40.0, 50.0] - g = GNNGraph(s, t, w, edata = e) - - g2 = to_unidirected(g) - @test g2.num_nodes == g.num_nodes - @test g2.num_edges == 4 - @test !has_multi_edges(g2) - - s2, t2 = edge_index(g2) - w2 = get_edge_weight(g2) - @test s2 == [1, 2, 3, 4] - @test t2 == [2, 3, 4, 4] - @test w2 == [1, 2, 3.5, 5] - @test g2.edata.e == [10.0, 20.0, 35.0, 50.0] -end end - -@testset "Graphs.Graph from GNNGraph" begin - g = rand_graph(10, 20, graph_type = GRAPH_T) - - G = Graphs.Graph(g) - @test nv(G) == g.num_nodes - @test ne(G) == g.num_edges ÷ 2 - - DG = Graphs.DiGraph(g) - @test nv(DG) == g.num_nodes - @test ne(DG) == g.num_edges +@testitem "Graphs.Graph from GNNGraph" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + g = rand_graph(10, 20, graph_type = GRAPH_T) + + G = Graphs.Graph(g) + @test nv(G) == g.num_nodes + @test ne(G) == g.num_edges ÷ 2 + + DG = Graphs.DiGraph(g) + @test nv(DG) == g.num_nodes + @test ne(DG) == g.num_edges + end end -@testset "random_walk_pe" begin - s = [1, 2, 2, 3] - t = [2, 1, 3, 2] - ndata = [-1, 0, 1] - g = GNNGraph(s, t, graph_type = GRAPH_T, ndata = ndata) - output = random_walk_pe(g, 3) - @test output == [0.0 0.0 0.0 - 0.5 1.0 0.5 - 0.0 0.0 0.0] +@testitem "random_walk_pe" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + s = [1, 2, 2, 3] + t = [2, 1, 3, 2] + ndata = [-1, 0, 1] + g = GNNGraph(s, t, graph_type = GRAPH_T, ndata = ndata) + output = random_walk_pe(g, 3) + @test output == [0.0 0.0 0.0 + 0.5 1.0 0.5 + 0.0 0.0 0.0] + end end -@testset "HeteroGraphs" begin +@testitem "HeteroGraphs" setup = [GraphsTestModule] begin + using .GraphsTestModule @testset "batch" begin gs = [rand_bipartite_heterograph((10, 15), 20) for _ in 1:5] g = MLUtils.batch(gs) @@ -612,22 +692,25 @@ end end end -@testset "ppr_diffusion" begin - if GRAPH_T == :coo - s = [1, 1, 2, 3] - t = [2, 3, 4, 5] - eweights = [0.1, 0.2, 0.3, 0.4] +@testitem "ppr_diffusion" setup=[GraphsTestModule] begin + using .GraphsTestModule + for GRAPH_T in GRAPH_TYPES + if GRAPH_T == :coo + s = [1, 1, 2, 3] + t = [2, 3, 4, 5] + eweights = [0.1, 0.2, 0.3, 0.4] - g = GNNGraph(s, t, eweights) + g = GNNGraph(s, t, eweights) - g_new = ppr_diffusion(g) - w_new = get_edge_weight(g_new) + g_new = ppr_diffusion(g) + w_new = get_edge_weight(g_new) - check_ew = Float32[0.012749999 - 0.025499998 - 0.038249996 - 0.050999995] + check_ew = Float32[0.012749999 + 0.025499998 + 0.038249996 + 0.050999995] - @test w_new ≈ check_ew + @test w_new ≈ check_ew + end end -end \ No newline at end of file +end diff --git a/GNNGraphs/test/utils.jl b/GNNGraphs/test/utils.jl index 31a1c7373..85995c795 100644 --- a/GNNGraphs/test/utils.jl +++ b/GNNGraphs/test/utils.jl @@ -1,4 +1,4 @@ -@testset "edge encoding/decoding" begin +@testitem "edge encoding/decoding" begin # not is_bidirected n = 5 s = [1, 1, 2, 3, 3, 4, 5] @@ -93,15 +93,19 @@ end end -@testset "color_refinement" begin - rng = MersenneTwister(17) - g = rand_graph(rng, 10, 20, graph_type = GRAPH_T) - x0 = ones(Int, 10) - x, ncolors, niters = color_refinement(g, x0) - @test ncolors == 8 - @test niters == 2 - @test x == [4, 5, 6, 7, 8, 5, 8, 9, 10, 11] - - x2, _, _ = color_refinement(g) - @test x2 == x -end \ No newline at end of file +@testitem "color_refinement" setup=[GraphsTestModule] begin + using .GraphsTestModule + using StableRNGs + for GRAPH_T in GRAPH_TYPES + rng = StableRNG(17) + g = rand_graph(rng, 10, 20, graph_type = GRAPH_T) + x0 = ones(Int, 10) + x, ncolors, niters = color_refinement(g, x0) + @test ncolors == 9 + @test niters == 2 + @test x == [6, 7, 8, 9, 10, 11, 12, 6, 13, 14] + + x2, _, _ = color_refinement(g) + @test x2 == x + end +end diff --git a/GNNLux/docs/make.jl b/GNNLux/docs/make.jl index 8603ef94d..aa4fedf91 100644 --- a/GNNLux/docs/make.jl +++ b/GNNLux/docs/make.jl @@ -71,7 +71,6 @@ makedocs(; "GNNGraph" => "GNNGraphs/api/gnngraph.md", "GNNHeteroGraph" => "GNNGraphs/api/heterograph.md", "TemporalSnapshotsGNNGraph" => "GNNGraphs/api/temporalgraph.md", - "Samplers" => "GNNGraphs/api/samplers.md", "Datasets" => "GNNGraphs/api/datasets.md", ] diff --git a/GNNlib/docs/make.jl b/GNNlib/docs/make.jl index bcf6aa352..9d1cc097b 100644 --- a/GNNlib/docs/make.jl +++ b/GNNlib/docs/make.jl @@ -38,7 +38,6 @@ makedocs(; "GNNGraph" => "GNNGraphs/api/gnngraph.md", "GNNHeteroGraph" => "GNNGraphs/api/heterograph.md", "TemporalSnapshotsGNNGraph" => "GNNGraphs/api/temporalgraph.md", - "Samplers" => "GNNGraphs/api/samplers.md", "Datasets" => "GNNGraphs/api/datasets.md", ], "Message Passing" => "api/messagepassing.md", diff --git a/GraphNeuralNetworks/docs/make.jl b/GraphNeuralNetworks/docs/make.jl index 5c74caf73..d4d42f564 100644 --- a/GraphNeuralNetworks/docs/make.jl +++ b/GraphNeuralNetworks/docs/make.jl @@ -75,7 +75,6 @@ makedocs(; "GNNGraph" => "GNNGraphs/api/gnngraph.md", "GNNHeteroGraph" => "GNNGraphs/api/heterograph.md", "TemporalSnapshotsGNNGraph" => "GNNGraphs/api/temporalgraph.md", - "Samplers" => "GNNGraphs/api/samplers.md", "Datasets" => "GNNGraphs/api/datasets.md", ] diff --git a/GraphNeuralNetworks/docs/src/tutorials/gnn_intro.md b/GraphNeuralNetworks/docs/src/tutorials/gnn_intro.md index 855fd8bfb..5eb908d73 100644 --- a/GraphNeuralNetworks/docs/src/tutorials/gnn_intro.md +++ b/GraphNeuralNetworks/docs/src/tutorials/gnn_intro.md @@ -92,7 +92,7 @@ karate.node_data.labels_comm Now we convert the single-graph dataset to a `GNNGraph`. Moreover, we add a an array of node features, a **34-dimensional feature vector** for each node which uniquely describes the members of the karate club. We also add a training mask selecting the nodes to be used for training in our semi-supervised node classification task. ````julia -g = mldataset2gnngraph(dataset) # convert a MLDataset.jl's dataset to a GNNGraphs (or a collection of graphs) +g = mldataset2gnngraph(dataset) # convert a MLDatasets.jl's dataset to a GNNGraphs (or a collection of graphs) x = zeros(Float32, g.num_nodes, g.num_nodes) x[diagind(x)] .= 1 diff --git a/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/gnn_intro.jl b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/gnn_intro.jl index 2bd674162..c2353d59e 100644 --- a/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/gnn_intro.jl +++ b/GraphNeuralNetworks/docs/src_tutorials/introductory_tutorials/gnn_intro.jl @@ -42,7 +42,7 @@ karate.node_data.labels_comm # Now we convert the single-graph dataset to a `GNNGraph`. Moreover, we add a an array of node features, a **34-dimensional feature vector** for each node which uniquely describes the members of the karate club. We also add a training mask selecting the nodes to be used for training in our semi-supervised node classification task. -g = mldataset2gnngraph(dataset) # convert a MLDataset.jl's dataset to a GNNGraphs (or a collection of graphs) +g = mldataset2gnngraph(dataset) # convert a MLDatasets.jl's dataset to a GNNGraphs (or a collection of graphs) x = zeros(Float32, g.num_nodes, g.num_nodes) x[diagind(x)] .= 1 diff --git a/GraphNeuralNetworks/examples/graph_classification_temporalbrains.jl b/GraphNeuralNetworks/examples/graph_classification_temporalbrains.jl index e25e9c1f0..a8b6846e4 100644 --- a/GraphNeuralNetworks/examples/graph_classification_temporalbrains.jl +++ b/GraphNeuralNetworks/examples/graph_classification_temporalbrains.jl @@ -1,6 +1,6 @@ # Example of graph classification when graphs are temporal and modeled as `TemporalSnapshotsGNNGraphs'. # In this code, we train a simple temporal graph neural network architecture to classify subjects' gender (female or male) using the temporal graphs extracted from their brain fMRI scan signals. -# The dataset used is the TemporalBrains dataset from the MLDataset.jl package, and the accuracy achieved with the model reaches 65-70% (it can be improved by fine-tuning the parameters of the model). +# The dataset used is the TemporalBrains dataset from the MLDatasets.jl package, and the accuracy achieved with the model reaches 65-70% (it can be improved by fine-tuning the parameters of the model). # Author: Aurora Rossi # Load packages diff --git a/GraphNeuralNetworks/notebooks/gnn_intro.ipynb b/GraphNeuralNetworks/notebooks/gnn_intro.ipynb index 798d4cc33..f42022556 100644 --- a/GraphNeuralNetworks/notebooks/gnn_intro.ipynb +++ b/GraphNeuralNetworks/notebooks/gnn_intro.ipynb @@ -176,7 +176,7 @@ } ], "source": [ - "# convert a MLDataset.jl's dataset to a GNNGraphs (or a collection of graphs)\n", + "# convert a MLDatasets.jl's dataset to a GNNGraphs (or a collection of graphs)\n", "g = mldataset2gnngraph(dataset)\n", "\n", "x = zeros(Float32, g.num_nodes, g.num_nodes)\n",