diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 23a0257..8b62c6f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,6 +29,9 @@ jobs: - ubuntu-latest arch: - x64 + env: + DATADEPS_ALWAYS_ACCEPT: true + LD_LIBRARY_PATH: "" steps: - name: Install matplotlib run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi @@ -40,6 +43,13 @@ jobs: arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 + - name: Configure test environment + shell: julia --color=yes {0} + run: | + using Pkg + Pkg.develop(PackageSpec(path=pwd())) + Pkg.instantiate() + Pkg.add(name="OpenSSL_jll", version="3") - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 @@ -54,6 +64,8 @@ jobs: actions: write # needed to allow julia-actions/cache to proactively delete old caches that it has created contents: write statuses: write + env: + LD_LIBRARY_PATH: "" steps: - name: Install matplotlib run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi @@ -65,6 +77,7 @@ jobs: arch: x64 os: ubuntu-latest - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 - name: Configure doc environment shell: julia --project=docs --color=yes {0} run: | @@ -72,7 +85,6 @@ jobs: Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() Pkg.add(name="OpenSSL_jll", version="3.0") - - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-docdeploy@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index cba9134..510c6d4 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -3,14 +3,42 @@ on: schedule: - cron: 0 0 * * * workflow_dispatch: +permissions: + contents: write + pull-requests: write jobs: CompatHelper: runs-on: ubuntu-latest steps: - - name: Pkg.add("CompatHelper") - run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: Check if Julia is already available in the PATH + id: julia_in_path + run: which julia + continue-on-error: true + - name: Install Julia, but only if it is not already available in the PATH + uses: julia-actions/setup-julia@v2 + with: + version: '1' + arch: ${{ runner.arch }} + if: steps.julia_in_path.outcome != 'success' + - name: "Add the General registry via Git" + run: | + import Pkg + ENV["JULIA_PKG_SERVER"] = "" + Pkg.Registry.add("General") + shell: julia --color=yes {0} + - name: "Install CompatHelper" + run: | + import Pkg + name = "CompatHelper" + uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" + version = "3" + Pkg.add(; name, uuid, version) + shell: julia --color=yes {0} - name: CompatHelper.main() + run: | + import CompatHelper + CompatHelper.main() + shell: julia --color=yes {0} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} - run: julia -e 'using CompatHelper; CompatHelper.main()' + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} \ No newline at end of file diff --git a/Project.toml b/Project.toml index d9e628a..8efb9a3 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" GNNGraphs = "aed8fd31-079b-4b5a-b342-a13352159b8c" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Lux = "b2108857-7c20-44ae-9111-449ecde12c47" MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54" NNlib = "872c559c-99b0-510c-b3b7-b6c96a88d5cd" @@ -22,7 +23,8 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] -julia = ">= 1.10.0" +LinearAlgebra = ">= 1.10.0" Reactant = ">= 0.2.0" +SimpleDirectedHypergraphs = "0.1.0" SimpleHypergraphs = "0.3.0" -SimpleDirectedHypergraphs = "0.1.0" \ No newline at end of file +julia = ">= 1.10.0" diff --git a/src/HyperGraphNeuralNetworks.jl b/src/HyperGraphNeuralNetworks.jl index d41a33c..0f68b6f 100644 --- a/src/HyperGraphNeuralNetworks.jl +++ b/src/HyperGraphNeuralNetworks.jl @@ -3,11 +3,12 @@ module HyperGraphNeuralNetworks using Random using DataStructures: counter using StatsBase: FrequencyWeights, sample +using LinearAlgebra +using InvertedIndices using Graphs using GNNGraphs import GNNGraphs: getn, getdata, normalize_graphdata, cat_features, shortsummary using MLUtils -using InvertedIndices using SimpleHypergraphs using SimpleDirectedHypergraphs @@ -27,22 +28,26 @@ include("core/query.jl") export hyperedge_index, get_hyperedge_weight, get_hyperedge_weights export has_vertex, vertices, isolated_vertices -export degree, indegree, outdegree -export all_neighbors, in_neighbors, out_neighbors, hyperedge_neighbors -export incidence_matrix, complex_incidence_matrix, hyperedge_weight_matrix, vertex_weight_matrix -export normalized_laplacian -export hypergraph_ids, has_self_loops, has_multi_hyperedges +export hyperedge_neighbors +export hyperedge_weight_matrix, hyperedge_degree_matrix, vertex_weight_matrix, vertex_degree_matrix +export complex_incidence_matrix, normalized_laplacian_matrix +export hypergraph_ids, has_multi_hyperedges include("core/transform.jl") export add_self_loops, remove_self_loops, remove_multi_hyperedges export rewire_hyperedges, to_undirected -export combine_hypergraphs, get_hypergraph, negative_sample -export random_split_vertices, random_split_hyperedges +export combine_hypergraphs, get_hypergraph +export AbstractNegativeSamplingStrategy, UniformSample, SizedSample, MotifSample, CliqueSample, negative_sample_hyperedge + +include("core/split.jl") + +export split_vertices, split_hyperedges, split_hypergraphs +export random_split_vertices, random_split_hyperedges, random_split_hypergraphs include("core/utils.jl") export check_num_vertices, check_num_hyperedges export normalize_graphdata -end +end \ No newline at end of file diff --git a/src/core/generate.jl b/src/core/generate.jl index 8c0e5e8..70cae49 100644 --- a/src/core/generate.jl +++ b/src/core/generate.jl @@ -17,7 +17,7 @@ kws... ) where {H <: AbstractHGNNDiHypergraph} - Generate a *random* undirected hypergraph (in the style of Erdős–Rényi random graphs) without any structural + Generate a *random* hypergraph (in the style of Erdős–Rényi random graphs) without any structural constraints. See `SimpleHypergraphs.random_model`. The user can optionally seed the random number generator with kwarg `seed`. The default value is -1; if the value is greater than or equal to 0, then `seed` will be used. @@ -30,7 +30,7 @@ function erdos_renyi_hypergraph( kws... ) where {H <: AbstractHGNNHypergraph} if seed >= 0 - seed!(seed) + Random.seed!(seed) end basic = random_model(nVertices, nEdges, Hypergraph) @@ -47,7 +47,7 @@ function erdos_renyi_hypergraph( kws... ) where {H <: AbstractHGNNDiHypergraph} if seed >= 0 - seed!(seed) + Random.seed!(seed) end basic = random_model( @@ -94,7 +94,7 @@ function random_kuniform_hypergraph( kws... ) where {H <: AbstractHGNNHypergraph} if seed >= 0 - seed!(seed) + Random.seed!(seed) end kuniform = random_kuniform_model(nVertices, nEdges, k, Hypergraph) @@ -112,7 +112,7 @@ function random_kuniform_hypergraph( kws... ) where {H <: AbstractHGNNDiHypergraph} if seed >= 0 - seed!(seed) + Random.seed!(seed) end kuniform = random_kuniform_model( @@ -159,7 +159,7 @@ function random_dregular_hypergraph( kws... ) where {H <: AbstractHGNNHypergraph} if seed >= 0 - seed!(seed) + Random.seed!(seed) end dregular = random_dregular_model(nVertices, nEdges, d, Hypergraph) @@ -177,7 +177,7 @@ function random_dregular_hypergraph( kws... ) where {H <: AbstractHGNNDiHypergraph} if seed >= 0 - seed!(seed) + Random.seed!(seed) end dregular = random_dregular_model( @@ -214,11 +214,11 @@ function random_preferential_hypergraph( HType::Type{HO}; seed::Int = -1, HTypeStart::Type{HI} = Hypergraph, - hg::HI = random_model(5,5, HI), + hg::HI = random_model(5,5, HTypeStart), kws... ) where {HI<:AbstractSimpleHypergraph, HO<:AbstractHGNNHypergraph} if seed >= 0 - seed!(seed) + Random.seed!(seed) end pref_hg = random_preferential_model( diff --git a/src/core/hypergraphs.jl b/src/core/hypergraphs.jl index c7f620a..56d5418 100644 --- a/src/core/hypergraphs.jl +++ b/src/core/hypergraphs.jl @@ -320,7 +320,7 @@ function add_vertex( ) end - return HGNNHypergraph( + return HGNNHypergraph{T,D}( v2he, he2v, ix, @@ -334,6 +334,61 @@ function add_vertex( end +""" + add_vertices( + hg::HGNNHypergraph{T, D}, + n::Int, + features::DataStore; + hyperedges::AbstractVector{D} = Vector{D}(D(), n), + hypergraph_ids::AbstractVector{Int} = ones(Int, n) + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Adds a set of vertices to an undirected hypergraph `hg`. Optionally, the user can specify the hyperedges on which + these vertices are incident. +""" +function add_vertices( + hg::HGNNHypergraph{T, D}, + n::Int, + features::DataStore; + hyperedges::AbstractVector{D} = Vector{D}(D(), n), + hypergraph_ids::AbstractVector{Int} = ones(Int, n) +) where {T <: Real, D <: AbstractDict{Int, T}} + + for i in 1:n + hg = add_vertex(hg, getobs(features, i); hyperedges=hyperedges[i], hypergraph_id=hypergraph_ids[i]) + end + + hg + +end + + +""" + add_hyperedges( + hg::HGNNHypergraph{T, D}, + n::Int, + features::DataStore; + vertices::AbstractVector{D} = Vector{D}(D(), n) + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Adds a set of hyperedges to an undirected hypergraph `hg`. Optionally, the user can specify the vertices that are + incident on these hyperedges. +""" +function add_hyperedges( + hg::HGNNHypergraph{T, D}, + n::Int, + features::DataStore; + vertices::AbstractVector{D} = Vector{D}(D(), n) +) where {T <: Real, D <: AbstractDict{Int, T}} + + for i in 1:n + hg = add_hyperedge(hg, getobs(features, i); vertices=vertices[i]) + end + + hg +end + + """ remove_vertex!(::HGNNHypergraph, ::Int) @@ -665,6 +720,11 @@ function Base.copy(hg::HGNNHypergraph; deep = false) end end +""" + print_all_features(io::IO, vdata, hedata, hgdata)\ + + Helper function that reports the vertex, hyperedge, and hypergraph features of a hypergraph. +""" function print_all_features(io::IO, vdata, hedata, hgdata) print(io, "vertex features: $(vdata), hyperedge features: $(hedata), @@ -685,7 +745,7 @@ function Base.show(io::IO, ::MIME"text/plain", hg::HGNNHypergraph) else print(io, "HGNNHypergraph:\n num_vertices: $(hg.num_vertices)\n num_hyperedges: $(hg.num_hyperedges)") - hg.num_hypergraphs > 1 && print(io, "\n num_hypergraphs: $(hg.num_graphs)") + hg.num_hypergraphs > 1 && print(io, "\n num_hypergraphs: $(hg.num_hypergraphs)") if !isempty(hg.vdata) print(io, "\n vdata (vertex data):") for k in keys(hg.vdata) @@ -708,8 +768,6 @@ function Base.show(io::IO, ::MIME"text/plain", hg::HGNNHypergraph) end MLUtils.numobs(hg::HGNNHypergraph) = hg.num_hypergraphs -# TODO: implement gethypergraph function -# MLUtils.getobs(hg::HGNNHypergraph, i) = gethypergraph(hg, i) function Base.:(==)(hg1::HGNNHypergraph, hg2::HGNNHypergraph) hg1 === hg2 && return true @@ -744,8 +802,6 @@ function Base.getproperty(hg::HGNNHypergraph, s::Symbol) end -# TODO: update docstring -# ALSO: test new constructors """ HGNNDiHypergraph{T<:Real, D<:AbstractDict{Int,T}} <: AbstractHGNNDiHypergraph{Tuple{Union{T, Nothing}, Union{T, Nothing}}} @@ -753,37 +809,80 @@ A directed hypergraph type for use in hypergraph neural networks **Constructors** - HGNNDiHypergraph( - h::AbstractDirectedHypergraph{T}; + HGNNDiHypergraph{T,D}( + h::DirectedHypergraph{T,V,E,D}; hypergraph_ids::Union{Nothing, AbstractVector{<:Integer}} = nothing, - vdata::Union{DataStore, Nothing} = nothing, - hedata::Union{DataStore, Nothing} = nothing, - hgdata::Union{DataStore, Nothing} = nothing + vdata = nothing, + hedata = nothing, + hgdata = nothing + ) where {T<:Real, V, E, D<:AbstractDict{Int,T}} + + function HGNNDiHypergraph{T}( + h::DirectedHypergraph; + hypergraph_ids::Union{Nothing, AbstractVector{<:Integer}} = nothing, + vdata = nothing, + hedata = nothing, + hgdata = nothing ) where {T<:Real} + HGNNDiHypergraph( + h::DirectedHypergraph; + hypergraph_ids::Union{Nothing, AbstractVector{<:Integer}} = nothing, + vdata = nothing, + hedata = nothing, + hgdata = nothing + ) + Construct a `HGNNDiHypergraph` from a previously constructed directed hypergraph. Optionally, the user can specify what hypergraph each vertex belongs to (if multiple distinct hypergraphs are included), as well as vertex, hyperedge, and hypergraph features. HGNNDiHypergraph{T, D}( - hg_tail::Hypergraph{T, D}, - hg_head::Hypergraph{T, D}; + hg_tail::Hypergraph{T,V,E,D}, + hg_head::Hypergraph{T,V,E,D}; hypergraph_ids::Union{Nothing, AbstractVector{<:Integer}} = nothing, vdata = nothing, hedata = nothing, hgdata = nothing - ) where {T<:Real, D<:AbstractDict{Int, T}} + ) where {T<:Real, V, E, D<:AbstractDict{Int, T}} + + HGNNDiHypergraph{T}( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T}; + hypergraph_ids::Union{Nothing, AbstractVector{<:Integer}} = nothing, + vdata = nothing, + hedata = nothing, + hgdata = nothing + ) where {T<:Real} + + HGNNDiHypergraph( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T}; + hypergraph_ids::Union{Nothing, AbstractVector{<:Integer}} = nothing, + vdata = nothing, + hedata = nothing, + hgdata = nothing + ) where {T<:Real} Construct a `HGNNDiHypergraph` from two undirected hypergraphs representing the tails and the heads of hyperedges in a directed hypergraph. + HGNNDiHypergraph{T}( + incidence_tail::AbstractMatrix{Union{T, Nothing}}, + incidence_head::AbstractMatrix{Union{T, Nothing}}; + hypergraph_ids::Union{Nothing, AbstractVector{<:Integer}} = nothing, + vdata = nothing, + hedata = nothing, + hgdata = nothing + ) where {T<:Real} + HGNNDiHypergraph( incidence_tail::AbstractMatrix{Union{T, Nothing}}, incidence_head::AbstractMatrix{Union{T, Nothing}}; hypergraph_ids::Union{Nothing, AbstractVector{<:Integer}} = nothing, - vdata::Union{DataStore, Nothing} = nothing, - hedata::Union{DataStore, Nothing} = nothing, - hgdata::Union{DataStore, Nothing} = nothing + vdata = nothing, + hedata = nothing, + hgdata = nothing ) where {T<:Real} Construct a `HGNNDiHypergraph` from incidence matrices `incidence_tail` (containing information regarding which @@ -791,11 +890,11 @@ A directed hypergraph type for use in hypergraph neural networks about the heads). The incidence matrices have dimensions `M`x`N`, where `M` is the number of vertices and `N` is the number of hyperedges. - function HGNNDiHypergraph(num_nodes::T; vdata=nothing, kws...) where {T<:Integer} + HGNNDiHypergraph(num_nodes::T; vdata=nothing, kws...) where {T<:Integer} Construct a `HGNNDiHypergraph` with no hyperedges and `num_nodes` vertices. - function HGNNDiHypergraph(; num_nodes=nothing, vdata=nothing, kws...) + HGNNDiHypergraph(; num_nodes=nothing, vdata=nothing, kws...) Construct a `HGNNDiHypergraph` with minimal (perhaps no) information. @@ -1318,6 +1417,72 @@ function add_hyperedge( end + +""" + add_vertices( + hg::HGNNDiHypergraph{T, D}, + n::Int, + features::DataStore; + hyperedges_tail::AbstractVector{D} = Vector{D}(D(), n), + hyperedges_head::AbstractVector{D} = Vector{D}(D(), n), + hypergraph_ids::AbstractVector{Int} = ones(Int, n) + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Adds a set of vertices to an directed hypergraph `hg`. Optionally, the user can specify the hyperedges on which + these vertices are incident. +""" +function add_vertices( + hg::HGNNDiHypergraph{T, D}, + n::Int, + features::DataStore; + hyperedges_tail::AbstractVector{D} = Vector{D}(D(), n), + hyperedges_head::AbstractVector{D} = Vector{D}(D(), n), + hypergraph_ids::AbstractVector{Int} = ones(Int, n) +) where {T <: Real, D <: AbstractDict{Int, T}} + + for i in 1:n + hg = add_vertex( + hg, + getobs(features, i); + hyperedges_tail = hyperedges_tail[i], + hyperedges_head = hyperedges_head[i], + hypergraph_id = hypergraph_ids[i] + ) + end + + hg + +end + + +""" + add_hyperedges( + hg::HGNNDiHypergraph{T, D}, + n::Int, + features::DataStore; + vertices_tail::AbstractVector{D} = Vector{D}(D(), n), + vertices_head::AbstractVector{D} = Vector{D}(D(), n) + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Adds a set of hyperedges to a directed hypergraph `hg`. Optionally, the user can specify the vertices that are + incident on these hyperedges. +""" +function add_hyperedges( + hg::HGNNDiHypergraph{T, D}, + n::Int, + features::DataStore; + vertices_tail::AbstractVector{D} = Vector{D}(D(), n), + vertices_head::AbstractVector{D} = Vector{D}(D(), n) +) where {T <: Real, D <: AbstractDict{Int, T}} + + for i in 1:n + hg = add_hyperedge(hg, getobs(features, i); vertices_tail=vertices_tail[i], vertices_head=vertices_head[i]) + end + + hg +end + + """ remove_hyperedge!(::HGNNHypergraph, ::Int) @@ -1560,74 +1725,6 @@ function remove_hyperedges(hg::HGNNDiHypergraph{T, D}, to_remove::AbstractVector ) end -function add_vertices( - hg::HGNNHypergraph{T, D}, - n::Int, - features::DataStore; - hyperedges::AbstractVector{D} = Vector{D}(D(), n), - hypergraph_ids::AbstractVector{Int} = ones(Int, n) -) where {T <: Real, D <: AbstractDict{Int, T}} - - for i in 1:n - hg = add_vertex(hg, getobs(features, i); hyperedges=hyperedges[i], hypergraph_id=hypergraph_ids[i]) - end - - hg - -end - -function add_vertices( - hg::HGNNDiHypergraph{T, D}, - n::Int, - features::DataStore; - hyperedges_tail::AbstractVector{D} = Vector{D}(D(), n), - hyperedges_head::AbstractVector{D} = Vector{D}(D(), n), - hypergraph_ids::AbstractVector{Int} = ones(Int, n) -) where {T <: Real, D <: AbstractDict{Int, T}} - - for i in 1:n - hg = add_vertex( - hg, - getobs(features, i); - hyperedges_tail = hyperedges_tail[i], - hyperedges_head = hyperedges_head[i], - hypergraph_id = hypergraph_ids[i] - ) - end - - hg - -end - -function add_hyperedges( - hg::HGNNHypergraph{T, D}, - n::Int, - features::DataStore; - vertices::AbstractVector{D} = Vector{D}(D(), n) -) where {T <: Real, D <: AbstractDict{Int, T}} - - for i in 1:n - hg = add_hyperedge(hg, getobs(features, i); vertices=vertices[i]) - end - - hg -end - -function add_hyperedges( - hg::HGNNDiHypergraph{T, D}, - n::Int, - features::DataStore; - vertices_tail::AbstractVector{D} = Vector{D}(D(), n), - vertices_head::AbstractVector{D} = Vector{D}(D(), n) -) where {T <: Real, D <: AbstractDict{Int, T}} - - for i in 1:n - hg = add_hyperedge(hg, getobs(features, i); vertices_tail=vertices_tail[i], vertices_head=vertices_head[i]) - end - - hg -end - function Base.show(io::IO, hg::HGNNDiHypergraph) print(io, "HGNNDiHypergraph($(hg.num_vertices), $(hg.num_hyperedges), $(hg.num_hypergraphs)) with ") @@ -1643,7 +1740,7 @@ function Base.show(io::IO, ::MIME"text/plain", hg::HGNNDiHypergraph) else print(io, "HGNNDiHypergraph:\n num_vertices: $(hg.num_vertices)\n num_hyperedges: $(hg.num_hyperedges)") - hg.num_hypergraphs > 1 && print(io, "\n num_hypergraphs: $(hg.num_graphs)") + hg.num_hypergraphs > 1 && print(io, "\n num_hypergraphs: $(hg.num_hypergraphs)") if !isempty(hg.vdata) print(io, "\n vdata (vertex data):") for k in keys(hg.vdata) @@ -1690,8 +1787,6 @@ function Base.copy(hg::HGNNDiHypergraph; deep = false) end MLUtils.numobs(hg::HGNNDiHypergraph) = hg.num_hypergraphs -# TODO: implement gethypergraph function -# MLUtils.getobs(hg::HGNNDiHypergraph, i) = gethypergraph(hg, i) function Base.:(==)(hg1::HGNNDiHypergraph, hg2::HGNNDiHypergraph) hg1 === hg2 && return true diff --git a/src/core/query.jl b/src/core/query.jl index aad59fd..d0729db 100644 --- a/src/core/query.jl +++ b/src/core/query.jl @@ -10,7 +10,7 @@ TODO: pad things like hyperedge indices with zeros to make linalg more efficient element of the index contains the indices of all vertices present in hyperedge `i`. """ function hyperedge_index(hg::H) where {H <: AbstractHGNNHypergraph} - collect.(keys.(hg.he2v)) + sort.(collect.(keys.(hg.he2v))) end """ @@ -22,8 +22,8 @@ end indices of the vertices in the head of hyperedge `i`. """ function hyperedge_index(hg::H) where {H <: AbstractHGNNDiHypergraph} - ind_tail = collect.(keys.(hg.hg_tail.he2v)) - ind_head = collect.(keys.(hg.hg_head.he2v)) + ind_tail = sort.(collect.(keys.(hg.hg_tail.he2v))) + ind_head = sort.(collect.(keys.(hg.hg_head.he2v))) return ind_tail, ind_head end @@ -136,13 +136,12 @@ function Base.eltype(hg::HGNNHypergraph) end # From Graphs.jl, but not directly implemented for hypergraphs -has_vertex(hg::H, i::Int) where {H <: AbstractHGNNHypergraph} = 1 <= i <= hg.num_vertices -has_vertex(hg::H, i::Int) where {H <: AbstractHGNNDiHypergraph} = 1 <= i <= hg.num_vertices +Graphs.has_vertex(hg::H, i::Int) where {H <: AbstractHGNNHypergraph} = 1 <= i <= hg.num_vertices +Graphs.has_vertex(hg::H, i::Int) where {H <: AbstractHGNNDiHypergraph} = 1 <= i <= hg.num_vertices -vertices(hg::H) where {H <: AbstractHGNNHypergraph} = 1:(hg.num_vertices) -vertices(hg::H) where {H <: AbstractHGNNDiHypergraph} = 1:(hg.num_vertices) +Graphs.vertices(hg::H) where {H <: AbstractHGNNHypergraph} = 1:(hg.num_vertices) +Graphs.vertices(hg::H) where {H <: AbstractHGNNDiHypergraph} = 1:(hg.num_vertices) -#TODO: docstrings """ degree(hg::H) where {H <: AbstractHGNNHypergraph} @@ -151,9 +150,9 @@ vertices(hg::H) where {H <: AbstractHGNNDiHypergraph} = 1:(hg.num_vertices) Return the degree of all vertices (if no index is provided) or of a specific group of vertices. """ -degree(hg::H) where {H <: AbstractHGNNHypergraph} = length.(keys.(hg.v2he)) -degree(hg::H, i::Int) where {H <: AbstractHGNNHypergraph} = length(hg.v2he[i]) -degree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNHypergraph} = [degree(hg, i) for i in inds] +Graphs.degree(hg::H) where {H <: AbstractHGNNHypergraph} = length.(keys.(hg.v2he)) +Graphs.degree(hg::H, i::Int) where {H <: AbstractHGNNHypergraph} = length(hg.v2he[i]) +Graphs.degree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNHypergraph} = [degree(hg, i) for i in inds] """ degree(hg::H) where {H <: AbstractHGNNDiHypergraph} @@ -164,9 +163,9 @@ degree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNHypergraph} = [ For directed hypergraphs, the total degree is the sum of the incoming degree (see `indegree`) and the outgoing degree (see `outdegree`). """ -degree(hg::H) where {H <: AbstractHGNNDiHypergraph} = indegree(hg) .+ outdegree(hg) -degree(hg::H, i::Int) where {H <: AbstractHGNNDiHypergraph} = indegree(hg, i) + outdegree(hg, i) -degree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHypergraph} = [degree(hg, i) for i in inds] +Graphs.degree(hg::H) where {H <: AbstractHGNNDiHypergraph} = indegree(hg) .+ outdegree(hg) +Graphs.degree(hg::H, i::Int) where {H <: AbstractHGNNDiHypergraph} = indegree(hg, i) + outdegree(hg, i) +Graphs.degree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHypergraph} = [degree(hg, i) for i in inds] """ indegree(hg::H) where {H <: AbstractHGNNDiHypergraph} @@ -176,9 +175,9 @@ degree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHypergraph} = Return the incoming degree of all vertices (if no index is provided) or of a specific group of indices for a directed hypergraph. The incoming degree is the number of directed hyperedges containing a vertex in the head. """ -indegree(hg::H) where {H <: AbstractHGNNDiHypergraph} = length.(keys.(hg.hg_head.v2he)) -indegree(hg::H, i::Int) where {H <: AbstractHGNNDiHypergraph} = length(hg.hg_head.v2he[i]) -indegree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHypergraph} = [indegree(hg, i) for i in inds] +Graphs.indegree(hg::H) where {H <: AbstractHGNNDiHypergraph} = length.(keys.(hg.hg_head.v2he)) +Graphs.indegree(hg::H, i::Int) where {H <: AbstractHGNNDiHypergraph} = length(hg.hg_head.v2he[i]) +Graphs.indegree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHypergraph} = [indegree(hg, i) for i in inds] """ outdegree(hg::H) where {H <: AbstractHGNNDiHypergraph} @@ -188,16 +187,16 @@ indegree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHypergraph} Return the outgoing degree of all vertices (if no index is provided) or of a specific group of indices for a directed hypergraph. The outgoing degree is the number of directed hyperedges containing a vertex in the tail. """ -outdegree(hg::H) where {H <: AbstractHGNNDiHypergraph} = length.(keys.(hg.hg_tail.v2he)) -outdegree(hg::H, i::Int) where {H <: AbstractHGNNDiHypergraph} = length(hg.hg_tail.v2he[i]) -outdegree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHypergraph} = [indegree(hg, i) for i in inds] +Graphs.outdegree(hg::H) where {H <: AbstractHGNNDiHypergraph} = length.(keys.(hg.hg_tail.v2he)) +Graphs.outdegree(hg::H, i::Int) where {H <: AbstractHGNNDiHypergraph} = length(hg.hg_tail.v2he[i]) +Graphs.outdegree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHypergraph} = [outdegree(hg, i) for i in inds] """ all_neighbors(hg::H) where {H <: AbstractHGNNHypergraph} Collect all neighbors for all vertices in an undirected hypergraph. """ -function all_neighbors(hg::H) where {H <: AbstractHGNNHypergraph} +function Graphs.all_neighbors(hg::H) where {H <: AbstractHGNNHypergraph} # For undirected hypergraph, neighbor vertices are those that share a hyperedge with the vertex of interest hes = collect.(keys.(hg.v2he)) @@ -205,10 +204,11 @@ function all_neighbors(hg::H) where {H <: AbstractHGNNHypergraph} # There's probably a more efficient way to do this neighbors = Vector{Vector{Int}}(undef,length(hes)) for (i, hes_i) in enumerate(hes) + neighbors[i] = [] for he in hes_i - cat(neighbors[i], collect(keys(hg.he2v[he]))) + neighbors[i] = vcat(neighbors[i], collect(keys(hg.he2v[he]))) end - neighbors[i] = sort!(unique!(neighbors[i])) + neighbors[i] = sort!(filter!(x -> x != i, unique!(neighbors[i]))) end neighbors @@ -219,17 +219,17 @@ end Returns the neighbors of vertex `i` in undirected hypergraph `hg`. """ -function all_neighbors(hg::H, i::Int) where {H <: AbstractHGNNHypergraph} +function Graphs.all_neighbors(hg::H, i::Int) where {H <: AbstractHGNNHypergraph} hes = collect(keys(hg.v2he[i])) # There's probably a more efficient way to do this - neighbors = Vector{Int}[] + neighbors = Int[] for he in hes - cat(neighbors, collect(keys(hg.he2v[he]))) + neighbors = vcat(neighbors, collect(keys(hg.he2v[he]))) end - sort!(unique!(neighbors)) + sort!(filter!(x -> x != i, unique!(neighbors))) end """ @@ -247,12 +247,12 @@ end `j` is in the head of `e`, then `i` and `j` are neighbors. Default is false. """ -function all_neighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} +function Graphs.all_neighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} - incoming = in_neighbors(hg; same_side=same_side) - outgoing = out_neighbors(hg; same_side=same_side) + incoming = inneighbors(hg; same_side=same_side) + outgoing = outneighbors(hg; same_side=same_side) - sort!(unique!([incoming; outgoing])) + sort!.(unique!.(vcat.(incoming, outgoing))) end @@ -272,17 +272,17 @@ end `j` is in the head of `e`, then `i` and `j` are neighbors. Default is false. """ -function all_neighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} +function Graphs.all_neighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} - incoming = in_neighbors(hg, i; same_side=same_side) - outgoing = out_neighbors(hg, i; same_side=same_side) + incoming = inneighbors(hg, i; same_side=same_side) + outgoing = outneighbors(hg, i; same_side=same_side) - sort!(unique!([incoming; outgoing])) + sort!(unique!(vcat(incoming, outgoing))) end """ - in_neighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} + inneighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} Return the incoming neighbors for a directed hypergraph `hg`. Note that the definition of `incoming` depends on if `same_side` is true or false; see below. @@ -294,28 +294,29 @@ end return the neighbors on the opposite side of the hyperedge; i.e., if vertex `i` is in the head of `e` and vertex `j` is in the tail of `e`, then `j` is an incoming neighbor to `i`. Default is false. """ -function in_neighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} - if same_side - all_neighbors(hg.hg_head) - else - # Find the hyperedges for which each node is in the head - hes = collect.(keys.(hg.hg_head.v2he)) - - # Neighbors are the vertices in the associated tails - neighbors = Vector{Vector{Int}}(undef,length(hes)) - for (i, hes_i) in enumerate(hes) - for he in hes_i - cat(neighbors[i], collect(keys(hg.hg_tail.he2v[he]))) +function Graphs.inneighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} + # Find the hyperedges for which each node is in the head + hes = collect.(keys.(hg.hg_head.v2he)) + + # Neighbors are the vertices in the associated tails + neighbors = Vector{Vector{Int}}(undef,length(hes)) + for (i, hes_i) in enumerate(hes) + neighbors[i] = [] + for he in hes_i + neighbors[i] = vcat(neighbors[i], collect(keys(hg.hg_tail.he2v[he]))) + # Consider vertices in the head with this vertex to be neighbors + if same_side + neighbors[i] = vcat(neighbors[i], collect(keys(hg.hg_head.he2v[he]))) end - neighbors[i] = sort!(unique!(neighbors[i])) end - - neighbors + neighbors[i] = sort!(filter!(x -> x != i, unique!(neighbors[i]))) end + + neighbors end """ - in_neighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} + inneighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} Return the incoming neighbors for a vertex `i` of a directed hypergraph `hg`. Note that the definition of `incoming` depends on if `same_side` is true or false; see below. @@ -328,25 +329,25 @@ end return the neighbors on the opposite side of the hyperedge; i.e., if vertex `i` is in the head of `e` and vertex `j` is in the tail of `e`, then `j` is an incoming neighbor to `i`. Default is false. """ -function in_neighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} - if same_side - all_neighbors(hg.hg_head, i) - else - # Find the hyperedges for which this node is in the head - hes = collect(keys(hg.hg_head.v2he[i])) +function Graphs.inneighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} + # Find the hyperedges for which this node is in the head + hes = collect(keys(hg.hg_head.v2he[i])) - # Neighbors are the vertices in the associated tails - neighbors = Vector{Int}[] - for he in hes - cat(neighbors, collect(keys(hg.hg_tail.he2v[he]))) + # Neighbors are the vertices in the associated tails + neighbors = Vector{Int}[] + for he in hes + neighbors = vcat(neighbors, collect(keys(hg.hg_tail.he2v[he]))) + # Consider vertices in the head with this vertex to be neighbors + if same_side + neighbors = vcat(neighbors, collect(keys(hg.hg_head.he2v[he]))) end - - sort!(unique!(neighbors)) end + + sort!(filter!(x -> x != i, unique!(neighbors))) end """ - out_neighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} + outneighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} Return the outgoing neighbors for a directed hypergraph `hg`. Note that the definition of `outgoing` depends on if `same_side` is true or false; see below. @@ -358,28 +359,29 @@ end return the neighbors on the opposite side of the hyperedge; i.e., if vertex `i` is in the tail of `e` and vertex `j` is in the head of `e`, then `j` is an outgoing neighbor to `i`. Default is false. """ -function out_neighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} - if same_side - all_neighbors(hg.hg_tail) - else - # Find the hyperedges for which each node is in the tail - hes = collect.(keys.(hg.hg_tail.v2he)) - - # Neighbors are the vertices in the associated heads - neighbors = Vector{Vector{Int}}(undef,length(hes)) - for (i, hes_i) in enumerate(hes) - for he in hes_i - cat(neighbors[i], collect(keys(hg.hg_head.he2v[he]))) +function Graphs.outneighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} + # Find the hyperedges for which each node is in the tail + hes = collect.(keys.(hg.hg_tail.v2he)) + + # Neighbors are the vertices in the associated heads + neighbors = Vector{Vector{Int}}(undef,length(hes)) + for (i, hes_i) in enumerate(hes) + neighbors[i] = [] + for he in hes_i + neighbors[i] = vcat(neighbors[i], collect(keys(hg.hg_head.he2v[he]))) + # Consider vertices in the tail with this vertex to be neighbors + if same_side + neighbors[i] = vcat(neighbors[i], collect(keys(hg.hg_tail.he2v[he]))) end - neighbors[i] = sort!(unique!(neighbors[i])) end - - neighbors + neighbors[i] = sort!(filter!(x -> x != i, unique!(neighbors[i]))) end + + neighbors end """ - out_neighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} + outneighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} Return the outgoing neighbors for a vertex `i` of a directed hypergraph `hg`. Note that the definition of `outgoing` depends on if `same_side` is true or false; see below. @@ -392,24 +394,24 @@ end return the neighbors on the opposite side of the hyperedge; i.e., if vertex `i` is in the tail of `e` and vertex `j` is in the head of `e`, then `j` is an outgoing neighbor to `i`. Default is false. """ -function out_neighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} - if same_side - all_neighbors(hg.hg_tail, i) - else - # Find the hyperedges for which this node is in the head - hes = collect(keys(hg.hg_head.v2he[i])) +function Graphs.outneighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} + # Find the hyperedges for which this node is in the tail + hes = collect(keys(hg.hg_tail.v2he[i])) - # Neighbors are the vertices in the associated tails - neighbors = Vector{Int}[] - for he in hes - cat(neighbors, collect(keys(hg.hg_tail.he2v[he]))) + # Neighbors are the vertices in the associated tails + neighbors = Vector{Int}[] + for he in hes + neighbors = vcat(neighbors, collect(keys(hg.hg_head.he2v[he]))) + + # Consider vertices in the tail with this vertex to be neighbors + if same_side + neighbors = vcat(neighbors, collect(keys(hg.hg_tail.he2v[he]))) end - - sort!(unique!(neighbors)) end + + sort!(filter!(x -> x != i, unique!(neighbors))) end - """ hyperedge_neighbors(hg::H) where {H <: AbstractHGNNHypergraph} @@ -421,8 +423,9 @@ function hyperedge_neighbors(hg::H) where {H <: AbstractHGNNHypergraph} # Two hyperedges are considered "neighbors" if they share at least one vertex vs = Set.(collect.(keys.(hg.he2v))) - neighbors = Vector{Vector{Int}}(undef,length(vs)) + neighbors = [Int[] for _ in eachindex(vs)] for i in eachindex(vs) + for j in eachindex(vs)[i+1:end] if length(intersect(vs[i], vs[j])) > 0 push!(neighbors[i], j) @@ -442,9 +445,9 @@ end """ function hyperedge_neighbors(hg::H, i::Int) where {H <: AbstractHGNNHypergraph} # Two hyperedges are considered "neighbors" if they share at least one vertex - vs = Set(collect(keys(hg.he2v[i]))) + vs = Set.(collect.(keys.(hg.he2v))) - neighbors = Vector{Int}[] + neighbors = Int[] for j in eachindex(vs) if i == j continue @@ -561,12 +564,12 @@ isolated_vertices(hg::H) where {H <: AbstractHGNNDiHypergraph} = [ Convert the matrix representation of an undirected hypergraph `hg` to an incidence matrix. The (i,j)-th element of the incidence matrix `M` is 1 if vertex `i` is in hyperedge `j` and 0 otherwise. """ -function incidence_matrix(hg::H) where {H <: AbstractSimpleHypergraph} +function Graphs.incidence_matrix(hg::H) where {H <: AbstractSimpleHypergraph} M = Matrix(hg) M[M .!== nothing] .= 1 M[M .=== nothing] .= 0 - M + convert(Matrix{eltype(M).b}, M) # Ugly, but ensures that Nothing is not included in type end """ @@ -576,7 +579,7 @@ end incidence matrix) and `Mh` (the head incidence matrix). The (i,j)-th element of `Mt` is 1 if vertex `i` is in the tail of hyperedge `j` and 0 otherwise. Likewise, `Mh`[i, j] is 1 if `i` is in the head of `j` and 0 otherwise """ -function incidence_matrix(hg::H) where {H <: AbstractDirectedHypergraph} +function Graphs.incidence_matrix(hg::H) where {H <: AbstractDirectedHypergraph} Mt = incidence_matrix(hg.hg_tail) Mh = incidence_matrix(hg.hg_head) @@ -598,7 +601,7 @@ end function complex_incidence_matrix(hg::H) where {H <: AbstractDirectedHypergraph} Mt, Mh = incidence_matrix(hg) - M = convert(Complex{typeof(Mh)}, Mh) .- (im .* convert(Complex{typeof(Mt)}, Mt)) + M = Mh .- (im .* Mt) # Means that there's overlap between head and hg_tail if any(abs.(M) .> 1) @@ -618,7 +621,10 @@ end """ function vertex_weight_matrix(hg::H; weighting_function::Function=sum) where {H <: AbstractSimpleHypergraph} # Weight matrix is the diagonal matrix of vertex weights - weights = [weighting_function(hg[i,:]) for i in 1:nhv(hg)] + M = Matrix(hg) + M[M .=== nothing] .= 0 + + weights = [weighting_function(M[i,:]) for i in 1:hg.num_vertices] Diagonal(weights) end @@ -633,7 +639,10 @@ end """ function hyperedge_weight_matrix(hg::H; weighting_function::Function=sum) where {H <: AbstractSimpleHypergraph} # Weight matrix is the diagonal matrix of hyperedge weights - weights = [weighting_function(hg[:,i]) for i in 1:nhe(hg)] + M = Matrix(hg) + M[M .=== nothing] .= 0 + + weights = [weighting_function(M[:,i]) for i in 1:hg.num_hyperedges] Diagonal(weights) end @@ -648,10 +657,16 @@ end (default is `sum`) that operates on each row of the hypergraph tail/head weighted incidence matrix. """ function vertex_weight_matrix(hg::H; weighting_function::Function=sum) where {H <: AbstractDirectedHypergraph} - Wt = vertex_weight_matrix(hg.hg_tail; weighting_function=weighting_function) - Wh = vertex_weight_matrix(hg.hg_head; weighting_function=weighting_function) + Wt = Matrix(hg.hg_tail) + Wt[Wt .=== nothing] .= 0 - (Wt, Wh) + Wh = Matrix(hg.hg_head) + Wh[Wh .=== nothing] .= 0 + + weights_t = [weighting_function(Wt[i,:]) for i in 1:hg.num_vertices] + weights_h = [weighting_function(Wh[i,:]) for i in 1:hg.num_vertices] + + (Diagonal(weights_t), Diagonal(weights_h)) end """ @@ -664,19 +679,44 @@ end hypergraph tail/head weighted incidence matrix. """ function hyperedge_weight_matrix(hg::H; weighting_function::Function=sum) where {H <: AbstractDirectedHypergraph} - Wt = hyperedge_weight_matrix(hg.hg_tail; weighting_function=weighting_function) - Wh = hyperedge_weight_matrix(hg.hg_head; weighting_function=weighting_function) + Wt = Matrix(hg.hg_tail) + Wt[Wt .=== nothing] .= 0 + + Wh = Matrix(hg.hg_head) + Wh[Wh .=== nothing] .= 0 - (Wt, Wh) + weights_t = [weighting_function(Wt[:,i]) for i in 1:hg.num_hyperedges] + weights_h = [weighting_function(Wh[:,i]) for i in 1:hg.num_hyperedges] + + (Diagonal(weights_t), Diagonal(weights_h)) end -vertex_degree_matrix(hg::H) where {H <: AbstractSimpleHypergraph} = Diagonal(length.(keys.(hg.v2he))) + +""" + vertex_degree_matrix(hg::H) where {H <: AbstractSimpleHypergraph} + vertex_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} + + A `VxV` matrix (where `V` is the number of vertices in `hg`) where the diagonal element `(i,i)` is the degree of + vertex `i` and all other elements are 0. For directed hypergraphs, two degree matrices are provided: one with + diagonal elements equal to the number of hyperedges in which a given vertex is in the tail and the other with + diagonal elements equal to the number of hyperedges in which a given vertex is in the head. +""" +vertex_degree_matrix(hg::H) where {H <: AbstractSimpleHypergraph} = Diagonal(degree(hg)) vertex_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} = ( - Diagonal(length.(keys.(hg.hg_tail.v2he))), - Diagonal(length.(keys.(hg.hg_head.v2he))) + Diagonal(outdegree(hg)), + Diagonal(indegree(hg)) ) +""" + hyperedge_degree_matrix(hg::H) where {H <: AbstractSimpleHypergraph} + hyperedge_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} + + An `ExE` matrix (where `E` is the number of hyperedges in `hg`) where the diagonal element `(i,i)` is the degree of + hyperedge `i` and all other elements are 0. For directed hypergraphs, two degree matrices are provided: one with + diagonal elements equal to the number of vertices in the tail of each hyperedge and the other with diagonal + elements equal to the number of vertices in the head of each hyperedge. +""" hyperedge_degree_matrix(hg::H) where {H <: AbstractSimpleHypergraph} = Diagonal(length.(keys.(hg.he2v))) hyperedge_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} = ( @@ -685,7 +725,7 @@ hyperedge_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} = ( ) """ - normalized_laplacian(hg::H; weighting_function::Function=sum) where {H <: AbstractSimpleHypergraph} + normalized_laplacian_matrix(hg::H; weighting_function::Function=sum) where {H <: AbstractSimpleHypergraph} Returns the normalized Laplacian for an undirected hypergraph `hg`. @@ -713,7 +753,7 @@ hyperedge_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} = ( Fiorini, S., Coniglio, S., Ciavotta, M., Del Bue, A., Let There be Direction in Hypergraph Neural Networks. Transactions on Machine Learning Research, 2024. """ -function normalized_laplacian(hg::H; weighting_function::Function=sum) where {H <: AbstractSimpleHypergraph} +function normalized_laplacian_matrix(hg::H; weighting_function::Function=sum) where {H <: AbstractSimpleHypergraph} Dv = vertex_degree_matrix(hg) De = hyperedge_degree_matrix(hg) W = hyperedge_weight_matrix(hg; weighting_function=weighting_function) @@ -727,7 +767,7 @@ end _matrix_avg(a::AbstractMatrix{T}, b::AbstractMatrix{T}) where {T <: Real} = (a .+ b) ./ 2 """ - normalized_laplacian( + normalized_laplacian_matrix( h::H; weighting_function::Function=sum, combining_function::Function=_matrix_avg @@ -761,7 +801,7 @@ _matrix_avg(a::AbstractMatrix{T}, b::AbstractMatrix{T}) where {T <: Real} = (a . Fiorini, S., Coniglio, S., Ciavotta, M., Del Bue, A., Let There be Direction in Hypergraph Neural Networks. Transactions on Machine Learning Research, 2024. """ -function normalized_laplacian( +function normalized_laplacian_matrix( h::H; weighting_function::Function=sum, combining_function::Function=_matrix_avg @@ -784,7 +824,7 @@ end hypergraph_ids(hg::H) where {H <: AbstractHGNNHypergraph} hypergraph_ids(hg::H) where {H <: AbstractHGNNDiHypergraph} -Return a vector containing the graph membership of each vertex in the hypergraph `hg`. + Returns a vector containing the graph membership of each vertex in the hypergraph `hg`. """ function hypergraph_ids(hg::H) where {H <: AbstractHGNNHypergraph} if isnothing(hg.hypergraph_ids) @@ -803,17 +843,31 @@ function hypergraph_ids(hg::H) where {H <: AbstractHGNNDiHypergraph} end # TODO: be consistent in handling; see transform.jl -has_self_loops(hg::H) where {H <: AbstractHGNNHypergraph} = false +""" + has_self_loops(_::H) where {H <: AbstractHGNNHypergraph} + has_self_loops(hg::H) where {H <: AbstractHGNNDiHypergraph} + + Does the hypergraph contain self-loops? For an undirected hypergraph, this is defined to always be `false`. For a + directed hypergraph, this function checks the intersection between the tail and the head of each hyperedge in `hg`. + If any intersections are nonempty, then the dihypergraph has a self-loop. +""" +Graphs.has_self_loops(_::H) where {H <: AbstractHGNNHypergraph} = false -function has_self_loops(hg::H) where {H <: AbstractHGNNDiHypergraph} +function Graphs.has_self_loops(hg::H) where {H <: AbstractHGNNDiHypergraph} vs_tail = Set.(collect.(keys.(hg.hg_tail.he2v))) vs_head = Set.(collect.(keys.(hg.hg_head.he2v))) - any([length(intersect(vs_tail[i], vs_head[i])) for i in eachindex(vs_tail)]) + any([length(intersect(vs_tail[i], vs_head[i])) > 0 for i in eachindex(vs_tail)]) end -# has_multi_hyperedges -#TODO: docstring +""" + has_multi_hyperedges(hg::H) where {H <: AbstractHGNNHypergraph} + has_multi_hyperedges(hg::H) where {H <: AbstractHGNNDiHypergraph} + + Checks if there are any hyperedges with multiplicity greater than 1, i.e., if there are two or more hyperedges + containing identical vertices. For directed hyperedges, both the tail vertices and the head vertices have to be + identical to be considered duplicates. +""" function has_multi_hyperedges(hg::H) where {H <: AbstractHGNNHypergraph} vs = sort!.(collect.(keys.(hg.he2v))) diff --git a/src/core/split.jl b/src/core/split.jl new file mode 100644 index 0000000..e58634a --- /dev/null +++ b/src/core/split.jl @@ -0,0 +1,1267 @@ +""" + split_vertices( + hg::HGNNHypergraph{T,D}, + masks::AbstractVector{BitVector} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_vertices( + hg::HGNNHypergraph{T,D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_vertices( + hg::HGNNHypergraph{T,D}, + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + + split_vertices( + hg::HGNNHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Split a single `HGNNHypergraph` into an arbitrary number of `HGNNHypergraph`s by partitioning vertices. This will + also partition hyperedges and hypergraphs. If no vertex in a particular partition is incident on a hyperedge, then + that hyperedge and its associated features will not be present in the resulting `HGNNHypergraph`. Similarly, if no + vertex in the partition belongs to a particular sub-hypergraph (based on `hypergraph_id`s), then that hypergraph + and its associated features will not be present in the resulting `HGNNHypergraph`. + + Users can provide partitions as `BitVector` masks (length `V`, where `V` is the number of vertices) or vectors of + indices (which will be converted into masks). To facilitate train-val-test splits, users can specify which + mask/indices correspond to the train set, test set, and (optionally) validation set. In these cases, the output + of `split_vertices` will be a `NamedTuple` with keys "train", "test", and (optionally) "val" and values of + `HGNNHypergraph`s; otherwise, the output is a vector of `HGNNHypergraph`s. +""" +function split_vertices( + hg::HGNNHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + + @assert all(length.(masks) .== hg.num_vertices) + + res = HGNNHypergraph{T,D}[] + + # Partition v2he and he2v, being careful of indices + for mask in masks + v2he = hg.v2he[mask] + he2v = D[] + + vmap = Dict{Int, Int}() + index = 1 + for (i, x) in enumerate(mask) + if x + vmap[i] = index + index += 1 + end + end + + hemap = Dict{Int, Int}() + hgmap = Dict{Int, Int}() + + # Remove vertices from hyperedges not part of this partition + for (i, he) in enumerate(hg.he2v) + newhe = filter(((k,v), ) -> mask[k], he) + if length(newhe) > 0 + newhe = D(vmap[k] => v for (k, v) in newhe) + push!(he2v, newhe) + hemap[i] = length(he2v) + end + end + + # Re-label hyperedges + for i in eachindex(v2he) + v2he[i] = D(hemap[k] => v for (k, v) in v2he[i]) + end + + # Grab `hypergraph_id`s for vertices in partition, if relevant + if isnothing(hg.hypergraph_ids) + hypergraph_ids = nothing + unique_hgids = [1] + else + unique_hgids = sort(collect(Set(hg.hypergraph_ids[mask]))) + for (i, e) in enumerate(unique_hgids) + hgmap[e] = i + end + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] + end + + push!( + res, + HGNNHypergraph{T,D}( + v2he, + he2v, + length(v2he), + length(he2v), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, mask), + getobs(hg.hedata, sort(collect(keys(hemap)))), + getobs(hg.hgdata, unique_hgids) + ) + ) + end + + return res +end + +function split_vertices( + hg::HGNNHypergraph{T,D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, +) where {T <: Real, D <: AbstractDict{Int, T}} + + if !isnothing(val_mask) + masks = [train_mask, val_mask, test_mask] + else + masks = [train_mask, test_mask] + end + + hgs = split_vertices(hg, masks) + + val_data = isnothing(val_mask) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +function split_vertices( + hg::HGNNHypergraph{T,D}, + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_vertices)) + end + + split_vertices(hg, masks) +end + +function split_vertices( + hg::HGNNHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_inds) + masks = [ + BitArray(i in train_inds for i in 1:hg.num_vertices), + BitArray(i in val_inds for i in 1:hg.num_vertices), + BitArray(i in test_inds for i in 1:hg.num_vertices) + ] + else + masks = [ + BitArray(i in train_inds for i in 1:hg.num_vertices), + BitArray(i in test_inds for i in 1:hg.num_vertices) + ] + end + + hgs = split_vertices(hg, masks) + + val_data = isnothing(val_inds) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +""" + split_vertices( + hg::HGNNDiHypergraph{T,D}, + masks::AbstractVector{BitVector} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_vertices( + hg::HGNNDiHypergraph{T,D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_vertices( + hg::HGNNDiHypergraph{T,D}, + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + + split_vertices( + hg::HGNNDiHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Split a single `HGNNDiHypergraph` into an arbitrary number of `HGNNDiHypergraph`s by partitioning vertices. This + will also partition hyperedges and hypergraphs. If no vertex in a particular partition is incident on a hyperedge, + then that hyperedge and its associated features will not be present in the resulting `HGNNDiHypergraph`. Similarly, + if no vertex in the partition belongs to a particular sub-hypergraph (based on `hypergraph_id`s), then that + hypergraph and its associated features will not be present in the resulting `HGNNDiHypergraph`. + + Users can provide partitions as `BitVector` masks (length `V`, where `V` is the number of vertices) or vectors of + indices (which will be converted into masks). To facilitate train-val-test splits, users can specify which + mask/indices correspond to the train set, test set, and (optionally) validation set. In these cases, the output of + `split_vertices` will be a `NamedTuple` with keys "train", "test", and (optionally) "val" and values of + `HGNNDiHypergraph`s; otherwise, the output is a vector of `HGNNDiHypergraph`s. +""" +function split_vertices( + hg::HGNNDiHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + res = HGNNDiHypergraph{T,D}[] + + # Partition v2he and he2v, being careful of indices + for mask in masks + v2he_tail = hg.hg_tail.v2he[mask] + v2he_head = hg.hg_head.v2he[mask] + + he2v_tail = D[] + he2v_head = D[] + + vmap = Dict{Int, Int}() + index = 1 + for (i, x) in enumerate(mask) + if x + vmap[i] = index + index += 1 + end + end + + hemap = Dict{Int, Int}() + hgmap = Dict{Int, Int}() + + # Remove vertices from hyperedges not part of this partition + for i in 1:hg.num_hyperedges + he_tail = hg.hg_tail.he2v[i] + he_head = hg.hg_head.he2v[i] + + newhe_tail = filter(((k,v), ) -> mask[k], he_tail) + newhe_head = filter(((k,v), ) -> mask[k], he_head) + if length(newhe_tail) > 0 || length(newhe_head) > 0 + newhe_tail = D(vmap[k] => v for (k, v) in newhe_tail) + newhe_head = D(vmap[k] => v for (k, v) in newhe_head) + push!(he2v_tail, newhe_tail) + push!(he2v_head, newhe_head) + hemap[i] = length(he2v_tail) + end + end + + # Relabel hyperedges in `v2he` + for i in eachindex(v2he_tail) + v2he_tail[i] = D(hemap[k] => v for (k, v) in v2he_tail[i]) + v2he_head[i] = D(hemap[k] => v for (k, v) in v2he_head[i]) + end + + # If relevant, grab `hypergraph_id`s for vertices in partition + if isnothing(hg.hypergraph_ids) + hypergraph_ids = nothing + unique_hgids = [1] + else + unique_hgids = sort(collect(Set(hg.hypergraph_ids[mask]))) + for (i, e) in enumerate(unique_hgids) + hgmap[e] = i + end + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] + end + + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] + + # Construct tail and head hypergraphs + hg_tail = Hypergraph{T, Nothing, Nothing, D}(length(v2he_tail), length(he2v_tail)) + hg_tail.v2he .= v2he_tail + hg_tail.he2v .= he2v_tail + + hg_head = Hypergraph{T, Nothing, Nothing, D}(length(v2he_head), length(he2v_head)) + hg_head.v2he .= v2he_head + hg_head.he2v .= he2v_head + + push!( + res, + HGNNDiHypergraph{T,D}( + hg_tail, + hg_head, + length(v2he_tail), + length(he2v_tail), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, mask), + getobs(hg.hedata, sort(collect(keys(hemap)))), + getobs(hg.hgdata, unique_hgids) + ) + ) + end + + return res +end + +function split_vertices( + hg::HGNNDiHypergraph{T,D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_mask) + masks = [train_mask, val_mask, test_mask] + else + masks = [train_mask, test_mask] + end + + hgs = split_vertices(hg, masks) + + val_data = isnothing(val_mask) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +function split_vertices( + hg::HGNNDiHypergraph{T,D}, + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_vertices)) + end + + split_vertices(hg, masks) +end + +function split_vertices( + hg::HGNNDiHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_inds) + masks = [ + BitArray(i in train_inds for i in 1:hg.num_vertices), + BitArray(i in val_inds for i in 1:hg.num_vertices), + BitArray(i in test_inds for i in 1:hg.num_vertices) + ] + else + masks = [ + BitArray(i in train_inds for i in 1:hg.num_vertices), + BitArray(i in test_inds for i in 1:hg.num_vertices) + ] + end + + hgs = split_vertices(hg, masks) + + val_data = isnothing(val_inds) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +""" + split_hyperedges( + hg::HGNNHypergraph{T,D}, + masks::AbstractVector{BitVector} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_hyperedges( + hg::HGNNHypergraph{T, D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_hyperedges( + hg::HGNNHypergraph{T,D}, + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + + split_hyperedges( + hg::HGNNHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Split a single `HGNNHypergraph` into an arbitrary number of `HGNNHypergraph`s by partitioning hyperedges. This will + also partition vertices and hypergraphs. If a vertex is not incident on any hyperedge in a particular partition, + then that vertex and its associated features will not be present in the resulting `HGNNHypergraph`. If none of the + relevant vertices in a partition belong to a particular sub-hypergraph (based on `hypergraph_id`s), then that + hypergraph and its associated features will not be present in the resulting `HGNNHypergraph`. + + Users can provide partitions as `BitVector` masks (length `E`, where `E` is the number of hyperedges) or vectors + of indices (which will be converted into masks). To facilitate train-val-test splits, users can specify which + mask/indices correspond to the train set, test set, and (optionally) validation set. In these cases, the output + of `split_vertices` will be a `NamedTuple` with keys "train", "test", and (optionally) "val" and values of + `HGNNHypergraph`s; otherwise, the output is a vector of `HGNNHypergraph`s. +""" +function split_hyperedges( + hg::HGNNHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + @assert all(length.(masks) .== hg.num_hyperedges) + + res = HGNNHypergraph{T,D}[] + + # Partition v2he and he2v, being careful of indices + for mask in masks + he2v = hg.he2v[mask] + v2he = D[] + + hemap = Dict{Int, Int}() + index = 1 + for (i, x) in enumerate(mask) + if x + hemap[i] = index + index += 1 + end + end + + vmap = Dict{Int, Int}() + hgmap = Dict{Int, Int}() + + # Remove hyperedges not in partition + for (i, v) in enumerate(hg.v2he) + newv = filter(((key,val), ) -> mask[key], v) + if length(newv) > 0 + newv = D(hemap[key] => val for (key, val) in newv) + push!(v2he, newv) + vmap[i] = length(v2he) + end + end + + # Remap vertices in hyperedges + for i in eachindex(he2v) + he2v[i] = D(vmap[key] => val for (key, val) in he2v[i]) + end + + # What vertices are in any hyperedge in this partition? + rel_vs = sort(collect(keys(vmap))) + + # If relevant, grab `hypergraph_id`s + if isnothing(hg.hypergraph_ids) + unique_hgids = [1] + hypergraph_ids = nothing + else + unique_hgids = sort(collect(Set(hg.hypergraph_ids[rel_vs]))) + for (i, e) in enumerate(unique_hgids) + hgmap[e] = i + end + + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[rel_vs]] + end + + push!( + res, + HGNNHypergraph{T,D}( + v2he, + he2v, + length(v2he), + length(he2v), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, rel_vs), + getobs(hg.hedata, mask), + getobs(hg.hgdata, unique_hgids) + ) + ) + end + + return res +end + +function split_hyperedges( + hg::HGNNHypergraph{T, D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, +) where {T <: Real, D <: AbstractDict{Int, T}} + + if !isnothing(val_mask) + masks = [train_mask, val_mask, test_mask] + else + masks = [train_mask, test_mask] + end + + hgs = split_hyperedges(hg, masks) + + val_data = isnothing(val_mask) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +function split_hyperedges( + hg::HGNNHypergraph{T,D}, + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_hyperedges)) + end + + split_hyperedges(hg, masks) +end + +function split_hyperedges( + hg::HGNNHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_inds) + masks = [ + BitArray(i in train_inds for i in 1:hg.num_hyperedges), + BitArray(i in val_inds for i in 1:hg.num_hyperedges), + BitArray(i in test_inds for i in 1:hg.num_hyperedges) + ] + else + masks = [ + BitArray(i in train_inds for i in 1:hg.num_hyperedges), + BitArray(i in test_inds for i in 1:hg.num_hyperedges) + ] + end + + hgs = split_hyperedges(hg, masks) + + val_data = isnothing(val_inds) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +""" + split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + masks::AbstractVector{BitVector} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_hyperedges( + hg::HGNNDiHypergraph{T, D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + + split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Split a single `HGNNDiHypergraph` into an arbitrary number of `HGNNDiHypergraph`s by partitioning hyperedges. This + will also partition vertices and hypergraphs. If a vertex is not incident on any hyperedge in a particular + partition, then that vertex and its associated features will not be present in the resulting `HGNNDiHypergraph`. + Similarly, if no relevant vertex in the partition belongs to a particular sub-hypergraph (based on + `hypergraph_id`s), then that hypergraph and its associated features will not be present in the resulting + `HGNNDiHypergraph`. + + Users can provide partitions as `BitVector` masks (length `E`, where `E` is the number of hyperedges) or vectors of + indices (which will be converted into masks). To facilitate train-val-test splits, users can specify which + mask/indices correspond to the train set, test set, and (optionally) validation set. In these cases, the output of + `split_vertices` will be a `NamedTuple` with keys "train", "test", and (optionally) "val" and values of + `HGNNDiHypergraph`s; otherwise, the output is a vector of `HGNNDiHypergraph`s. +""" +function split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + @assert all(length.(masks) .== hg.num_hyperedges) + + res = HGNNDiHypergraph{T,D}[] + + # Partition v2he and he2v, being careful of indices + for mask in masks + he2v_tail = hg.hg_tail.he2v[mask] + he2v_head = hg.hg_head.he2v[mask] + + v2he_tail = D[] + v2he_head = D[] + + hemap = Dict{Int, Int}() + index = 1 + for (i, x) in enumerate(mask) + if x + hemap[i] = index + index += 1 + end + end + + vmap = Dict{Int, Int}() + hgmap = Dict{Int, Int}() + + # Remove hyperedges not in partition from vertices + for i in 1:hg.num_vertices + v_tail = hg.hg_tail.v2he[i] + v_head = hg.hg_head.v2he[i] + + newv_tail = filter(((k,v), ) -> mask[k], v_tail) + newv_head = filter(((k,v), ) -> mask[k], v_head) + if length(newv_tail) > 0 || length(newv_head) > 0 + newv_tail = D(hemap[k] => v for (k, v) in newv_tail) + newv_head = D(hemap[k] => v for (k, v) in newv_head) + push!(v2he_tail, newv_tail) + push!(v2he_head, newv_head) + vmap[i] = length(v2he_tail) + end + end + + # Remap vertices in hyperedges + for i in eachindex(he2v_tail) + he2v_tail[i] = D(vmap[k] => v for (k, v) in he2v_tail[i]) + he2v_head[i] = D(vmap[k] => v for (k, v) in he2v_head[i]) + end + + rel_vs = sort(collect(keys(vmap))) + + # If relevant, grab `hypergraph_id`s + if isnothing(hg.hypergraph_ids) + unique_hgids = [1] + hypergraph_ids = nothing + else + unique_hgids = sort(collect(Set(hg.hypergraph_ids[rel_vs]))) + for (i, e) in enumerate(unique_hgids) + hgmap[e] = i + end + + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[rel_vs]] + end + + # Construct tail and head hypergraphs + hg_tail = Hypergraph{T, Nothing, Nothing, D}(length(v2he_tail), length(he2v_tail)) + hg_tail.v2he .= v2he_tail + hg_tail.he2v .= he2v_tail + + hg_head = Hypergraph{T, Nothing, Nothing, D}(length(v2he_head), length(he2v_head)) + hg_head.v2he .= v2he_head + hg_head.he2v .= he2v_head + + push!( + res, + HGNNDiHypergraph{T,D}( + hg_tail, + hg_head, + length(v2he_tail), + length(he2v_tail), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, rel_vs), + getobs(hg.hedata, mask), + getobs(hg.hgdata, unique_hgids) + ) + ) + end + + return res +end + +function split_hyperedges( + hg::HGNNDiHypergraph{T, D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_mask) + masks = [train_mask, val_mask, test_mask] + else + masks = [train_mask, test_mask] + end + + hgs = split_hyperedges(hg, masks) + + val_data = isnothing(val_mask) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +function split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_hyperedges)) + end + + split_hyperedges(hg, masks) +end + +function split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_inds) + masks = [ + BitArray(i in train_inds for i in 1:hg.num_hyperedges), + BitArray(i in val_inds for i in 1:hg.num_hyperedges), + BitArray(i in test_inds for i in 1:hg.num_hyperedges) + ] + else + masks = [ + BitArray(i in train_inds for i in 1:hg.num_hyperedges), + BitArray(i in test_inds for i in 1:hg.num_hyperedges) + ] + end + + hgs = split_hyperedges(hg, masks) + + val_data = isnothing(val_inds) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +""" + split_hypergraphs( + hg::HGNNHypergraph{T,D}, + masks::AbstractVector{BitVector} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_hypergraphs( + hg::HGNNHypergraph{T,D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_hypergraphs( + hg::HGNNHypergraph{T,D}, + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + + split_hypergraphs( + hg::HGNNHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Split a single `HGNNHypergraph` into an arbitrary number of `HGNNHypergraph`s by partitioning hypergraphs (from + vertex `hypergraph_id`s). This will also partition vertices and hyperedges. If a vertex's `hypergraph_id` is not + included in the partition, then that vertex and its associated features will not be present in the resulting + `HGNNHypergraph`. If none of the relevant vertices in a partition are incident on a particular hyperedge, then that + hyperedge and its associated features will not be present in the resulting `HGNNHypergraph`. + + Users can provide partitions as `BitVector` masks (length `G`, where `G` is the number of hypergraphs in this + `HGNNHypergraph`) or vectors of indices (which will be converted into masks). To facilitate train-val-test + splits, users can specify which mask/indices correspond to the train set, test set, and (optionally) validation + set. In these cases, the output of `split_vertices` will be a `NamedTuple` with keys "train", "test", and + (optionally) "val" and values of `HGNNHypergraph`s; otherwise, the output is a vector of `HGNNHypergraph`s. +""" +function split_hypergraphs( + hg::HGNNHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + @assert !isnothing(hg.hypergraph_ids) + + @assert all(length.(masks) .== hg.num_hypergraphs) + + res = HGNNHypergraph{T,D}[] + + # Partition v2he and he2v, being careful of indices + for mask in masks + # Determine which vertices will be part of this partition based on `hypergraph_id`s + rel_vs = [i for (i, e) in enumerate(hg.hypergraph_ids) if mask[e]] + + v2he = hg.v2he[rel_vs] + he2v = D[] + + vmap = Dict{Int, Int}(x => i for (i, x) in enumerate(rel_vs)) + hemap = Dict{Int, Int}() + hgmap = Dict{Int, Int}() + index = 1 + for (i, x) in enumerate(mask) + if x + hgmap[i] = index + index += 1 + end + end + + # Remove vertices not in partition from hyperedges + for (i, he) in enumerate(hg.he2v) + newhe = filter(((k,v), ) -> k in rel_vs, he) + if length(newhe) > 0 + newhe = D(vmap[k] => v for (k, v) in newhe) + push!(he2v, newhe) + hemap[i] = length(he2v) + end + end + + # Relabel hyperedges in vertices + for i in eachindex(v2he) + v2he[i] = D(hemap[k] => v for (k, v) in v2he[i]) + end + + hypergraph_ids = [hgmap[hg.hypergraph_ids[v]] for v in rel_vs] + + push!( + res, + HGNNHypergraph{T,D}( + v2he, + he2v, + length(v2he), + length(he2v), + length(keys(hgmap)), + hypergraph_ids, + getobs(hg.vdata, rel_vs), + getobs(hg.hedata, sort(collect(keys(hemap)))), + getobs(hg.hgdata, mask) + ) + ) + end + + return res +end + +function split_hypergraphs( + hg::HGNNHypergraph{T,D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_mask) + masks = [train_mask, val_mask, test_mask] + else + masks = [train_mask, test_mask] + end + + hgs = split_hypergraphs(hg, masks) + + val_data = isnothing(val_mask) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +function split_hypergraphs( + hg::HGNNHypergraph{T,D}, + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) + end + + split_hypergraphs(hg, masks) +end + +function split_hypergraphs( + hg::HGNNHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_inds) + masks = [ + BitArray(i in train_inds for i in 1:hg.num_hypergraphs), + BitArray(i in val_inds for i in 1:hg.num_hypergraphs), + BitArray(i in test_inds for i in 1:hg.num_hypergraphs) + ] + else + masks = [ + BitArray(i in train_inds for i in 1:hg.num_hypergraphs), + BitArray(i in test_inds for i in 1:hg.num_hypergraphs) + ] + end + + hgs = split_hypergraphs(hg, masks) + + val_data = isnothing(val_inds) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +""" + split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + masks::AbstractVector{BitVector} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, + ) where {T <: Real, D <: AbstractDict{Int, T}} + + split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + + split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Split a single `HGNNDiHypergraph` into an arbitrary number of `HGNNDiHypergraph`s by partitioning hypergraphs (from + vertex `hypergraph_id`s). This will also partition vertices and hyperedges. If a vertex's `hypergraph_id` is not + included in the partition, then that vertex and its associated features will not be present in the resulting + `HGNNDiHypergraph`. If none of the relevant vertices in a partition are incident on a particular hyperedge, then + that hyperedge and its associated features will not be present in the resulting `HGNNDiHypergraph`. + + Users can provide partitions as `BitVector` masks (length `G`, where `G` is the number of hypergraphs in this + `HGNNDiHypergraph`) or vectors of indices (which will be converted into masks). To facilitate train-val-test + splits, users can specify which mask/indices correspond to the train set, test set, and (optionally) validation + set. In these cases, the output of `split_vertices` will be a `NamedTuple` with keys "train", "test", and + (optionally) "val" and values of `HGNNDiHypergraph`s; otherwise, the output is a vector of `HGNNDiHypergraph`s. +""" +function split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + @assert !isnothing(hg.hypergraph_ids) + + @assert all(length.(masks) .== hg.num_hypergraphs) + + res = HGNNDiHypergraph{T,D}[] + + for mask in masks + # Determine which vertices are part of this partition based on `hypergraph_id`s + rel_vs = [i for (i, e) in enumerate(hg.hypergraph_ids) if mask[e]] + + v2he_tail = hg.hg_tail.v2he[rel_vs] + v2he_head = hg.hg_head.v2he[rel_vs] + + he2v_tail = D[] + he2v_head = D[] + + vmap = Dict{Int, Int}(x => i for (i, x) in enumerate(rel_vs)) + hemap = Dict{Int, Int}() + hgmap = Dict{Int, Int}() + index = 1 + for (i, x) in enumerate(mask) + if x + hgmap[i] = index + index += 1 + end + end + + # Remove vertices not in partition from hyperedges + for (i, _) in enumerate(hg.hg_tail.he2v) + newhe_tail = filter(((k,v), ) -> k in rel_vs, hg.hg_tail.he2v[i]) + newhe_head = filter(((k,v), ) -> k in rel_vs, hg.hg_head.he2v[i]) + if length(newhe_tail) > 0 || length(newhe_head) > 0 + newhe_tail = D(vmap[k] => v for (k, v) in newhe_tail) + newhe_head = D(vmap[k] => v for (k, v) in newhe_head) + push!(he2v_tail, newhe_tail) + push!(he2v_head, newhe_head) + hemap[i] = length(he2v_tail) + end + end + + # Relabel hyperedges in vertices + for i in eachindex(v2he_tail) + v2he_tail[i] = D(hemap[k] => v for (k, v) in v2he_tail[i]) + v2he_head[i] = D(hemap[k] => v for (k, v) in v2he_head[i]) + end + + hypergraph_ids = [hgmap[hg.hypergraph_ids[v]] for v in rel_vs] + + # Construct tail and head hypergraphs + hg_tail = Hypergraph{T, Nothing, Nothing, D}(length(v2he_tail), length(he2v_tail)) + hg_tail.v2he .= v2he_tail + hg_tail.he2v .= he2v_tail + + hg_head = Hypergraph{T, Nothing, Nothing, D}(length(v2he_head), length(he2v_head)) + hg_head.v2he .= v2he_head + hg_head.he2v .= he2v_head + + push!( + res, + HGNNDiHypergraph{T,D}( + hg_tail, + hg_head, + length(v2he_tail), + length(he2v_tail), + length(keys(hgmap)), + hypergraph_ids, + getobs(hg.vdata, rel_vs), + getobs(hg.hedata, sort(collect(keys(hemap)))), + getobs(hg.hgdata, mask) + ) + ) + end + + return res +end + +function split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_mask) + masks = [train_mask, val_mask, test_mask] + else + masks = [train_mask, test_mask] + end + + hgs = split_hypergraphs(hg, masks) + + val_data = isnothing(val_mask) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +function split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) + end + + split_hypergraphs(hg, masks) +end + +function split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + train_inds::AbstractVector{Int}, + test_inds::AbstractVector{Int}; + val_inds::Union{AbstractVector{Int}, Nothing} = nothing +) where {T <: Real, D <: AbstractDict{Int, T}} + if !isnothing(val_inds) + masks = [ + BitArray(i in train_inds for i in 1:hg.num_hypergraphs), + BitArray(i in val_inds for i in 1:hg.num_hypergraphs), + BitArray(i in test_inds for i in 1:hg.num_hypergraphs) + ] + else + masks = [ + BitArray(i in train_inds for i in 1:hg.num_hypergraphs), + BitArray(i in test_inds for i in 1:hg.num_hypergraphs) + ] + end + + hgs = split_hypergraphs(hg, masks) + + val_data = isnothing(val_inds) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end + +""" + random_split_vertices( + hg::HGNNHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + random_split_vertices( + hg::HGNNDiHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Randomly partition a hypergraph `hg` by dividing the vertices (see `split_vertices`). Users provide the (relative) + sizes of the partitions via `fracs`, a vector of real numbers. The sum of the fractions must equal 1, and all + fractions must be between 0 and 1. Users must additionally provide a random number generator (`rng`). +""" +function random_split_vertices( + hg::HGNNHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG +) where {T <: Real, D <: AbstractDict{Int, T}} + # For all f ∈ fracs, 0 < f <= 1 + @assert all(fracs .> 0) && all(fracs .<= 1) + # Fractions must sum to 1 + @assert abs(sum(fracs) - 1) <= 1e-5 + + num_choices = convert.(Int, round.(fracs .* hg.num_vertices)) + rand_inds = shuffle(rng, Vector{Int}(1:hg.num_vertices)) + + masks = BitVector[] + start_point = 1 + + # Provide the (approximate) right amount of (randomly selected) vertex indices per partition + for i in 1:(length(num_choices) - 1) + part = rand_inds[start_point:(start_point + num_choices[i] - 1)] + start_point += num_choices[i] + push!(masks, BitArray(i in part for i in 1:hg.num_vertices)) + end + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices)) + + split_vertices(hg, masks) +end + +function random_split_vertices( + hg::HGNNDiHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG +) where {T <: Real, D <: AbstractDict{Int, T}} + # For all f ∈ fracs, 0 < f <= 1 + @assert all(fracs .> 0) && all(fracs .<= 1) + # Fractions must sum to 1 + @assert abs(sum(fracs) - 1) <= 1e-5 + + num_choices = convert.(Int, round.(fracs .* hg.num_vertices)) + rand_inds = shuffle(rng, Vector{Int}(1:hg.num_vertices)) + + masks = BitVector[] + start_point = 1 + + # Provide the (approximate) right amount of (randomly selected) vertex indices per partition + for i in 1:(length(num_choices) - 1) + part = rand_inds[start_point:(start_point + num_choices[i] - 1)] + start_point += num_choices[i] + push!(masks, BitArray(i in part for i in 1:hg.num_vertices)) + end + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices)) + + split_vertices(hg, masks) +end + +""" + random_split_hyperedges( + hg::HGNNHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + random_split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Randomly partition a hypergraph `hg` by dividing the hyperedges (see `split_hyperedges`). Users provide the + (relative) sizes of the partitions via `fracs`, a vector of real numbers. The sum of the fractions must equal 1, + and all fractions must be between 0 and 1. Users must additionally provide a random number generator (`rng`). +""" +function random_split_hyperedges( + hg::HGNNHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG +) where {T <: Real, D <: AbstractDict{Int, T}} + # For all f ∈ fracs, 0 < f <= 1 + @assert all(fracs .> 0) && all(fracs .<= 1) + # Fractions must sum to 1 + @assert abs(sum(fracs) - 1) <= 1e-5 + + num_choices = convert.(Int, round.(fracs .* hg.num_hyperedges)) + rand_inds = shuffle(rng, Vector{Int}(1:hg.num_hyperedges)) + + masks = BitVector[] + start_point = 1 + + # Provide the (approximate) right amount of (randomly selected) vertex indices per partition + for i in 1:(length(num_choices) - 1) + part = rand_inds[start_point:(start_point + num_choices[i] - 1)] + start_point += num_choices[i] + push!(masks, BitArray(i in part for i in 1:hg.num_hyperedges)) + end + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:hg.num_hyperedges)) + + split_hyperedges(hg, masks) +end + +function random_split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG +) where {T <: Real, D <: AbstractDict{Int, T}} + # For all f ∈ fracs, 0 < f <= 1 + @assert all(fracs .> 0) && all(fracs .<= 1) + # Fractions must sum to 1 + @assert abs(sum(fracs) - 1) <= 1e-5 + + num_choices = convert.(Int, round.(fracs .* hg.num_hyperedges)) + rand_inds = shuffle(rng, Vector{Int}(1:hg.num_hyperedges)) + + masks = BitVector[] + start_point = 1 + + # Provide the (approximate) right amount of (randomly selected) vertex indices per partition + for i in 1:(length(num_choices) - 1) + part = rand_inds[start_point:(start_point + num_choices[i] - 1)] + start_point += num_choices[i] + push!(masks, BitArray(i in part for i in 1:hg.num_hyperedges)) + end + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:hg.num_hyperedges)) + + split_hyperedges(hg, masks) +end + +""" + random_split_hypergraphs( + hg::HGNNHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + random_split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Randomly partition a hypergraph `hg` by dividing the (sub)-hypergraphs (see `split_hypergraphs`). Users provide the + (relative) sizes of the partitions via `fracs`, a vector of real numbers. The sum of the fractions must equal 1, + and all fractions must be between 0 and 1. Users must additionally provide a random number generator (`rng`). +""" +function random_split_hypergraphs( + hg::HGNNHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG +) where {T <: Real, D <: AbstractDict{Int, T}} + # For all f ∈ fracs, 0 < f <= 1 + @assert all(fracs .> 0) && all(fracs .<= 1) + # Fractions must sum to 1 + @assert abs(sum(fracs) - 1) <= 1e-5 + + num_choices = convert.(Int, round.(fracs .* hg.num_hypergraphs)) + rand_inds = shuffle(rng, Vector{Int}(1:hg.num_hypergraphs)) + + masks = BitVector[] + start_point = 1 + + # Provide the (approximate) right amount of (randomly selected) vertex indices per partition + for i in 1:(length(num_choices) - 1) + part = rand_inds[start_point:(start_point + num_choices[i] - 1)] + start_point += num_choices[i] + push!(masks, BitArray(i in part for i in 1:hg.num_hypergraphs)) + end + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:hg.num_hypergraphs)) + + split_hypergraphs(hg, masks) +end + +function random_split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG +) where {T <: Real, D <: AbstractDict{Int, T}} + # For all f ∈ fracs, 0 < f <= 1 + @assert all(fracs .> 0) && all(fracs .<= 1) + # Fractions must sum to 1 + @assert abs(sum(fracs) - 1) <= 1e-5 + + num_choices = convert.(Int, round.(fracs .* hg.num_hypergraphs)) + rand_inds = shuffle(rng, Vector{Int}(1:hg.num_hypergraphs)) + + masks = BitVector[] + start_point = 1 + + # Provide the (approximate) right amount of (randomly selected) vertex indices per partition + for i in 1:(length(num_choices) - 1) + part = rand_inds[start_point:(start_point + num_choices[i] - 1)] + start_point += num_choices[i] + push!(masks, BitArray(i in part for i in 1:hg.num_hypergraphs)) + end + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:hg.num_hypergraphs)) + + split_hypergraphs(hg, masks) +end \ No newline at end of file diff --git a/src/core/transform.jl b/src/core/transform.jl index ce22a46..9a91413 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1,4 +1,3 @@ - """ add_self_loops( hg::HGNNHypergraph{T, D}; @@ -307,8 +306,27 @@ function to_undirected(hg::HGNNDiHypergraph{T,D}) where {T <: Real, D <: Abstrac ) end -#TODO: docstrings -function combine_hypergraphs(hg1::HGNNHypergraph{T, D}, hg2::HGNNHypergraph{T, D}) where {T <: Real, D <: AbstractDict{Int, T}} +""" + combine_hypergraphs( + hg1::HGNNHypergraph{T, D}, + hg2::HGNNHypergraph{T, D} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + combine_hypergraphs( + hg1::HGNNHypergraph{T,D}, + hgothers::HGNNHypergraph{T,D}... + ) where {T <: Real, D <: AbstractDict{Int, T}} + + combine_hypergraphs( + hgs::AbstractVector{HGNNHypergraph{T,D}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Merge two (or more) undirected hypergraphs into a single hypergraph. This is used for batching (`MLUtils.batch`). +""" +function combine_hypergraphs( + hg1::HGNNHypergraph{T, D}, + hg2::HGNNHypergraph{T, D} +) where {T <: Real, D <: AbstractDict{Int, T}} num_vertices = hg1.num_vertices + hg2.num_vertices num_hyperedges = hg1.num_hyperedges + hg2.num_hyperedges num_hypergraphs = hg1.num_hypergraphs + hg2.num_hypergraphs @@ -358,7 +376,10 @@ function combine_hypergraphs(hg1::HGNNHypergraph{T, D}, hg2::HGNNHypergraph{T, D ) end -function combine_hypergraphs(hg1::HGNNHypergraph{T,D}, hgothers::HGNNHypergraph{T,D}...) where {T <: Real, D <: AbstractDict{Int, T}} +function combine_hypergraphs( + hg1::HGNNHypergraph{T,D}, + hgothers::HGNNHypergraph{T,D}... +) where {T <: Real, D <: AbstractDict{Int, T}} hg = hg1 for hgo in hgothers @@ -368,7 +389,9 @@ function combine_hypergraphs(hg1::HGNNHypergraph{T,D}, hgothers::HGNNHypergraph{ hg end -function combine_hypergraphs(hgs::AbstractVector{HGNNHypergraph{T,D}}) where {T <: Real, D <: AbstractDict{Int, T}} +function combine_hypergraphs( + hgs::AbstractVector{HGNNHypergraph{T,D}} +) where {T <: Real, D <: AbstractDict{Int, T}} num_vs = [hg.num_vertices for hg in hgs] num_hes = [hg.num_hyperedges for hg in hgs] num_hgs = [hg.num_hypergraphs for hg in hgs] @@ -419,7 +442,27 @@ function combine_hypergraphs(hgs::AbstractVector{HGNNHypergraph{T,D}}) where {T ) end -function combine_hypergraphs(hg1::HGNNDiHypergraph{T, D}, hg2::HGNNDiHypergraph{T, D}) where {T <: Real, D <: AbstractDict{Int, T}} +""" + combine_hypergraphs( + hg1::HGNNDiHypergraph{T, D}, + hg2::HGNNDiHypergraph{T, D} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + combine_hypergraphs( + hg1::HGNNDiHypergraph{T,D}, + hgothers::HGNNDiHypergraph{T,D}... + ) where {T <: Real, D <: AbstractDict{Int, T}} + + combine_hypergraphs( + hgs::AbstractVector{HGNNDiHypergraph{T,D}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Merge two (or more) directed hypergraphs into a single hypergraph. This is used for batching (`MLUtils.batch`). +""" +function combine_hypergraphs( + hg1::HGNNDiHypergraph{T, D}, + hg2::HGNNDiHypergraph{T, D} +) where {T <: Real, D <: AbstractDict{Int, T}} num_vertices = hg1.num_vertices + hg2.num_vertices num_hyperedges = hg1.num_hyperedges + hg2.num_hyperedges num_hypergraphs = hg1.num_hypergraphs + hg2.num_hypergraphs @@ -487,7 +530,10 @@ function combine_hypergraphs(hg1::HGNNDiHypergraph{T, D}, hg2::HGNNDiHypergraph{ ) end -function combine_hypergraphs(hg1::HGNNDiHypergraph{T,D}, hgothers::HGNNDiHypergraph{T,D}...) where {T <: Real, D <: AbstractDict{Int, T}} +function combine_hypergraphs( + hg1::HGNNDiHypergraph{T,D}, + hgothers::HGNNDiHypergraph{T,D}... +) where {T <: Real, D <: AbstractDict{Int, T}} hg = hg1 for hgo in hgothers @@ -497,7 +543,9 @@ function combine_hypergraphs(hg1::HGNNDiHypergraph{T,D}, hgothers::HGNNDiHypergr hg end -function combine_hypergraphs(hgs::AbstractVector{HGNNDiHypergraph{T,D}}) where {T <: Real, D <: AbstractDict{Int, T}} +function combine_hypergraphs( + hgs::AbstractVector{HGNNDiHypergraph{T,D}} +) where {T <: Real, D <: AbstractDict{Int, T}} num_vs = [hg.num_vertices for hg in hgs] num_hes = [hg.num_hyperedges for hg in hgs] num_hgs = [hg.num_hypergraphs for hg in hgs] @@ -576,7 +624,16 @@ function MLUtils.batch(hgs::AbstractVector{HGNNDiHypergraph{T,D}}) where {T <: R combine_hypergraphs(hgs) end -get_hypergraph(hg::HGNNHypergraph, i::Int; kws...) = getgraph(hg, [i]; kws...) +""" + get_hypergraph(hg::HGNNHypergraph, i::Int; kws...) + + get_hypergraph(hg::HGNNHypergraph, i::AbstractVector{Int}; map_vertices::Bool = false) + + Extract one or more hypergraphs (based on `hypergraph_ids`) from an `HGNNHypergraph` containing multiple + hypergraphs. This is used for unbatching (`MLUtils.unbatch`). + +""" +get_hypergraph(hg::HGNNHypergraph, i::Int; kws...) = get_hypergraph(hg, [i]; kws...) function get_hypergraph(hg::HGNNHypergraph, i::AbstractVector{Int}; map_vertices::Bool = false) if hg.hypergraph_ids === nothing @@ -646,7 +703,15 @@ function MLUtils.unbatch(hg::HGNNHypergraph) return [get_hypergraph(hg, i) for i in 1:(hg.num_hypergraphs)] end -get_hypergraph(hg::HGNNDiHypergraph, i::Int; kws...) = getgraph(hg, [i]; kws...) +""" + get_hypergraph(hg::HGNNDiHypergraph, i::Int; kws...) + + get_hypergraph(hg::HGNNDiHypergraph, i::AbstractVector{Int}; map_vertices::Bool = false) + + Extract one or more hypergraphs (based on `hypergraph_ids`) from an `HGNNHypergraph` containing multiple + hypergraphs. This is used for unbatching (`MLUtils.unbatch`). +""" +get_hypergraph(hg::HGNNDiHypergraph, i::Int; kws...) = get_hypergraph(hg, [i]; kws...) function get_hypergraph(hg::HGNNDiHypergraph, i::AbstractVector{Int}; map_vertices::Bool = false) if hg.hypergraph_ids === nothing @@ -738,12 +803,37 @@ function MLUtils.unbatch(hg::HGNNDiHypergraph) end +""" + abstract type AbstractNegativeSamplingStrategy + struct UniformSample <: AbstractNegativeSamplingStrategy + struct SizedSample <: AbstractNegativeSamplingStrategy + struct MotifSample <: AbstractNegativeSamplingStrategy + struct CliqueSample <: AbstractNegativeSamplingStrategy + + Types for negative sampling of hyperedges in hypergraphs (see `negative_sample_hyperedge`). +""" abstract type AbstractNegativeSamplingStrategy end struct UniformSample <: AbstractNegativeSamplingStrategy end struct SizedSample <: AbstractNegativeSamplingStrategy end struct MotifSample <: AbstractNegativeSamplingStrategy end struct CliqueSample <: AbstractNegativeSamplingStrategy end +""" + uniform_negative_sample( + hg::HGNNHypergraph{T, D}, + n::Int, + rng::AbstractRNG; + max_trials::Int = 10 + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Sample `n` negative hyperedges in an undirected hypergraph `hg` using a random number generator `rng` + and a uniform sampling strategy. + + It is possible that any given hyperedge generated using uniform sampling may be a duplicate or may not be a + negative hyperedge (i.e., it may exist in the hyperedge set of `hg`). To minimize this possibility, the algorithm + will attempt to generate negative hyperedges `max_trials` number of times (default: 10). However, it is still + possible that less than `n` unique negative hyperedges will be produced. +""" function uniform_negative_sample( hg::HGNNHypergraph{T, D}, n::Int, @@ -779,6 +869,22 @@ function uniform_negative_sample( end +""" + uniform_negative_sample( + hg::HGNNDiHypergraph{T, D}, + n::Int, + rng::AbstractRNG; + max_trials::Int = 10 + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Sample `n` negative hyperedges in a directed hypergraph `hg` using a random number generator `rng` + and a uniform sampling strategy. + + It is possible that any given hyperedge generated using uniform sampling may be a duplicate or may not be a + negative hyperedge (i.e., it may exist in the hyperedge set of `hg`). To minimize this possibility, the algorithm + will attempt to generate negative hyperedges `max_trials` number of times (default: 10). However, it is still + possible that less than `n` unique negative hyperedges will be produced. +""" function uniform_negative_sample( hg::HGNNDiHypergraph{T, D}, n::Int, @@ -824,7 +930,23 @@ function uniform_negative_sample( return HGNNDiHypergraph(DirectedHypergraph(hg_tail, hg_head)) end - +""" + sized_negative_sample( + hg::HGNNHypergraph{T, D}, + n::Int, + rng::AbstractRNG; + max_trials::Int = 10 + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Sample `n` negative hyperedges in an undirected hypergraph `hg` using a random number generator `rng` in such a way + as to ensure that the size distribution of negative hyperedges generated approximately matches that of hyperedges + in `hg`. + + It is possible that any given hyperedge generated using sized sampling may be a duplicate or may not be a + negative hyperedge (i.e., it may exist in the hyperedge set of `hg`). To minimize this possibility, the algorithm + will attempt to generate negative hyperedges `max_trials` number of times (default: 10). However, it is still + possible that less than `n` unique negative hyperedges will be produced. +""" function sized_negative_sample( hg::HGNNHypergraph{T, D}, n::Int, @@ -863,6 +985,25 @@ function sized_negative_sample( end +""" + sized_negative_sample( + hg::HGNNDiHypergraph{T, D}, + n::Int, + rng::AbstractRNG; + max_trials::Int = 10 + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Sample `n` negative hyperedges in a directed hypergraph `hg` using a random number generator `rng` in such a way + as to ensure that the size distribution of negative hyperedges generated approximately matches that of hyperedges + in `hg`. Specifically, this algorithm samples hypergraphs with total size `|e| = |e_tail| + |e_head|` based on the + total size distribution in the hyperedge set of `hg` and further samples the tail sizes `|e_tail|` based on the + tail size distribution in the hyperedge set of `hg`. + + It is possible that any given hyperedge generated using sized sampling may be a duplicate or may not be a + negative hyperedge (i.e., it may exist in the hyperedge set of `hg`). To minimize this possibility, the algorithm + will attempt to generate negative hyperedges `max_trials` number of times (default: 10). However, it is still + possible that less than `n` unique negative hyperedges will be produced. +""" function sized_negative_sample( hg::HGNNDiHypergraph{T, D}, n::Int, @@ -918,6 +1059,24 @@ function sized_negative_sample( end +""" + motif_negative_sample( + hg::HGNNHypergraph{T, D}, + n::Int, + rng::AbstractRNG; + max_trials::Int = 10 + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Sample `n` negative hyperedges in an undirected hypergraph `hg` using a random number generator `rng` using the + "motif negative sampling" (MNS) strategy described in Patil et al., "Negative Sampling for Hyperlink Prediction in + Networks", DOI: 10.1007/978-3-030-47436-2_46 (2020). MNS samples in such a way as to approximate the edge-density + distribution of hyperedges in `hg`. + + It is possible that any given hyperedge generated using motif negatiive sampling may be a duplicate or may not be a + negative hyperedge (i.e., it may exist in the hyperedge set of `hg`). To minimize this possibility, the algorithm + will attempt to generate negative hyperedges `max_trials` number of times (default: 10). However, it is still + possible that less than `n` unique negative hyperedges will be produced. +""" function motif_negative_sample( hg::HGNNHypergraph{T, D}, n::Int, @@ -970,7 +1129,26 @@ function motif_negative_sample( return HGNNHypergraph(base_h) end -# TODO: does this have the same nice properties re: edge distribution as the undirected case? +""" + motif_negative_sample( + hg::HGNNHypergraph{T, D}, + n::Int, + rng::AbstractRNG; + max_trials::Int = 10 + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Sample `n` negative hyperedges in a directed hypergraph `hg` using a random number generator `rng` using the + "motif negative sampling" (MNS) strategy described in Patil et al., "Negative Sampling for Hyperlink Prediction in + Networks", DOI: 10.1007/978-3-030-47436-2_46 (2020). MNS samples in such a way as to approximate the edge-density + distribution of hyperedges in `hg`. Note that MNS was not tested on directed hypergraphs in Patil et al., and it + is possible that this algorithm does not produce the same or similar edge-density distributions to the hyperedge + set in `hg`. + + It is possible that any given hyperedge generated using motif negative sampling may be a duplicate or may not be a + negative hyperedge (i.e., it may exist in the hyperedge set of `hg`). To minimize this possibility, the algorithm + will attempt to generate negative hyperedges `max_trials` number of times (default: 10). However, it is still + possible that less than `n` unique negative hyperedges will be produced. +""" function motif_negative_sample( hg::HGNNDiHypergraph{T, D}, n::Int, @@ -1040,7 +1218,24 @@ function motif_negative_sample( return HGNNDiHypergraph(DirectedHypergraph(hg_tail, hg_head)) end -# TODO: you are here +""" + clique_negative_sample( + hg::HGNNHypergraph{T, D}, + n::Int, + rng::AbstractRNG; + max_trials::Int = 10 + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Sample `n` negative hyperedges in an undirected hypergraph `hg` using a random number generator `rng` using the + "clique negative sampling" (CNS) strategy described in Patil et al., "Negative Sampling for Hyperlink Prediction in + Networks", DOI: 10.1007/978-3-030-47436-2_46 (2020). CNS samples negative hyperedges using the clique-expanded + graph of `hg`, ensuring that the edge density of sampled hyperedges will always be 1. + + It is possible that any given hyperedge generated using clique negative sampling may be a duplicate or may not be a + negative hyperedge (i.e., it may exist in the hyperedge set of `hg`). To minimize this possibility, the algorithm + will attempt to generate negative hyperedges `max_trials` number of times (default: 10). However, it is still + possible that less than `n` unique negative hyperedges will be produced. +""" function clique_negative_sample( hg::HGNNHypergraph{T, D}, n::Int, @@ -1098,16 +1293,33 @@ end # TODO: clique only defined for undirected graph # Is there a way to adapt this for dihypergraphs? -# function clique_negative_sample( -# hg::HGNNDiHypergraph{T, D}, -# n::Int, -# rng::AbstractRNG; -# max_trials::Int = 10 -# ) where {T <: Real, D <: AbstractDict{Int, T}} -# end - -function negative_sample_hyperedge(hg::H, n::Int, rng::AbstractRNG, ::S; max_trials::Int = 10) where {H <: AbstractSimpleHypergraph, S <: AbstractNegativeSamplingStrategy} +""" + negative_sample_hyperedge( + hg::H, + n::Int, + rng::AbstractRNG, + ::S; + max_trials::Int = 10 + ) where {H <: AbstractSimpleHypergraph, S <: AbstractNegativeSamplingStrategy} + + Sample `n` negative hyperedges in an undirected hypergraph `hg` using a random number generator `rng` using the + sampling strategy of type `S <: AbstractNegativeSamplingStrategy`. Currently, uniform sampling + (`uniform_negative_sample`), sized negative sampling (`sized_negative_sample`), motif negative sampling + (`motif_negative_sample`), and clique negative sampling (`clique_negative_sample`) are implemented. + + It is possible that any given hyperedge generated using this function may be a duplicate or may not be a + negative hyperedge (i.e., it may exist in the hyperedge set of `hg`). To minimize this possibility, the algorithm + will attempt to generate negative hyperedges `max_trials` number of times (default: 10). However, it is still + possible that less than `n` unique negative hyperedges will be produced. +""" +function negative_sample_hyperedge( + hg::H, + n::Int, + rng::AbstractRNG, + ::S; + max_trials::Int = 10 +) where {H <: AbstractSimpleHypergraph, S <: AbstractNegativeSamplingStrategy} if S <: UniformSample return uniform_negative_sample(hg, n, rng; max_trials=max_trials) elseif S <: SizedSample @@ -1121,7 +1333,33 @@ function negative_sample_hyperedge(hg::H, n::Int, rng::AbstractRNG, ::S; max_tri end end -function negative_sample_hyperedge(hg::H, n::Int, rng::AbstractRNG, ::S; max_trials::Int = 10) where {H <: AbstractDirectedHypergraph, S <: AbstractNegativeSamplingStrategy} +""" + negative_sample_hyperedge( + hg::H, + n::Int, + rng::AbstractRNG, + ::S; + max_trials::Int = 10 + ) where {H <: AbstractDirectedHypergraph, S <: AbstractNegativeSamplingStrategy} + + Sample `n` negative hyperedges in a directed hypergraph `hg` using a random number generator `rng` using the + sampling strategy of type `S <: AbstractNegativeSamplingStrategy`. Currently, uniform sampling + (`uniform_negative_sample`), sized negative sampling (`sized_negative_sample`), and motif negative sampling + (`motif_negative_sample`) are implemented. + + It is possible that any given hyperedge generated using this function may be a duplicate or may not be a + negative hyperedge (i.e., it may exist in the hyperedge set of `hg`). To minimize this possibility, the algorithm + will attempt to generate negative hyperedges `max_trials` number of times (default: 10). However, it is still + possible that less than `n` unique negative hyperedges will be produced. + +""" +function negative_sample_hyperedge( + hg::H, + n::Int, + rng::AbstractRNG, + ::S; + max_trials::Int = 10 +) where {H <: AbstractDirectedHypergraph, S <: AbstractNegativeSamplingStrategy} if S <: UniformSample return uniform_negative_sample(hg, n, rng; max_trials=max_trials) elseif S <: SizedSample @@ -1131,337 +1369,4 @@ function negative_sample_hyperedge(hg::H, n::Int, rng::AbstractRNG, ::S; max_tri else throw("negative_sample not implemented for strategy of type $S; please call a sampling function directly.") end -end - -function random_split_vertices( - hg::HGNNHypergraph{T,D}, - fracs::AbstractVector{<:Real}, - rng::AbstractRNG -) where {T <: Real, D <: AbstractDict{Int, T}} - # For all f ∈ fracs, 0 < f <= 1 - @assert all(fracs .> 0) && all(fracs .<= 1) - # Fractions must sum to 1 - @assert abs(sum(fracs) - 1) <= 1e-5 - - num_choices = round.(fracs .* hg.num_vertices) - rand_inds = shuffle(rng, Vector(1:hg.num_vertices)) - - partitions = Vector{Int}[] - start_point = 1 - - # Provide the (approximate) right amount of (randomly selected) vertex indices per partition - for i in 1:(length(num_choices) - 1) - part = rand_inds[start_point:start_point + num_choices[i] - 1] - start_point += num_choices - push!(partitions, sort(part)) - end - push!(partitions, sort(rand_inds[start_point:end])) - - res = HGNNHypergraph{T,D}[] - - # Partition v2he and he2v, being careful of indices - for part in partitions - v2he = hg.v2he[part] - he2v = D[] - - vmap = Dict{Int, Int}(x => i for (i, x) in enumerate(part)) - hemap = Dict{Int, Int}() - hgmap = Dict{Int, Int}() - - for (i, he) in enumerate(hg.he2v) - newhe = filter(((k,v), ) -> k in part, he) - if length(newhe) > 0 - newhe = D(vmap[k] => v for (k, v) in newhe) - push!(he2v, newhe) - hemap[i] = length(he2v) - end - end - - for i in eachindex(v2he) - v2he[i] = D(hemap[k] => v for (k, v) in v2he[i]) - end - - unique_hgids = sort(collect(Set(hg.hypergraph_ids[part]))) - for (i, e) in enumerate(unique_hgids) - hgmap[e] = i - end - hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[part]] - - push!( - res, - HGNNHypergraph{T,D}( - v2he, - he2v, - length(v2he), - length(he2v), - length(unique_hgids), - hypergraph_ids, - getobs(hg.vdata, part), - getobs(hg.hedata, collect(keys(hemap))), - getobs(hg.hgdata, unique_hgids) - ) - ) - end - - return res -end - -function random_split_vertices( - hg::HGNNDiHypergraph{T,D}, - fracs::AbstractVector{<:Real}, - rng::AbstractRNG -) where {T <: Real, D <: AbstractDict{Int, T}} - # For all f ∈ fracs, 0 < f <= 1 - @assert all(fracs .> 0) && all(fracs .<= 1) - # Fractions must sum to 1 - @assert abs(sum(fracs) - 1) <= 1e-5 - - num_choices = round.(fracs .* hg.num_vertices) - rand_inds = shuffle(rng, Vector(1:hg.num_vertices)) - - partitions = Vector{Int}[] - start_point = 1 - - # Provide the (approximate) right amount of (randomly selected) vertex indices per partition - for i in 1:(length(num_choices) - 1) - part = rand_inds[start_point:start_point + num_choices[i] - 1] - start_point += num_choices - push!(partitions, sort(part)) - end - push!(partitions, sort(rand_inds[start_point:end])) - - res = HGNNDiHypergraph{T,D}[] - - # Partition v2he and he2v, being careful of indices - for part in partitions - v2he_tail = hg.hg_tail.v2he[part] - v2he_head = hg.hg_head.v2he[part] - - # TODO: you are here - he2v_tail = D[] - he2v_head = D[] - - vmap = Dict{Int, Int}(x => i for (i, x) in enumerate(part)) - hemap = Dict{Int, Int}() - hgmap = Dict{Int, Int}() - - for i in 1:hg.num_hyperedges - he_tail = hg.hg_tail.he2v[i] - hg_head = hg.hg_head.he2v[i] - - newhe_tail = filter(((k,v), ) -> k in part, he_tail) - newhe_head = filter(((k,v), ) -> k in part, he_head) - if length(newhe_tail) > 0 || length(newhe_head) > 0 - newhe_tail = D(vmap[k] => v for (k, v) in newhe_tail) - newhe_head = D(vmap[k] => v for (k, v) in newhe_head) - push!(he2v_tail, newhe_tail) - push!(he2v_head, newhe_head) - hemap[i] = length(he2v) - end - end - - for i in eachindex(v2he_tail) - v2he_tail[i] = D(hemap[k] => v for (k, v) in v2he_tail[i]) - v2he_head[i] = D(hemap[k] => v for (k, v) in v2he_head[i]) - end - - unique_hgids = sort(collect(Set(hg.hypergraph_ids[part]))) - for (i, e) in enumerate(unique_hgids) - hgmap[e] = i - end - - hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[part]] - - push!( - res, - HGNNHypergraph{T,D}( - Hypergraph{T, Nothing, Nothing, D}( - v2he_tail, - he2v_tail, - Vector{Nothing}(undef, length(part)), - Vector{Nothing}(undef, length(newhe_tail)) - ), - Hypergraph{T, Nothing, Nothing, D}( - v2he_head, - he2v_head, - Vector{Nothing}(undef, length(part)), - Vector{Nothing}(undef, length(newhe_head)) - ), - length(part), - length(he2v_tail), - length(unique_hgids), - hypergraph_ids, - getobs(hg.vdata, part), - getobs(hg.hedata, collect(keys(hemap))), - getobs(hg.hgdata, unique_hgids) - ) - ) - end - - return res - -end - -function random_split_hyperedges( - hg::HGNNHypergraph{T,D}, - fracs::AbstractVector{<:Real}, - rng::AbstractRNG -) where {T <: Real, D <: AbstractDict{Int, T}} - # For all f ∈ fracs, 0 < f <= 1 - @assert all(fracs .> 0) && all(fracs .<= 1) - # Fractions must sum to 1 - @assert abs(sum(fracs) - 1) <= 1e-5 - - num_choices = round.(fracs .* hg.num_hyperedges) - rand_inds = shuffle(rng, Vector(1:hg.num_hyperedges)) - - partitions = Vector{Int}[] - start_point = 1 - - # Provide the (approximate) right amount of (randomly selected) vertex indices per partition - for i in 1:(length(num_choices) - 1) - part = rand_inds[start_point:start_point + num_choices[i] - 1] - start_point += num_choices - push!(partitions, sort(part)) - end - push!(partitions, sort(rand_inds[start_point:end])) - - res = HGNNHypergraph{T,D}[] - - # Partition v2he and he2v, being careful of indices - for part in partitions - he2v = hg.he2v[part] - v2he = D[] - - hemap = Dict{Int, Int}(x => i for (i, x) in enumerate(part)) - vmap = Dict{Int, Int}() - hgmap = Dict{Int, Int}() - - for (i, v) in enumerate(hg.v2he) - newv = filter(((key,val), ) -> key in part, v) - if length(newv) > 0 - newv = D(hemap[key] => val for (key, val) in newv) - push!(v2he, newv) - vmap[i] = length(v2he) - end - end - - for i in eachindex(he2v) - he2v[i] = D(vmap[key] => val for (key, val) in he2v[i]) - end - - rel_vs = collect(keys(vmap)) - - unique_hgids = sort(collect(Set(hg.hypergraph_ids[rel_vs]))) - for (i, e) in enumerate(unique_hgids) - hgmap[e] = i - end - - hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[rel_vs]] - - push!( - res, - HGNNHypergraph{T,D}( - v2he, - he2v, - length(v2he), - length(he2v), - length(unique_hgids), - hypergraph_ids, - getobs(hg.vdata, rel_vs), - getobs(hg.hedata, part), - getobs(hg.hgdata, unique_hgids) - ) - ) - end - - return res -end - -# TODO: you are here -function random_split_hyperedges( - hg::HGNNDiHypergraph{T,D}, - fracs::AbstractVector{<:Real}, - rng::AbstractRNG -) where {T <: Real, D <: AbstractDict{Int, T}} -end - -function random_split_hypergraphs( - hg::HGNNHypergraph{T,D}, - fracs::AbstractVector{<:Real}, - rng::AbstractRNG -) where {T <: Real, D <: AbstractDict{Int, T}} - # For all f ∈ fracs, 0 < f <= 1 - @assert all(fracs .> 0) && all(fracs .<= 1) - # Fractions must sum to 1 - @assert abs(sum(fracs) - 1) <= 1e-5 - - num_choices = round.(fracs .* hg.num_hypergraphs) - rand_inds = shuffle(rng, Vector(1:hg.num_hypergraphs)) - - partitions = Vector{Int}[] - start_point = 1 - - # Provide the (approximate) right amount of (randomly selected) vertex indices per partition - for i in 1:(length(num_choices) - 1) - part = rand_inds[start_point:start_point + num_choices[i] - 1] - start_point += num_choices - push!(partitions, sort(part)) - end - push!(partitions, sort(rand_inds[start_point:end])) - - res = HGNNHypergraph{T,D}[] - - # Partition v2he and he2v, being careful of indices - for part in partitions - rel_vs = [i for (i, e) in enumerate(hg.hypergraph_ids) if e in part] - - v2he = hg.v2he[rel_vs] - he2v = D[] - - vmap = Dict{Int, Int}(x => i for (i, x) in enumerate(rel_vs)) - hemap = Dict{Int, Int}() - hgmap = Dict{Int, Int}(x => i for (i, x) in enumerate(part)) - - for (i, he) in enumerate(hg.he2v) - newhe = filter(((k,v), ) -> k in rel_vs, he) - if length(newhe) > 0 - newhe = D(vmap[k] => v for (k, v) in newhe) - push!(he2v, newhe) - hemap[i] = length(he2v) - end - end - - for i in eachindex(v2he) - v2he[i] = D(hemap[k] => v for (k, v) in v2he[i]) - end - - hypergraph_ids = [hgmap[hg.hypergraph_ids[v]] for v in rel_vs] - - push!( - res, - HGNNHypergraph{T,D}( - v2he, - he2v, - length(v2he), - length(he2v), - length(part), - hypergraph_ids, - getobs(hg.vdata, rel_vs), - getobs(hg.hedata, collect(keys(hemap))), - getobs(hg.hgdata, part) - ) - ) - end - - return res -end - -function random_split_hypergraphs( - hg::HGNNDiHypergraph{T,D}, - fracs::AbstractVector{<:Real}, - rng::AbstractRNG -) where {T <: Real, D <: AbstractDict{Int, T}} - - -end +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 5ad0a2c..189b935 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,64 +1,91 @@ using Random using StatsBase +using LinearAlgebra using Test using Graphs using GNNGraphs using MLUtils -using HyperGraphNeuralNetworks using SimpleHypergraphs using SimpleDirectedHypergraphs +using HyperGraphNeuralNetworks + +# Necessary for MLDatasets +ENV["DATADEPS_ALWAYS_ACCEPT"] = true + +# Example undirected hypergraph +uh1 = Hypergraph{Float64, Int, String}(11,5) +#1st graph +uh1[1, 1] = 1.0 +uh1[2, 1] = 2.0 +uh1[4, 1] = 4.0 +uh1[2, 2] = 3.0 +uh1[5, 2] = 12.0 +uh1[3, 2] = 0.0 +uh1[4, 3] = 1.0 +uh1[6, 3] = 4.0 +#2nd graph +uh1[7, 4] = 3.5 +uh1[10, 4] = 1.0 +uh1[11, 4] = 4.0 +uh1[8, 5] = 1.0 +uh1[9, 5] = 5.0 +uh1[10, 5] = 7.0 + +uid1 = [1,1,1,1,1,1,2,2,2,2,2] +uhedata1 = [10, 20, 30, 40, 50] + +# Example directed hypergraph +dh1 = DirectedHypergraph{Float64, Int, String}(11,5) +dh1[1,1,1] = 1.0 +dh1[1,2,1] = 2.0 +dh1[2,4,1] = 4.0 +dh1[1,2,2] = 3.0 +dh1[1,5,2] = 12.0 +dh1[2,3,2] = 0.0 +dh1[1,4,3] = 1.0 +dh1[2,6,3] = 4.0 +#2nd graph +dh1[1,7,4] = 3.5 +dh1[1,10,4] = 1.0 +dh1[2,11,4] = 4.0 +dh1[2,8,5] = 1.0 +dh1[2,9,5] = 5.0 +dh1[1,10,5] = 7.0 +did1 = [1,1,1,1,1,1,2,2,2,2,2] +dhedata1 = [10, 20, 30, 40, 50] + @testset "HyperGraphNeuralNetworks HGNNHypergraph" begin - h1 = Hypergraph{Float64, Int, String}(11,5) - #1st graph - h1[1, 1] = 1.0 - h1[2, 1] = 2.0 - h1[4, 1] = 4.0 - h1[2, 2] = 3.0 - h1[5, 2] = 12.0 - h1[3, 2] = 0.0 - h1[4, 3] = 1.0 - h1[6, 3] = 4.0 - #2nd graph - h1[7, 4] = 3.5 - h1[10, 4] = 1.0 - h1[11, 4] = 4.0 - h1[8, 5] = 1.0 - h1[9, 5] = 5.0 - h1[10, 5] = 7.0 - - id1 = [1,1,1,1,1,1,2,2,2,2,2] - hedata1 = [10, 20, 30, 40, 50] # Direct Construction - HGNN0 = HGNNHypergraph(h1.v2he, h1.he2v, 11, 5, 2, id1, DataStore(), DataStore(), DataStore()) + HGNN0 = HGNNHypergraph(uh1.v2he, uh1.he2v, 11, 5, 2, uid1, DataStore(), DataStore(), DataStore()) @test size(HGNN0) == (11, 5) @test nhv(HGNN0) == 11 @test nhe(HGNN0) == 5 - @test HGNN0.hypergraph_ids == id1 + @test HGNN0.hypergraph_ids == uid1 @test HGNN0.vdata == DataStore() @test HGNN0.hedata == DataStore() @test HGNN0.hgdata == DataStore() # Test type equality - @test HGNN0 == HGNNHypergraph{Float64, Dict{Int, Float64}}(h1.v2he, h1.he2v, 11, 5, 2, id1, DataStore(), DataStore(), DataStore()) + @test HGNN0 == HGNNHypergraph{Float64, Dict{Int, Float64}}(uh1.v2he, uh1.he2v, 11, 5, 2, uid1, DataStore(), DataStore(), DataStore()) # Construct using existing hypergraph - HGNN1 = HGNNHypergraph(h1; hypergraph_ids = id1, hedata = hedata1) + HGNN1 = HGNNHypergraph(uh1; hypergraph_ids = uid1, hedata = uhedata1) @test size(HGNN1) == (11, 5) @test nhv(HGNN1) == 11 @test nhe(HGNN1) == 5 - @test HGNN1.hypergraph_ids == id1 - @test HGNN1.hedata == DataStore(e = hedata1) + @test HGNN1.hypergraph_ids == uid1 + @test HGNN1.hedata == DataStore(e = uhedata1) @test HGNN1.hgdata == DataStore(2) # Test type equality - @test HGNN1 == HGNNHypergraph{Float64}(h1; hypergraph_ids = id1, hedata=hedata1) - @test HGNN1 == HGNNHypergraph{Float64, Dict{Int, Float64}}(h1; hypergraph_ids = id1, hedata=hedata1) + @test HGNN1 == HGNNHypergraph{Float64}(uh1; hypergraph_ids = uid1, hedata=uhedata1) + @test HGNN1 == HGNNHypergraph{Float64, Dict{Int, Float64}}(uh1; hypergraph_ids = uid1, hedata=uhedata1) # Construct using matrix - m = Matrix(h1) - @test m == h1 + m = Matrix(uh1) + @test m == uh1 @test m == [1.0 nothing nothing nothing nothing 2.0 3.0 nothing nothing nothing nothing 0.0 nothing nothing nothing @@ -70,12 +97,12 @@ using SimpleDirectedHypergraphs nothing nothing nothing nothing 5.0 nothing nothing nothing 1.0 7.0 nothing nothing nothing 4.0 nothing] - HGNN2 = HGNNHypergraph(m; hypergraph_ids = id1, hedata = hedata1) + HGNN2 = HGNNHypergraph(m; hypergraph_ids = uid1, hedata = uhedata1) @test HGNN2 == HGNN1 # Test type equality - @test HGNN2 == HGNNHypergraph{Float64}(m; hypergraph_ids = id1, hedata = hedata1) - @test HGNN2 == HGNNHypergraph{Float64, Dict{Int, Float64}}(m; hypergraph_ids = id1, hedata = hedata1) + @test HGNN2 == HGNNHypergraph{Float64}(m; hypergraph_ids = uid1, hedata = uhedata1) + @test HGNN2 == HGNNHypergraph{Float64, Dict{Int, Float64}}(m; hypergraph_ids = uid1, hedata = uhedata1) # Construct with no hypergraph and num_nodes vertices HGNN3 = HGNNHypergraph(3) @@ -254,37 +281,18 @@ end end @testset "HyperGraphNeuralNetworks HGNNDiHypergraph" begin - h1 = DirectedHypergraph{Float64, Int, String}(11,5) - h1[1,1,1] = 1.0 - h1[1,2,1] = 2.0 - h1[2,4,1] = 4.0 - h1[1,2,2] = 3.0 - h1[1,5,2] = 12.0 - h1[2,3,2] = 0.0 - h1[1,4,3] = 1.0 - h1[2,6,3] = 4.0 - #2nd graph - h1[1,7,4] = 3.5 - h1[1,10,4] = 1.0 - h1[2,11,4] = 4.0 - h1[2,8,5] = 1.0 - h1[2,9,5] = 5.0 - h1[1,10,5] = 7.0 - id1 = [1,1,1,1,1,1,2,2,2,2,2] - hedata1 = [10, 20, 30, 40, 50] - #construct using exsiting directedhypergraph - HGNN1 = HGNNDiHypergraph(h1, hypergraph_ids = id1, hedata = hedata1) + HGNN1 = HGNNDiHypergraph(dh1, hypergraph_ids = did1, hedata = dhedata1) @test size(HGNN1) == (11, 5) @test nhv(HGNN1) == 11 @test nhe(HGNN1) == 5 - @test HGNN1.hypergraph_ids == id1 - @test HGNN1.hedata == DataStore(e = hedata1) + @test HGNN1.hypergraph_ids == did1 + @test HGNN1.hedata == DataStore(e = dhedata1) @test HGNN1.hgdata == DataStore(2) #construct using matrix - m = Matrix(h1) - @test m == h1 + m = Matrix(dh1) + @test m == dh1 tailMatrix = getindex.(m, 1) headMatrix = getindex.(m, 2) @test tailMatrix == [1.0 nothing nothing nothing nothing @@ -309,7 +317,7 @@ end nothing nothing nothing nothing 5.0 nothing nothing nothing nothing nothing nothing nothing nothing 4.0 nothing] - HGNN2 = HGNNDiHypergraph(tailMatrix, headMatrix; hypergraph_ids = id1, hedata = hedata1) + HGNN2 = HGNNDiHypergraph(tailMatrix, headMatrix; hypergraph_ids = did1, hedata = dhedata1) @test HGNN2 == HGNN1 #construct with no hypergraph and num_nodes vertices @@ -437,7 +445,7 @@ end @test_throws "Not implemented! Number of hyperedges in HGNNDiHypergraph is fixed." SimpleHypergraphs.remove_hyperedge!(HGNN1, 1) end -@testset "Base function of HGNN Undirected Hypergraph" begin +@testset "HyperGraphNeuralNetworks HGNNDiHypergraph Base functions" begin h = DirectedHypergraph{Float64, Int, String}(2,1) h[1, 1, 1] = 1.0 h[2, 2, 1] = 2.0 @@ -505,3 +513,979 @@ end end +@testset "HyperGraphNeuralNetworks random generation" begin + # Erdos-Renyi random hypergraphs + + # Undirected + Her_un = erdos_renyi_hypergraph(5, 5, HGNNHypergraph) + @test nhv(Her_un) == 5 + @test nhe(Her_un) == 5 + @test all(length.(Her_un.v2he) .> 0) + @test all(length.(Her_un.v2he) .<= 5) + + # With specified seed + Her_un = erdos_renyi_hypergraph(5, 5, HGNNHypergraph; seed=1) + @test Matrix(Her_un) == [ + nothing nothing 1 1 1 + 1 1 1 nothing 1 + nothing 1 1 1 1 + nothing 1 1 nothing 1 + nothing 1 nothing nothing nothing + ] + + # Directed + Her_di = erdos_renyi_hypergraph(5, 5, HGNNDiHypergraph) + @test nhv(Her_un) == 5 + @test nhe(Her_un) == 5 + @test all(length.(Her_di.hg_tail.v2he) .> 0) + @test all(length.(Her_di.hg_head.v2he) .> 0) + @test all(length.(Her_di.hg_tail.v2he) .<= 5) + @test all(length.(Her_di.hg_head.v2he) .<= 5) + + # With specified seed + Her_di = erdos_renyi_hypergraph(5, 5, HGNNDiHypergraph; seed=42) + @test Matrix(Her_di) == [ + (1, 1) (nothing, 1) (nothing, 1) (1, 1) (1, 1) + (nothing, nothing) (1, 1) (nothing, nothing) (1, 1) (1, 1) + (nothing, 1) (1, 1) (1, nothing) (1, 1) (1, nothing) + (nothing, 1) (nothing, 1) (nothing, nothing) (1, 1) (1, 1) + (1, 1) (1, 1) (1, 1) (1, 1) (1, 1) + ] + + # With no self-loops + DHr_nsl = erdos_renyi_hypergraph(5, 5, HGNNDiHypergraph; no_self_loops=true) + for i in 1:5 + @test length(intersect(keys(DHr_nsl.hg_tail.v2he[i]), keys(DHr_nsl.hg_head.v2he[i]))) == 0 + end + + # Random k-uniform hypergraph + + # Undirected + Hk = random_kuniform_hypergraph(5, 5, 3, HGNNHypergraph) + @test nhv(Hk) == 5 + @test nhe(Hk) == 5 + @test all(length.(Hk.he2v) .== 3) + + # With specified seed + Hk = random_kuniform_hypergraph(5, 5, 3, HGNNHypergraph; seed=42) + @test Matrix(Hk) == [ + 1 1 1 1 nothing + nothing nothing 1 1 1 + 1 1 nothing nothing 1 + 1 1 1 1 1 + nothing nothing nothing nothing nothing + ] + + # Directed + DHk = random_kuniform_hypergraph(5, 5, 3, HGNNDiHypergraph) + @test nhv(DHk) == 5 + @test nhe(DHk) == 5 + @test all(length.(DHk.hg_tail.he2v) .+ length.(DHk.hg_head.he2v) .== 3) + + # With specified seed + DHk = random_kuniform_hypergraph(5, 5, 3, HGNNDiHypergraph; seed=42) + @test Matrix(DHk) == [ + (nothing, nothing) (1, nothing) (nothing, nothing) (nothing, 1) (1, nothing) + (1, nothing) (nothing, nothing) (1, nothing) (1, nothing) (nothing, 1) + (1, nothing) (nothing, nothing) (1, nothing) (1, nothing) (nothing, 1) + (nothing, 1) (1, nothing) (nothing, 1) (nothing, nothing) (nothing, nothing) + (nothing, nothing) (1, nothing) (nothing, nothing) (nothing, nothing) (nothing, nothing) + ] + + # Random d-regular hypergraph + + # Undirected + Hd = random_dregular_hypergraph(5, 5, 3, HGNNHypergraph) + @test nhv(Hd) == 5 + @test nhe(Hd) == 5 + @test all(length.(Hd.v2he) .== 3) + + # With specified seed + Hd = random_dregular_hypergraph(5, 5, 3, HGNNHypergraph; seed=42) + @test Matrix(Hd) == [ + 1 nothing 1 1 nothing + 1 nothing 1 1 nothing + 1 1 nothing 1 nothing + 1 1 nothing 1 nothing + nothing 1 1 1 nothing + ] + + # Directed + DHd = random_dregular_hypergraph(5, 5, 3, HGNNDiHypergraph) + @test nhv(DHd) == 5 + @test nhe(DHd) == 5 + @test all(length.(DHd.hg_tail.v2he) .+ length.(DHd.hg_head.v2he) .== 3) + + # With specified seed + DHd = random_dregular_hypergraph(5, 5, 3, HGNNDiHypergraph; seed=42) + @test Matrix(DHd) == [ + (nothing, nothing) (1, nothing) (1, nothing) (nothing, 1) (nothing, nothing) + (1, nothing) (nothing, nothing) (nothing, nothing) (1, nothing) (1, nothing) + (nothing, nothing) (1, nothing) (1, nothing) (nothing, 1) (nothing, nothing) + (nothing, 1) (1, nothing) (1, nothing) (nothing, nothing) (nothing, nothing) + (1, nothing) (nothing, 1) (nothing, 1) (nothing, nothing) (nothing, nothing) + ] + + # Random hypergraph with preferential attachment (undirected only, for now) + + H∂ = random_preferential_hypergraph(20, 0.5, HGNNHypergraph) + @test nhv(H∂) == 20 + + uh2 = Hypergraph{Bool}(5,5) + uh2[1, 1] = true + uh2[2, 1] = true + uh2[4, 1] = true + uh2[2, 2] = true + uh2[5, 2] = true + uh2[4, 3] = true + uh2[2, 3] = true + uh2[2, 4] = true + uh2[4, 4] = true + uh2[5, 4] = true + uh2[4, 5] = true + uh2[5, 5] = true + + # With specified seed + H∂ = random_preferential_hypergraph(8, 0.5, HGNNHypergraph; seed=42, hg=uh2) + @test Matrix(H∂) == [ + 1 nothing nothing nothing nothing nothing nothing 1 nothing nothing + 1 1 1 1 nothing 1 1 1 nothing nothing + nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing + 1 nothing 1 1 1 1 1 1 1 nothing + nothing 1 nothing 1 1 1 1 1 nothing nothing + nothing nothing nothing nothing nothing nothing 1 nothing nothing nothing + nothing nothing nothing nothing nothing nothing nothing nothing 1 nothing + nothing nothing nothing nothing nothing nothing nothing nothing nothing 1 + ] +end + +@testset "HyperGraphNeuralNetworks query" begin + # TODO: do this for directed hypergraphs + + hgnn = HGNNHypergraph(uh1.v2he, uh1.he2v, 11, 5, 2, uid1, DataStore(), DataStore(), DataStore()) + dhgnn = HGNNDiHypergraph( + dh1; + hypergraph_ids = did1, + vdata = rand(Float64, 5, 11), + hedata = rand(Float64, 5, 5), + hgdata = rand(Float64, 5, 2) + ) + + # hyperedge_index + @test hyperedge_index(hgnn) == [ + [1, 2, 4], + [2, 3, 5], + [4, 6], + [7, 10, 11], + [8, 9, 10], + ] + @test hyperedge_index(dhgnn) == ( + [[1, 2], [2, 5], [4], [7, 10], [10]], + [[4], [3], [6], [11], [8, 9]] + ) + + # get_hyperedge_weights + @test get_hyperedge_weights(hgnn) == [ + [1.0, 2.0, 4.0], + [3.0, 0.0, 12.0], + [1.0, 4.0], + [3.5, 1.0, 4.0], + [1.0, 5.0, 7.0] + ] + @test get_hyperedge_weights(hgnn, sum) == [7.0, 15.0, 5.0, 8.5, 13.0] + + dweights = ( + [[1.0, 2.0], [3.0, 12.0], [1.0], [3.5, 1.0], [7.0]], + [[4.0], [0.0], [4.0], [4.0], [1.0, 5.0]] + ) + + @test get_hyperedge_weights(dhgnn) == dweights + @test get_hyperedge_weights(dhgnn; side=:both) == dweights + @test get_hyperedge_weights(dhgnn; side=:tail) == dweights[1] + @test get_hyperedge_weights(dhgnn; side=:head) == dweights[2] + @test get_hyperedge_weights(dhgnn, sum) == ([3.0, 15.0, 1.0, 4.5, 7.0], [4.0, 0.0, 4.0, 4.0, 6.0]) + + # get_hyperedge_weight + @test get_hyperedge_weight(hgnn, 2) == [3.0, 0.0, 12.0] + @test get_hyperedge_weight(hgnn, 2, sum) == 15.0 + + @test get_hyperedge_weight(dhgnn, 2) == ([3.0, 12.0], [0.0]) + @test get_hyperedge_weight(dhgnn, 2; side=:both) == ([3.0, 12.0], [0.0]) + @test get_hyperedge_weight(dhgnn, 2; side=:tail) == [3.0, 12.0] + @test get_hyperedge_weight(dhgnn, 2; side=:head) == [0.0] + @test get_hyperedge_weight(dhgnn, 2, sum) == (15.0, 0.0) + + # has_vertex + @test has_vertex(hgnn, 10) + @test !(has_vertex(hgnn, 12)) + + @test has_vertex(dhgnn, 10) + @test !(has_vertex(dhgnn, 25)) + + # vertices + @test vertices(hgnn) == 1:11 + @test vertices(dhgnn) == 1:11 + + # degree + @test degree(hgnn) == [1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1] + @test degree(hgnn, 2) == 2 + @test degree(hgnn, [1,3,5]) == [1, 1, 1] + + @test degree(dhgnn) == [1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1] + @test degree(dhgnn, 1) == 1 + @test degree(dhgnn, [1,2,3]) == [1, 2, 1] + + # indegree + @test indegree(dhgnn) == [0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1] + @test indegree(dhgnn, 5) == 0 + @test indegree(dhgnn, [1,2,4,8]) == [0, 0, 1, 1] + + # outdegree + @test outdegree(dhgnn) == [1, 2, 0, 1, 1, 0, 1, 0, 0, 2, 0] + @test outdegree(dhgnn, 2) == 2 + @test outdegree(dhgnn, [5, 10]) == [1, 2] + + # all_neighbors + @test all_neighbors(hgnn) == [ + [2, 4], + [1, 3, 4, 5], + [2, 5], + [1, 2, 6], + [2, 3], + [4], + [10, 11], + [9, 10], + [8, 10], + [7, 8, 9, 11], + [7, 10] + ] + @test all_neighbors(hgnn, 2) == [1,3,4,5] + + @test all_neighbors(dhgnn) == [ + [4], + [3, 4], + [2, 5], + [1, 2, 6], + [3], + [4], + [11], + [10], + [10], + [8, 9, 11], + [7, 10] + ] + @test all_neighbors(dhgnn; same_side=true) == [ + [2, 4], + [1, 3, 4, 5], + [2, 5], + [1, 2, 6], + [2, 3], + [4], + [10, 11], + [9, 10], + [8, 10], + [7, 8, 9, 11], + [7, 10] + ] + + @test all_neighbors(dhgnn, 2) == [3, 4] + @test all_neighbors(dhgnn, 2; same_side=true) == [1, 3, 4, 5] + + # inneighbors + @test inneighbors(dhgnn) == [ + [], + [], + [2, 5], + [1, 2], + [], + [4], + [], + [10], + [10], + [], + [7, 10] + ] + @test inneighbors(dhgnn; same_side=true) == [ + [], + [], + [2, 5], + [1, 2], + [], + [4], + [], + [9, 10], + [8, 10], + [], + [7, 10] + ] + + @test inneighbors(dhgnn, 9) == [10] + @test inneighbors(dhgnn, 9; same_side=true) == [8, 10] + + # outneighbors + @test outneighbors(dhgnn) == [ + [4], + [3, 4], + [], + [6], + [3], + [], + [11], + [], + [], + [8, 9, 11], + [] + ] + @test outneighbors(dhgnn; same_side=true) == [ + [2, 4], + [1, 3, 4, 5], + [], + [6], + [2, 3], + [], + [10, 11], + [], + [], + [7, 8, 9, 11], + [] + ] + + @test outneighbors(dhgnn, 2) == [3, 4] + @test outneighbors(dhgnn, 2; same_side=true) == [1, 3, 4, 5] + + # hyperedge_neighbors + @test hyperedge_neighbors(hgnn) == [[2, 3], [1], [1], [5], [4]] + @test hyperedge_neighbors(hgnn, 4) == [5] + + # isolated_vertices + @test length(isolated_vertices(hgnn)) == 0 + @test isolated_vertices(HGNNHypergraph(Hypergraph(5,0))) == [1,2,3,4,5] + + # incidence_matrix + @test incidence_matrix(hgnn) == [ + 1.0 0.0 0.0 0.0 0.0 + 1.0 1.0 0.0 0.0 0.0 + 0.0 1.0 0.0 0.0 0.0 + 1.0 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 0.0 1.0 0.0 + 0.0 0.0 0.0 0.0 1.0 + 0.0 0.0 0.0 0.0 1.0 + 0.0 0.0 0.0 1.0 1.0 + 0.0 0.0 0.0 1.0 0.0 + ] + + inc = incidence_matrix(dhgnn) + @test inc[1] == [ + 1.0 0.0 0.0 0.0 0.0 + 1.0 1.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 1.0 0.0 0.0 + 0.0 1.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 1.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 1.0 1.0 + 0.0 0.0 0.0 0.0 0.0 + ] + @test inc[2] == [ + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 1.0 0.0 0.0 0.0 + 1.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 1.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 1.0 + 0.0 0.0 0.0 0.0 1.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 1.0 0.0 + ] + + # complex_incidence_matrix + @test complex_incidence_matrix(dhgnn) == [ + 0.0-1.0im 0.0-0.0im 0.0-0.0im 0.0-0.0im 0.0-0.0im + 0.0-1.0im 0.0-1.0im 0.0-0.0im 0.0-0.0im 0.0-0.0im + 0.0-0.0im 1.0-0.0im 0.0-0.0im 0.0-0.0im 0.0-0.0im + 1.0-0.0im 0.0-0.0im 0.0-1.0im 0.0-0.0im 0.0-0.0im + 0.0-0.0im 0.0-1.0im 0.0-0.0im 0.0-0.0im 0.0-0.0im + 0.0-0.0im 0.0-0.0im 1.0-0.0im 0.0-0.0im 0.0-0.0im + 0.0-0.0im 0.0-0.0im 0.0-0.0im 0.0-1.0im 0.0-0.0im + 0.0-0.0im 0.0-0.0im 0.0-0.0im 0.0-0.0im 1.0-0.0im + 0.0-0.0im 0.0-0.0im 0.0-0.0im 0.0-0.0im 1.0-0.0im + 0.0-0.0im 0.0-0.0im 0.0-0.0im 0.0-1.0im 0.0-1.0im + 0.0-0.0im 0.0-0.0im 0.0-0.0im 1.0-0.0im 0.0-0.0im + ] + + # vertex_weight_matrix + @test vertex_weight_matrix(hgnn) == Diagonal([1.0, 5.0, 0.0, 5.0, 12.0, 4.0, 3.5, 1.0, 5.0, 8.0, 4.0]) + # Non-standard weighting function + @test vertex_weight_matrix(hgnn; weighting_function=prod) == Diagonal(zeros(11)) + + @test vertex_weight_matrix(dhgnn)[1] == Diagonal([1.0, 5.0, 0.0, 1.0, 12.0, 0.0, 3.5, 0.0, 0.0, 8.0, 0.0]) + @test vertex_weight_matrix(dhgnn; weighting_function=prod)[1] == Diagonal(zeros(11)) + + @test vertex_weight_matrix(dhgnn)[2] == Diagonal([0.0, 0.0, 0.0, 4.0, 0.0, 4.0, 0.0, 1.0, 5.0, 0.0, 4.0]) + @test vertex_weight_matrix(dhgnn; weighting_function=prod)[2] == Diagonal(zeros(11)) + + # hyperedge_weight_matrix + @test hyperedge_weight_matrix(hgnn) == Diagonal([7.0, 15.0, 5.0, 8.5, 13.0]) + # Non-standard weighting function + @test hyperedge_weight_matrix(hgnn; weighting_function=prod) == Diagonal(zeros(5)) + + @test hyperedge_weight_matrix(dhgnn)[1] == Diagonal([3.0, 15.0, 1.0, 4.5, 7.0]) + @test hyperedge_weight_matrix(dhgnn; weighting_function=prod)[1] == Diagonal(zeros(5)) + + @test hyperedge_weight_matrix(dhgnn)[2] == Diagonal([4.0, 0.0, 4.0, 4.0, 6.0]) + @test hyperedge_weight_matrix(dhgnn; weighting_function=prod)[2] == Diagonal(zeros(5)) + + # vertex_degree_matrix + @test vertex_degree_matrix(hgnn) == Diagonal([1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1]) + + @test vertex_degree_matrix(dhgnn)[1] == Diagonal([1, 2, 0, 1, 1, 0, 1, 0, 0, 2, 0]) + @test vertex_degree_matrix(dhgnn)[2] == Diagonal([0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1]) + + # hyperedge_degree_matrix + @test hyperedge_degree_matrix(hgnn) == Diagonal([3, 3, 2, 3, 3]) + + @test hyperedge_degree_matrix(dhgnn)[1] == Diagonal([2, 2, 1, 2, 1]) + @test hyperedge_degree_matrix(dhgnn)[2] == Diagonal([1, 1, 1, 1, 2]) + + # normalized_laplacian + L = normalized_laplacian_matrix(hgnn) + @test size(L) == (11,11) + @test L[1,3] == 0.0 + @test isapprox(L, L'; rtol=1e-5) + + L = normalized_laplacian_matrix(dhgnn) + @test size(L) == (11, 11) + @test L[1,3] == 0.0-0.0im + @test isapprox(L, L'; rtol=1e-5) + + # hypergraph_ids + @test hypergraph_ids(hgnn) == uid1 + @test hypergraph_ids(dhgnn) == did1 + + # Case with no hypergraph_ids + hgnn2 = HGNNHypergraph(uh1.v2he, uh1.he2v, 11, 5, 1, nothing, DataStore(), DataStore(), DataStore()) + @test hypergraph_ids(hgnn2) == ones(11) + + dhgnn2 = HGNNDiHypergraph(dh1; hypergraph_ids = nothing) + @test hypergraph_ids(dhgnn2) == ones(11) + + # has_self_loops + @test !(has_self_loops(hgnn)) + @test !(has_self_loops(dhgnn)) + + # has_multi_hyperedges + @test !(has_multi_hyperedges(hgnn)) + hg3 = Hypergraph{Bool}(2,2) + hg3[:,:] .= true + hgnn3 = HGNNHypergraph(hg3) + @test has_multi_hyperedges(hgnn3) + + @test !(has_multi_hyperedges(dhgnn)) + dhg3 = DirectedHypergraph{Bool}(2, 3) + dhg3.hg_tail[:,:] .= true + dhg3.hg_head[:,:] .= true + dhgnn3 = HGNNDiHypergraph(dhg3) + @test has_multi_hyperedges(dhgnn3) + +end + +@testset "HyperGraphNeuralNetworks transforms" begin + +end + +@testset "HyperGraphNeuralNetworks split vertices" begin + # Split vertices of undirected hypergraphs + hgnn1 = HGNNHypergraph( + uh1; + hypergraph_ids = uid1, + vdata = rand(Float64, 5, 11), + hedata = rand(Float64, 5, 5), + hgdata = rand(Float64, 5, 2) + ) + + vmasks = [ + BitVector((false, true, true, false, true, false, true, false, false, false, true)), + BitVector((false, false, false, true, false, true, false, true, false, true, false)), + BitVector((true, false, false, false, false, false, false, false, true, false, false)) + ] + + # Split vertices using masks + hgnns = split_vertices(hgnn1, vmasks) + @test length(hgnns) == 3 + @test hgnns[1].num_vertices == 5 + @test hgnns[1].num_hyperedges == 3 + @test hgnns[1].num_hypergraphs == 2 + @test getobs(hgnns[1].vdata, 1).x == getobs(hgnn1.vdata, 2).x + @test getobs(hgnns[1].hedata, 1).e == getobs(hgnn1.hedata, 1).e + @test getobs(hgnns[1].hgdata, 1).u == getobs(hgnn1.hgdata, 1).u + @test hgnns[2].num_vertices == 4 + @test hgnns[2].num_hyperedges == 4 + @test hgnns[2].num_hypergraphs == 2 + @test getobs(hgnns[2].vdata, 1).x == getobs(hgnn1.vdata, 4).x + @test getobs(hgnns[2].hedata, 2).e == getobs(hgnn1.hedata, 3).e + @test getobs(hgnns[2].hgdata, 2).u == getobs(hgnn1.hgdata, 2).u + @test hgnns[3].num_vertices == 2 + @test hgnns[3].num_hyperedges == 2 + @test hgnns[3].num_hypergraphs == 2 + @test getobs(hgnns[3].vdata, 2).x == getobs(hgnn1.vdata, 9).x + @test getobs(hgnns[3].hedata, 2).e == getobs(hgnn1.hedata, 5).e + @test getobs(hgnns[3].hgdata, 1).u == getobs(hgnn1.hgdata, 1).u + + # Split vertices by train-val-test labeled masks + hgnns_tvt = split_vertices(hgnn1, vmasks[1], vmasks[3]; val_mask=vmasks[2]) + @test hgnns_tvt.train == hgnns[1] + @test hgnns_tvt.val == hgnns[2] + @test hgnns_tvt.test == hgnns[3] + + # Split without validation set + hgnns_tvt_noval = split_vertices(hgnn1, vmasks[1], vmasks[3]) + @test hgnns_tvt_noval.train == hgnns[1] + @test hgnns_tvt_noval.val === nothing + @test hgnns_tvt_noval.test == hgnns[3] + + vinds = [ + [2, 3, 5, 7, 11], + [4, 6, 8, 10], + [1, 9] + ] + + # Split vertices using vertex indices + hgnns_ind = split_vertices(hgnn1, vinds) + @test length(hgnns_ind) == 3 + @test hgnns_ind[1] == hgnns[1] + @test hgnns_ind[2] == hgnns[2] + @test hgnns_ind[3] == hgnns[3] + + # Split vertices by train-val-test labeled indices + hgnns_ind_tvt = split_vertices(hgnn1, vinds[1], vinds[3]; val_inds=vinds[2]) + @test hgnns_ind_tvt.train == hgnns[1] + @test hgnns_ind_tvt.val == hgnns[2] + @test hgnns_ind_tvt.test == hgnns[3] + + # Split without validation set + hgnns_ind_tvt_noval = split_vertices(hgnn1, vinds[1], vinds[3]) + @test hgnns_ind_tvt_noval.train == hgnns[1] + @test hgnns_ind_tvt_noval.val === nothing + @test hgnns_ind_tvt_noval.test == hgnns[3] + + # "Random" split + rng = Xoshiro(42) + hgnns_rand = random_split_vertices(hgnn1, [0.7, 0.1, 0.2], rng) + @test length(hgnns_rand) == 3 + @test hgnns_rand[1].num_vertices == 8 + @test hgnns_rand[2].num_vertices == 1 + @test hgnns_rand[3].num_vertices == 2 + + # Split vertices of directed hypergraphs + dhgnn1 = HGNNDiHypergraph( + dh1; + hypergraph_ids = did1, + vdata = rand(Float64, 5, 11), + hedata = rand(Float64, 5, 5), + hgdata = rand(Float64, 5, 2) + ) + + vmasks = [ + BitVector((false, true, true, false, true, false, true, false, false, false, true)), + BitVector((false, false, false, true, false, true, false, true, false, true, false)), + BitVector((true, false, false, false, false, false, false, false, true, false, false)) + ] + + # Split vertices using masks + dhgnns = split_vertices(dhgnn1, vmasks) + @test length(dhgnns) == 3 + @test dhgnns[1].num_vertices == 5 + @test dhgnns[1].num_hyperedges == 3 + @test dhgnns[1].num_hypergraphs == 2 + @test getobs(dhgnns[1].vdata, 1).x == getobs(dhgnn1.vdata, 2).x + @test getobs(dhgnns[1].hedata, 1).e == getobs(dhgnn1.hedata, 1).e + @test getobs(dhgnns[1].hgdata, 1).u == getobs(dhgnn1.hgdata, 1).u + @test dhgnns[2].num_vertices == 4 + @test dhgnns[2].num_hyperedges == 4 + @test dhgnns[2].num_hypergraphs == 2 + @test getobs(dhgnns[2].vdata, 1).x == getobs(dhgnn1.vdata, 4).x + @test getobs(dhgnns[2].hedata, 2).e == getobs(dhgnn1.hedata, 3).e + @test getobs(dhgnns[2].hgdata, 2).u == getobs(dhgnn1.hgdata, 2).u + @test dhgnns[3].num_vertices == 2 + @test dhgnns[3].num_hyperedges == 2 + @test dhgnns[3].num_hypergraphs == 2 + @test getobs(dhgnns[3].vdata, 2).x == getobs(dhgnn1.vdata, 9).x + @test getobs(dhgnns[3].hedata, 2).e == getobs(dhgnn1.hedata, 5).e + @test getobs(dhgnns[3].hgdata, 1).u == getobs(dhgnn1.hgdata, 1).u + + # Split vertices by train-val-test labeled masks + dhgnns_tvt = split_vertices(dhgnn1, vmasks[1], vmasks[3]; val_mask=vmasks[2]) + @test dhgnns_tvt.train == dhgnns[1] + @test dhgnns_tvt.val == dhgnns[2] + @test dhgnns_tvt.test == dhgnns[3] + + # Split without validation set + dhgnns_tvt_noval = split_vertices(dhgnn1, vmasks[1], vmasks[3]) + @test dhgnns_tvt_noval.train == dhgnns[1] + @test dhgnns_tvt_noval.val === nothing + @test dhgnns_tvt_noval.test == dhgnns[3] + + vinds = [ + [2, 3, 5, 7, 11], + [4, 6, 8, 10], + [1, 9] + ] + + # Split vertices using vertex indices + dhgnns_ind = split_vertices(dhgnn1, vinds) + @test length(dhgnns_ind) == 3 + @test dhgnns_ind[1] == dhgnns[1] + @test dhgnns_ind[2] == dhgnns[2] + @test dhgnns_ind[3] == dhgnns[3] + + # Split vertices by train-val-test labeled indices + dhgnns_ind_tvt = split_vertices(dhgnn1, vinds[1], vinds[3]; val_inds=vinds[2]) + @test dhgnns_ind_tvt.train == dhgnns[1] + @test dhgnns_ind_tvt.val == dhgnns[2] + @test dhgnns_ind_tvt.test == dhgnns[3] + + # Split without validation set + dhgnns_ind_tvt_noval = split_vertices(dhgnn1, vinds[1], vinds[3]) + @test dhgnns_ind_tvt_noval.train == dhgnns[1] + @test dhgnns_ind_tvt_noval.val === nothing + @test dhgnns_ind_tvt_noval.test == dhgnns[3] + + # "Random" split + rng = Xoshiro(42) + dhgnns_rand = random_split_vertices(dhgnn1, [0.7, 0.1, 0.2], rng) + @test length(dhgnns_rand) == 3 + @test dhgnns_rand[1].num_vertices == 8 + @test dhgnns_rand[2].num_vertices == 1 + @test dhgnns_rand[3].num_vertices == 2 +end + +@testset "HyperGraphNeuralNetworks split hyperedges" begin + # Split hyperedges of undirected hypergraphs + hgnn1 = HGNNHypergraph( + uh1; + hypergraph_ids = uid1, + vdata = rand(Float64, 5, 11), + hedata = rand(Float64, 5, 5), + hgdata = rand(Float64, 5, 2) + ) + + hemasks = [ + BitVector((false, true, true, false, true)), + BitVector((false, false, false, true, false)), + BitVector((true, false, false, false, false)) + ] + + # Split hyperedges using masks + hgnns = split_hyperedges(hgnn1, hemasks) + @test length(hgnns) == 3 + @test hgnns[1].num_vertices == 8 + @test hgnns[1].num_hyperedges == 3 + @test hgnns[1].num_hypergraphs == 2 + @test getobs(hgnns[1].vdata, 1).x == getobs(hgnn1.vdata, 2).x + @test getobs(hgnns[1].hedata, 1).e == getobs(hgnn1.hedata, 2).e + @test getobs(hgnns[1].hgdata, 1).u == getobs(hgnn1.hgdata, 1).u + @test hgnns[2].num_vertices == 3 + @test hgnns[2].num_hyperedges == 1 + @test hgnns[2].num_hypergraphs == 1 + @test getobs(hgnns[2].vdata, 1).x == getobs(hgnn1.vdata, 7).x + @test getobs(hgnns[2].hedata, 1).e == getobs(hgnn1.hedata, 4).e + @test getobs(hgnns[2].hgdata, 1).u == getobs(hgnn1.hgdata, 2).u + @test hgnns[3].num_vertices == 3 + @test hgnns[3].num_hyperedges == 1 + @test hgnns[3].num_hypergraphs == 1 + @test getobs(hgnns[3].vdata, 1).x == getobs(hgnn1.vdata, 1).x + @test getobs(hgnns[3].hedata, 1).e == getobs(hgnn1.hedata, 1).e + @test getobs(hgnns[3].hgdata, 1).u == getobs(hgnn1.hgdata, 1).u + + # Split hyperedges by train-val-test labeled masks + hgnns_tvt = split_hyperedges(hgnn1, hemasks[1], hemasks[3]; val_mask=hemasks[2]) + @test hgnns_tvt.train == hgnns[1] + @test hgnns_tvt.val == hgnns[2] + @test hgnns_tvt.test == hgnns[3] + + # Split without validation set + hgnns_tvt_noval = split_hyperedges(hgnn1, hemasks[1], hemasks[3]) + @test hgnns_tvt_noval.train == hgnns[1] + @test hgnns_tvt_noval.val === nothing + @test hgnns_tvt_noval.test == hgnns[3] + + heinds = [ + [2, 3, 5], + [4], + [1] + ] + + # Split hyperedges using hyperedge indices + hgnns_ind = split_hyperedges(hgnn1, heinds) + @test length(hgnns_ind) == 3 + @test hgnns_ind[1] == hgnns[1] + @test hgnns_ind[2] == hgnns[2] + @test hgnns_ind[3] == hgnns[3] + + # Split hyperedges by train-val-test labeled indices + hgnns_ind_tvt = split_hyperedges(hgnn1, heinds[1], heinds[3]; val_inds=heinds[2]) + @test hgnns_ind_tvt.train == hgnns[1] + @test hgnns_ind_tvt.val == hgnns[2] + @test hgnns_ind_tvt.test == hgnns[3] + + # Split without validation set + hgnns_ind_tvt_noval = split_hyperedges(hgnn1, heinds[1], heinds[3]) + @test hgnns_ind_tvt_noval.train == hgnns[1] + @test hgnns_ind_tvt_noval.val === nothing + @test hgnns_ind_tvt_noval.test == hgnns[3] + + # "Random" split + rng = Xoshiro(42) + hgnns_rand = random_split_hyperedges(hgnn1, [0.7, 0.3], rng) + @test length(hgnns_rand) == 2 + @test hgnns_rand[1].num_hyperedges == 4 + @test hgnns_rand[2].num_hyperedges == 1 + + # Split hyperedges of directed hypergraphs + dhgnn1 = HGNNDiHypergraph( + dh1; + hypergraph_ids = did1, + vdata = rand(Float64, 5, 11), + hedata = rand(Float64, 5, 5), + hgdata = rand(Float64, 5, 2) + ) + + hemasks = [ + BitVector((false, true, true, false, true)), + BitVector((false, false, false, true, false)), + BitVector((true, false, false, false, false)) + ] + + # Split hyperedges using masks + dhgnns = split_hyperedges(dhgnn1, hemasks) + @test length(dhgnns) == 3 + @test dhgnns[1].num_vertices == 8 + @test dhgnns[1].num_hyperedges == 3 + @test dhgnns[1].num_hypergraphs == 2 + @test getobs(dhgnns[1].vdata, 1).x == getobs(dhgnn1.vdata, 2).x + @test getobs(dhgnns[1].hedata, 1).e == getobs(dhgnn1.hedata, 2).e + @test getobs(dhgnns[1].hgdata, 1).u == getobs(dhgnn1.hgdata, 1).u + @test dhgnns[2].num_vertices == 3 + @test dhgnns[2].num_hyperedges == 1 + @test dhgnns[2].num_hypergraphs == 1 + @test getobs(dhgnns[2].vdata, 1).x == getobs(dhgnn1.vdata, 7).x + @test getobs(dhgnns[2].hedata, 1).e == getobs(dhgnn1.hedata, 4).e + @test getobs(dhgnns[2].hgdata, 1).u == getobs(dhgnn1.hgdata, 2).u + @test dhgnns[3].num_vertices == 3 + @test dhgnns[3].num_hyperedges == 1 + @test dhgnns[3].num_hypergraphs == 1 + @test getobs(dhgnns[3].vdata, 1).x == getobs(dhgnn1.vdata, 1).x + @test getobs(dhgnns[3].hedata, 1).e == getobs(dhgnn1.hedata, 1).e + @test getobs(dhgnns[3].hgdata, 1).u == getobs(dhgnn1.hgdata, 1).u + + # Split hyperedges by train-val-test labeled masks + dhgnns_tvt = split_hyperedges(dhgnn1, hemasks[1], hemasks[3]; val_mask=hemasks[2]) + @test dhgnns_tvt.train == dhgnns[1] + @test dhgnns_tvt.val == dhgnns[2] + @test dhgnns_tvt.test == dhgnns[3] + + # Split without validation set + dhgnns_tvt_noval = split_hyperedges(dhgnn1, hemasks[1], hemasks[3]) + @test dhgnns_tvt_noval.train == dhgnns[1] + @test dhgnns_tvt_noval.val === nothing + @test dhgnns_tvt_noval.test == dhgnns[3] + + heinds = [ + [2, 3, 5], + [4], + [1] + ] + + # Split hyperedges using hyperedge indices + dhgnns_ind = split_hyperedges(dhgnn1, heinds) + @test length(dhgnns_ind) == 3 + @test dhgnns_ind[1] == dhgnns[1] + @test dhgnns_ind[2] == dhgnns[2] + @test dhgnns_ind[3] == dhgnns[3] + + # Split hyperedges by train-val-test labeled indices + dhgnns_ind_tvt = split_hyperedges(dhgnn1, heinds[1], heinds[3]; val_inds=heinds[2]) + @test dhgnns_ind_tvt.train == dhgnns[1] + @test dhgnns_ind_tvt.val == dhgnns[2] + @test dhgnns_ind_tvt.test == dhgnns[3] + + # Split without validation set + dhgnns_ind_tvt_noval = split_hyperedges(dhgnn1, heinds[1], heinds[3]) + @test dhgnns_ind_tvt_noval.train == dhgnns[1] + @test dhgnns_ind_tvt_noval.val === nothing + @test dhgnns_ind_tvt_noval.test == dhgnns[3] + + # "Random" split + rng = Xoshiro(42) + dhgnns_rand = random_split_hyperedges(dhgnn1, [0.7, 0.3], rng) + @test length(dhgnns_rand) == 2 + @test dhgnns_rand[1].num_hyperedges == 4 + @test dhgnns_rand[2].num_hyperedges == 1 +end + +@testset "HyperGraphNeuralNetworks split hypergraphs" begin + uid2 = [1,1,1,1,2,2,3,3,3,3,3] + + # Split hypergraphs of undirected hypergraphs + hgnn1 = HGNNHypergraph( + uh1; + hypergraph_ids = uid2, + vdata = rand(Float64, 5, 11), + hedata = rand(Float64, 5, 5), + hgdata = rand(Float64, 5, 3) + ) + + hgmasks = [ + BitVector((true, false, false)), + BitVector((false, true, false)), + BitVector((false, false, true)) + ] + + # Split hypergraphs using masks + hgnns = split_hypergraphs(hgnn1, hgmasks) + @test length(hgnns) == 3 + @test hgnns[1].num_vertices == 4 + @test hgnns[1].num_hyperedges == 3 + @test hgnns[1].num_hypergraphs == 1 + @test getobs(hgnns[1].vdata, 1).x == getobs(hgnn1.vdata, 1).x + @test getobs(hgnns[1].hedata, 2).e == getobs(hgnn1.hedata, 2).e + @test getobs(hgnns[1].hgdata, 1).u == getobs(hgnn1.hgdata, 1).u + @test hgnns[2].num_vertices == 2 + @test hgnns[2].num_hyperedges == 2 + @test hgnns[2].num_hypergraphs == 1 + @test getobs(hgnns[2].vdata, 1).x == getobs(hgnn1.vdata, 5).x + @test getobs(hgnns[2].hedata, 2).e == getobs(hgnn1.hedata, 3).e + @test getobs(hgnns[2].hgdata, 1).u == getobs(hgnn1.hgdata, 2).u + @test hgnns[3].num_vertices == 5 + @test hgnns[3].num_hyperedges == 2 + @test hgnns[3].num_hypergraphs == 1 + @test getobs(hgnns[3].vdata, 1).x == getobs(hgnn1.vdata, 7).x + @test getobs(hgnns[3].hedata, 2).e == getobs(hgnn1.hedata, 5).e + @test getobs(hgnns[3].hgdata, 1).u == getobs(hgnn1.hgdata, 3).u + + # Split hypergraphs by train-val-test labeled masks + hgnns_tvt = split_hypergraphs(hgnn1, hgmasks[1], hgmasks[3]; val_mask=hgmasks[2]) + @test hgnns_tvt.train == hgnns[1] + @test hgnns_tvt.val == hgnns[2] + @test hgnns_tvt.test == hgnns[3] + + # Split without validation set + hgnns_tvt_noval = split_hypergraphs(hgnn1, hgmasks[1], hgmasks[3]) + @test hgnns_tvt_noval.train == hgnns[1] + @test hgnns_tvt_noval.val === nothing + @test hgnns_tvt_noval.test == hgnns[3] + + hginds = [[1], [2], [3]] + + # Split hypergraphs using vertex indices + hgnns_ind = split_hypergraphs(hgnn1, hginds) + @test length(hgnns_ind) == 3 + @test hgnns_ind[1] == hgnns[1] + @test hgnns_ind[2] == hgnns[2] + @test hgnns_ind[3] == hgnns[3] + + # Split hypergraphs by train-val-test labeled indices + hgnns_ind_tvt = split_hypergraphs(hgnn1, hginds[1], hginds[3]; val_inds=hginds[2]) + @test hgnns_ind_tvt.train == hgnns[1] + @test hgnns_ind_tvt.val == hgnns[2] + @test hgnns_ind_tvt.test == hgnns[3] + + # Split without validation set + hgnns_ind_tvt_noval = split_hypergraphs(hgnn1, hginds[1], hginds[3]) + @test hgnns_ind_tvt_noval.train == hgnns[1] + @test hgnns_ind_tvt_noval.val === nothing + @test hgnns_ind_tvt_noval.test == hgnns[3] + + # "Random" split + rng = Xoshiro(42) + hgnns_rand = random_split_hypergraphs(hgnn1, [0.34, 0.33, 0.33], rng) + @test length(hgnns_rand) == 3 + @test hgnns_rand[1].num_hypergraphs == 1 + @test hgnns_rand[2].num_hypergraphs == 1 + @test hgnns_rand[3].num_hypergraphs == 1 + + + # Split hypergraphs of directed hypergraphs + dhgnn1 = HGNNDiHypergraph( + dh1; + hypergraph_ids = uid2, + vdata = rand(Float64, 5, 11), + hedata = rand(Float64, 5, 5), + hgdata = rand(Float64, 5, 3) + ) + + hgmasks = [ + BitVector((true, false, false)), + BitVector((false, true, false)), + BitVector((false, false, true)) + ] + + # Split hypergraphs using masks + dhgnns = split_hypergraphs(dhgnn1, hgmasks) + @test length(dhgnns) == 3 + @test dhgnns[1].num_vertices == 4 + @test dhgnns[1].num_hyperedges == 3 + @test dhgnns[1].num_hypergraphs == 1 + @test getobs(dhgnns[1].vdata, 1).x == getobs(dhgnn1.vdata, 1).x + @test getobs(dhgnns[1].hedata, 2).e == getobs(dhgnn1.hedata, 2).e + @test getobs(dhgnns[1].hgdata, 1).u == getobs(dhgnn1.hgdata, 1).u + @test dhgnns[2].num_vertices == 2 + @test dhgnns[2].num_hyperedges == 2 + @test dhgnns[2].num_hypergraphs == 1 + @test getobs(dhgnns[2].vdata, 1).x == getobs(dhgnn1.vdata, 5).x + @test getobs(dhgnns[2].hedata, 2).e == getobs(dhgnn1.hedata, 3).e + @test getobs(dhgnns[2].hgdata, 1).u == getobs(dhgnn1.hgdata, 2).u + @test dhgnns[3].num_vertices == 5 + @test dhgnns[3].num_hyperedges == 2 + @test dhgnns[3].num_hypergraphs == 1 + @test getobs(dhgnns[3].vdata, 1).x == getobs(dhgnn1.vdata, 7).x + @test getobs(dhgnns[3].hedata, 2).e == getobs(dhgnn1.hedata, 5).e + @test getobs(dhgnns[3].hgdata, 1).u == getobs(dhgnn1.hgdata, 3).u + + # Split hypergraphs by train-val-test labeled masks + dhgnns_tvt = split_hypergraphs(dhgnn1, hgmasks[1], hgmasks[3]; val_mask=hgmasks[2]) + @test dhgnns_tvt.train == dhgnns[1] + @test dhgnns_tvt.val == dhgnns[2] + @test dhgnns_tvt.test == dhgnns[3] + + # Split without validation set + dhgnns_tvt_noval = split_hypergraphs(dhgnn1, hgmasks[1], hgmasks[3]) + @test dhgnns_tvt_noval.train == dhgnns[1] + @test dhgnns_tvt_noval.val === nothing + @test dhgnns_tvt_noval.test == dhgnns[3] + + hginds = [[1], [2], [3]] + + # Split hypergraphs using vertex indices + dhgnns_ind = split_hypergraphs(dhgnn1, hginds) + @test length(dhgnns_ind) == 3 + @test dhgnns_ind[1] == dhgnns[1] + @test dhgnns_ind[2] == dhgnns[2] + @test dhgnns_ind[3] == dhgnns[3] + + # Split hypergraphs by train-val-test labeled indices + dhgnns_ind_tvt = split_hypergraphs(dhgnn1, hginds[1], hginds[3]; val_inds=hginds[2]) + @test dhgnns_ind_tvt.train == dhgnns[1] + @test dhgnns_ind_tvt.val == dhgnns[2] + @test dhgnns_ind_tvt.test == dhgnns[3] + + # Split without validation set + dhgnns_ind_tvt_noval = split_hypergraphs(dhgnn1, hginds[1], hginds[3]) + @test dhgnns_ind_tvt_noval.train == dhgnns[1] + @test dhgnns_ind_tvt_noval.val === nothing + @test dhgnns_ind_tvt_noval.test == dhgnns[3] + + # "Random" split + rng = Xoshiro(42) + dhgnns_rand = random_split_hypergraphs(dhgnn1, [0.34, 0.33, 0.33], rng) + @test length(dhgnns_rand) == 3 + @test dhgnns_rand[1].num_hypergraphs == 1 + @test dhgnns_rand[2].num_hypergraphs == 1 + @test dhgnns_rand[3].num_hypergraphs == 1 +end \ No newline at end of file