From 02d2cf81c2d4338add6c66ede8042de1a9fd1a0d Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 13 Aug 2025 08:57:23 -0400 Subject: [PATCH 01/65] Finished implementing v1! --- src/core/transform.jl | 189 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 179 insertions(+), 10 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index ce22a46..86b4dd9 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1040,7 +1040,6 @@ function motif_negative_sample( return HGNNDiHypergraph(DirectedHypergraph(hg_tail, hg_head)) end -# TODO: you are here function clique_negative_sample( hg::HGNNHypergraph{T, D}, n::Int, @@ -1237,7 +1236,6 @@ function random_split_vertices( v2he_tail = hg.hg_tail.v2he[part] v2he_head = hg.hg_head.v2he[part] - # TODO: you are here he2v_tail = D[] he2v_head = D[] @@ -1247,7 +1245,7 @@ function random_split_vertices( for i in 1:hg.num_hyperedges he_tail = hg.hg_tail.he2v[i] - hg_head = hg.hg_head.he2v[i] + he_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) @@ -1278,15 +1276,15 @@ function random_split_vertices( Hypergraph{T, Nothing, Nothing, D}( v2he_tail, he2v_tail, - Vector{Nothing}(undef, length(part)), - Vector{Nothing}(undef, length(newhe_tail)) - ), + Vector{Nothing}(nothing, length(part)), + Vector{Nothing}(nothing, length(newhe_tail)) + ), Hypergraph{T, Nothing, Nothing, D}( v2he_head, he2v_head, - Vector{Nothing}(undef, length(part)), - Vector{Nothing}(undef, length(newhe_head)) - ), + Vector{Nothing}(nothing, length(part)), + Vector{Nothing}(nothing, length(newhe_head)) + ), length(part), length(he2v_tail), length(unique_hgids), @@ -1378,12 +1376,104 @@ function random_split_hyperedges( 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}} + # 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 = HGNNDiHypergraph{T,D}[] + + # Partition v2he and he2v, being careful of indices + for part in partitions + he2v_tail = hg.hg_tail.he2v[part] + he2v_head = hg.hg_head.he2v[part] + + v2he_tail = D[] + v2he_head = D[] + + vmap = Dict{Int, Int}() + hemap = Dict{Int, Int}(x => i for (i, x) in enumerate(part)) + hgmap = Dict{Int, Int}() + + 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), ) -> k in part, v_tail) + newv_head = filter(((k,v), ) -> k in part, v_head) + if length(newv_tail) > 0 || length(newv_head) > 0 + newv_tail = D(vmap[k] => v for (k, v) in newv_tail) + newv_head = D(vmap[k] => v for (k, v) in newv_head) + push!(v2he_tail, newv_tail) + push!(v2he_tail, newv_head) + vmap[i] = length(v2he) + end + end + + 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 + + unique_hgids = sort(collect(Set(hg.hypergraph_ids[part]))) + for (i, e) in enumerate(unique_hgids) + hgmap[e] = 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}( + Hypergraph{T, Nothing, Nothing, D}( + v2he_tail, + he2v_tail, + Vector{Nothing}(nothing, length(newv_tail)), + Vector{Nothing}(nothing, length(part)) + ), + Hypergraph{T, Nothing, Nothing, D}( + v2he_head, + he2v_head, + Vector{Nothing}(nothing, length(newv_head)), + Vector{Nothing}(nothing, length(part)) + ), + length(v2he_tail), + length(part), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, collect(keys(vmap))), + getobs(hg.hedata, part), + getobs(hg.hgdata, unique_hgids) + ) + ) + end + + return res end function random_split_hypergraphs( @@ -1462,6 +1552,85 @@ function random_split_hypergraphs( 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 = HGNNDiHypergraph{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_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}(x => i for (i, x) in enumerate(part)) + + for i in 1:hg.num_hyperedges + 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) + 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 + + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[rel_vs]] + + push!( + res, + HGNNHypergraph{T,D}( + Hypergraph{T, Nothing, Nothing, D}( + v2he_tail, + he2v_tail, + Vector{Nothing}(nothing, length(newv_tail)), + Vector{Nothing}(nothing, length(part)) + ), + Hypergraph{T, Nothing, Nothing, D}( + v2he_head, + he2v_head, + Vector{Nothing}(nothing, length(newv_head)), + Vector{Nothing}(nothing, length(part)) + ), + length(v2he_tail), + length(he2v_tail), + length(part), + hypergraph_ids, + getobs(hg.vdata, rel_vs), + getobs(hg.hedata, collect(keys(hemap))), + getobs(hg.hgdata, part) + ) + ) + end + return res end From 07c028b977ada71cb7ddde12ae6cf6bf9c98e6a8 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 13 Aug 2025 09:01:18 -0400 Subject: [PATCH 02/65] Update docstrings --- src/core/hypergraphs.jl | 74 +++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/src/core/hypergraphs.jl b/src/core/hypergraphs.jl index 5ba047e..c7a5810 100644 --- a/src/core/hypergraphs.jl +++ b/src/core/hypergraphs.jl @@ -708,8 +708,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 @@ -745,7 +743,6 @@ 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 +750,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 +831,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. @@ -1690,8 +1730,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 From 915feb2e808eb70ef5c3823f204d80ef3b4d5d67 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 13 Aug 2025 09:31:10 -0400 Subject: [PATCH 03/65] Docstrings --- src/core/transform.jl | 276 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 261 insertions(+), 15 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index 86b4dd9..5b66a71 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,6 +1218,24 @@ function motif_negative_sample( return HGNNDiHypergraph(DirectedHypergraph(hg_tail, hg_head)) end +""" + 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, @@ -1105,8 +1301,32 @@ end # ) 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 @@ -1120,7 +1340,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 From fab3dac9cc8be9397645349078f3bd582251f10e Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 13 Aug 2025 09:38:31 -0400 Subject: [PATCH 04/65] Get docstringed! --- src/core/transform.jl | 68 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index 5b66a71..e3048fd 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1378,6 +1378,17 @@ function negative_sample_hyperedge( end end +""" + random_split_vertices( + hg::HGNNHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Randomly decompose an undirected hypergraph `hg` into multiple hypergraphs by randomly splitting vertices using the + random number generator `rng`. Vertices will be split according to the vector of fractions `fracs`. Note that the + sum of the fractions in `fracs` must be equal to 1, and all fractions must be between 0 and 1 (inclusive). +""" function random_split_vertices( hg::HGNNHypergraph{T,D}, fracs::AbstractVector{<:Real}, @@ -1451,6 +1462,17 @@ function random_split_vertices( return res end +""" + random_split_vertices( + hg::HGNNDiHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Randomly decompose a directed hypergraph `hg` into multiple hypergraphs by randomly splitting vertices using the + random number generator `rng`. Vertices will be split according to the vector of fractions `fracs`. Note that the + sum of the fractions in `fracs` must be equal to 1, and all fractions must be between 0 and 1 (inclusive). +""" function random_split_vertices( hg::HGNNDiHypergraph{T,D}, fracs::AbstractVector{<:Real}, @@ -1546,6 +1568,17 @@ function random_split_vertices( end +""" + random_split_hyperedges( + hg::HGNNHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Randomly decompose an undirected hypergraph `hg` into multiple hypergraphs by randomly splitting hyperedges using + the random number generator `rng`. Hyperedges will be split according to the vector of fractions `fracs`. Note that + the sum of the fractions in `fracs` must be equal to 1, and all fractions must be between 0 and 1 (inclusive). +""" function random_split_hyperedges( hg::HGNNHypergraph{T,D}, fracs::AbstractVector{<:Real}, @@ -1622,6 +1655,17 @@ function random_split_hyperedges( return res end +""" + random_split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Randomly decompose a directed hypergraph `hg` into multiple hypergraphs by randomly splitting hyperedges using + the random number generator `rng`. Hyperedges will be split according to the vector of fractions `fracs`. Note that + the sum of the fractions in `fracs` must be equal to 1, and all fractions must be between 0 and 1 (inclusive). +""" function random_split_hyperedges( hg::HGNNDiHypergraph{T,D}, fracs::AbstractVector{<:Real}, @@ -1722,6 +1766,17 @@ function random_split_hyperedges( return res end +""" + random_split_hypergraphs( + hg::HGNNHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Randomly decompose an undirected hypergraph `hg` into multiple hypergraphs by randomly splitting hypergraphs using + the random number generator `rng`. Hyperedges will be split according to the vector of fractions `fracs`. Note that + the sum of the fractions in `fracs` must be equal to 1, and all fractions must be between 0 and 1 (inclusive). +""" function random_split_hypergraphs( hg::HGNNHypergraph{T,D}, fracs::AbstractVector{<:Real}, @@ -1793,6 +1848,17 @@ function random_split_hypergraphs( return res end +""" + random_split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + fracs::AbstractVector{<:Real}, + rng::AbstractRNG + ) where {T <: Real, D <: AbstractDict{Int, T}} + + Randomly decompose a directed hypergraph `hg` into multiple hypergraphs by randomly splitting hypergraphs using + the random number generator `rng`. Hyperedges will be split according to the vector of fractions `fracs`. Note that + the sum of the fractions in `fracs` must be equal to 1, and all fractions must be between 0 and 1 (inclusive). +""" function random_split_hypergraphs( hg::HGNNDiHypergraph{T,D}, fracs::AbstractVector{<:Real}, @@ -1879,4 +1945,4 @@ function random_split_hypergraphs( end return res -end +end \ No newline at end of file From a80aa4c0b7cabefd6c7b21ba81c6e7540bde813e Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 2 Sep 2025 10:32:14 +0100 Subject: [PATCH 05/65] Just catching up --- src/core/hypergraphs.jl | 1 - test/runtests.jl | 22 ++++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/core/hypergraphs.jl b/src/core/hypergraphs.jl index 1a5f2d8..2e2ca45 100644 --- a/src/core/hypergraphs.jl +++ b/src/core/hypergraphs.jl @@ -742,7 +742,6 @@ function Base.getproperty(hg::HGNNHypergraph, s::Symbol) end -# TODO: update docstring """ HGNNDiHypergraph{T<:Real, D<:AbstractDict{Int,T}} <: AbstractHGNNDiHypergraph{Tuple{Union{T, Nothing}, Union{T, Nothing}}} diff --git a/test/runtests.jl b/test/runtests.jl index 5ad0a2c..f695535 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -263,7 +263,7 @@ end h1[2,3,2] = 0.0 h1[1,4,3] = 1.0 h1[2,6,3] = 4.0 - #2nd graph + # 2nd hypergraph h1[1,7,4] = 3.5 h1[1,10,4] = 1.0 h1[2,11,4] = 4.0 @@ -273,7 +273,25 @@ end id1 = [1,1,1,1,1,1,2,2,2,2,2] hedata1 = [10, 20, 30, 40, 50] - #construct using exsiting directedhypergraph + # Direct construction + HGNN0 = HGNNDiHypergraph( + h1.hg_tail, + h1.hg_head, + 11, 5, 2, + id1, + DataStore(), + DataStore(), + DataStore() + ) + @test size(HGNN0) == (11, 5) + @test nhv(HGNN0) == 11 + @test nhe(HGNN0) == 5 + @test HGNN0.hypergraph_ids == id1 + @test HGNN0.vdata == DataStore() + @test HGNN0.hedata == DataStore() + @test HGNN0.hgdata == DataStore() + + # Construct using exsiting directedhypergraph HGNN1 = HGNNDiHypergraph(h1, hypergraph_ids = id1, hedata = hedata1) @test size(HGNN1) == (11, 5) @test nhv(HGNN1) == 11 From 38106c250a3373db9c6f5b0c0dbfc04a8219127e Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 2 Sep 2025 10:51:04 +0100 Subject: [PATCH 06/65] New docstrings in hypergraphs.jl --- src/core/hypergraphs.jl | 196 ++++++++++++++++++++++++++-------------- 1 file changed, 127 insertions(+), 69 deletions(-) diff --git a/src/core/hypergraphs.jl b/src/core/hypergraphs.jl index 2e2ca45..1ff1b90 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), @@ -1357,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) @@ -1599,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 ") From e5859bd6c8005a94072e43c8865750f33d51dcd1 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 2 Sep 2025 10:56:28 +0100 Subject: [PATCH 07/65] Docstrings are done (for now) --- src/core/query.jl | 2 -- src/core/transform.jl | 7 ------- 2 files changed, 9 deletions(-) diff --git a/src/core/query.jl b/src/core/query.jl index aad59fd..af32d98 100644 --- a/src/core/query.jl +++ b/src/core/query.jl @@ -812,8 +812,6 @@ function has_self_loops(hg::H) where {H <: AbstractHGNNDiHypergraph} any([length(intersect(vs_tail[i], vs_head[i])) for i in eachindex(vs_tail)]) end -# has_multi_hyperedges -#TODO: docstring function has_multi_hyperedges(hg::H) where {H <: AbstractHGNNHypergraph} vs = sort!.(collect.(keys.(hg.he2v))) diff --git a/src/core/transform.jl b/src/core/transform.jl index e3048fd..dccb4fc 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1293,13 +1293,6 @@ 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 """ negative_sample_hyperedge( From 004bdc1c8b8996deb621a4fa1832089ed7665641 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 4 Sep 2025 13:25:40 +0100 Subject: [PATCH 08/65] Minor work on tests --- src/core/generate.jl | 16 +++++++------- test/runtests.jl | 51 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/core/generate.jl b/src/core/generate.jl index 8c0e5e8..bb8a1e7 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( @@ -218,7 +218,7 @@ function random_preferential_hypergraph( kws... ) where {HI<:AbstractSimpleHypergraph, HO<:AbstractHGNNHypergraph} if seed >= 0 - seed!(seed) + Random.seed!(seed) end pref_hg = random_preferential_model( diff --git a/test/runtests.jl b/test/runtests.jl index f695535..2fc756c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -254,7 +254,7 @@ end end @testset "HyperGraphNeuralNetworks HGNNDiHypergraph" begin - h1 = DirectedHypergraph{Float64, Int, String}(11,5) + h1 = DirectedHypergraph{Float64, Nothing, Nothing}(11,5) h1[1,1,1] = 1.0 h1[1,2,1] = 2.0 h1[2,4,1] = 4.0 @@ -455,7 +455,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 @@ -523,3 +523,50 @@ 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) + ] +end + +@testset "HyperGraphNeuralNetworks query" begin + +end + +@testset "HyperGraphNeuralNetworks transforms" begin + +end \ No newline at end of file From f5d0044715dfb0f87a829682874d1378469a0c90 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 9 Sep 2025 10:39:46 +0100 Subject: [PATCH 09/65] WIP - adding utility functions for common datasets --- Project.toml | 6 ++-- src/HyperGraphNeuralNetworks.jl | 7 +++- src/core/transform.jl | 57 +++++++++++++++++++++++++++++++++ src/datasets/datasets.jl | 50 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 src/datasets/datasets.jl diff --git a/Project.toml b/Project.toml index d9e628a..7846324 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ GNNGraphs = "aed8fd31-079b-4b5a-b342-a13352159b8c" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" Lux = "b2108857-7c20-44ae-9111-449ecde12c47" +MLDatasets = "eb30cadb-4394-5ae3-aed4-317e484a6458" MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54" NNlib = "872c559c-99b0-510c-b3b7-b6c96a88d5cd" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -22,7 +23,8 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] -julia = ">= 1.10.0" +MLDatasets = "0.7.18" 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..d1e83f8 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 InvertedIndices using Graphs using GNNGraphs import GNNGraphs: getn, getdata, normalize_graphdata, cat_features, shortsummary using MLUtils -using InvertedIndices +import MLDatasets using SimpleHypergraphs using SimpleDirectedHypergraphs @@ -45,4 +46,8 @@ include("core/utils.jl") export check_num_vertices, check_num_hyperedges export normalize_graphdata +include("datasets/datasets.jl") + +export getHyperCora, getHyperCiteSeer + end diff --git a/src/core/transform.jl b/src/core/transform.jl index ce22a46..9d4710d 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1133,6 +1133,63 @@ function negative_sample_hyperedge(hg::H, n::Int, rng::AbstractRNG, ::S; max_tri end end +function split_vertices( + hg::HGNNHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + # TODO: this +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{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + masks = BitVector[] + + for indgroup in index_groups + + end +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}} + +end + +function split_vertices() end + +function split_hyperedges() end + +function split_hyperedges() end + +function split_hypergraphs() end + +function split_hypergraphs() end + function random_split_vertices( hg::HGNNHypergraph{T,D}, fracs::AbstractVector{<:Real}, diff --git a/src/datasets/datasets.jl b/src/datasets/datasets.jl new file mode 100644 index 0000000..5e6d53e --- /dev/null +++ b/src/datasets/datasets.jl @@ -0,0 +1,50 @@ +function _convert_citation_graphdata( + dset::AbstractDataset, + T::Type{R}; + split::Bool = false +) + nv = dset[1].num_nodes + + # Hypergraph vertices are graph vertices + hg = Hypergraph{T}(nv, nv) + + # Include vertices in hyperedges + # Datasets are symmetric, so if (s, d) is an edge, (d, s) is also an edge + for (s, d) in zip(dset[1].edge_index...) + hg[s, d] = convert(T, 1) + end + + # Construct HGNN Hypergraph + # Citation datasets have vertex features but no hyperedge- or hypergraph-level features + hgnnhypergraph = HGNNHypergraph{T}( + hg; + vdata=(features=dset[1].node_data.features, targets=dset[1].node_data.targets) + ) + + if !split + return hgnnhypergraph + else + # Cora comes with default train-val-test splits + return split_vertices( + hgnnhypergraph, + dset[1].node_data.train_mask, + dset[1].node_data.test_mask; + val_mask=dset[1].node_data.val_mask + ) + end + +end + +function getHyperCora( + T::Type{R}; + split::Bool = false +) where {R<:Real} + _convert_citation_graphdata(MLDatasets.Cora(), T; split=split) +end + +function getHyperCiteSeer( + T::Type{R}; + split::Bool = false +) + _convert_citation_graphdata(MLDatasets.CiteSeer(), T; split=split) +end \ No newline at end of file From 68f145208c0731f0f17dff1ff4ab81c7599e1d3b Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 9 Sep 2025 11:01:31 +0100 Subject: [PATCH 10/65] Splitting progress --- src/core/transform.jl | 133 +++++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 52 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index 9d4710d..da7ffa7 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1137,7 +1137,62 @@ function split_vertices( hg::HGNNHypergraph{T,D}, masks::AbstractVector{BitVector} ) where {T <: Real, D <: AbstractDict{Int, T}} - # TODO: this + + 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}() + + 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 + + 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[mask]))) + for (i, e) in enumerate(unique_hgids) + hgmap[e] = i + end + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] + + push!( + res, + HGNNHypergraph{T,D}( + v2he, + he2v, + length(v2he), + length(he2v), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, mask), + getobs(hg.hedata, collect(keys(hemap))), + getobs(hg.hgdata, unique_hgids) + ) + ) + end + + return res end function split_vertices( @@ -1167,8 +1222,10 @@ function split_vertices( masks = BitVector[] for indgroup in index_groups - + push!(masks, BitArray(i in indgroup for i in 1:nhv(hg))) end + + split_vertices(hg, masks) end function split_vertices( @@ -1177,7 +1234,24 @@ function split_vertices( 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:nhv(hg)), + BitArray(i in val_inds for i in 1:nhv(hg)), + BitArray(i in test_inds for i in 1:nhv(hg)) + ] + else + masks = [ + BitArray(i in train_inds for i in 1:nhv(hg)), + BitArray(i in test_inds for i in 1:nhv(hg)) + ] + 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 function split_vertices() end @@ -1203,64 +1277,19 @@ function random_split_vertices( num_choices = round.(fracs .* hg.num_vertices) rand_inds = shuffle(rng, Vector(1:hg.num_vertices)) - partitions = Vector{Int}[] + 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 - push!(partitions, sort(part)) + push!(masks, BitArray(i in part for i in 1:nhv(hg))) 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}() + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:nhv(hg))) - 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 + split_vertices(hg, masks) end function random_split_vertices( From 20726c0accdeff7ccdaef3d101e2e5059d3dd4c6 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 9 Sep 2025 11:57:21 +0100 Subject: [PATCH 11/65] Hyperedge splitting --- src/core/transform.jl | 195 ++++++++++++++++++++++++++++++------------ 1 file changed, 141 insertions(+), 54 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index da7ffa7..97d91af 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1106,7 +1106,6 @@ end # ) 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} if S <: UniformSample return uniform_negative_sample(hg, n, rng; max_trials=max_trials) @@ -1138,6 +1137,8 @@ function split_vertices( masks::AbstractVector{BitVector} ) where {T <: Real, D <: AbstractDict{Int, T}} + @assert all(length.(masks) .== nhv(hg)) + res = HGNNHypergraph{T,D}[] # Partition v2he and he2v, being careful of indices @@ -1254,14 +1255,146 @@ function split_vertices( return (train=hgs[1], val=val_data, test=hgs[end]) end +# TODO: directed hypergraph function split_vertices() end -function split_hyperedges() end +function split_hyperedges( + hg::HGNNHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + @assert all(length.(masks) .== nhe(hg)) + + 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}() + + 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 + + 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, mask), + getobs(hg.hgdata, unique_hgids) + ) + ) + end + + return res + +end + +function split_hyperedges( + hg::Hypergraph{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{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:nhe(hg))) + 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:nhe(hg)), + BitArray(i in val_inds for i in 1:nhe(hg)), + BitArray(i in test_inds for i in 1:nhe(hg)) + ] + else + masks = [ + BitArray(i in train_inds for i in 1:nhe(hg)), + BitArray(i in test_inds for i in 1:nhe(hg)) + ] + 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 + +# TODO: directed hypergraphs function split_hyperedges() end +# TODO: undireced hypergraphs function split_hypergraphs() end +# TODO: directed hypergraphs function split_hypergraphs() end function random_split_vertices( @@ -1292,6 +1425,7 @@ function random_split_vertices( split_vertices(hg, masks) end +# TODO: rewrite using mask approach function random_split_vertices( hg::HGNNDiHypergraph{T,D}, fracs::AbstractVector{<:Real}, @@ -1408,60 +1542,12 @@ function random_split_hyperedges( 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) - ) - ) + push!(partitions, BitArray(i in part for i in 1:nhe(hg))) end + remainder = rand_inds[start_point:end] + push!(partitions, BitArray(i in remainder for i in 1:nhe(hg))) - return res + split_hyperedges(hg, masks) end # TODO: you are here @@ -1543,6 +1629,7 @@ function random_split_hypergraphs( return res end +# TODO function random_split_hypergraphs( hg::HGNNDiHypergraph{T,D}, fracs::AbstractVector{<:Real}, From 1f9e4f433508b72ecf22bbcc093072337e89ed7e Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 9 Sep 2025 12:36:07 +0100 Subject: [PATCH 12/65] Initial draft of splitting by v/he/hg for undirected hypergraphs done --- src/core/transform.jl | 214 ++++++++++++++++++++++++++++-------------- 1 file changed, 142 insertions(+), 72 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index 97d91af..d1cf3c8 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1137,7 +1137,7 @@ function split_vertices( masks::AbstractVector{BitVector} ) where {T <: Real, D <: AbstractDict{Int, T}} - @assert all(length.(masks) .== nhv(hg)) + @assert all(length.(masks) .== hg.num_vertices) res = HGNNHypergraph{T,D}[] @@ -1223,7 +1223,7 @@ function split_vertices( masks = BitVector[] for indgroup in index_groups - push!(masks, BitArray(i in indgroup for i in 1:nhv(hg))) + push!(masks, BitArray(i in indgroup for i in 1:hg.num_vertices)) end split_vertices(hg, masks) @@ -1237,14 +1237,14 @@ function split_vertices( ) where {T <: Real, D <: AbstractDict{Int, T}} if !isnothing(val_inds) masks = [ - BitArray(i in train_inds for i in 1:nhv(hg)), - BitArray(i in val_inds for i in 1:nhv(hg)), - BitArray(i in test_inds for i in 1:nhv(hg)) + 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:nhv(hg)), - BitArray(i in test_inds for i in 1:nhv(hg)) + 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 @@ -1262,7 +1262,7 @@ function split_hyperedges( hg::HGNNHypergraph{T,D}, masks::AbstractVector{BitVector} ) where {T <: Real, D <: AbstractDict{Int, T}} - @assert all(length.(masks) .== nhe(hg)) + @assert all(length.(masks) .== hg.num_hyperedges) res = HGNNHypergraph{T,D}[] @@ -1343,7 +1343,6 @@ function split_hyperedges( val_data = isnothing(val_mask) ? nothing : hgs[2] return (train=hgs[1], val=val_data, test=hgs[end]) - end function split_hyperedges( @@ -1354,11 +1353,10 @@ function split_hyperedges( masks = BitVector[] for indgroup in index_groups - push!(masks, BitArray(i in indgroup for i in 1:nhe(hg))) + push!(masks, BitArray(i in indgroup for i in 1:hg.num_hyperedges)) end split_hyperedges(hg, masks) - end function split_hyperedges( @@ -1369,14 +1367,14 @@ function split_hyperedges( ) where {T <: Real, D <: AbstractDict{Int, T}} if !isnothing(val_inds) masks = [ - BitArray(i in train_inds for i in 1:nhe(hg)), - BitArray(i in val_inds for i in 1:nhe(hg)), - BitArray(i in test_inds for i in 1:nhe(hg)) + 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:nhe(hg)), - BitArray(i in test_inds for i in 1:nhe(hg)) + 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 @@ -1385,14 +1383,129 @@ function split_hyperedges( val_data = isnothing(val_inds) ? nothing : hgs[2] return (train=hgs[1], val=val_data, test=hgs[end]) - end # TODO: directed hypergraphs function split_hyperedges() end # TODO: undireced hypergraphs -function split_hypergraphs() end +function split_hypergraphs( + hg::HGNNHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + @assert all(length.(masks) .== hg.num_hypergraphs) + + res = HGNNHypergraph{T,D}[] + + # Partition v2he and he2v, being careful of indices + for mask in masks + 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 + + 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(keys(hgmap)), + hypergraph_ids, + getobs(hg.vdata, rel_vs), + getobs(hg.hedata, 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{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) + end + + split_hyperedges(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_hyperedges(hg, masks) + + val_data = isnothing(val_inds) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end # TODO: directed hypergraphs function split_hypergraphs() end @@ -1417,10 +1530,10 @@ function random_split_vertices( for i in 1:(length(num_choices) - 1) part = rand_inds[start_point:start_point + num_choices[i] - 1] start_point += num_choices - push!(masks, BitArray(i in part for i in 1:nhv(hg))) + push!(masks, BitArray(i in part for i in 1:hg.num_vertices(hg))) end remainder = rand_inds[start_point:end] - push!(masks, BitArray(i in remainder for i in 1:nhv(hg))) + push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices(hg))) split_vertices(hg, masks) end @@ -1535,22 +1648,22 @@ function random_split_hyperedges( num_choices = round.(fracs .* hg.num_hyperedges) rand_inds = shuffle(rng, Vector(1:hg.num_hyperedges)) - partitions = Vector{Int}[] + 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 - push!(partitions, BitArray(i in part for i in 1:nhe(hg))) + push!(masks, BitArray(i in part for i in 1:hg.num_hyperedges)) end remainder = rand_inds[start_point:end] - push!(partitions, BitArray(i in remainder for i in 1:nhe(hg))) + push!(masks, BitArray(i in remainder for i in 1:hg.num_hyperedges)) split_hyperedges(hg, masks) end -# TODO: you are here +# TODO: directed hypergraphs function random_split_hyperedges( hg::HGNNDiHypergraph{T,D}, fracs::AbstractVector{<:Real}, @@ -1571,62 +1684,19 @@ function random_split_hypergraphs( num_choices = round.(fracs .* hg.num_hypergraphs) rand_inds = shuffle(rng, Vector(1:hg.num_hypergraphs)) - partitions = Vector{Int}[] + 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 - 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) - ) - ) + 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)) - return res + split_hypergraphs(hg, masks) end # TODO From 9e6f2631bd31fe2ffe72c3e16e5f42473ac195e2 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 9 Sep 2025 12:52:02 +0100 Subject: [PATCH 13/65] Incremental progress --- src/core/transform.jl | 209 ++++++++++++++++++++++++++++-------------- 1 file changed, 139 insertions(+), 70 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index d1cf3c8..42eb21e 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1255,8 +1255,144 @@ function split_vertices( return (train=hgs[1], val=val_data, test=hgs[end]) end -# TODO: directed hypergraph -function split_vertices() end +function split_vertices( + hg::HGNNDiHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + # 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] + + # TODO: you are here + 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}() + + 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) + 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[mask]))) + for (i, e) in enumerate(unique_hgids) + hgmap[e] = i + end + + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] + + push!( + res, + HGNNDiHypergraph{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(v2he_tail), + length(he2v_tail), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, mask), + getobs(hg.hedata, 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::HGNNHypergraph{T,D}, + index_groups::AbstractVector{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + 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 function split_hyperedges( hg::HGNNHypergraph{T,D}, @@ -1565,74 +1701,7 @@ function random_split_vertices( 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 - + # TODO: you are here end function random_split_hyperedges( From 2bf8e421002339aa0f9901f5d62741120d8cbe16 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 9 Sep 2025 12:57:33 +0100 Subject: [PATCH 14/65] Almost there w/ first draft of splitting --- src/core/transform.jl | 91 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index 42eb21e..ab302ca 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1259,6 +1259,8 @@ 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] @@ -1458,7 +1460,6 @@ function split_hyperedges( end return res - end function split_hyperedges( @@ -1522,7 +1523,38 @@ function split_hyperedges( end # TODO: directed hypergraphs -function split_hyperedges() end +# TODO: you are here +function split_hyperedges( + hg::HGNNDiHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + +end + +function split_hyperedges( + hg::Hypergraph{T, D}, + train_mask::BitVector, + test_mask::BitVector; + val_mask::Union{BitVector, Nothing} = nothing, +) where {T <: Real, D <: AbstractDict{Int, T}} + +end + +function split_hyperedges( + hg::HGNNHypergraph{T,D}, + index_groups::AbstractVector{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + +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}} + +end # TODO: undireced hypergraphs function split_hypergraphs( @@ -1687,21 +1719,20 @@ function random_split_vertices( num_choices = round.(fracs .* hg.num_vertices) rand_inds = shuffle(rng, Vector(1:hg.num_vertices)) - - partitions = Vector{Int}[] + + 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 - push!(partitions, sort(part)) + push!(masks, BitArray(i in part for i in 1:hg.num_vertices(hg))) end - push!(partitions, sort(rand_inds[start_point:end])) - - res = HGNNDiHypergraph{T,D}[] + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices(hg))) - # TODO: you are here + split_vertices(hg, masks) end function random_split_hyperedges( @@ -1732,12 +1763,32 @@ function random_split_hyperedges( split_hyperedges(hg, masks) end -# TODO: directed hypergraphs 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 = round.(fracs .* hg.num_hyperedges) + rand_inds = shuffle(rng, Vector(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 + 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_hypergraphs( @@ -1768,12 +1819,30 @@ function random_split_hypergraphs( split_hypergraphs(hg, masks) end -# TODO 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 = round.(fracs .* hg.num_hypergraphs) + rand_inds = shuffle(rng, Vector(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 + 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 From cd37209e2467ec26d0314d2a292fd54cc5ae1d68 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 10 Sep 2025 09:08:00 +0100 Subject: [PATCH 15/65] Hyperedge splitting for directed hypergraphs --- src/core/transform.jl | 123 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 8 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index ab302ca..3e679a0 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1293,7 +1293,7 @@ function split_vertices( 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) + hemap[i] = length(he2v_tail) end end @@ -1463,7 +1463,7 @@ function split_hyperedges( end function split_hyperedges( - hg::Hypergraph{T, D}, + hg::HGNNHypergraph{T, D}, train_mask::BitVector, test_mask::BitVector; val_mask::Union{BitVector, Nothing} = nothing, @@ -1522,41 +1522,148 @@ function split_hyperedges( return (train=hgs[1], val=val_data, test=hgs[end]) end -# TODO: directed hypergraphs -# TODO: you are here function split_hyperedges( 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 + 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}() + + 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 + + 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 = 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, + HGNNDiHypergraph{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(v2he_tail), + length(he2v_tail), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, collect(keys(vmap))), + getobs(hg.hedata, mask), + getobs(hg.hgdata, unique_hgids) + ) + ) + end + return res end function split_hyperedges( - hg::Hypergraph{T, D}, + 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::HGNNHypergraph{T,D}, + hg::HGNNDiHypergraph{T,D}, index_groups::AbstractVector{AbstractVector{Int}} ) where {T <: Real, D <: AbstractDict{Int, T}} + 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}, + 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 -# TODO: undireced hypergraphs function split_hypergraphs( hg::HGNNHypergraph{T,D}, masks::AbstractVector{BitVector} From 881ad1840a5e610e18cf93807ff2e11924a2bbd0 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 10 Sep 2025 09:29:48 +0100 Subject: [PATCH 16/65] Aaaaand done. Now just the docs and tests.... --- src/core/transform.jl | 133 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index 3e679a0..136314c 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1783,7 +1783,137 @@ function split_hypergraphs( end # TODO: directed hypergraphs -function split_hypergraphs() end +function split_hypergraphs( + hg::HGNNDiHypergraph{T,D}, + masks::AbstractVector{BitVector} +) where {T <: Real, D <: AbstractDict{Int, T}} + res = HGNNDiHypergraph{T,D}[] + + for mask in masks + 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 + + 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 + + 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] + + push!( + res, + HGNNDiHypergraph{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(v2he_tail), + length(he2v_tail), + length(keys(hgmap)), + hypergraph_ids, + getobs(hg.vdata, rel_vs), + getobs(hg.hedata, 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::HGNNHypergraph{T,D}, + index_groups::AbstractVector{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) + end + + split_hyperedges(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_hyperedges(hg, masks) + + val_data = isnothing(val_inds) ? nothing : hgs[2] + + return (train=hgs[1], val=val_data, test=hgs[end]) +end function random_split_vertices( hg::HGNNHypergraph{T,D}, @@ -1813,7 +1943,6 @@ function random_split_vertices( split_vertices(hg, masks) end -# TODO: rewrite using mask approach function random_split_vertices( hg::HGNNDiHypergraph{T,D}, fracs::AbstractVector{<:Real}, From f0406f6977b37bb7827b9d64d7aa718fac10227b Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 10 Sep 2025 11:29:51 +0100 Subject: [PATCH 17/65] Docs for main splitting functions --- src/core/transform.jl | 229 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 225 insertions(+), 4 deletions(-) diff --git a/src/core/transform.jl b/src/core/transform.jl index 136314c..caceb09 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1040,7 +1040,7 @@ function motif_negative_sample( return HGNNDiHypergraph(DirectedHypergraph(hg_tail, hg_head)) end -# TODO: you are here + function clique_negative_sample( hg::HGNNHypergraph{T, D}, n::Int, @@ -1132,6 +1132,43 @@ function negative_sample_hyperedge(hg::H, n::Int, rng::AbstractRNG, ::S; max_tri end end +""" + 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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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} @@ -1255,6 +1292,43 @@ function split_vertices( 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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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} @@ -1266,7 +1340,6 @@ function split_vertices( v2he_tail = hg.hg_tail.v2he[mask] v2he_head = hg.hg_head.v2he[mask] - # TODO: you are here he2v_tail = D[] he2v_head = D[] @@ -1396,6 +1469,43 @@ function split_vertices( 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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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} @@ -1522,6 +1632,44 @@ function split_hyperedges( 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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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} @@ -1664,6 +1812,43 @@ function split_hyperedges( 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_hyperedges( + hg::HGNNHypergraph{T,D}, + index_groups::AbstractVector{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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 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} @@ -1782,7 +1967,43 @@ function split_hypergraphs( return (train=hgs[1], val=val_data, test=hgs[end]) end -# TODO: directed hypergraphs +""" + 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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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} @@ -1877,7 +2098,7 @@ function split_hypergraphs( end function split_hypergraphs( - hg::HGNNHypergraph{T,D}, + hg::HGNNDiHypergraph{T,D}, index_groups::AbstractVector{AbstractVector{Int}} ) where {T <: Real, D <: AbstractDict{Int, T}} masks = BitVector[] From 06d3c5f0e973a3d8eb32d0e1f11541373461d591 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 10 Sep 2025 11:35:52 +0100 Subject: [PATCH 18/65] Docs done --- src/core/transform.jl | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/core/transform.jl b/src/core/transform.jl index caceb09..6643dd9 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -2136,6 +2136,23 @@ function split_hypergraphs( 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}, @@ -2192,6 +2209,23 @@ function random_split_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}, @@ -2248,6 +2282,23 @@ function random_split_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}, From 28ef27701a583ed29d4ae206192463481b0d9f27 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 10 Sep 2025 11:40:04 +0100 Subject: [PATCH 19/65] Little refactor before I dive into tests --- src/HyperGraphNeuralNetworks.jl | 9 +- src/core/split.jl | 1222 ++++++++++++++++++++++++++++++ src/core/transform.jl | 1225 +------------------------------ 3 files changed, 1230 insertions(+), 1226 deletions(-) create mode 100644 src/core/split.jl diff --git a/src/HyperGraphNeuralNetworks.jl b/src/HyperGraphNeuralNetworks.jl index d1e83f8..14cba46 100644 --- a/src/HyperGraphNeuralNetworks.jl +++ b/src/HyperGraphNeuralNetworks.jl @@ -38,8 +38,13 @@ 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") diff --git a/src/core/split.jl b/src/core/split.jl new file mode 100644 index 0000000..567b5a4 --- /dev/null +++ b/src/core/split.jl @@ -0,0 +1,1222 @@ +""" + 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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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}() + + 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 + + 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[mask]))) + for (i, e) in enumerate(unique_hgids) + hgmap[e] = i + end + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] + + push!( + res, + HGNNHypergraph{T,D}( + v2he, + he2v, + length(v2he), + length(he2v), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, mask), + getobs(hg.hedata, 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{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + 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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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}() + + 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 + + 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[mask]))) + for (i, e) in enumerate(unique_hgids) + hgmap[e] = i + end + + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] + + push!( + res, + HGNNDiHypergraph{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(v2he_tail), + length(he2v_tail), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, mask), + getobs(hg.hedata, 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::HGNNHypergraph{T,D}, + index_groups::AbstractVector{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + 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_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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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}() + + 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 + + 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, 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{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + + 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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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}} + 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}() + + 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 + + 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 = 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, + HGNNDiHypergraph{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(v2he_tail), + length(he2v_tail), + length(unique_hgids), + hypergraph_ids, + getobs(hg.vdata, collect(keys(vmap))), + 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{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + 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_hyperedges( + hg::HGNNHypergraph{T,D}, + index_groups::AbstractVector{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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 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 all(length.(masks) .== hg.num_hypergraphs) + + res = HGNNHypergraph{T,D}[] + + # Partition v2he and he2v, being careful of indices + for mask in masks + 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 + + 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(keys(hgmap)), + hypergraph_ids, + getobs(hg.vdata, rel_vs), + getobs(hg.hedata, 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{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) + end + + split_hyperedges(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_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::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{AbstractVector{Int}} + ) where {T <: Real, D <: AbstractDict{Int, T}} + + 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 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}} + res = HGNNDiHypergraph{T,D}[] + + for mask in masks + 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 + + 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 + + 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] + + push!( + res, + HGNNDiHypergraph{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(v2he_tail), + length(he2v_tail), + length(keys(hgmap)), + hypergraph_ids, + getobs(hg.vdata, rel_vs), + getobs(hg.hedata, 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{AbstractVector{Int}} +) where {T <: Real, D <: AbstractDict{Int, T}} + masks = BitVector[] + + for indgroup in index_groups + push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) + end + + split_hyperedges(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_hyperedges(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 = round.(fracs .* hg.num_vertices) + rand_inds = shuffle(rng, Vector(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 + push!(masks, BitArray(i in part for i in 1:hg.num_vertices(hg))) + end + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices(hg))) + + 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 = round.(fracs .* hg.num_vertices) + rand_inds = shuffle(rng, Vector(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 + push!(masks, BitArray(i in part for i in 1:hg.num_vertices(hg))) + end + remainder = rand_inds[start_point:end] + push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices(hg))) + + 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 = round.(fracs .* hg.num_hyperedges) + rand_inds = shuffle(rng, Vector(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 + 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 = round.(fracs .* hg.num_hyperedges) + rand_inds = shuffle(rng, Vector(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 + 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 = round.(fracs .* hg.num_hypergraphs) + rand_inds = shuffle(rng, Vector(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 + 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 = round.(fracs .* hg.num_hypergraphs) + rand_inds = shuffle(rng, Vector(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 + 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 6643dd9..6c470b9 100644 --- a/src/core/transform.jl +++ b/src/core/transform.jl @@ -1130,1227 +1130,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 - -""" - 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{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} - - 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 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}() - - 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 - - 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[mask]))) - for (i, e) in enumerate(unique_hgids) - hgmap[e] = i - end - hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] - - push!( - res, - HGNNHypergraph{T,D}( - v2he, - he2v, - length(v2he), - length(he2v), - length(unique_hgids), - hypergraph_ids, - getobs(hg.vdata, mask), - getobs(hg.hedata, 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{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} - 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{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} - - 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 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}() - - 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 - - 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[mask]))) - for (i, e) in enumerate(unique_hgids) - hgmap[e] = i - end - - hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] - - push!( - res, - HGNNDiHypergraph{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(v2he_tail), - length(he2v_tail), - length(unique_hgids), - hypergraph_ids, - getobs(hg.vdata, mask), - getobs(hg.hedata, 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::HGNNHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} - 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_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{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} - - 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 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}() - - 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 - - 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, 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{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} - - 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{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} - - 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 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}} - 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}() - - 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 - - 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 = 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, - HGNNDiHypergraph{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(v2he_tail), - length(he2v_tail), - length(unique_hgids), - hypergraph_ids, - getobs(hg.vdata, collect(keys(vmap))), - 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{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} - 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_hyperedges( - hg::HGNNHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} - - 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 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 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 all(length.(masks) .== hg.num_hypergraphs) - - res = HGNNHypergraph{T,D}[] - - # Partition v2he and he2v, being careful of indices - for mask in masks - 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 - - 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(keys(hgmap)), - hypergraph_ids, - getobs(hg.vdata, rel_vs), - getobs(hg.hedata, 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{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} - masks = BitVector[] - - for indgroup in index_groups - push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) - end - - split_hyperedges(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_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::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{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} - - 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 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}} - res = HGNNDiHypergraph{T,D}[] - - for mask in masks - 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 - - 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 - - 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] - - push!( - res, - HGNNDiHypergraph{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(v2he_tail), - length(he2v_tail), - length(keys(hgmap)), - hypergraph_ids, - getobs(hg.vdata, rel_vs), - getobs(hg.hedata, 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{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} - masks = BitVector[] - - for indgroup in index_groups - push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) - end - - split_hyperedges(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_hyperedges(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 = round.(fracs .* hg.num_vertices) - rand_inds = shuffle(rng, Vector(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 - push!(masks, BitArray(i in part for i in 1:hg.num_vertices(hg))) - end - remainder = rand_inds[start_point:end] - push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices(hg))) - - 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 = round.(fracs .* hg.num_vertices) - rand_inds = shuffle(rng, Vector(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 - push!(masks, BitArray(i in part for i in 1:hg.num_vertices(hg))) - end - remainder = rand_inds[start_point:end] - push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices(hg))) - - 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 = round.(fracs .* hg.num_hyperedges) - rand_inds = shuffle(rng, Vector(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 - 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 = round.(fracs .* hg.num_hyperedges) - rand_inds = shuffle(rng, Vector(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 - 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 = round.(fracs .* hg.num_hypergraphs) - rand_inds = shuffle(rng, Vector(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 - 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 = round.(fracs .* hg.num_hypergraphs) - rand_inds = shuffle(rng, Vector(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 - 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 +end \ No newline at end of file From 6a8861af53513a4e71393cede8cf22a72544678f Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 10 Sep 2025 13:08:33 +0100 Subject: [PATCH 20/65] Very prelim test scaffold (need to actually write) --- src/HyperGraphNeuralNetworks.jl | 2 +- test/runtests.jl | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/HyperGraphNeuralNetworks.jl b/src/HyperGraphNeuralNetworks.jl index 14cba46..6e2ddc1 100644 --- a/src/HyperGraphNeuralNetworks.jl +++ b/src/HyperGraphNeuralNetworks.jl @@ -55,4 +55,4 @@ include("datasets/datasets.jl") export getHyperCora, getHyperCiteSeer -end +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 5ad0a2c..d5cbe68 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -505,3 +505,10 @@ end end +@testset "HyperGraphNeuralNetworks split" begin + +end + +@testset "HyperGraphNeuralNetworks undirected hypergraph datasets" begin + +end \ No newline at end of file From a3e27bd11e27a324ea79cf146fa3b4dbb910f8aa Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 11 Sep 2025 10:04:20 +0100 Subject: [PATCH 21/65] Undirected vertex splitting - tests pass (now just need to test random) --- src/core/split.jl | 64 +++++++++++++-------------- src/datasets/datasets.jl | 8 ++-- test/runtests.jl | 94 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 37 deletions(-) diff --git a/src/core/split.jl b/src/core/split.jl index 567b5a4..ea2d393 100644 --- a/src/core/split.jl +++ b/src/core/split.jl @@ -13,8 +13,8 @@ split_vertices( hg::HGNNHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} split_vertices( hg::HGNNHypergraph{T,D}, @@ -90,7 +90,7 @@ function split_vertices( length(unique_hgids), hypergraph_ids, getobs(hg.vdata, mask), - getobs(hg.hedata, collect(keys(hemap))), + getobs(hg.hedata, sort(collect(keys(hemap)))), getobs(hg.hgdata, unique_hgids) ) ) @@ -121,8 +121,8 @@ end function split_vertices( hg::HGNNHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} masks = BitVector[] for indgroup in index_groups @@ -173,8 +173,8 @@ end split_vertices( hg::HGNNDiHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} split_vertices( hg::HGNNDiHypergraph{T,D}, @@ -268,7 +268,7 @@ function split_vertices( length(unique_hgids), hypergraph_ids, getobs(hg.vdata, mask), - getobs(hg.hedata, collect(keys(hemap))), + getobs(hg.hedata, sort(collect(keys(hemap)))), getobs(hg.hgdata, unique_hgids) ) ) @@ -297,9 +297,9 @@ function split_vertices( end function split_vertices( - hg::HGNNHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} + 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 @@ -310,7 +310,7 @@ function split_vertices( end function split_vertices( - hg::HGNNHypergraph{T,D}, + hg::HGNNDiHypergraph{T,D}, train_inds::AbstractVector{Int}, test_inds::AbstractVector{Int}; val_inds::Union{AbstractVector{Int}, Nothing} = nothing @@ -350,8 +350,8 @@ end split_hyperedges( hg::HGNNHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} split_hyperedges( hg::HGNNHypergraph{T,D}, @@ -410,7 +410,7 @@ function split_hyperedges( he2v[i] = D(vmap[key] => val for (key, val) in he2v[i]) end - rel_vs = collect(keys(vmap)) + rel_vs = sort(collect(keys(vmap))) unique_hgids = sort(collect(Set(hg.hypergraph_ids[rel_vs]))) for (i, e) in enumerate(unique_hgids) @@ -460,8 +460,8 @@ end function split_hyperedges( hg::HGNNHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} masks = BitVector[] @@ -513,8 +513,8 @@ end split_hyperedges( hg::HGNNDiHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} split_hyperedges( hg::HGNNDiHypergraph{T,D}, @@ -582,7 +582,7 @@ function split_hyperedges( he2v_head[i] = D(vmap[k] => v for (k, v) in he2v_head[i]) end - rel_vs = collect(keys(vmap)) + rel_vs = sort(collect(keys(vmap))) unique_hgids = sort(collect(Set(hg.hypergraph_ids[rel_vs]))) for (i, e) in enumerate(unique_hgids) @@ -641,8 +641,8 @@ end function split_hyperedges( hg::HGNNDiHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} masks = BitVector[] for indgroup in index_groups @@ -693,8 +693,8 @@ end split_hyperedges( hg::HGNNHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} split_hyperedges( hg::HGNNHypergraph{T,D}, @@ -766,7 +766,7 @@ function split_hypergraphs( length(keys(hgmap)), hypergraph_ids, getobs(hg.vdata, rel_vs), - getobs(hg.hedata, collect(keys(hemap))), + getobs(hg.hedata, sort(collect(keys(hemap)))), getobs(hg.hgdata, mask) ) ) @@ -796,8 +796,8 @@ end function split_hypergraphs( hg::HGNNHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} masks = BitVector[] for indgroup in index_groups @@ -848,8 +848,8 @@ end split_hypergraphs( hg::HGNNDiHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} - ) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} + ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} split_hypergraphs( hg::HGNNDiHypergraph{T,D}, @@ -935,7 +935,7 @@ function split_hypergraphs( length(keys(hgmap)), hypergraph_ids, getobs(hg.vdata, rel_vs), - getobs(hg.hedata, collect(keys(hemap))), + getobs(hg.hedata, sort(collect(keys(hemap)))), getobs(hg.hgdata, mask) ) ) @@ -965,8 +965,8 @@ end function split_hypergraphs( hg::HGNNDiHypergraph{T,D}, - index_groups::AbstractVector{AbstractVector{Int}} -) where {T <: Real, D <: AbstractDict{Int, T}} + index_groups::AbstractVector{V} +) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} masks = BitVector[] for indgroup in index_groups diff --git a/src/datasets/datasets.jl b/src/datasets/datasets.jl index 5e6d53e..3078a57 100644 --- a/src/datasets/datasets.jl +++ b/src/datasets/datasets.jl @@ -1,8 +1,8 @@ function _convert_citation_graphdata( - dset::AbstractDataset, + dset::MLDatasets.AbstractDataset, T::Type{R}; split::Bool = false -) +) where {R <: Real} nv = dset[1].num_nodes # Hypergraph vertices are graph vertices @@ -38,13 +38,13 @@ end function getHyperCora( T::Type{R}; split::Bool = false -) where {R<:Real} +) where {R <: Real} _convert_citation_graphdata(MLDatasets.Cora(), T; split=split) end function getHyperCiteSeer( T::Type{R}; split::Bool = false -) +) where {R <: Real} _convert_citation_graphdata(MLDatasets.CiteSeer(), T; split=split) end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index d5cbe68..2691b1b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -505,7 +505,99 @@ end end -@testset "HyperGraphNeuralNetworks split" begin +@testset "HyperGraphNeuralNetworks split vertices" begin + 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] + + 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) == getobs(hgnn1.vdata, 2) + @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) == getobs(hgnn1.vdata, 4) + @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) == getobs(hgnn1.vdata, 9) + @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] end From 5062931ce4554b7c43d57b9ac1fdd4c7ab92a3f1 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 11 Sep 2025 10:16:47 +0100 Subject: [PATCH 22/65] Minor test refactor --- test/runtests.jl | 142 +++++++++++++++++++++-------------------------- 1 file changed, 63 insertions(+), 79 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 2691b1b..ca0a28c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,57 +8,80 @@ using HyperGraphNeuralNetworks using SimpleHypergraphs using SimpleDirectedHypergraphs +# 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 +93,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 +277,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 +313,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 @@ -505,27 +509,7 @@ end end -@testset "HyperGraphNeuralNetworks split vertices" begin - 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] - +@testset "HyperGraphNeuralNetworks split vertices" begin hgnn1 = HGNNHypergraph( uh1; hypergraph_ids = uid1, From a34f51a2e074a2fd68fa5cf449ddc57ba1c8686a Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 11 Sep 2025 13:16:08 +0100 Subject: [PATCH 23/65] A solid round of bugfixes --- src/core/hypergraphs.jl | 4 +-- src/core/split.jl | 56 ++++++++++++++++++++--------------------- test/runtests.jl | 14 +++++++++++ 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/core/hypergraphs.jl b/src/core/hypergraphs.jl index c7f620a..b0d5cb9 100644 --- a/src/core/hypergraphs.jl +++ b/src/core/hypergraphs.jl @@ -685,7 +685,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) @@ -1643,7 +1643,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) diff --git a/src/core/split.jl b/src/core/split.jl index ea2d393..1735248 100644 --- a/src/core/split.jl +++ b/src/core/split.jl @@ -1029,20 +1029,20 @@ function random_split_vertices( # 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)) + 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 - push!(masks, BitArray(i in part for i in 1:hg.num_vertices(hg))) + 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(hg))) + push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices)) split_vertices(hg, masks) end @@ -1057,20 +1057,20 @@ function random_split_vertices( # 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)) + 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 - push!(masks, BitArray(i in part for i in 1:hg.num_vertices(hg))) + part = rand_inds[start_point:(start_point + num_choices[i] - 1)] + start_point += num_choices[1] + 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(hg))) + push!(masks, BitArray(i in remainder for i in 1:hg.num_vertices)) split_vertices(hg, masks) end @@ -1102,16 +1102,16 @@ function random_split_hyperedges( # 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)) + 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 + 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] @@ -1130,16 +1130,16 @@ function random_split_hyperedges( # 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)) + 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 + 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] @@ -1175,16 +1175,16 @@ function random_split_hypergraphs( # 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)) + 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 + 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] @@ -1203,16 +1203,16 @@ function random_split_hypergraphs( # 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)) + 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 + 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] diff --git a/test/runtests.jl b/test/runtests.jl index ca0a28c..d1600ea 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -510,6 +510,7 @@ end end @testset "HyperGraphNeuralNetworks split vertices" begin + # Split vertices of undirected hypergraphs hgnn1 = HGNNHypergraph( uh1; hypergraph_ids = uid1, @@ -583,6 +584,19 @@ end @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[1].num_hyperedges == 5 + @test hgnns_rand[1].num_hypergraphs == 2 + @test hgnns_rand[2].num_vertices == 1 + @test hgnns_rand[2].num_hyperedges == 1 + @test hgnns_rand[2].num_hypergraphs == 1 + @test hgnns_rand[3].num_vertices == 2 + @test hgnns_rand[3].num_hyperedges == 2 + @test hgnns_rand[3].num_hypergraphs == 1 end @testset "HyperGraphNeuralNetworks undirected hypergraph datasets" begin From ccfb68b14f61637a3f2e23226169170b37cebb0c Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 12 Sep 2025 09:39:42 +0100 Subject: [PATCH 24/65] Tests pass hyperedge splitting --- test/runtests.jl | 181 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index d1600ea..0a8af49 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -599,6 +599,183 @@ end @test hgnns_rand[3].num_hypergraphs == 1 end -@testset "HyperGraphNeuralNetworks undirected hypergraph datasets" begin +@testset "HyperGraphNeuralNetworks split hyperedges" 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) + ) + + hemasks = [ + BitVector((false, true, true, false, true)), + BitVector((false, false, false, true, false)), + BitVector((true, false, false, false, false)) + ] + + # Split vertices 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) == getobs(hgnn1.vdata, 2) + @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) == getobs(hgnn1.vdata, 7) + @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) == getobs(hgnn1.vdata, 1) + @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 vertices 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 vertices 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 vertices 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_vertices == 9 + @test hgnns_rand[1].num_hyperedges == 4 + @test hgnns_rand[1].num_hypergraphs == 2 + @test hgnns_rand[2].num_vertices == 3 + @test hgnns_rand[2].num_hyperedges == 1 + @test hgnns_rand[2].num_hypergraphs == 1 +end -end \ No newline at end of file +# @testset "HyperGraphNeuralNetworks split hypergraphs" 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) == getobs(hgnn1.vdata, 2) +# @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) == getobs(hgnn1.vdata, 4) +# @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) == getobs(hgnn1.vdata, 9) +# @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[1].num_hyperedges == 5 +# @test hgnns_rand[1].num_hypergraphs == 2 +# @test hgnns_rand[2].num_vertices == 1 +# @test hgnns_rand[2].num_hyperedges == 1 +# @test hgnns_rand[2].num_hypergraphs == 1 +# @test hgnns_rand[3].num_vertices == 2 +# @test hgnns_rand[3].num_hyperedges == 2 +# @test hgnns_rand[3].num_hypergraphs == 1 +# end + +# @testset "HyperGraphNeuralNetworks undirected hypergraph datasets" begin + +# end \ No newline at end of file From 0d68ddfbd6acc3667c6fbd140d57e64c0c00ae6b Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 12 Sep 2025 09:55:58 +0100 Subject: [PATCH 25/65] ...aaaaand hypergraphs. Now just datasets until I'm ready-ish to merge --- src/core/split.jl | 8 +-- test/runtests.jl | 176 +++++++++++++++++++++++----------------------- 2 files changed, 91 insertions(+), 93 deletions(-) diff --git a/src/core/split.jl b/src/core/split.jl index 1735248..ba52ed1 100644 --- a/src/core/split.jl +++ b/src/core/split.jl @@ -804,7 +804,7 @@ function split_hypergraphs( push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) end - split_hyperedges(hg, masks) + split_hypergraphs(hg, masks) end function split_hypergraphs( @@ -826,7 +826,7 @@ function split_hypergraphs( ] end - hgs = split_hyperedges(hg, masks) + hgs = split_hypergraphs(hg, masks) val_data = isnothing(val_inds) ? nothing : hgs[2] @@ -973,7 +973,7 @@ function split_hypergraphs( push!(masks, BitArray(i in indgroup for i in 1:hg.num_hypergraphs)) end - split_hyperedges(hg, masks) + split_hypergraphs(hg, masks) end function split_hypergraphs( @@ -995,7 +995,7 @@ function split_hypergraphs( ] end - hgs = split_hyperedges(hg, masks) + hgs = split_hypergraphs(hg, masks) val_data = isnothing(val_inds) ? nothing : hgs[2] diff --git a/test/runtests.jl b/test/runtests.jl index 0a8af49..24ab0ca 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -686,95 +686,93 @@ end @test hgnns_rand[2].num_hypergraphs == 1 end -# @testset "HyperGraphNeuralNetworks split hypergraphs" 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) == getobs(hgnn1.vdata, 2) -# @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) == getobs(hgnn1.vdata, 4) -# @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) == getobs(hgnn1.vdata, 9) -# @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[1].num_hyperedges == 5 -# @test hgnns_rand[1].num_hypergraphs == 2 -# @test hgnns_rand[2].num_vertices == 1 -# @test hgnns_rand[2].num_hyperedges == 1 -# @test hgnns_rand[2].num_hypergraphs == 1 -# @test hgnns_rand[3].num_vertices == 2 -# @test hgnns_rand[3].num_hyperedges == 2 -# @test hgnns_rand[3].num_hypergraphs == 1 -# end +@testset "HyperGraphNeuralNetworks split hypergraphs" begin + uid2 = [1,1,1,1,2,2,3,3,3,3,3] + + # Split vertices 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 vertices 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) == getobs(hgnn1.vdata, 1) + @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) == getobs(hgnn1.vdata, 5) + @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) == getobs(hgnn1.vdata, 7) + @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 vertices 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 vertices 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 vertices 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_vertices == 2 + @test hgnns_rand[1].num_hyperedges == 2 + @test hgnns_rand[1].num_hypergraphs == 1 + @test hgnns_rand[2].num_vertices == 5 + @test hgnns_rand[2].num_hyperedges == 2 + @test hgnns_rand[2].num_hypergraphs == 1 + @test hgnns_rand[3].num_vertices == 4 + @test hgnns_rand[3].num_hyperedges == 3 + @test hgnns_rand[3].num_hypergraphs == 1 +end # @testset "HyperGraphNeuralNetworks undirected hypergraph datasets" begin From ddd240ddbc3f31a8f1779a1c2f0743839dd6da05 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 12 Sep 2025 10:36:02 +0100 Subject: [PATCH 26/65] Tests pass --- src/core/split.jl | 63 ++++++++++++++++++++++++++++++++++------------- test/runtests.jl | 42 ++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/src/core/split.jl b/src/core/split.jl index ba52ed1..4fe8128 100644 --- a/src/core/split.jl +++ b/src/core/split.jl @@ -74,11 +74,16 @@ function split_vertices( v2he[i] = D(hemap[k] => v for (k, v) in v2he[i]) end - unique_hgids = sort(collect(Set(hg.hypergraph_ids[mask]))) - for (i, e) in enumerate(unique_hgids) - hgmap[e] = i + 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]] push!( res, @@ -241,9 +246,15 @@ function split_vertices( v2he_head[i] = D(hemap[k] => v for (k, v) in v2he_head[i]) end - unique_hgids = sort(collect(Set(hg.hypergraph_ids[mask]))) - for (i, e) in enumerate(unique_hgids) - hgmap[e] = i + 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]] @@ -412,12 +423,17 @@ function split_hyperedges( rel_vs = sort(collect(keys(vmap))) - unique_hgids = sort(collect(Set(hg.hypergraph_ids[rel_vs]))) - for (i, e) in enumerate(unique_hgids) - hgmap[e] = i - end + 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]] + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[rel_vs]] + end push!( res, @@ -540,6 +556,8 @@ 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 @@ -584,12 +602,17 @@ function split_hyperedges( rel_vs = sort(collect(keys(vmap))) - unique_hgids = sort(collect(Set(hg.hypergraph_ids[rel_vs]))) - for (i, e) in enumerate(unique_hgids) - hgmap[e] = i - end + 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]] + hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[rel_vs]] + end push!( res, @@ -719,6 +742,8 @@ 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}[] @@ -874,6 +899,10 @@ 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 diff --git a/test/runtests.jl b/test/runtests.jl index 24ab0ca..62552ee 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -774,6 +774,42 @@ end @test hgnns_rand[3].num_hypergraphs == 1 end -# @testset "HyperGraphNeuralNetworks undirected hypergraph datasets" begin - -# end \ No newline at end of file +@testset "HyperGraphNeuralNetworks undirected hypergraph datasets" begin + # Full Cora dataset + cora = getHyperCora(Float64) + + @test nhv(cora) == 2708 + @test nhe(cora) == 2708 + @test cora.vdata.features.size == (1433, 2708) + @test cora.vdata.targets.size == (2708,) + @test cora.hedata == DataStore() + @test cora.hgdata == DataStore() + + # Split into train, val, and test + cora = getHyperCora(Float64; split=true) + @test cora.train.num_vertices == 140 + @test cora.train.num_hyperedges == 535 + @test cora.val.num_vertices == 500 + @test cora.val.num_hyperedges == 1237 + @test cora.test.num_vertices == 1000 + @test cora.test.num_hyperedges == 1882 + + # Full CiteSeer dataset + cs = getHyperCiteSeer(Int) + @test nhv(cs) == 3327 + @test nhe(cs) == 3327 + @test cs.vdata.features.size == (3703, 3327) + @test cs.vdata.targets.size == (3327,) + @test cs.hedata == DataStore() + @test cs.hgdata == DataStore() + + # Split into train, val, and test + cs = getHyperCiteSeer(Int; split=true) + @test cs.train.num_vertices == 120 + @test cs.train.num_hyperedges == 337 + @test cs.val.num_vertices == 500 + @test cs.val.num_hyperedges == 1036 + @test cs.test.num_vertices == 1015 + @test cs.test.num_hyperedges == 1752 + +end \ No newline at end of file From 8ae4240967044d29da6c91658dc38de2b239b927 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 12 Sep 2025 10:43:03 +0100 Subject: [PATCH 27/65] Docs --- src/datasets/datasets.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/datasets/datasets.jl b/src/datasets/datasets.jl index 3078a57..9a9e3b0 100644 --- a/src/datasets/datasets.jl +++ b/src/datasets/datasets.jl @@ -35,6 +35,16 @@ function _convert_citation_graphdata( end +""" + getHyperCora( + T::Type{R}; + split::Bool = false + ) where {R <: Real} + + Represent the Cora dataset (Sen et al., 2008. DOI: 10.1609/aimag.v29i3.2157) as an undirected hypergraph. If + `split` is `true` (default `false`), use standard partitions to split the data into train, validation, and test + sets. +""" function getHyperCora( T::Type{R}; split::Bool = false @@ -42,6 +52,16 @@ function getHyperCora( _convert_citation_graphdata(MLDatasets.Cora(), T; split=split) end +""" + getHyperCiteSeer( + T::Type{R}; + split::Bool = false + ) where {R <: Real} + + Represent the CiteSeer dataset (Sen et al., 2008. DOI: 10.1609/aimag.v29i3.2157) as an undirected hypergraph. If + `split` is `true` (default `false`), use standard partitions to split the data into train, validation, and test + sets. +""" function getHyperCiteSeer( T::Type{R}; split::Bool = false From be3bb082392e69ecda19faaf1a0c4d67c8b73a8e Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 12 Sep 2025 10:46:29 +0100 Subject: [PATCH 28/65] Attempt at auto-downloading datasets --- .github/workflows/CI.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 439b8c8..a7171c8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -33,6 +33,8 @@ jobs: - name: Install matplotlib run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi shell: bash + - name: Setup shell + run: export DATADEPS_ALWAYS_ACCEPT = true - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 with: From 32f1dfab8434e798b02b80cf6800cf4cdca2bba8 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 12 Sep 2025 10:47:02 +0100 Subject: [PATCH 29/65] Resolve merge issue --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a7171c8..9d5b3a6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -35,7 +35,7 @@ jobs: shell: bash - name: Setup shell run: export DATADEPS_ALWAYS_ACCEPT = true - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.julia-version }} From d82728dff09ddd2e537b9ba02208aca8fae4e940 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 15 Sep 2025 09:28:33 +0100 Subject: [PATCH 30/65] Bash syntax issue? --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b05c5ce..a036664 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -34,7 +34,7 @@ jobs: run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi shell: bash - name: Setup shell - run: export DATADEPS_ALWAYS_ACCEPT = true + run: export DATADEPS_ALWAYS_ACCEPT=true - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: From 08f7628eb18da3e64878a5c01cecc9ae58e20e61 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 15 Sep 2025 09:38:09 +0100 Subject: [PATCH 31/65] Still an issue with the environment --- test/runtests.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 62552ee..1ecd255 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,7 +6,10 @@ using GNNGraphs using MLUtils using HyperGraphNeuralNetworks using SimpleHypergraphs -using SimpleDirectedHypergraphs +using SimpleDirectedHypergraph + +# Necessary for MLDatasets +ENV["DATADEPS_ALWAYS_ACCEPT"] = true # Example undirected hypergraph uh1 = Hypergraph{Float64, Int, String}(11,5) From c970a6ce2ec268c306f3269a1fc39496e76b5b52 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 15 Sep 2025 09:43:38 +0100 Subject: [PATCH 32/65] import silliness --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 1ecd255..6c2b8a8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,7 +6,7 @@ using GNNGraphs using MLUtils using HyperGraphNeuralNetworks using SimpleHypergraphs -using SimpleDirectedHypergraph +using SimpleDirectedHypergraphs # Necessary for MLDatasets ENV["DATADEPS_ALWAYS_ACCEPT"] = true From 059bb475a1f85639e432d09b35388943e8ef5b71 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 15 Sep 2025 09:59:01 +0100 Subject: [PATCH 33/65] Silly me, I just realized that I only implemented tests for undirected hypergraphs --- test/runtests.jl | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 6c2b8a8..b63588d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -592,14 +592,8 @@ end 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[1].num_hyperedges == 5 - @test hgnns_rand[1].num_hypergraphs == 2 @test hgnns_rand[2].num_vertices == 1 - @test hgnns_rand[2].num_hyperedges == 1 - @test hgnns_rand[2].num_hypergraphs == 1 @test hgnns_rand[3].num_vertices == 2 - @test hgnns_rand[3].num_hyperedges == 2 - @test hgnns_rand[3].num_hypergraphs == 1 end @testset "HyperGraphNeuralNetworks split hyperedges" begin @@ -681,12 +675,8 @@ end rng = Xoshiro(42) hgnns_rand = random_split_hyperedges(hgnn1, [0.7, 0.3], rng) @test length(hgnns_rand) == 2 - @test hgnns_rand[1].num_vertices == 9 @test hgnns_rand[1].num_hyperedges == 4 - @test hgnns_rand[1].num_hypergraphs == 2 - @test hgnns_rand[2].num_vertices == 3 @test hgnns_rand[2].num_hyperedges == 1 - @test hgnns_rand[2].num_hypergraphs == 1 end @testset "HyperGraphNeuralNetworks split hypergraphs" begin @@ -766,14 +756,8 @@ end 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_vertices == 2 - @test hgnns_rand[1].num_hyperedges == 2 @test hgnns_rand[1].num_hypergraphs == 1 - @test hgnns_rand[2].num_vertices == 5 - @test hgnns_rand[2].num_hyperedges == 2 @test hgnns_rand[2].num_hypergraphs == 1 - @test hgnns_rand[3].num_vertices == 4 - @test hgnns_rand[3].num_hyperedges == 3 @test hgnns_rand[3].num_hypergraphs == 1 end From bc414cb33eabda3bc19a87ddfdd168b6dde8199f Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 15 Sep 2025 13:26:19 +0100 Subject: [PATCH 34/65] NOW we're ready for action --- src/core/split.jl | 74 ++++++------ test/runtests.jl | 280 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 295 insertions(+), 59 deletions(-) diff --git a/src/core/split.jl b/src/core/split.jl index 4fe8128..9e69727 100644 --- a/src/core/split.jl +++ b/src/core/split.jl @@ -259,21 +259,19 @@ function split_vertices( hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[mask]] + 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}( - 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)) - ), + hg_tail, + hg_head, length(v2he_tail), length(he2v_tail), length(unique_hgids), @@ -614,26 +612,24 @@ function split_hyperedges( hypergraph_ids = [hgmap[x] for x in hg.hypergraph_ids[rel_vs]] end + 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}( - 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)) - ), + hg_tail, + hg_head, length(v2he_tail), length(he2v_tail), length(unique_hgids), hypergraph_ids, - getobs(hg.vdata, collect(keys(vmap))), + getobs(hg.vdata, rel_vs), getobs(hg.hedata, mask), getobs(hg.hgdata, unique_hgids) ) @@ -714,12 +710,12 @@ end val_mask::Union{BitVector, Nothing} = nothing, ) where {T <: Real, D <: AbstractDict{Int, T}} - split_hyperedges( + split_hypergraphs( hg::HGNNHypergraph{T,D}, index_groups::AbstractVector{V} ) where {T <: Real, D <: AbstractDict{Int, T}, V <: AbstractVector{Int}} - split_hyperedges( + split_hypergraphs( hg::HGNNHypergraph{T,D}, train_inds::AbstractVector{Int}, test_inds::AbstractVector{Int}; @@ -944,21 +940,19 @@ function split_hypergraphs( hypergraph_ids = [hgmap[hg.hypergraph_ids[v]] for v in rel_vs] + 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}( - 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)) - ), + hg_tail, + hg_head, length(v2he_tail), length(he2v_tail), length(keys(hgmap)), @@ -1095,7 +1089,7 @@ function random_split_vertices( # 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[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] diff --git a/test/runtests.jl b/test/runtests.jl index b63588d..1ce4330 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -534,19 +534,19 @@ end @test hgnns[1].num_vertices == 5 @test hgnns[1].num_hyperedges == 3 @test hgnns[1].num_hypergraphs == 2 - @test getobs(hgnns[1].vdata, 1) == getobs(hgnn1.vdata, 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) == getobs(hgnn1.vdata, 4) + @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) == getobs(hgnn1.vdata, 9) + @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 @@ -594,10 +594,92 @@ end @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 vertices of undirected hypergraphs + # Split hyperedges of undirected hypergraphs hgnn1 = HGNNHypergraph( uh1; hypergraph_ids = uid1, @@ -612,29 +694,29 @@ end BitVector((true, false, false, false, false)) ] - # Split vertices using masks + # 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) == getobs(hgnn1.vdata, 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) == getobs(hgnn1.vdata, 7) + @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) == getobs(hgnn1.vdata, 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 vertices by train-val-test labeled masks + # 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] @@ -652,14 +734,14 @@ end [1] ] - # Split vertices using hyperedge indices + # 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 vertices by train-val-test labeled indices + # 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] @@ -677,12 +759,93 @@ end @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 vertices of undirected hypergraphs + # Split hypergraphs of undirected hypergraphs hgnn1 = HGNNHypergraph( uh1; hypergraph_ids = uid2, @@ -697,29 +860,29 @@ end BitVector((false, false, true)) ] - # Split vertices using masks + # 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) == getobs(hgnn1.vdata, 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) == getobs(hgnn1.vdata, 5) + @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) == getobs(hgnn1.vdata, 7) + @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 vertices by train-val-test labeled masks + # 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] @@ -733,14 +896,14 @@ end hginds = [[1], [2], [3]] - # Split vertices using vertex indices + # 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 vertices by train-val-test labeled indices + # 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] @@ -759,6 +922,85 @@ end @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 @testset "HyperGraphNeuralNetworks undirected hypergraph datasets" begin From 898f765149e4c9ba1ea0a3a234b3e0eb71e2da62 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 15 Sep 2025 13:36:10 +0100 Subject: [PATCH 35/65] One last fix? --- test/runtests.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 1ce4330..7490523 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1009,8 +1009,8 @@ end @test nhv(cora) == 2708 @test nhe(cora) == 2708 - @test cora.vdata.features.size == (1433, 2708) - @test cora.vdata.targets.size == (2708,) + @test size(cora.vdata.features) == (1433, 2708) + @test size(cora.vdata.targets) == (2708,) @test cora.hedata == DataStore() @test cora.hgdata == DataStore() @@ -1027,8 +1027,8 @@ end cs = getHyperCiteSeer(Int) @test nhv(cs) == 3327 @test nhe(cs) == 3327 - @test cs.vdata.features.size == (3703, 3327) - @test cs.vdata.targets.size == (3327,) + @test size(cs.vdata.features) == (3703, 3327) + @test size(cs.vdata.targets) == (3327,) @test cs.hedata == DataStore() @test cs.hgdata == DataStore() From 7d0fc5ec3a7fc9eeb6f37327487ca2a330aea995 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 16 Sep 2025 09:42:46 +0100 Subject: [PATCH 36/65] Documentation --- src/core/split.jl | 82 ++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/src/core/split.jl b/src/core/split.jl index 9e69727..e58634a 100644 --- a/src/core/split.jl +++ b/src/core/split.jl @@ -29,11 +29,11 @@ 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 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. + 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}, @@ -61,6 +61,7 @@ function split_vertices( 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 @@ -70,10 +71,12 @@ function split_vertices( 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] @@ -194,11 +197,11 @@ end 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 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. + 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}, @@ -226,6 +229,7 @@ function split_vertices( 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] @@ -241,11 +245,13 @@ function split_vertices( 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] @@ -259,6 +265,7 @@ function split_vertices( 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 @@ -375,11 +382,11 @@ end 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 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. + 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}, @@ -406,6 +413,7 @@ function split_hyperedges( 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 @@ -415,12 +423,15 @@ function split_hyperedges( 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 @@ -544,11 +555,11 @@ end `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 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. + 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}, @@ -578,6 +589,7 @@ function split_hyperedges( 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] @@ -593,6 +605,7 @@ function split_hyperedges( 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]) @@ -600,6 +613,7 @@ function split_hyperedges( rel_vs = sort(collect(keys(vmap))) + # If relevant, grab `hypergraph_id`s if isnothing(hg.hypergraph_ids) unique_hgids = [1] hypergraph_ids = nothing @@ -612,6 +626,7 @@ function split_hyperedges( 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 @@ -728,11 +743,11 @@ end `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 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. + 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}, @@ -746,6 +761,7 @@ function split_hypergraphs( # 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] @@ -762,6 +778,7 @@ function split_hypergraphs( 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 @@ -771,6 +788,7 @@ function split_hypergraphs( end end + # Relabel hyperedges in vertices for i in eachindex(v2he) v2he[i] = D(hemap[k] => v for (k, v) in v2he[i]) end @@ -885,11 +903,11 @@ end `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 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. + 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}, @@ -902,6 +920,7 @@ function split_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] @@ -921,6 +940,7 @@ function split_hypergraphs( 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]) @@ -933,6 +953,7 @@ function split_hypergraphs( 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]) @@ -940,6 +961,7 @@ function split_hypergraphs( 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 From 1807b24b41aa5218e91427f2ed816d61735230fa Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 06:47:48 +0100 Subject: [PATCH 37/65] Update to CompatHelper workflow; currently having permissions issues --- .github/workflows/CompatHelper.yml | 36 ++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) 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 From 3549c32844f3568e5f06ce51fe1846a39da542df Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 08:09:39 +0100 Subject: [PATCH 38/65] Random tests --- src/core/generate.jl | 2 +- test/runtests.jl | 118 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 7 deletions(-) diff --git a/src/core/generate.jl b/src/core/generate.jl index bb8a1e7..70cae49 100644 --- a/src/core/generate.jl +++ b/src/core/generate.jl @@ -214,7 +214,7 @@ 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 diff --git a/test/runtests.jl b/test/runtests.jl index b2d99ff..e034730 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -519,8 +519,8 @@ end 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) + @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) @@ -536,10 +536,10 @@ end 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) + @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) @@ -550,6 +550,112 @@ end (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 From 56d9dc0663eed4eedb03a9b1972da1e166bb5eda Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 08:32:29 +0100 Subject: [PATCH 39/65] Documentation and prep for query tests --- src/core/query.jl | 29 ++++++++++++++++++++++------- test/runtests.jl | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/core/query.jl b/src/core/query.jl index af32d98..baebd79 100644 --- a/src/core/query.jl +++ b/src/core/query.jl @@ -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} @@ -784,7 +783,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,7 +802,15 @@ 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. +""" +has_self_loops(_::H) where {H <: AbstractHGNNHypergraph} = false function has_self_loops(hg::H) where {H <: AbstractHGNNDiHypergraph} vs_tail = Set.(collect.(keys.(hg.hg_tail.he2v))) @@ -812,6 +819,14 @@ function has_self_loops(hg::H) where {H <: AbstractHGNNDiHypergraph} any([length(intersect(vs_tail[i], vs_head[i])) for i in eachindex(vs_tail)]) end +""" + 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/test/runtests.jl b/test/runtests.jl index e034730..af77796 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -660,6 +660,52 @@ end @testset "HyperGraphNeuralNetworks query" begin + # hyperedge_index + + # get_hyperedge_weights + + # get_hyperedge_weight + + # degree + + # indegree + + # outdegree + + # has_vertex + + # vertices + + # all_neighbors + + # in_neighbors + + # out_neighbors + + # hyperedge_neighbors + + # isolated_vertices + + # incidence_matrix + + # complex_incidence_matrix + + # vertex_weight_matrix + + # hyperedge_weight_matrix + + # vertex_degree_matrix + + # hyperedge_degree_matrix + + # normalized_laplacian + + # hypergraph_ids + + # has_self_loops + + # has_multi_hyperedges + end @testset "HyperGraphNeuralNetworks transforms" begin From 6c3ff9e60821febdce11f43844e3c1b231994df8 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 08:36:18 +0100 Subject: [PATCH 40/65] More missing docs --- src/core/query.jl | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/core/query.jl b/src/core/query.jl index baebd79..b148733 100644 --- a/src/core/query.jl +++ b/src/core/query.jl @@ -669,6 +669,16 @@ function hyperedge_weight_matrix(hg::H; weighting_function::Function=sum) where (Wt, Wh) end + +""" + 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, degrees are provided as a tuple `(Dt, Dh)`, + where `Dt` is the number of hyperedges in which a given vertex is in the tail and `Dh` is the number of hyperedges + in which that vertex is in the head. +""" vertex_degree_matrix(hg::H) where {H <: AbstractSimpleHypergraph} = Diagonal(length.(keys.(hg.v2he))) vertex_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} = ( @@ -676,6 +686,15 @@ vertex_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} = ( Diagonal(length.(keys.(hg.hg_head.v2he))) ) +""" + 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, degrees are provided as a tuple `(Dt, Dh)`, + where `Dt` is the number of vertices in the tail of the hyperedge and `Dh` is the number of vertices in the head of + the hyperedge. +""" hyperedge_degree_matrix(hg::H) where {H <: AbstractSimpleHypergraph} = Diagonal(length.(keys.(hg.he2v))) hyperedge_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} = ( From 2c0675ee9532afed5f548fb39799171e7ab9c680 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 09:24:11 +0100 Subject: [PATCH 41/65] Bunch of small fixes and small tests; still very much WIP --- src/HyperGraphNeuralNetworks.jl | 2 - src/core/query.jl | 84 ++++++++++++++++----------------- test/runtests.jl | 46 +++++++++++++++--- 3 files changed, 82 insertions(+), 50 deletions(-) diff --git a/src/HyperGraphNeuralNetworks.jl b/src/HyperGraphNeuralNetworks.jl index 6e2ddc1..ae6ae5d 100644 --- a/src/HyperGraphNeuralNetworks.jl +++ b/src/HyperGraphNeuralNetworks.jl @@ -28,8 +28,6 @@ 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 diff --git a/src/core/query.jl b/src/core/query.jl index b148733..3b58b81 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 @@ -150,9 +150,9 @@ Graphs.vertices(hg::H) where {H <: AbstractHGNNDiHypergraph} = 1:(hg.num_vertice 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} @@ -163,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} @@ -175,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} @@ -187,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} = [indegree(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)) @@ -204,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 @@ -218,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 """ @@ -246,7 +247,7 @@ 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) @@ -271,7 +272,7 @@ 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) @@ -281,7 +282,7 @@ function all_neighbors(hg::H, i::Int; same_side::Bool=false) where {H <: Abstrac 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. @@ -293,7 +294,7 @@ 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} +function Graphs.inneighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} if same_side all_neighbors(hg.hg_head) else @@ -314,7 +315,7 @@ function in_neighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHy 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. @@ -327,7 +328,7 @@ 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} +function Graphs.inneighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} if same_side all_neighbors(hg.hg_head, i) else @@ -345,7 +346,7 @@ function in_neighbors(hg::H, i::Int; same_side::Bool=false) where {H <: Abstract 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. @@ -357,7 +358,7 @@ 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} +function Graphs.outneighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} if same_side all_neighbors(hg.hg_tail) else @@ -378,7 +379,7 @@ function out_neighbors(hg::H; same_side::Bool=false) where {H <: AbstractHGNNDiH 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. @@ -391,7 +392,7 @@ 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} +function Graphs.outneighbors(hg::H, i::Int; same_side::Bool=false) where {H <: AbstractHGNNDiHypergraph} if same_side all_neighbors(hg.hg_tail, i) else @@ -408,7 +409,6 @@ function out_neighbors(hg::H, i::Int; same_side::Bool=false) where {H <: Abstrac end end - """ hyperedge_neighbors(hg::H) where {H <: AbstractHGNNHypergraph} @@ -675,15 +675,15 @@ end 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, degrees are provided as a tuple `(Dt, Dh)`, - where `Dt` is the number of hyperedges in which a given vertex is in the tail and `Dh` is the number of hyperedges - in which that vertex is in the head. + 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(length.(keys.(hg.v2he))) +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)) ) """ @@ -691,9 +691,9 @@ vertex_degree_matrix(hg::H) where {H <: AbstractDirectedHypergraph} = ( 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, degrees are provided as a tuple `(Dt, Dh)`, - where `Dt` is the number of vertices in the tail of the hyperedge and `Dh` is the number of vertices in the head of - the hyperedge. + 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))) diff --git a/test/runtests.jl b/test/runtests.jl index af77796..3c531e4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,9 +4,9 @@ using Test using Graphs using GNNGraphs using MLUtils -using HyperGraphNeuralNetworks using SimpleHypergraphs using SimpleDirectedHypergraphs +using HyperGraphNeuralNetworks # Necessary for MLDatasets ENV["DATADEPS_ALWAYS_ACCEPT"] = true @@ -659,24 +659,58 @@ end end @testset "HyperGraphNeuralNetworks query" begin + hgnn = HGNNHypergraph(uh1.v2he, uh1.he2v, 11, 5, 2, uid1, DataStore(), DataStore(), DataStore()) # hyperedge_index - + @test hyperedge_index(hgnn) == [ + [1, 2, 4], + [2, 3, 5], + [4, 6], + [7, 10, 11], + [8, 9, 10], + ] + # 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] + ] # get_hyperedge_weight + @test get_hyperedge_weight(hgnn, 2) == [3.0, 0.0, 12.0] + + # has_vertex + @test has_vertex(hgnn, 10) + @test !(has_vertex(hgnn, 12)) + + # vertices + @test vertices(hgnn) == 1:11 # degree + @test degree(hgnn) == [1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1] # indegree # outdegree - # has_vertex - - # vertices - # 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] # in_neighbors From 49bb7ff611696853dc694cb49fee58cb97f89043 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 13:29:39 +0100 Subject: [PATCH 42/65] Tests pass for query for UNDIRECTED hypergraphs only --- Project.toml | 2 ++ src/HyperGraphNeuralNetworks.jl | 8 ++++--- src/core/query.jl | 33 ++++++++++++++++---------- test/runtests.jl | 42 +++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/Project.toml b/Project.toml index 7846324..3dc1dcd 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" MLDatasets = "eb30cadb-4394-5ae3-aed4-317e484a6458" MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54" @@ -23,6 +24,7 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] +LinearAlgebra = "1.11.0" MLDatasets = "0.7.18" Reactant = ">= 0.2.0" SimpleDirectedHypergraphs = "0.1.0" diff --git a/src/HyperGraphNeuralNetworks.jl b/src/HyperGraphNeuralNetworks.jl index ae6ae5d..9dccb1e 100644 --- a/src/HyperGraphNeuralNetworks.jl +++ b/src/HyperGraphNeuralNetworks.jl @@ -3,6 +3,7 @@ module HyperGraphNeuralNetworks using Random using DataStructures: counter using StatsBase: FrequencyWeights, sample +using LinearAlgebra using InvertedIndices using Graphs using GNNGraphs @@ -28,9 +29,10 @@ include("core/query.jl") export hyperedge_index, get_hyperedge_weight, get_hyperedge_weights export has_vertex, vertices, isolated_vertices -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") diff --git a/src/core/query.jl b/src/core/query.jl index 3b58b81..27b9cf9 100644 --- a/src/core/query.jl +++ b/src/core/query.jl @@ -420,8 +420,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) @@ -441,9 +442,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 @@ -560,7 +561,7 @@ 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 @@ -575,7 +576,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) @@ -617,7 +618,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 @@ -632,7 +636,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 @@ -703,7 +710,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`. @@ -731,7 +738,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) @@ -745,7 +752,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 @@ -779,7 +786,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 @@ -829,9 +836,9 @@ end 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. """ -has_self_loops(_::H) where {H <: AbstractHGNNHypergraph} = false +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))) diff --git a/test/runtests.jl b/test/runtests.jl index 3c531e4..ba9e864 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ using Random using StatsBase +using LinearAlgebra using Test using Graphs using GNNGraphs @@ -717,28 +718,69 @@ end # out_neighbors # 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 + ] # complex_incidence_matrix + # 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)) # 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)) # vertex_degree_matrix + @test vertex_degree_matrix(hgnn) == Diagonal([1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1]) # hyperedge_degree_matrix + @test hyperedge_degree_matrix(hgnn) == Diagonal([3, 3, 2, 3, 3]) # 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) # hypergraph_ids + @test hypergraph_ids(hgnn) == uid1 + + # Case with no hypergraph_ids + hgnn2 = HGNNHypergraph(uh1.v2he, uh1.he2v, 11, 5, 2, nothing, DataStore(), DataStore(), DataStore()) + @test hypergraph_ids(hgnn2) == ones(11) # has_self_loops + @test !(has_self_loops(hgnn)) # has_multi_hyperedges + @test !(has_multi_hyperedges(hgnn)) + hg3 = Hypergraph{Bool}(2,2) + hg3[:,:] .= true + hgnn3 = HGNNHypergraph(hg3) + @test has_multi_hyperedges(hgnn3) end From 936c4e63f59df916969815a9ca6bc379355b786c Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 13:33:30 +0100 Subject: [PATCH 43/65] Version issue --- Project.toml | 2 +- test/runtests.jl | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 3dc1dcd..d36c51c 100644 --- a/Project.toml +++ b/Project.toml @@ -24,7 +24,7 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] -LinearAlgebra = "1.11.0" +LinearAlgebra = ">= 1.10.0" MLDatasets = "0.7.18" Reactant = ">= 0.2.0" SimpleDirectedHypergraphs = "0.1.0" diff --git a/test/runtests.jl b/test/runtests.jl index ba9e864..d6e50cf 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -660,6 +660,8 @@ end end @testset "HyperGraphNeuralNetworks query" begin + # TODO: do this for directed hypergraphs + hgnn = HGNNHypergraph(uh1.v2he, uh1.he2v, 11, 5, 2, uid1, DataStore(), DataStore(), DataStore()) # hyperedge_index From 56c47cadf43b4946b18a03a01acf07d3ee7b29a4 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 13:49:51 +0100 Subject: [PATCH 44/65] CI issues --- .github/workflows/CI.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a036664..d8b7edd 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -41,6 +41,13 @@ jobs: version: ${{ matrix.julia-version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 + - name: Configure test environment + shell: julia --project=. --color=yes {0} + run: | + using Pkg + 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-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 From 342237730ff590ee32434a298b0625c5409ee2fc Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 13:52:47 +0100 Subject: [PATCH 45/65] Another CI tweak --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d8b7edd..53e0cdb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -42,7 +42,7 @@ jobs: arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 - name: Configure test environment - shell: julia --project=. --color=yes {0} + shell: julia --color=yes {0} run: | using Pkg Pkg.develop(PackageSpec(path=pwd())) From 7bb39b7e7c7b1e1d2d33523a747fbf77350c4092 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 14:08:17 +0100 Subject: [PATCH 46/65] Conda issue... --- .github/workflows/CI.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 53e0cdb..5ea7767 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -30,11 +30,10 @@ jobs: arch: - x64 steps: - - name: Install matplotlib - run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi - shell: bash - name: Setup shell - run: export DATADEPS_ALWAYS_ACCEPT=true + run: | + export DATADEPS_ALWAYS_ACCEPT=true + export PYTHON="" - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: @@ -48,6 +47,10 @@ jobs: Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() Pkg.add(name="OpenSSL_jll", version="3.0") + Pkg.add("Conda") + Pkg.add("PyCall") + using Conda + Conda.add("matplotlib"; channel="") - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 From 3674b2d5177b72f55c1f469938a417be6f4da00a Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 14:35:30 +0100 Subject: [PATCH 47/65] Damn PyCall --- .github/workflows/CI.yml | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 5ea7767..cdd6bbb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,17 +29,32 @@ jobs: - ubuntu-latest arch: - x64 + env: + DATADEPS_ALWAYS_ACCEPT: true + PYTHON: "" + ROOTENV: "/home/runner/.julia/conda/3/x86_64" + MINICONDA_VERSION: "3" + USE_MINIFORGE: true + CONDA_EXE: "/home/runner/.julia/conda/3/x86_64/bin/conda" steps: - - name: Setup shell - run: | - export DATADEPS_ALWAYS_ACCEPT=true - export PYTHON="" - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.julia-version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 + - name: Conda/PyCall + shell: julia --color=yes {0} + run: | + using Pkg + Pkg.develop(PackageSpec(path=pwd())) + Pkg.instantiate() + Pkg.build("PyCall") + using Conda + print(read(joinpath(dirname(pathof(Conda)), "..", "deps", "deps.jl"),String)) + Conda.pip_interop(true) + Conda.pip("install", "pythtb") + Conda.pip("install", "matplotlib") - name: Configure test environment shell: julia --color=yes {0} run: | @@ -47,10 +62,6 @@ jobs: Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() Pkg.add(name="OpenSSL_jll", version="3.0") - Pkg.add("Conda") - Pkg.add("PyCall") - using Conda - Conda.add("matplotlib"; channel="") - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 From adaab20ca16df42a10c1e52a9b944d02950575c9 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 14:56:09 +0100 Subject: [PATCH 48/65] Now trying not to use Conda at all --- .github/workflows/CI.yml | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index cdd6bbb..723f208 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -31,36 +31,24 @@ jobs: - x64 env: DATADEPS_ALWAYS_ACCEPT: true - PYTHON: "" - ROOTENV: "/home/runner/.julia/conda/3/x86_64" - MINICONDA_VERSION: "3" - USE_MINIFORGE: true - CONDA_EXE: "/home/runner/.julia/conda/3/x86_64/bin/conda" + PYTHON: "/usr/bin/python3" steps: + - name: Install matplotlib + run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi + shell: bash - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.julia-version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 - - name: Conda/PyCall - shell: julia --color=yes {0} - run: | - using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate() - Pkg.build("PyCall") - using Conda - print(read(joinpath(dirname(pathof(Conda)), "..", "deps", "deps.jl"),String)) - Conda.pip_interop(true) - Conda.pip("install", "pythtb") - Conda.pip("install", "matplotlib") - name: Configure test environment shell: julia --color=yes {0} run: | using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() + Pkg.add("PyCall") Pkg.add(name="OpenSSL_jll", version="3.0") - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 From b13b3524d4702d0d3b2389979f9d5182d13954bd Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 15:19:26 +0100 Subject: [PATCH 49/65] One more try... --- .github/workflows/CI.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 723f208..4c7896e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -31,11 +31,15 @@ jobs: - x64 env: DATADEPS_ALWAYS_ACCEPT: true - PYTHON: "/usr/bin/python3" + CONDA_JL_HOME: "/home/runner/miniconda/envs/conda_jl" steps: - - name: Install matplotlib - run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi + - name: Setup mamba + - uses: mamba-org/setup-micromamba@v2 + with: + micromamba-binary-path: ${{ runner.temp }}/bin/conda + - name: Mamba/conda environment shell: bash + run: ${{ runner.temp }}/bin/conda create -p /home/runner/miniconda/envs/conda_jl python=3.10 matplotlib - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: @@ -48,7 +52,6 @@ jobs: using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() - Pkg.add("PyCall") Pkg.add(name="OpenSSL_jll", version="3.0") - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 From 6aabeefc2f80bb049285db3a0952d460e7da7712 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 17 Sep 2025 15:23:16 +0100 Subject: [PATCH 50/65] CI syntax fix --- .github/workflows/CI.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4c7896e..b3f308a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -33,10 +33,9 @@ jobs: DATADEPS_ALWAYS_ACCEPT: true CONDA_JL_HOME: "/home/runner/miniconda/envs/conda_jl" steps: - - name: Setup mamba - - uses: mamba-org/setup-micromamba@v2 - with: - micromamba-binary-path: ${{ runner.temp }}/bin/conda + - uses: mamba-org/setup-micromamba@v2 + with: + micromamba-binary-path: ${{ runner.temp }}/bin/conda - name: Mamba/conda environment shell: bash run: ${{ runner.temp }}/bin/conda create -p /home/runner/miniconda/envs/conda_jl python=3.10 matplotlib From e3bf67bdae5f1cfbd59881ddec0eb552e7929138 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 18 Sep 2025 10:44:52 +0100 Subject: [PATCH 51/65] Now the issue is matplotlib again. Gosh dangit --- .github/workflows/CI.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b3f308a..02b15e4 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -33,12 +33,15 @@ jobs: DATADEPS_ALWAYS_ACCEPT: true CONDA_JL_HOME: "/home/runner/miniconda/envs/conda_jl" steps: - - uses: mamba-org/setup-micromamba@v2 - with: - micromamba-binary-path: ${{ runner.temp }}/bin/conda - - name: Mamba/conda environment + # - uses: mamba-org/setup-micromamba@v2 + # with: + # micromamba-binary-path: ${{ runner.temp }}/bin/conda + # - name: Mamba/conda environment + # shell: bash + # run: ${{ runner.temp }}/bin/conda create -p /home/runner/miniconda/envs/conda_jl python=3.10 matplotlib + - name: Install matplotlib + run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi shell: bash - run: ${{ runner.temp }}/bin/conda create -p /home/runner/miniconda/envs/conda_jl python=3.10 matplotlib - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: From 11fe772a506cffd6efc864bb7edfe3342046002a Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 18 Sep 2025 10:57:46 +0100 Subject: [PATCH 52/65] Some preliminary tests involving directed hypergraphs for query.jl --- test/runtests.jl | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index d6e50cf..f908456 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -663,6 +663,13 @@ end # 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) == [ @@ -672,6 +679,10 @@ end [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) == [ @@ -680,10 +691,23 @@ end [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 # has_vertex @test has_vertex(hgnn, 10) From 3e076bc7a6b2f1446193022dadcb178419ffe209 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 18 Sep 2025 11:51:02 +0100 Subject: [PATCH 53/65] Come on, openssl... --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 02b15e4..3519bc1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -49,7 +49,7 @@ jobs: arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 - name: Configure test environment - shell: julia --color=yes {0} + shell: julia --project=. --color=yes {0} run: | using Pkg Pkg.develop(PackageSpec(path=pwd())) From bbeb7ab60aa770e526e898c53d604178e796c982 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 18 Sep 2025 11:55:20 +0100 Subject: [PATCH 54/65] Trying swapping the order --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3519bc1..840ba9b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -48,14 +48,14 @@ jobs: version: ${{ matrix.julia-version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 - name: Configure test environment - shell: julia --project=. --color=yes {0} + shell: julia --color=yes {0} run: | using Pkg 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-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 From 0d057eb7a37d8fa8b611ae7561d928f5777ef1f2 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 18 Sep 2025 12:38:50 +0100 Subject: [PATCH 55/65] SSL version --- .github/workflows/CI.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 840ba9b..1434807 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -53,9 +53,7 @@ jobs: shell: julia --color=yes {0} run: | using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate() - Pkg.add(name="OpenSSL_jll", version="3.0") + Pkg.add(name="OpenSSL_jll", version="3.3") - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 @@ -85,9 +83,7 @@ jobs: shell: julia --project=docs --color=yes {0} run: | using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate() - Pkg.add(name="OpenSSL_jll", version="3.0") + Pkg.add(name="OpenSSL_jll", version="3.3") - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-docdeploy@v1 env: From cf105219f1b5ec870df514d30a542a63fb04be8f Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 18 Sep 2025 12:49:10 +0100 Subject: [PATCH 56/65] SSL version, again --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1434807..1e98a55 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -53,7 +53,7 @@ jobs: shell: julia --color=yes {0} run: | using Pkg - Pkg.add(name="OpenSSL_jll", version="3.3") + Pkg.add(name="OpenSSL_jll", version="3.5") - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 @@ -83,7 +83,7 @@ jobs: shell: julia --project=docs --color=yes {0} run: | using Pkg - Pkg.add(name="OpenSSL_jll", version="3.3") + Pkg.add(name="OpenSSL_jll", version="3.5") - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-docdeploy@v1 env: From 3b15bfd3e620cde779ff4758473c790884ae6b5b Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 18 Sep 2025 12:59:20 +0100 Subject: [PATCH 57/65] why, oh why, is this my fate? --- .github/workflows/CI.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1e98a55..334c7d7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -53,7 +53,9 @@ jobs: shell: julia --color=yes {0} run: | using Pkg - Pkg.add(name="OpenSSL_jll", version="3.5") + 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 @@ -83,7 +85,9 @@ jobs: shell: julia --project=docs --color=yes {0} run: | using Pkg - Pkg.add(name="OpenSSL_jll", version="3.5") + Pkg.develop(PackageSpec(path=pwd())) + Pkg.instantiate() + Pkg.add(name="OpenSSL_jll", version="3") - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-docdeploy@v1 env: From a26d3b78464285e2351b43854f1fcd15d5811a1c Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 19 Sep 2025 08:01:15 +0100 Subject: [PATCH 58/65] Wild swing --- .github/workflows/CI.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 334c7d7..4c1bf08 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -32,6 +32,7 @@ jobs: env: DATADEPS_ALWAYS_ACCEPT: true CONDA_JL_HOME: "/home/runner/miniconda/envs/conda_jl" + LD_LIBRARY_PATH: "" steps: # - uses: mamba-org/setup-micromamba@v2 # with: @@ -52,6 +53,7 @@ jobs: - name: Configure test environment shell: julia --color=yes {0} run: | + versioninfo() using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() From 2f588167d3b7f6d5c2de5aa95adc706b85e85e45 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 19 Sep 2025 08:06:03 +0100 Subject: [PATCH 59/65] Bugfix in tests; and more tests --- .github/workflows/CI.yml | 1 + test/runtests.jl | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4c1bf08..7edd229 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -53,6 +53,7 @@ jobs: - name: Configure test environment shell: julia --color=yes {0} run: | + using InteractiveUtils versioninfo() using Pkg Pkg.develop(PackageSpec(path=pwd())) diff --git a/test/runtests.jl b/test/runtests.jl index f908456..108604c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -709,12 +709,22 @@ end @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] From c7c18b09b5207edfe07dce6fbf577b0acf9dd810 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 19 Sep 2025 11:02:36 +0100 Subject: [PATCH 60/65] Done with it. Done with the datasets. That's split off now in HypergraphDatasets.jl --- .github/workflows/CI.yml | 9 ----- Project.toml | 2 - src/HyperGraphNeuralNetworks.jl | 5 --- src/core/query.jl | 24 +++++------ src/datasets/datasets.jl | 70 --------------------------------- test/runtests.jl | 52 ++++++------------------ 6 files changed, 25 insertions(+), 137 deletions(-) delete mode 100644 src/datasets/datasets.jl diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 7edd229..1562b80 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -31,15 +31,8 @@ jobs: - x64 env: DATADEPS_ALWAYS_ACCEPT: true - CONDA_JL_HOME: "/home/runner/miniconda/envs/conda_jl" LD_LIBRARY_PATH: "" steps: - # - uses: mamba-org/setup-micromamba@v2 - # with: - # micromamba-binary-path: ${{ runner.temp }}/bin/conda - # - name: Mamba/conda environment - # shell: bash - # run: ${{ runner.temp }}/bin/conda create -p /home/runner/miniconda/envs/conda_jl python=3.10 matplotlib - name: Install matplotlib run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi shell: bash @@ -53,8 +46,6 @@ jobs: - name: Configure test environment shell: julia --color=yes {0} run: | - using InteractiveUtils - versioninfo() using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() diff --git a/Project.toml b/Project.toml index d36c51c..8efb9a3 100644 --- a/Project.toml +++ b/Project.toml @@ -13,7 +13,6 @@ Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Lux = "b2108857-7c20-44ae-9111-449ecde12c47" -MLDatasets = "eb30cadb-4394-5ae3-aed4-317e484a6458" MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54" NNlib = "872c559c-99b0-510c-b3b7-b6c96a88d5cd" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -25,7 +24,6 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] LinearAlgebra = ">= 1.10.0" -MLDatasets = "0.7.18" Reactant = ">= 0.2.0" SimpleDirectedHypergraphs = "0.1.0" SimpleHypergraphs = "0.3.0" diff --git a/src/HyperGraphNeuralNetworks.jl b/src/HyperGraphNeuralNetworks.jl index 9dccb1e..0f68b6f 100644 --- a/src/HyperGraphNeuralNetworks.jl +++ b/src/HyperGraphNeuralNetworks.jl @@ -9,7 +9,6 @@ using Graphs using GNNGraphs import GNNGraphs: getn, getdata, normalize_graphdata, cat_features, shortsummary using MLUtils -import MLDatasets using SimpleHypergraphs using SimpleDirectedHypergraphs @@ -51,8 +50,4 @@ include("core/utils.jl") export check_num_vertices, check_num_hyperedges export normalize_graphdata -include("datasets/datasets.jl") - -export getHyperCora, getHyperCiteSeer - end \ No newline at end of file diff --git a/src/core/query.jl b/src/core/query.jl index 27b9cf9..6c80f95 100644 --- a/src/core/query.jl +++ b/src/core/query.jl @@ -189,7 +189,7 @@ Graphs.indegree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHype """ 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} = [indegree(hg, i) for i in inds] +Graphs.outdegree(hg::H, inds::AbstractVector{Int}) where {H <: AbstractHGNNDiHypergraph} = [outdegree(hg, i) for i in inds] """ all_neighbors(hg::H) where {H <: AbstractHGNNHypergraph} @@ -249,10 +249,10 @@ end """ 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 @@ -274,10 +274,10 @@ end """ 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 @@ -304,8 +304,9 @@ function Graphs.inneighbors(hg::H; same_side::Bool=false) where {H <: AbstractHG # 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 - cat(neighbors[i], collect(keys(hg.hg_tail.he2v[he]))) + neighbors[i] = vcat(neighbors[i], collect(keys(hg.hg_tail.he2v[he]))) end neighbors[i] = sort!(unique!(neighbors[i])) end @@ -338,7 +339,7 @@ function Graphs.inneighbors(hg::H, i::Int; same_side::Bool=false) where {H <: Ab # 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 = vcat(neighbors, collect(keys(hg.hg_tail.he2v[he]))) end sort!(unique!(neighbors)) @@ -368,8 +369,9 @@ function Graphs.outneighbors(hg::H; same_side::Bool=false) where {H <: AbstractH # 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 - cat(neighbors[i], collect(keys(hg.hg_head.he2v[he]))) + neighbors[i] = vcat(neighbors[i], collect(keys(hg.hg_head.he2v[he]))) end neighbors[i] = sort!(unique!(neighbors[i])) end @@ -402,7 +404,7 @@ function Graphs.outneighbors(hg::H, i::Int; same_side::Bool=false) where {H <: A # 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 = vcat(neighbors, collect(keys(hg.hg_tail.he2v[he]))) end sort!(unique!(neighbors)) diff --git a/src/datasets/datasets.jl b/src/datasets/datasets.jl deleted file mode 100644 index 9a9e3b0..0000000 --- a/src/datasets/datasets.jl +++ /dev/null @@ -1,70 +0,0 @@ -function _convert_citation_graphdata( - dset::MLDatasets.AbstractDataset, - T::Type{R}; - split::Bool = false -) where {R <: Real} - nv = dset[1].num_nodes - - # Hypergraph vertices are graph vertices - hg = Hypergraph{T}(nv, nv) - - # Include vertices in hyperedges - # Datasets are symmetric, so if (s, d) is an edge, (d, s) is also an edge - for (s, d) in zip(dset[1].edge_index...) - hg[s, d] = convert(T, 1) - end - - # Construct HGNN Hypergraph - # Citation datasets have vertex features but no hyperedge- or hypergraph-level features - hgnnhypergraph = HGNNHypergraph{T}( - hg; - vdata=(features=dset[1].node_data.features, targets=dset[1].node_data.targets) - ) - - if !split - return hgnnhypergraph - else - # Cora comes with default train-val-test splits - return split_vertices( - hgnnhypergraph, - dset[1].node_data.train_mask, - dset[1].node_data.test_mask; - val_mask=dset[1].node_data.val_mask - ) - end - -end - -""" - getHyperCora( - T::Type{R}; - split::Bool = false - ) where {R <: Real} - - Represent the Cora dataset (Sen et al., 2008. DOI: 10.1609/aimag.v29i3.2157) as an undirected hypergraph. If - `split` is `true` (default `false`), use standard partitions to split the data into train, validation, and test - sets. -""" -function getHyperCora( - T::Type{R}; - split::Bool = false -) where {R <: Real} - _convert_citation_graphdata(MLDatasets.Cora(), T; split=split) -end - -""" - getHyperCiteSeer( - T::Type{R}; - split::Bool = false - ) where {R <: Real} - - Represent the CiteSeer dataset (Sen et al., 2008. DOI: 10.1609/aimag.v29i3.2157) as an undirected hypergraph. If - `split` is `true` (default `false`), use standard partitions to split the data into train, validation, and test - sets. -""" -function getHyperCiteSeer( - T::Type{R}; - split::Bool = false -) where {R <: Real} - _convert_citation_graphdata(MLDatasets.CiteSeer(), T; split=split) -end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 108604c..1eff01f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -728,10 +728,22 @@ end # 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) == [ @@ -1313,44 +1325,4 @@ end @test dhgnns_rand[1].num_hypergraphs == 1 @test dhgnns_rand[2].num_hypergraphs == 1 @test dhgnns_rand[3].num_hypergraphs == 1 -end - -@testset "HyperGraphNeuralNetworks undirected hypergraph datasets" begin - # Full Cora dataset - cora = getHyperCora(Float64) - - @test nhv(cora) == 2708 - @test nhe(cora) == 2708 - @test size(cora.vdata.features) == (1433, 2708) - @test size(cora.vdata.targets) == (2708,) - @test cora.hedata == DataStore() - @test cora.hgdata == DataStore() - - # Split into train, val, and test - cora = getHyperCora(Float64; split=true) - @test cora.train.num_vertices == 140 - @test cora.train.num_hyperedges == 535 - @test cora.val.num_vertices == 500 - @test cora.val.num_hyperedges == 1237 - @test cora.test.num_vertices == 1000 - @test cora.test.num_hyperedges == 1882 - - # Full CiteSeer dataset - cs = getHyperCiteSeer(Int) - @test nhv(cs) == 3327 - @test nhe(cs) == 3327 - @test size(cs.vdata.features) == (3703, 3327) - @test size(cs.vdata.targets) == (3327,) - @test cs.hedata == DataStore() - @test cs.hgdata == DataStore() - - # Split into train, val, and test - cs = getHyperCiteSeer(Int; split=true) - @test cs.train.num_vertices == 120 - @test cs.train.num_hyperedges == 337 - @test cs.val.num_vertices == 500 - @test cs.val.num_hyperedges == 1036 - @test cs.test.num_vertices == 1015 - @test cs.test.num_hyperedges == 1752 - end \ No newline at end of file From f7e82ea21ca6862901fd48b829a9d0117a94607f Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Sun, 21 Sep 2025 08:33:52 +0100 Subject: [PATCH 61/65] Docs issue with JLL again --- .github/workflows/CI.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1562b80..a23a152 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -64,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 From 73e0fc58f9d4deab4c08d14bc35a2a251d85e3ed Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Sun, 21 Sep 2025 09:49:36 +0100 Subject: [PATCH 62/65] Tweak --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a23a152..8b62c6f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -77,14 +77,14 @@ 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: | using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() - Pkg.add(name="OpenSSL_jll", version="3") - - uses: julia-actions/julia-buildpkg@v1 + Pkg.add(name="OpenSSL_jll", version="3.0") - uses: julia-actions/julia-docdeploy@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From e6011248d862c40c69b5f3e0549737fb966033d3 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Sun, 21 Sep 2025 10:01:15 +0100 Subject: [PATCH 63/65] AHHHHHHHHH --- .github/workflows/CI.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8b62c6f..fd00be5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -68,8 +68,7 @@ jobs: LD_LIBRARY_PATH: "" steps: - name: Install matplotlib - run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi - shell: bash + run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get update && sudo apt-get install -y python3-matplotlib openssh-server; fi - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: @@ -84,7 +83,7 @@ jobs: using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() - Pkg.add(name="OpenSSL_jll", version="3.0") + Pkg.add(name="OpenSSL_jll", version="3") - uses: julia-actions/julia-docdeploy@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 6fb0dfb03236c8a92fe975ec90f6b4e5bcfa6b12 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Sun, 21 Sep 2025 10:33:44 +0100 Subject: [PATCH 64/65] Revert --- .github/workflows/CI.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index fd00be5..8b62c6f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -68,7 +68,8 @@ jobs: LD_LIBRARY_PATH: "" steps: - name: Install matplotlib - run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get update && sudo apt-get install -y python3-matplotlib openssh-server; fi + run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi + shell: bash - uses: actions/checkout@v5 - uses: julia-actions/setup-julia@v2 with: @@ -83,7 +84,7 @@ jobs: using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() - Pkg.add(name="OpenSSL_jll", version="3") + Pkg.add(name="OpenSSL_jll", version="3.0") - uses: julia-actions/julia-docdeploy@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 500d75fbad7df893317f94ce37d6ed6da0391e4e Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 22 Sep 2025 10:25:34 +0100 Subject: [PATCH 65/65] Query tests done for dihypergraph as well; phew! --- src/core/query.jl | 133 +++++++++++++++++++---------------- test/runtests.jl | 173 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 241 insertions(+), 65 deletions(-) diff --git a/src/core/query.jl b/src/core/query.jl index 6c80f95..d0729db 100644 --- a/src/core/query.jl +++ b/src/core/query.jl @@ -295,24 +295,24 @@ end `j` is in the tail of `e`, then `j` is an incoming neighbor to `i`. Default is false. """ function Graphs.inneighbors(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) - neighbors[i] = [] - for he in hes_i - neighbors[i] = vcat(neighbors[i], collect(keys(hg.hg_tail.he2v[he]))) + # 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 """ @@ -330,20 +330,20 @@ end `j` is in the tail of `e`, then `j` is an incoming neighbor to `i`. Default is false. """ function Graphs.inneighbors(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])) + # 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 - neighbors = vcat(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 """ @@ -360,24 +360,24 @@ end `j` is in the head of `e`, then `j` is an outgoing neighbor to `i`. Default is false. """ function Graphs.outneighbors(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) - neighbors[i] = [] - for he in hes_i - neighbors[i] = vcat(neighbors[i], collect(keys(hg.hg_head.he2v[he]))) + # 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 """ @@ -395,20 +395,21 @@ end `j` is in the head of `e`, then `j` is an outgoing neighbor to `i`. Default is false. """ function Graphs.outneighbors(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])) + # 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 + # 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 """ @@ -568,7 +569,7 @@ function Graphs.incidence_matrix(hg::H) where {H <: AbstractSimpleHypergraph} 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 """ @@ -600,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) @@ -656,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 + + Wh = Matrix(hg.hg_head) + Wh[Wh .=== nothing] .= 0 - (Wt, Wh) + 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 """ @@ -672,10 +679,16 @@ 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 - (Wt, Wh) + Wh = Matrix(hg.hg_head) + Wh[Wh .=== nothing] .= 0 + + 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 @@ -844,7 +857,7 @@ 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 """ diff --git a/test/runtests.jl b/test/runtests.jl index 1eff01f..189b935 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -761,9 +761,97 @@ end ] @test all_neighbors(hgnn, 2) == [1,3,4,5] - # in_neighbors + @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] + ] - # out_neighbors + @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]] @@ -788,40 +876,108 @@ end 0.0 0.0 0.0 1.0 0.0 ] - # complex_incidence_matrix - + 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, 2, nothing, DataStore(), DataStore(), DataStore()) + 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)) @@ -830,6 +986,13 @@ end 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