diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1c5a2fb..7814366 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -70,6 +70,7 @@ jobs: using Pkg Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() + Pkg.add(name="OpenSSL_jll", version="3.0") - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-docdeploy@latest env: @@ -78,6 +79,7 @@ jobs: - name: Run doctests shell: julia --project=docs --color=yes {0} run: | + using OpenSSL_jll using Documenter: DocMeta, doctest using SimpleDirectedHypergraphs DocMeta.setdocmeta!(SimpleDirectedHypergraphs, :DocTestSetup, :(using SimpleDirectedHypergraphs); recursive=true) diff --git a/LICENSE b/LICENSE index c1eb12b..02bf6aa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Evan Walter Clark Spotte-Smith +Copyright (c) 2025 Evan Walter Clark Spotte-Smith, the Community of Researchers Assessing Chemical Transformations and Exploring Reactivity (CoReACTER), and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Project.toml b/Project.toml index 2a7ec62..bfbac85 100644 --- a/Project.toml +++ b/Project.toml @@ -1,32 +1,38 @@ name = "SimpleDirectedHypergraphs" uuid = "a42c6b43-640c-4761-b4a8-4f98c31d02e9" -authors = ["Evan Walter Clark Spotte-Smith"] version = "0.1.0" +authors = ["Evan Walter Clark Spotte-Smith"] [deps] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -SimpleHypergraphs = "aa4a32ff-dd5d-5357-90e3-e7a9512f0501" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SimpleHypergraphs = "aa4a32ff-dd5d-5357-90e3-e7a9512f0501" SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" [compat] DataStructures = "^0.18.11" +GLPK = "1.2.1" Graphs = "1.12.0" +InvertedIndices = "1.3.1" JSON3 = "^1.0.1" +JuMP = "1.29.2" LinearAlgebra = "^1.6.7" +SimpleHypergraphs = "0.3.0" SimpleTraits = "^0.9.4" StatsBase = "^0.34.0" StructTypes = "^1.0.1" -SimpleHypergraphs = "0.3.0" julia = "^1.6.7" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] \ No newline at end of file +test = ["Test"] diff --git a/docs/Project.toml b/docs/Project.toml index 070792c..b602432 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,8 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" -SimpleHypergraphs = "aa4a32ff-dd5d-5357-90e3-e7a9512f0501" SimpleDirectedHypergraphs = "a42c6b43-640c-4761-b4a8-4f98c31d02e9" +SimpleHypergraphs = "aa4a32ff-dd5d-5357-90e3-e7a9512f0501" + +[sources] +SimpleDirectedHypergraphs = {path = ".."} diff --git a/docs/make.jl b/docs/make.jl index 7878ea7..6727147 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -15,6 +15,7 @@ makedocs(; pages=[ "Home" => "index.md", ], + checkdocs=:exports ) deploydocs(; diff --git a/docs/src/reference.md b/docs/src/reference.md index 84be58b..0f7073d 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -121,10 +121,27 @@ Pathfinding ----------- ```@docs -_visit(h::H, v::Int) where {H <: AbstractDirectedHypergraph} -SimpleHypergraphs.shortest_path(b::BipartiteView{H}, source::Int, target::Int) where {H<:AbstractDirectedHypergraph} -SimpleHypergraphs.shortest_path(t::TwoSectionView{H}, source::Int, target::Int) where {H<:AbstractDirectedHypergraph} +SimpleHypergraphs.shortest_path(::BipartiteView{H}, ::Int, ::Int) where {H<:AbstractDirectedHypergraph} +SimpleHypergraphs.shortest_path(::TwoSectionView{H}, ::Int, ::Int) where {H<:AbstractDirectedHypergraph} +forward_reachable(::H, ::Int) where {H<:AbstractDirectedHypergraph} +backward_traceable(::H, ::Int) where {H<:AbstractDirectedHypergraph} +is_reachable(::H, ::Int, ::Int, ::Symbol) where {H<:AbstractDirectedHypergraph} + +all_hyperpaths + +shortest_hyperpath_kk_heuristic + +shortest_hyperpath_kk_ilp + +SnodeDistanceKKHeuristic +SnodeDistanceKKILP + +SimpleHypergraphs.distance(::H, ::SnodeDistanceKKHeuristic, ::AbstractVector{T}) where {H<:AbstractDirectedHypergraph, T<:Real} +SimpleHypergraphs.distance(::H, ::SnodeDistanceKKILP, ::AbstractVector{T}) where {H<:AbstractDirectedHypergraph, T<:Real} + +Graphs.diameter(::H, ::SnodeDistanceKKHeuristic) where {H<:AbstractDirectedHypergraph} +Graphs.diameter(::H, ::SnodeDistanceKKILP) where {H<:AbstractDirectedHypergraph} ``` diff --git a/src/SimpleDirectedHypergraphs.jl b/src/SimpleDirectedHypergraphs.jl index 8c42f5d..7d8b84e 100644 --- a/src/SimpleDirectedHypergraphs.jl +++ b/src/SimpleDirectedHypergraphs.jl @@ -9,6 +9,9 @@ using JSON3 using Random using LinearAlgebra using SimpleTraits +using InvertedIndices +import JuMP +import GLPK export AbstractDirectedHypergraph export DirectedHypergraph, HyperedgeDirection @@ -20,6 +23,10 @@ export to_undirected export get_weakly_connected_components, get_strongly_connected_components +export forward_reachable, backward_traceable, is_reachable +export all_hyperpaths, shortest_hyperpath_kk_heuristic, shortest_hyperpath_kk_ilp + +export SnodeDistanceKKHeuristic, SnodeDistanceKKILP include("abstracttypes.jl") include("dihypergraph.jl") @@ -30,4 +37,7 @@ include("models/twosection.jl") include("models/random-models.jl") include("models/dual.jl") +include("algorithms/paths.jl") +include("algorithms/distance.jl") + end # module diff --git a/src/algorithms/distance.jl b/src/algorithms/distance.jl new file mode 100644 index 0000000..abfa7e9 --- /dev/null +++ b/src/algorithms/distance.jl @@ -0,0 +1,192 @@ +""" + SnodeDistanceKKHeuristic <: AbstractDistance + +Constructor: SnodeDistanceKKHeuristic(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance + +Represent a distance between a set of *source* nodes (`sources`) and a set of *target* nodes (`targets`) in a directed +hypergraph, where a distance in this context is defined as the length of the shortest hyperpath that reaches all +`t ∈ targets`. + +This approach uses the *heuristic* shortest hyperpath algorithm of Krieger & Kececioglu 2022 +(DOI: 10.1186/s13015-022-00217-9). The Krieger & Kececioglu heuristic algorithm commonly (but not always) returns the +true optimal (shortest) hyperpath; this algorithm can thus be thought of as an upper bound of the true distance between +the sinks and the source. To calculate the exact (minimal) distance, instead use `SnodeDistanceKKExact`. +""" +struct SnodeDistanceKKHeuristic <: AbstractDistance + sources::Set{Int} + targets::Set{Int} +end + +""" + SnodeDistanceKKILP <: AbstractDistance + +Constructor: SnodeDistanceKKILP(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance + +Represent a distance between a set of *source* nodes (`sources`) and a set of *target* nodes (`targets`) in a directed +hypergraph, where a distance in this context is defined as the length of the shortest hyperpath that reaches all +`t ∈ targets`. + +This approach uses the *exact* shortest hyperpath algorithm of Krieger & Kececioglu 2023 (DOI: 10.1089/cmb.2023.0242). +The Krieger & Kececioglu algorithm solves an integer linear programming formalism related to hypergraph cutting. + +For a lower-cost heuristic algorithm, see `SnodeDistanceKKHeuristic`. +""" +struct SnodeDistanceKKILP <: AbstractDistance + sources::Set{Int} + targets::Set{Int} +end + +""" + distance( + hg::H, + distance_method::SnodeDistanceKKHeuristic, + hyperedge_weights::AbstractVector{T}; + ) where {H <: AbstractDirectedHypergraph, T <: Real} + + Return the shortest distance between the `distance_method.sources` and the `distance_method.targets`, + assuming tha the cost (i.e., distance) between two vertices is the sum of the (nonnegative) `hyperedge_weights` + of the hyperedges in the shortest path between the two sets of vertices. If there is no path between the `sources` + and `targets`, returns `typemax(T)`. + + Here, the heuristic directed hypergraph pathfinding algorithm of Krieger & Kececioglu (2022), + DOI: 10.1186/s13015-022-00217-9 is used to find the shortest path. This algorithm is not guaranteed to find the + optimal pathway, but in practice, it produces the optimal pathway approximately 99% of the time. +""" +function SimpleHypergraphs.distance( + hg::H, + distance_method::SnodeDistanceKKHeuristic, + hyperedge_weights::AbstractVector{T} +) where {H<:AbstractDirectedHypergraph,T<:Real} + + path = shortest_hyperpath_kk_heuristic( + hg, + distance_method.sources, + distance_method.targets, + hyperedge_weights + ) + + if length(path) == 0 + return typemax(T) + end + + return sum( + hyperedge_weights[e] + for e in path + ) +end + +""" + distance( + hg::H, + distance_method::SnodeDistanceKKILP, + hyperedge_weights::AbstractVector{T} + ) where {H <: AbstractDirectedHypergraph} + + Return the shortest distance between the `distance_method.sources` and the `distance_method.targets`, + assuming tha the cost (i.e., distance) between two vertices is the sum of the (nonnegative) `hyperedge_weights` + of the hyperedges in the shortest path between the two sets of vertices. If there is no path between the `sources` + and `targets`, returns `typemax(T)`. + + Here, the *exact* shortest hyperpath algorithm of Krieger & Kececioglu 2023 (DOI: 10.1089/cmb.2023.0242) is used. + The Krieger & Kececioglu algorithm solves an integer linear programming formalism related to hypergraph cutting. + +""" +function SimpleHypergraphs.distance( + hg::H, + distance_method::SnodeDistanceKKILP, + hyperedge_weights::AbstractVector{T} +) where {H<:AbstractDirectedHypergraph,T<:Real} + + path = shortest_hyperpath_kk_ilp( + hg, + distance_method.sources, + distance_method.targets, + hyperedge_weights + ) + + if length(path) == 0 + return typemax(T) + end + + return sum( + hyperedge_weights[e] + for e in path + ) +end + +""" + diameter( + hg::H, + distance_method::SnodeDistanceKKHeuristic, + ) where {H <: AbstractSimpleHypergraph, T <: Real} + + Return the diameter of a hypergraph `hg` (maximum number of hyperedges required to go between any two vertices) + based on Krieger & Kececioglu's heuristic algorithm (DOI: 10.1186/s13015-022-00217-9). If there exist some vertices + not reachable from other vertices (i.e., if `hg` is not strongly connected), then this will return `Inf`. +""" +function Graphs.diameter( + hg::H, + _::SnodeDistanceKKHeuristic, +) where {H<:AbstractDirectedHypergraph} + if length(get_strongly_connected_components(hg)) != 1 + return Inf64 + end + + # Since we're interested in the number of steps, assign each hyperedge a uniform weight of `1` + edge_weights = ones(nhe(hg)) + + max_dist = 0.0 + for i in 1:nhv(hg) + for j in i+1:nhv(hg) + dist = SimpleHypergraphs.distance( + hg, + SnodeDistanceKKHeuristic(Set{Int}(i), Set{Int}(j)), + edge_weights + ) + if dist > max_dist + max_dist = dist + end + end + end + + return max_dist +end + +""" + diameter( + hg::H, + distance_method::f, + ) where {H <: AbstractSimpleHypergraph, T <: Real} + + Return the diameter of a hypergraph `hg` (maximum number of hyperedges required to go between any two vertices) + based on Krieger & Kececioglu's exact integer linear programming algorithm (DOI: 10.1089/cmb.2023.0242). If there + exist some vertices not reachable from other vertices (i.e., if `hg` is not strongly connected), then this will + return `Inf64`. +""" +function Graphs.diameter( + hg::H, + _::SnodeDistanceKKILP, +) where {H<:AbstractDirectedHypergraph} + if length(get_strongly_connected_components(hg)) != 1 + return Inf64 + end + + # Since we're interested in the number of steps, assign each hyperedge a uniform weight of `1` + edge_weights = ones(nhe(hg)) + + max_dist = 0 + for i in 1:nhv(hg) + for j in i+1:nhv(hg) + dist = SimpleHypergraphs.distance( + hg, + SnodeDistanceKKILP(Set{Int}(i), Set{Int}(j)), + edge_weights + ) + if dist > max_dist + max_dist = dist + end + end + end + + return max_dist +end \ No newline at end of file diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl new file mode 100644 index 0000000..2cc36df --- /dev/null +++ b/src/algorithms/paths.jl @@ -0,0 +1,1001 @@ +""" + forward_reachable( + hg::H, + source::Int, + ) where {H <: AbstractDirectedHypergraph} + + Traverses a hypergraph `hg` starting from vertex with index `source` to determine all other vertices and hyperedges + that are reachable, following hyperedges along their forward direction (i.e., from tail to head). + +""" +function forward_reachable( + hg::H, + source::Int, +) where {H<:AbstractDirectedHypergraph} + # Priority queue of reached vertices + Q = Queue{Int}() + enqueue!(Q, source) + + reached_vs = BitVector(falses(nhv(hg))) + reached_vs[source] = true + + hes_tail_count = length.(keys.(hg.hg_tail.he2v)) + + # Which vertices/hyperedges have been reached? + vs = Set{Int}() + es = Set{Int}() + + while length(Q) > 0 + v = dequeue!(Q) + push!(vs, v) + + for out_e in keys(hg.hg_tail.v2he[v]) + # Following pseudocode exactly. This feels awkward; how slow would it be to just query reached_vs? + hes_tail_count[out_e] -= 1 + + if hes_tail_count[out_e] == 0 + push!(es, out_e) + + for w in keys(hg.hg_head.he2v[out_e]) + if !reached_vs[w] + enqueue!(Q, w) + reached_vs[w] = true + end + end + end + end + end + + # Return reached vertices and hyperedges + return (vs, es) +end + +""" + backward_traceable( + hg::H, + target::Int, + ) where {H <: AbstractDirectedHypergraph} + + Traverses a hypergraph `hg` starting from vertex with index `target` to determine all other vertices and hyperedges + that are reachable, following hyperedges along their reverse direction (i.e., from head to tail). +""" +function backward_traceable( + hg::H, + target::Int, +) where {H<:AbstractDirectedHypergraph} + # Priority queue of reached vertices + Q = Queue{Int}() + enqueue!(Q, target) + + reached_vs = BitVector(falses(nhv(hg))) + reached_vs[target] = true + + marked_hes = BitVector(falses(nhe(hg))) + + # Which vertices/hyperedges have been reached? + vs = Set{Int}() + es = Set{Int}() + + while length(Q) > 0 + v = dequeue!(Q) + push!(vs, v) + + for in_e in keys(hg.hg_head.v2he[v]) + if !marked_hes[in_e] + push!(es, in_e) + marked_hes[in_e] = true + + for w in keys(hg.hg_tail.he2v[in_e]) + if !reached_vs[w] + enqueue!(Q, w) + reached_vs[w] = true + end + end + end + end + end + + # Return reached vertices and hyperedges + return (vs, es) +end + + +""" + shortest_hyperpath_kk_heuristic( + hg::H, + source::Int, + target::Int, + hyperedge_weights::AbstractVector{T} + ) where {H <: AbstractDirectedHypergraph, T <: Real} + + shortest_hyperpath_kk_heuristic( + hg::DirectedHypergraph{T, V, E, D}, + source::Int, + targets::Set{Int}, + hyperedge_weights::AbstractVector{T} + ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} + + shortest_hyperpath_kk_heuristic( + hg::DirectedHypergraph{T, V, E, D}, + sources::Set{Int}, + target::Int, + hyperedge_weights::AbstractVector{T} + ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} + + shortest_hyperpath_kk_heuristic( + hg::DirectedHypergraph{T, V, E, D}, + sources::Set{Int}, + targets::Set{Int}, + hyperedge_weights::AbstractVector{T} + ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} + + Implements the heuristic directed hypergraph pathfinding algorithm of Krieger & Kececioglu (2022), + DOI: 10.1186/s13015-022-00217-9. This algorithm is not guaranteed to find the optimal pathway from `source` to + `target` based on some nonnegative `hyperedge_weights`), but in practice, it produces the optimal pathway + approximately 99% of the time. + + Note that, ostensibly, this algorithm only works for single-source, single-sink pathfinding (i.e., with a single + `source` and a single `target`). If the user provides multiple `sources` and/or multiple `targets`, the + multi-source/multi-sink problem will be reformulated as a single-source, single-sink problem by adding a + *metasource* vertex (connected to all source vertices by a single, 0-cost hyperedge) and/or *metatarget* vertex + (connected to all target vertices by a single, 0-cost hyperedge). +""" +function shortest_hyperpath_kk_heuristic( + hg::H, + source::Int, + target::Int, + hyperedge_weights::AbstractVector{T} +) where {H<:AbstractDirectedHypergraph,T<:Real} + + reached_vs = BitVector(falses(nhv(hg))) + reached_vs[source] = true + + marked_hes = BitVector(falses(nhe(hg))) + removed_hes = BitVector(falses(nhe(hg))) + + hyperedge_inedges = [Set{Int}() for _ in 1:nhe(hg)] + hes_tail_count = length.(keys.(hg.hg_tail.he2v)) + + hyperedge_costs = fill(typemax(T), nhe(hg)) + + hyperedge_heap_points = Vector{Union{Nothing,Int}}(nothing, nhe(hg)) + + # Verify that the target can be reached + fr = forward_reachable(hg, source) + @assert target ∈ fr[1] + + # Doubly reachable hyperedges + dr_hes = sort!(collect(intersect( + fr[2], + backward_traceable(hg, target)[2] + ))) + + # Eliminate non-doubly reachable hyperedges + hg_copy = deepcopy(hg) + hg_copy.hg_tail[:, InvertedIndices.Not(dr_hes)] .= nothing + hg_copy.hg_head[:, InvertedIndices.Not(dr_hes)] .= nothing + + # Min-heap for hyperedges + Hmin = MutableBinaryMinHeap{Tuple{T, Int}}() + for out_e in keys(hg.hg_tail.v2he[source]) + # If only the source is needed for this hyperedge + if length(hg.hg_tail.he2v[out_e]) == 1 + hyperedge_heap_points[out_e] = push!(Hmin, (hyperedge_weights[out_e], out_e)) + end + end + + while length(Hmin) > 0 + e = pop!(Hmin)[2] + removed_hes[e] = true + + path = short_hyperpath_vhe(hg, source, e, hyperedge_inedges, hyperedge_costs) + hyperedge_costs[e] = sum(hyperedge_weights[x] for x in path) + + out_edges = Set{Int}() + for v in keys(hg.hg_head.he2v[e]) + for f in keys(hg.hg_tail.v2he[v]) + if !marked_hes[f] + if !reached_vs[v] + hes_tail_count[f] -= 1 + end + + if hes_tail_count[f] == 0 + push!(out_edges, f) + marked_hes[f] = true + end + end + end + reached_vs[v] = true + end + + for f in out_edges + marked_hes[f] = false + end + + for f in out_edges + push!(hyperedge_inedges[f], e) + if !isnothing(hyperedge_heap_points[f]) && !removed_hes[f] + update!( + Hmin, + hyperedge_heap_points[f], + ( + sum( + hyperedge_weights[x] + for x in short_hyperpath_vhe(hg, source, f, hyperedge_inedges, hyperedge_costs) + ), + f + ) + ) + elseif isnothing(hyperedge_heap_points[f]) && hes_tail_count[f] == 0 + hyperedge_heap_points[f] = push!( + Hmin, + ( + sum( + hyperedge_weights[x] + for x in short_hyperpath_vhe(hg, source, f, hyperedge_inedges, hyperedge_costs) + ), + f + ) + ) + end + end + end + + path = Set{Int}() + cost = typemax(T) + for in_e in keys(hg.hg_head.v2he[target]) + if !isnothing(hyperedge_heap_points[in_e]) + p = short_hyperpath_vhe(hg, source, in_e, hyperedge_inedges, hyperedge_costs) + cost_p = sum(hyperedge_weights[e] for e in p) + if cost_p < cost + path = p + cost = cost_p + end + end + end + + return path +end + +function shortest_hyperpath_kk_heuristic( + hg::DirectedHypergraph{T,V,E,D}, + source::Int, + targets::Set{Int}, + hyperedge_weights::AbstractVector{S} +) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} + hg_copy = deepcopy(hg) + + # Add a single "metatarget" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the targets to the metatarget will have a cost of 0 associated with it + metatarget = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(x => convert(T, 0) for x in targets), + vertices_head=D(metatarget => convert(T, 0)) + ) + + path = shortest_hyperpath_kk_heuristic( + hg_copy, + source, + metatarget, + vcat(hyperedge_weights, convert(S, 0)) + ) + + # Remove the fictitious hyperedge from the targets to the metatarget + setdiff(path, Set{Int}(meta_he)) +end + +function shortest_hyperpath_kk_heuristic( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + target::Int, + hyperedge_weights::AbstractVector{S} +) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} + hg_copy = deepcopy(hg) + + # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the metasource to the sources will have a cost of 0 associated with it + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(metasource => convert(T, 0)), + vertices_head=D(x => convert(T, 0) for x in sources) + ) + + path = shortest_hyperpath_kk_heuristic( + hg_copy, + metasource, + target, + vcat(hyperedge_weights, convert(S, 0)) + ) + + # Remove the fictitious hyperedge from the metasource to the sources + setdiff(path, Set{Int}(meta_he)) +end + +function shortest_hyperpath_kk_heuristic( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + targets::Set{Int}, + hyperedge_weights::AbstractVector{S} +) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} + hg_copy = deepcopy(hg) + + # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the metasource to the sources will have a cost of 0 associated with it + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he_source = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(metasource => convert(T, 0)), + vertices_head=D(x => convert(T, 0) for x in sources) + ) + + # Add a single "metatarget" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the targets to the metatarget will have a cost of 0 associated with it + metatarget = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he_target = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(x => convert(T, 0) for x in targets), + vertices_head=D(metatarget => convert(T, 0)) + ) + + path = shortest_hyperpath_kk_heuristic( + hg_copy, + metasource, + metatarget, + vcat(hyperedge_weights, [convert(S, 0), convert(S, 0)]) + ) + + # Remove fictitious hyperedges + setdiff(path, Set{Int}([meta_he_source, meta_he_target])) +end + +""" + short_hyperpath_vhe( + hg::H, + v::Int, + he::Int, + ) where {H <: AbstractDirectedHypergraph, T <: Real} + + Obtain a (relatively, but not necessarily optimally) short hyperpath in hypergraph `hg` from a vertex `v` to a + hyperedge `he`, `short_hyperpath_vhe` uses a greedy algorithm to first select hyperedges for a superpath and then + prune unnecessary hyperedges to achieve a (generally shorter) hyperpath. +""" +function short_hyperpath_vhe( + hg::H, + v::Int, + he::Int, + he_inedges::Vector{Set{Int}}, + he_costs::AbstractVector{T} +) where {H<:AbstractDirectedHypergraph, T<:Real} + marked_hes = BitVector(falses(nhe(hg))) + + Q = Queue{Int}() + for e in he_inedges[he] + enqueue!(Q, e) + marked_hes[e] = true + end + + superpath = Set{Int}(he) + path = Set{Int}(he) + + # Construct (likely redundant) superpath by backtracking from target + while length(Q) > 0 + e = dequeue!(Q) + push!(superpath, e) + + for f in he_inedges[e] + if !marked_hes[f] + enqueue!(Q, f) + marked_hes[f] = true + end + end + end + + # TODO: try to be more clever about this + superpath = sort(collect(superpath), by=x -> he_costs[x], rev=true) + hg_copy = deepcopy(hg) + # Eliminate all hyperedges not on superpath + hg_copy.hg_tail[:, InvertedIndices.Not(superpath)] .= nothing + hg_copy.hg_head[:, InvertedIndices.Not(superpath)] .= nothing + + # Remove target from superpath; does not make sense to remove target in the next stage + filter!(x -> x != he, superpath) + + # Try to minimize the size of the path by eliminating unnecessary hyperedges + for e in superpath + hg_copy.hg_tail[:, e] .= nothing + hg_copy.hg_head[:, e] .= nothing + + # Only if hyperedge is essential for reaching target, + if !is_reachable(hg_copy, v, he, :hyperedge) + # Restore hyperedge to hypergraph copy + hg_copy[:, e] .= hg[:, e] + push!(path, e) + end + end + + return path +end + +""" + is_reachable( + hg::H, + source::Int, + target::Int, + target_type::Symbol + ) where {H<:AbstractDirectedHypergraph} + + Use `forward_reachable` to determine if `target` (either a vertex index, if `target_type === :vertex` or a + hyperedge index if `target_type === :hyperedge`) can be reached from vertex with index`source` in directed + hypergraph `hg`. +""" +function is_reachable( + hg::H, + source::Int, + target::Int, + target_type::Symbol +) where {H<:AbstractDirectedHypergraph} + @assert target_type ∈ [:vertex, :hyperedge] "`target_type` must be :vertex or :hyperedge" + + fr = forward_reachable(hg, source) + + #TODO: make a short-circuiting version of this. Can't for the life of me figure out why this isn't working... + if target_type === :vertex + return target ∈ fr[1] + else + return target ∈ fr[2] + end +end + +""" + get_hyperpath( + hg::H, + source::Int, + target::Int, + out::Set{Int} + ) where {H <: AbstractDirectedHypergraph} + + If one exists, obtain a hyperpath in directed hypergraph `hg` from a source vertex with index `source` to a target + vertex with index `target`. The hyperpath cannot include any hyperedge with index included in the set `out`. +""" +function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H<:AbstractDirectedHypergraph} + # Remove excluded hyperedges + hg_copy = deepcopy(hg) + inds = sort(collect(out)) + hg_copy.hg_tail[:, inds] .= nothing + hg_copy.hg_head[:, inds] .= nothing + + reached_vs, reached_es = forward_reachable(hg_copy, source) + + # Path does not exist + if target ∉ reached_vs + return Set{Int}() + end + + path = Set{Int}() + + # Try to minimize the size of the path by eliminating unnecessary hyperedges + for e in reached_es + hg_copy.hg_tail[:, e] .= nothing + hg_copy.hg_head[:, e] .= nothing + + # Only retain if hyperedge is essential for reaching target + if !is_reachable(hg_copy, source, target, :vertex) + # Restore hyperedge to hypergraph copy + hg_copy[:, e] .= hg[:, e] + push!(path, e) + end + end + + return path +end + +""" + all_hyperpaths(hg::H, source::Int, target::Int) where {H <: AbstractDirectedHypergraph} + + all_hyperpaths( + hg::DirectedHypergraph{T,V,E,D}, + source::Int, + targets::Set{Int} + ) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + + all_hyperpaths( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + target::Int + ) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + + all_hyperpaths( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + targets::Set{Int} + ) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + + Exhaustively (but efficiently) generate all hyperpaths in directed hypergraph `hg` from some source(s) to some + target(s), using the algorithm described by Krieger & Kececioglu (2022), DOI: 10.1186/s13015-022-00217-9. + + Note that, ostensibly, this algorithm only works for single-source, single-sink pathfinding (i.e., with a single + `source` index and a single `target` index). If the user provides multiple `sources` and/or multiple `targets`, the + multi-source/multi-sink problem will be reformulated as a single-source, single-sink problem by adding a + *metasource* vertex (connected to all source vertices by a single hyperedge) and/or *metatarget* vertex + (connected to all target vertices by a single hyperedge). +""" +function all_hyperpaths(hg::H, source::Int, target::Int) where {H<:AbstractDirectedHypergraph} + # Queue of subproblems + # Subproblem is defined as a "out" set of hyperedges (which must not be present in a path) and "keep" hyperedges + # which must be present in the path + Q = Queue{Tuple{Set{Int},Set{Int}}}() + + paths = Set{Set{Int}}() + + # Start with no restrictions + enqueue!(Q, (Set{Int}(), Set{Int}())) + + while length(Q) > 0 + out, keep = dequeue!(Q) + + path = get_hyperpath(hg, source, target, out) + + if length(path) > 0 && path ∉ paths + push!(paths, path) + + k = keep + for e in setdiff(path, keep) + enqueue!(Q, (union(out, e), k)) + push!(k, e) + end + end + end + + paths +end + +function all_hyperpaths( + hg::DirectedHypergraph{T,V,E,D}, + source::Int, + targets::Set{Int} +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + hg_copy = deepcopy(hg) + + # Add a single "metatarget" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the targets to the metatarget will have a cost of 0 associated with it + metatarget = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(x => convert(T, 0) for x in targets), + vertices_head=D(metatarget => convert(T, 0)) + ) + + paths = all_hyperpaths( + hg_copy, + source, + metatarget, + ) + + # Remove the fictitious hyperedge from the targets to the metatarget + return Set(setdiff(p, Set{Int}(meta_he)) for p in paths) +end + +function all_hyperpaths( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + target::Int +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + hg_copy = deepcopy(hg) + + # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the metasource to the sources will have a cost of 0 associated with it + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(metasource => convert(T, 0)), + vertices_head=D(x => convert(T, 0) for x in sources) + ) + + paths = all_hyperpaths( + hg_copy, + metasource, + target, + ) + + # Remove the fictitious hyperedge from the metasource to the sources + return Set(setdiff(p, Set{Int}(meta_he)) for p in paths) +end + +function all_hyperpaths( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + targets::Set{Int} +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + hg_copy = deepcopy(hg) + + # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the metasource to the sources will have a cost of 0 associated with it + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he_source = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(metasource => convert(T, 0)), + vertices_head=D(x => convert(T, 0) for x in sources) + ) + + # Add a single "metatarget" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the targets to the metatarget will have a cost of 0 associated with it + metatarget = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he_target = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(x => convert(T, 0) for x in targets), + vertices_head=D(metatarget => convert(T, 0)) + ) + + paths = all_hyperpaths( + hg_copy, + metasource, + metatarget, + ) + + # Remove fictitious hyperedges + return Set(setdiff(p, Set{Int}([meta_he_source, meta_he_target])) for p in paths) +end + +""" + initialize_ilp_model( + hg::H, + source::Int, + target::Int, + hyperedge_weights::AbstractVector{T} + ) where {H<:AbstractDirectedHypergraph, T<:Real} + + Define variables, objective, and initial constraints for integer linear programming-based optimization of hyperpaths + in hypergraph `hg` from vertex `source` to vertex `target`, where hyperedges have weights `hyperedge_weights` + +""" +function initialize_ilp_model( + hg::H, + source::Int, + target::Int, + hyperedge_weights::AbstractVector{T} +) where {H<:AbstractDirectedHypergraph, T<:Real} + + # First, verify that the problem is well-posed + # That is, can `target` be reached from `source` + @assert is_reachable(hg, source, target, :vertex) + + # Initialize integer linear programming model + model = JuMP.Model(GLPK.Optimizer) + + # Create one binary variable for each hyperedge in `hg` + @JuMP.variable(model, x[1:nhe(hg)], Bin) + JuMP.set_start_value.(x, 1) + + # Verify that all variables are bound to be either 0 (not present in hyperpath) or 1 (present in hyperpath) + @assert all(JuMP.is_binary.(x)) + + # Define initial constraints + for i in 1:nhe(hg) + # Tail-covering inequalities + for v in keys(hg.hg_tail.he2v[i]) + if v == source + continue + end + + in_hes = collect(keys(hg.hg_head.v2he[v])) + @JuMP.constraint(model, sum([x[ih] for ih in in_hes]) >= x[i]) + end + + # Head-hitting inequalities + if target ∉ keys(hg.hg_head.he2v[i]) + hits = Int[] + for j in 1:nhe(hg) + if i != j && length(intersect(Set(keys(hg.hg_head.he2v[i])), Set(keys(hg.hg_tail.he2v[j])))) >= 1 + push!(hits, j) + end + end + + @JuMP.constraint(model, sum([x[j] for j in hits]) >= x[i]) + end + end + + # Target-production inequality + @JuMP.constraint(model, sum([x[e] for e in keys(hg.hg_head.v2he[target])]) >= 1) + + # Distance-based inequalities + dist_ests = fill(typemax(T), nhv(hg)) + for v in forward_reachable(hg, source)[1] + # TODO: this is inefficient + # Currently, will repeat a lot of work + # TODO: you are here; don't yet even understand what the problem is... + dist_ests[v] = sum( + [hyperedge_weights[e] for e in shortest_hyperpath_kk_heuristic(hg, source, v, hyperedge_weights)] + ) + end + + cuts = Set{Int}[] + crosses = BitVector[] + + for d in unique(values(dist_ests)) + if d > dist_ests[target] + continue + end + + # Construct an s,t-cut, where the source is within the cut set of vertices and the target is not in the set + cut_d = Set(v for (v, dist) in enumerate(dist_ests) if dist < d) + push!(cut_d, source) + setdiff!(cut_d, Set(target)) + + push!(cuts, cut_d) + + cross = [ + issubset(Set(keys(hg.hg_tail.he2v[i])), cut_d) && !issubset(Set(keys(hg.hg_head.he2v[i])), cut_d) + for i in 1:nhe(hg) + ] + push!(crosses, BitVector(cross)) + + @JuMP.constraint(model, dot(x, cross) >= 1) + end + + # Define objective function + @JuMP.objective(model, Min, dot(x, hyperedge_weights)) + + return model, x, cuts, crosses +end + +""" + shortest_hyperpath_kk_ilp( + hg::H, + source::Int, + target::Int, + hyperedge_weights::AbstractVector{T} + ) where {H<:AbstractDirectedHypergraph, T<:Real} + + shortest_hyperpath_kk_ilp( + hg::DirectedHypergraph{T,V,E,D}, + source::Int, + targets::Set{Int}, + hyperedge_weights::AbstractVector{S} + ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} + + shortest_hyperpath_kk_ilp( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + target::Int, + hyperedge_weights::AbstractVector{S} + ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} + + shortest_hyperpath_kk_ilp( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + targets::Set{Int}, + hyperedge_weights::AbstractVector{S} + ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} + + Implements the exact directed hypergraph pathfinding algorithm of Krieger & Kececioglu (2023), + DOI: 10.1089/cmb.2023.0242. This algorithm is guaranteed to find the optimal pathway from `source` to + `target` based on some nonnegative `hyperedge_weights`, if and only if such a path exists. + + Note that, ostensibly, this algorithm only works for single-source, single-sink pathfinding (i.e., with a single + `source` and a single `target`). If the user provides multiple `sources` and/or multiple `targets`, the + multi-source/multi-sink problem will be reformulated as a single-source, single-sink problem by adding a + *metasource* vertex (connected to all source vertices by a single, 0-cost hyperedge) and/or *metatarget* vertex + (connected to all target vertices by a single, 0-cost hyperedge). +""" +function shortest_hyperpath_kk_ilp( + hg::H, + source::Int, + target::Int, + hyperedge_weights::AbstractVector{T} +) where {H<:AbstractDirectedHypergraph,T<:Real} + + # TODO: do I need to carry `x` over like this? Not sure about variable scope + model, x, cuts, crosses = initialize_ilp_model(hg, source, target, hyperedge_weights) + + JuMP.optimize!(model) + + # Convert floating-point solution into BitVector + # TODO: Is this necessary w/ JuMP? Or will the output really be binary? + solution = JuMP.value.(x) .> 0.5 + + # Check for s,t-cuts that are not crossed by the current solution + new_cuts, new_crosses = expand_cuts(hg, source, target, cuts, crosses, solution) + + while length(new_cuts) > 0 + # Add new constraints to the model + for cross in new_crosses + @JuMP.constraint(model, dot(x, cross) >= 1) + end + + # Re-optimize model with new cut-constraints + JuMP.optimize!(model) + solution = JuMP.value.(x) .> 0.5 + + new_cuts, new_crosses = expand_cuts(hg, source, target, new_cuts, new_crosses, solution) + end + + return Set(findall(solution)) +end + +function shortest_hyperpath_kk_ilp( + hg::DirectedHypergraph{T,V,E,D}, + source::Int, + targets::Set{Int}, + hyperedge_weights::AbstractVector{S} +) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} + hg_copy = deepcopy(hg) + + # Add a single "metatarget" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the targets to the metatarget will have a cost of 0 associated with it + metatarget = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(x => convert(T, 0) for x in targets), + vertices_head=D(metatarget => convert(T, 0)) + ) + + path = shortest_hyperpath_kk_ilp( + hg_copy, + source, + metatarget, + vcat(hyperedge_weights, convert(S, 0)) + ) + + # Remove the fictitious hyperedge from the targets to the metatarget + return setdiff(path, Set{Int}(meta_he)) +end + +function shortest_hyperpath_kk_ilp( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + target::Int, + hyperedge_weights::AbstractVector{S} +) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} + hg_copy = deepcopy(hg) + + # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the metasource to the sources will have a cost of 0 associated with it + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(metasource => convert(T, 0)), + vertices_head=D(x => convert(T, 0) for x in sources) + ) + + path = shortest_hyperpath_kk_ilp( + hg_copy, + metasource, + target, + vcat(hyperedge_weights, convert(S, 0)) + ) + + # Remove the fictitious hyperedge from the metasource to the sources + setdiff(path, Set{Int}(meta_he)) +end + +function shortest_hyperpath_kk_ilp( + hg::DirectedHypergraph{T,V,E,D}, + sources::Set{Int}, + targets::Set{Int}, + hyperedge_weights::AbstractVector{S} +) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} + hg_copy = deepcopy(hg) + + # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the metasource to the sources will have a cost of 0 associated with it + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he_source = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(metasource => convert(T, 0)), + vertices_head=D(x => convert(T, 0) for x in sources) + ) + + # Add a single "metatarget" vertex to reformulate as single-source, single-sink pathfinding problem + # The hyperedge from the targets to the metatarget will have a cost of 0 associated with it + metatarget = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he_target = SimpleHypergraphs.add_hyperedge!( + hg_copy; + vertices_tail=D(x => convert(T, 0) for x in targets), + vertices_head=D(metatarget => convert(T, 0)) + ) + + path = shortest_hyperpath_kk_ilp( + hg_copy, + metasource, + metatarget, + vcat(hyperedge_weights, [convert(T, 0), convert(T, 0)]) + ) + + # Remove fictitious hyperedges + setdiff(path, Set{Int}([meta_he_source, meta_he_target])) +end + +""" + expand_cuts( + hg::H, + source::Int, + target::Int, + cuts::Vector{Set{Int}}, + crosses::Vector{BitVector}, + curr_sol::BitVector + ) where {H<:AbstractDirectedHypergraph} + + A helper function for `shortest_hyperpath_kk_ilp`. + + Efficiently identify new `source`-`target` cuts of a directed hypergraph `hg` that are violated by current solution + of the integer linear programming problem `curr_sol`. +""" +function expand_cuts( + hg::H, + source::Int, + target::Int, + cuts::Vector{Set{Int}}, + crosses::Vector{BitVector}, + curr_sol::BitVector +) where {H<:AbstractDirectedHypergraph} + new_cuts = Set{Int}[] + new_crosses = BitVector[] + + for (old_cut, old_cross) in zip(cuts, crosses) + # Source augmentation + new_cut = old_cut + new_cross = old_cross + for e in 1:nhe(hg) + # If `e` is an active hyperedge that crosses the current cut, add the head of `e` to the cut so `e` no + # longer crosses it + if curr_sol[e] && new_cross[e] + new_cut = union(new_cut, Set(keys(hg.hg_head.he2v[e]))) + new_cross = [ + issubset(Set(keys(hg.hg_tail.he2v[i])), new_cut) && !issubset(Set(keys(hg.hg_head.he2v[i])), new_cut) + for i in 1:nhe(hg) + ] + end + end + + # If this is a valid s,t-cut that no active hyperedges cross, add it to the new cut list + if !(any(new_cross) || target ∈ new_cut) + push!(new_cuts, new_cut) + push!(new_crosses, new_cross) + end + + # Sink augmentation + # TODO: you could (should) combine these two into one loop + new_cut = old_cut + new_cross = old_cross + while any(curr_sol .&& new_cross) + for e in 1:nhe(hg) + # If `e` is an active hyperedge that crosses the current cut, remove vertices from the tail of `e` to the + # cut so `e` no longer crosses it + if curr_sol[e] && new_cross[e] + # Greedily pick the vertex in the tail of `e` that causes the fewest hyperedges to newly cross this cut + new_cut_ev = Dict{Int,Set{Int}}() + new_cross_ev = Dict{Int,Vector{Bool}}() + greedy_v = 0 + min_length = typemax(Int) + for v in keys(hg.hg_tail.he2v[e]) + new_cut_ev[v] = setdiff(new_cut, Set(v)) + new_cross_ev[v] = [ + issubset(Set(keys(hg.hg_tail.he2v[i])), new_cut_ev[v]) && + !issubset(Set(keys(hg.hg_head.he2v[i])), new_cut_ev[v]) + for i in 1:nhe(hg) + ] + v_length = length(findall(map(!, old_cross) .&& new_cross_ev[v])) + if v_length < min_length + min_length = v_length + greedy_v = v + end + end + + new_cut = new_cut_ev[greedy_v] + new_cross = new_cross_ev[greedy_v] + end + end + end + + # If this is still a valid s,t-cut + if !any(new_cross) && source ∈ new_cut + push!(new_cuts, new_cut) + push!(new_crosses, new_cross) + end + + end + + return new_cuts, new_crosses +end \ No newline at end of file diff --git a/src/dihypergraph.jl b/src/dihypergraph.jl index 758835c..a9cc481 100644 --- a/src/dihypergraph.jl +++ b/src/dihypergraph.jl @@ -12,31 +12,23 @@ TODO: reconsider this design choice **Constructors** - DirectedHypergraph{T}(n::Integer,k::Integer) where {T<:Real} - DirectedHypergraph{T,V}(n::Integer, k::Integer; - v_meta=Vector{Union{V,Nothing}}(nothing, n) - ) where {T<:Real, V} - DirectedHypergraph{T,E}(n::Integer, k::Integer; - he_meta_tail=Vector{Union{E,Nothing}}(nothing, k), - he_meta_head=Vector{Union{E,Nothing}}(nothing, k) - ) where {T<:Real, E} - DirectedHypergraph{T,V,E}(n::Integer, k::Integer; - v_meta=Vector{Union{V,Nothing}}(nothing, n), - he_meta_tail=Vector{Union{E,Nothing}}(nothing, k), - he_meta_head=Vector{Union{E,Nothing}}(nothing, k) - ) where {T<:Real, V, E} - DirectedHypergraph{T,V,E,D}(n::Integer, k::Integer, - v_meta=Vector{Union{V,Nothing}}(nothing, n), - he_meta_tail=Vector{Union{E,Nothing}}(nothing, k), - he_meta_head=Vector{Union{E,Nothing}}(nothing, k) - ) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + DirectedHypergraph{T,V,E,D}( + n::Integer, k::Integer, + v_meta=Vector{Union{V, Nothing}}(nothing, n), + he_meta_tail=Vector{Union{E, Nothing}}(nothing, k), + he_meta_head=Vector{Union{E, Nothing}}(nothing, k) + ) where {T<:Real,V,E,D<:AbstractDict{Int, T}} + DirectedHypergraph{T,V,E}(n::Integer, k::Integer) where {T<:Real, V, E} + DirectedHypergraph{T,V}(n::Integer, k::Integer) where {T<:Real, V} + DirectedHypergraph{T}(n::Integer, k::Integer) where {T<:Real} + DirectedHypergraph(n::Integer, k::Integer) Construct a hypergraph with a given number of vertices and hyperedges. Optionally, values of type `V` can be stored at vertices and values of type `E` can be stored at hyperedges. By default the hypergraph uses a `Dict{Int,T}` for the internal data storage, however a different dictionary such as `SortedDict` -to ensure result replicability can be used (e.g. when doing stochastic -simulations on hypergraphs). +to ensure result replicability can be used (e.g., when doing stochastic +simulations on directed hypergraphs). DirectedHypergraph( m_tail::AbstractMatrix{Union{T, Nothing}}, @@ -86,23 +78,32 @@ Constructs a directed hypergraph of degree 2 by making a deep copy of a Graphs.DiGraph. A `SortedDict` will be used for internal data storage of the hypergraph. - DirectedHypergraph{T,V,D}( - hg_tail::Hypergraph{T,D}, - hg_head::Hypergraph{T,D}; - v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(m,1)), - ) where {T<:Real,V,D<:AbstractDict{Int, T}} - DirectedHypergraph{T,E,D}( - hg_tail::Hypergraph{T,D}, - hg_head::Hypergraph{T,D}; - he_meta_tail::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m,2)), - he_meta_head::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m,2)) - ) where {T<:Real,E,D<:AbstractDict{Int, T}} - DirectedHypergraph{T,V,E,D}( - hg_tail::Hypergraph{T,D}, - hg_head::Hypergraph{T,D}; - v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(m,1)), - he_meta_tail::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m,2)), - he_meta_head::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m,2)) + DirectedHypergraph( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T} + ) where {T<:Real} + DirectedHypergraph{T}( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T} + ) where {T<:Real} + DirectedHypergraph{T,V}( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T}; + v_meta::Union{Nothing, Vector{Union{V, Nothing}}}=nothing, + ) where {T<:Real,V} + DirectedHypergraph{T,V,E}( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T}; + v_meta::Union{Nothing, Vector{Union{V, Nothing}}}=nothing, + he_meta_tail::Union{Nothing, Vector{Union{E, Nothing}}}=nothing, + he_meta_head::Union{Nothing, Vector{Union{E, Nothing}}}=nothing + ) where {T<:Real,V,E} + DirectedHypergraph{T,V,E,D}( + hg_tail::Hypergraph{T,Nothing,Nothing,D}, + hg_head::Hypergraph{T,Nothing,Nothing,D}; + v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(hg_tail,1)), + he_meta_tail::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(hg_tail,2)), + he_meta_head::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(hg_tail,2)) ) where {T<:Real,V,E,D<:AbstractDict{Int, T}} Constructs a directed hypergraph from two undirected basic hypergraphs, one with hyperedges @@ -132,9 +133,9 @@ an error if the vertex metadata of the two hypergraphs is not element-for-elemen * `hg_head`: an undirected hypergraph representing the head half of the directed hypergraph """ -struct DirectedHypergraph{T<:Real,V,E,D<:AbstractDict{Int, T}} <: AbstractDirectedHypergraph{Tuple{Union{T, Nothing}, Union{T, Nothing}}} - hg_tail::Hypergraph{T,D} - hg_head::Hypergraph{T,D} +struct DirectedHypergraph{T<:Real,V,E,D<:AbstractDict{Int,T}} <: AbstractDirectedHypergraph{Tuple{Union{T,Nothing},Union{T,Nothing}}} + hg_tail::Hypergraph{T,Nothing,Nothing,D} + hg_head::Hypergraph{T,Nothing,Nothing,D} v_meta::Vector{Union{V,Nothing}} he_meta_tail::Vector{Union{E,Nothing}} @@ -142,28 +143,28 @@ struct DirectedHypergraph{T<:Real,V,E,D<:AbstractDict{Int, T}} <: AbstractDirect DirectedHypergraph{T,V,E,D}( n::Integer, k::Integer, - v_meta=Vector{Union{V, Nothing}}(nothing, n), - he_meta_tail=Vector{Union{E, Nothing}}(nothing, k), - he_meta_head=Vector{Union{E, Nothing}}(nothing, k) - ) where {T<:Real,V,E,D<:AbstractDict{Int, T}} = + v_meta=Vector{Union{V,Nothing}}(nothing, n), + he_meta_tail=Vector{Union{E,Nothing}}(nothing, k), + he_meta_head=Vector{Union{E,Nothing}}(nothing, k) + ) where {T<:Real,V,E,D<:AbstractDict{Int,T}} = new{T,V,E,D}( - Hypergraph{T,D}(n, k), - Hypergraph{T,D}(n, k), + Hypergraph{T,Nothing,Nothing,D}(n, k), + Hypergraph{T,Nothing,Nothing,D}(n, k), v_meta, he_meta_tail, he_meta_head ) function DirectedHypergraph{T,V,E,D}( - hg_tail::Hypergraph{T,D}, - hg_head::Hypergraph{T,D}; - v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(hg_tail,1)), - he_meta_tail::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(hg_tail,2)), - he_meta_head::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(hg_tail,2)) - ) where {T<:Real,V,E,D<:AbstractDict{Int, T}} + hg_tail::Hypergraph{T,Nothing,Nothing,D}, + hg_head::Hypergraph{T,Nothing,Nothing,D}; + v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(hg_tail, 1)), + he_meta_tail::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(hg_tail, 2)), + he_meta_head::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(hg_tail, 2)) + ) where {T<:Real,V,E,D<:AbstractDict{Int,T}} @assert size(hg_tail) == size(hg_head) - @assert length(v_meta) == size(hg_tail,1) - @assert length(he_meta_tail) == size(hg_tail,2) - @assert length(he_meta_head) == size(hg_head,2) + @assert length(v_meta) == size(hg_tail, 1) + @assert length(he_meta_tail) == size(hg_tail, 2) + @assert length(he_meta_head) == size(hg_head, 2) new{T,V,E,D}( hg_tail, @@ -175,79 +176,132 @@ struct DirectedHypergraph{T<:Real,V,E,D<:AbstractDict{Int, T}} <: AbstractDirect end end -DirectedHypergraph{T,V,E}(n::Integer, k::Integer) where {T<:Real, V, E} = DirectedHypergraph{T,V,E,Dict{Int,T}}(n, k) -DirectedHypergraph{T,V}(n::Integer, k::Integer) where {T<:Real, V} = DirectedHypergraph{T,V,Nothing,Dict{Int,T}}(n, k) +DirectedHypergraph{T,V,E}(n::Integer, k::Integer) where {T<:Real,V,E} = DirectedHypergraph{T,V,E,Dict{Int,T}}(n, k) -DirectedHypergraph{T,D}(n::Integer, k::Integer) where {T<:Real, D<:AbstractDict{Int, T}} = DirectedHypergraph{T,Nothing,Nothing,D}(n, k) +DirectedHypergraph{T,V}(n::Integer, k::Integer) where {T<:Real,V} = DirectedHypergraph{T,V,Nothing,Dict{Int,T}}(n, k) DirectedHypergraph{T}(n::Integer, k::Integer) where {T<:Real} = DirectedHypergraph{T,Nothing,Nothing,Dict{Int,T}}(n, k) DirectedHypergraph(n::Integer, k::Integer) = DirectedHypergraph{Bool,Nothing,Nothing,Dict{Int,Bool}}(n, k) -function DirectedHypergraph{T,D}( - hg_tail::Hypergraph{T,D}, - hg_head::Hypergraph{T,D} - ) where {T<:Real,D<:AbstractDict{Int, T}} +function DirectedHypergraph{T,V,E}( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T}; + v_meta::Union{Nothing,Vector{Union{V,Nothing}}}=nothing, + he_meta_tail::Union{Nothing,Vector{Union{E,Nothing}}}=nothing, + he_meta_head::Union{Nothing,Vector{Union{E,Nothing}}}=nothing +) where {T<:Real,V,E} + @assert size(hg_tail) == size(hg_head) + + n, k = size(hg_tail) + shg_tail = Hypergraph{T}(n, k) + shg_head = Hypergraph{T}(n, k) + + # TODO: test behavior on this + shg_tail .= hg_tail + shg_head .= hg_head - DirectedHypergraph{T,Nothing,Nothing,D}( + if E === Nothing + he_meta_tail = fill(nothing, size(hg_tail, 2)) + he_meta_head = fill(nothing, size(hg_tail, 2)) + else + if isnothing(he_meta_tail) + he_meta_tail = hg_tail.he_meta + end + if isnothing(he_meta_head) + he_meta_head = hg_head.he_meta + end + end + + if V === Nothing + v_meta = fill(nothing, size(hg_tail, 1)) + end + + + if isnothing(v_meta) + if !all(hg_tail.v_meta .== hg_head.v_meta) + @warn "Vertex metadata for tail and head hypergraphs not identical; discarding vertex metadata." + DirectedHypergraph{T,V,E,Dict{Int,T}}( + shg_tail, + shg_head; + he_meta_tail=he_meta_tail, + he_meta_head=he_meta_head + ) + else + DirectedHypergraph{T,V,E,Dict{Int,T}}( + shg_tail, + shg_head; + v_meta=hg_tail.v_meta, + he_meta_tail=he_meta_tail, + he_meta_head=he_meta_head + ) + end + else + DirectedHypergraph{T,V,E,Dict{Int,T}}( + shg_tail, + shg_head; + v_meta=v_meta, + he_meta_tail=he_meta_tail, + he_meta_head=he_meta_head + ) + end +end + +function DirectedHypergraph{T,V}( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T}; + v_meta::Union{Nothing,Vector{Union{V,Nothing}}}=nothing, +) where {T<:Real,V} + + DirectedHypergraph{T,V,Nothing}( hg_tail, - hg_head + hg_head; + v_meta=v_meta ) end -function DirectedHypergraph{T,V,D}( - hg_tail::Hypergraph{T,D}, - hg_head::Hypergraph{T,D} - ) where {T<:Real,V,D<:AbstractDict{Int, T}} +function DirectedHypergraph{T}( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T} +) where {T<:Real} - DirectedHypergraph{T,V,Nothing,D}( + DirectedHypergraph{T,Nothing,Nothing}( hg_tail, hg_head ) end -function DirectedHypergraph{T,V,E,D}( - hg_tail::Hypergraph{T,V,E,D}, - hg_head::Hypergraph{T,V,E,D} -) where {T<:Real,V,E,D<:AbstractDict{Int, T}} - @assert size(hg_tail) == size(hg_head) - - n, k = size(hg_tail) - shg_tail = Hypergraph(n, k) - shg_head = Hypergraph(n, k) - - # TODO: test behavior on this - shg_tail .= hg_tail - shg_head .= hg_head +function DirectedHypergraph( + hg_tail::Hypergraph{T}, + hg_head::Hypergraph{T} +) where {T<:Real} - if all(hg_tail.v_meta .== hg_head.v_meta) - DirectedHypergraph{T,V,E,D}(shg_tail, shg_head; v_meta=hg_tail.v_meta, he_meta_tail=hg_tail.he_meta, he_meta_head=hg_head.he_meta) - else - @warn "Vertex metadata for tail and head hypergraphs not identical; discarding vertex metadata." - DirectedHypergraph{T,V,E,D}(shg_tail, shg_head; he_meta_tail=hg_tail.he_meta, he_meta_head=hg_head.he_meta) - end + DirectedHypergraph{T}( + hg_tail, + hg_head + ) end function DirectedHypergraph{T,V,E,D}( - m_tail::AbstractMatrix{Union{T, Nothing}}, - m_head::AbstractMatrix{Union{T, Nothing}}; - v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(m_tail,1)), - he_meta_tail::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m_tail,2)), - he_meta_head::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m_tail,2)) - ) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + m_tail::AbstractMatrix{Union{T,Nothing}}, + m_head::AbstractMatrix{Union{T,Nothing}}; + v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(m_tail, 1)), + he_meta_tail::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m_tail, 2)), + he_meta_head::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m_tail, 2)) +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} - # TODO: ensure that sizes of matrices identical? + @assert size(m_tail) == size(m_head) # Arbitrary, since sizes are identical n, k = size(m_tail) - hg_tail = Hypergraph{T,D}(n, k) + hg_tail = Hypergraph{T,Nothing,Nothing,D}(n, k) hg_tail .= m_tail - - hg_head = Hypergraph{T,D}(n, k) + + hg_head = Hypergraph{T,Nothing,Nothing,D}(n, k) hg_head .= m_head DirectedHypergraph{T,V,E,D}( @@ -259,23 +313,23 @@ function DirectedHypergraph{T,V,E,D}( end function DirectedHypergraph{T,V,E}( - m_tail::AbstractMatrix{Union{T, Nothing}}, - m_head::AbstractMatrix{Union{T, Nothing}}; - v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(m_tail,1)), - he_meta_tail::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m_tail,2)), - he_meta_head::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m_tail,2)) + m_tail::AbstractMatrix{Union{T,Nothing}}, + m_head::AbstractMatrix{Union{T,Nothing}}; + v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(m_tail, 1)), + he_meta_tail::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m_tail, 2)), + he_meta_head::Vector{Union{Nothing,E}}=Vector{Union{Nothing,E}}(nothing, size(m_tail, 2)) ) where {T<:Real,V,E} # Arbitrary, since sizes are identical n, k = size(m_tail) - hg_tail = Hypergraph{T,Dict{Int,T}}(n, k) + hg_tail = Hypergraph{T}(n, k) hg_tail .= m_tail - hg_head = Hypergraph{T,Dict{Int,T}}(n, k) + hg_head = Hypergraph{T}(n, k) hg_head .= m_head - DirectedHypergraph{T,V,E,Dict{Int,T}}( + DirectedHypergraph{T,V,E}( hg_tail, hg_head; v_meta=v_meta, he_meta_tail=he_meta_tail, @@ -284,21 +338,21 @@ function DirectedHypergraph{T,V,E}( end function DirectedHypergraph{T,V}( - m_tail::AbstractMatrix{Union{T, Nothing}}, - m_head::AbstractMatrix{Union{T, Nothing}}; - v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(m_tail,1)), + m_tail::AbstractMatrix{Union{T,Nothing}}, + m_head::AbstractMatrix{Union{T,Nothing}}; + v_meta::Vector{Union{Nothing,V}}=Vector{Union{Nothing,V}}(nothing, size(m_tail, 1)), ) where {T<:Real,V} # Arbitrary, since sizes are identical n, k = size(m_tail) - hg_tail = Hypergraph{T,Dict{Int,T}}(n, k) + hg_tail = Hypergraph{T}(n, k) hg_tail .= m_tail - hg_head = Hypergraph{T,Dict{Int,T}}(n, k) + hg_head = Hypergraph{T}(n, k) hg_head .= m_head - DirectedHypergraph{T,V,Nothing,Dict{Int,T}}( + DirectedHypergraph{T,V}( hg_tail, hg_head; v_meta=v_meta @@ -306,40 +360,37 @@ function DirectedHypergraph{T,V}( end function DirectedHypergraph{T}( - m_tail::AbstractMatrix{Union{T, Nothing}}, - m_head::AbstractMatrix{Union{T, Nothing}} + m_tail::AbstractMatrix{Union{T,Nothing}}, + m_head::AbstractMatrix{Union{T,Nothing}} ) where {T<:Real} # Arbitrary, since sizes are identical n, k = size(m_tail) - hg_tail = Hypergraph{T,Dict{Int,T}}(n, k) + hg_tail = Hypergraph{T}(n, k) hg_tail .= m_tail - hg_head = Hypergraph{T,Dict{Int,T}}(n, k) + hg_head = Hypergraph{T}(n, k) hg_head .= m_head - DirectedHypergraph{T,Nothing,Nothing,Dict{Int,T}}(hg_tail, hg_head) + DirectedHypergraph{T}(hg_tail, hg_head) end function DirectedHypergraph( - m_tail::AbstractMatrix{Union{T, Nothing}}, - m_head::AbstractMatrix{Union{T, Nothing}} + m_tail::AbstractMatrix{Union{T,Nothing}}, + m_head::AbstractMatrix{Union{T,Nothing}} ) where {T<:Real} # Arbitrary, since sizes are identical n, k = size(m_tail) - hg_tail = Hypergraph{T,Dict{Int,T}}(n, k) + hg_tail = Hypergraph{T}(n, k) hg_tail .= m_tail - hg_head = Hypergraph{T,Dict{Int,T}}(n, k) + hg_head = Hypergraph{T}(n, k) hg_head .= m_head - DirectedHypergraph{T,Nothing,Nothing,Dict{Int,T}}( - hg_tail, - hg_head - ) + DirectedHypergraph{T}(hg_tail, hg_head) end @@ -347,16 +398,16 @@ function DirectedHypergraph(g::Graphs.DiGraph) h = DirectedHypergraph{Bool,Nothing,Nothing,SortedDict{Int,Bool}}(maximum(vertices(g)), ne(g)) e = 0 for edge in edges(g) - e+=1 - h[1,edge.src,e] = true - h[2,edge.dst,e] = true + e += 1 + h[1, edge.src, e] = true + h[2, edge.dst, e] = true end h end # TODO: this is awkward... -const DIRECTED_HYPERGRAPH_VALID_FIRST_INDICES = [1,2] +const DIRECTED_HYPERGRAPH_VALID_FIRST_INDICES = [1, 2] # TODO: can this entirely replace the above? Index setting seems problematic... @enum HyperedgeDirection begin @@ -374,7 +425,7 @@ Returns a value for a given vertex-hyperedge pair `idx` for a directed hypergrap If a vertex does not belong to a hyperedge `nothing` is returned. """ -@inline function Base.getindex(h::H, idx::Vararg{Int,2}) where {H <: AbstractDirectedHypergraph} +@inline function Base.getindex(h::H, idx::Vararg{Int,2}) where {H<:AbstractDirectedHypergraph} @boundscheck checkbounds(h.hg_tail, idx...) @boundscheck checkbounds(h.hg_head, idx...) @@ -391,7 +442,7 @@ Removes a vertex from a given hyperedge for a directed hypergraph `h` and a give Note that trying to remove a vertex from a hyperedge when it is not present will not throw an error. """ -@inline function Base.setindex!(h::H, ::Nothing, idx::Vararg{Int,2}) where {H <: AbstractDirectedHypergraph} +@inline function Base.setindex!(h::H, ::Nothing, idx::Vararg{Int,2}) where {H<:AbstractDirectedHypergraph} @boundscheck checkbounds(h.hg_tail, idx...) @boundscheck checkbounds(h.hg_head, idx...) setindex!(h.hg_tail, nothing, idx...) @@ -407,7 +458,7 @@ Adds a vertex to a hyperedge (represented by indices `idx`) and assigns value `v` to be stored with that assignment. """ -@inline function Base.setindex!(h::H, v::Real, idx::Vararg{Int,2}) where {H <: AbstractDirectedHypergraph} +@inline function Base.setindex!(h::H, v::Real, idx::Vararg{Int,2}) where {H<:AbstractDirectedHypergraph} @boundscheck checkbounds(h.hg_tail, idx...) @boundscheck checkbounds(h.hg_head, idx...) @@ -429,10 +480,10 @@ and the second element is the value that will be assigned to the head part. A va vertex will be removed from that side of the hyperedge. """ -@inline function Base.setindex!(h::H, v::Tuple{Union{Real, Nothing}, Union{Real, Nothing}}, idx::Vararg{Int,2}) where {H <: AbstractDirectedHypergraph} +@inline function Base.setindex!(h::H, v::Tuple{Union{Real,Nothing},Union{Real,Nothing}}, idx::Vararg{Int,2}) where {H<:AbstractDirectedHypergraph} @boundscheck checkbounds(h.hg_tail, idx...) @boundscheck checkbounds(h.hg_head, idx...) - + setindex!(h.hg_tail, v[1], idx...) setindex!(h.hg_head, v[2], idx...) @@ -449,7 +500,7 @@ the vertex will be removed from the head hyperedge. Note that trying to remove a vertex from a hyperedge when it is not present will not throw an error. """ -@inline function Base.setindex!(h::H, ::Nothing, idx::Vararg{Int,3}) where {H <: AbstractDirectedHypergraph} +@inline function Base.setindex!(h::H, ::Nothing, idx::Vararg{Int,3}) where {H<:AbstractDirectedHypergraph} @boundscheck checkbounds(DIRECTED_HYPERGRAPH_VALID_FIRST_INDICES, idx[1]) if idx[1] == 1 @@ -457,9 +508,9 @@ Note that trying to remove a vertex from a hyperedge when it is not present will else side = h.hg_head end - + @boundscheck checkbounds(side, idx[2:end]...) - + setindex!(side, nothing, idx[2], idx[3]) h @@ -474,7 +525,7 @@ Adds a vertex to a hyperedge (represented by indices `idx`, where the first inde `v` to be stored with that assignment. """ -@inline function Base.setindex!(h::H, v::Real, idx::Vararg{Int,3}) where {H <: AbstractDirectedHypergraph} +@inline function Base.setindex!(h::H, v::Real, idx::Vararg{Int,3}) where {H<:AbstractDirectedHypergraph} @boundscheck checkbounds(DIRECTED_HYPERGRAPH_VALID_FIRST_INDICES, idx[1]) if idx[1] == 1 @@ -482,9 +533,9 @@ Adds a vertex to a hyperedge (represented by indices `idx`, where the first inde else side = h.hg_head end - + @boundscheck checkbounds(side, idx[2:end]...) - + setindex!(side, v, idx[2]..., idx[3]...) h @@ -499,7 +550,7 @@ Vertex indices are given in a tuple `(in, out)`, where `in` are the tail vertice and `out` are the head vertices """ -@inline SimpleHypergraphs.getvertices(h::H, he_id::Int) where {H <: AbstractDirectedHypergraph} = (h.hg_tail.he2v[he_id], h.hg_head.he2v[he_id]) +@inline SimpleHypergraphs.getvertices(h::H, he_id::Int) where {H<:AbstractDirectedHypergraph} = (h.hg_tail.he2v[he_id], h.hg_head.he2v[he_id]) """ @@ -511,7 +562,7 @@ vertex `v_ind` is on the tail side and `head` are the hyperedges where `v_ind` i the head side. """ -@inline SimpleHypergraphs.gethyperedges(h::H, v_id::Int) where {H <: AbstractDirectedHypergraph} = (h.hg_tail.v2he[v_id], h.hg_head.v2he[v_id]) +@inline SimpleHypergraphs.gethyperedges(h::H, v_id::Int) where {H<:AbstractDirectedHypergraph} = (h.hg_tail.v2he[v_id], h.hg_head.v2he[v_id]) """ to_undirected(h::DirectedHypergraph) @@ -530,9 +581,9 @@ combine the weights, so we simply set the values to 1.0 if a given vertex is in a given hyperedge """ -function to_undirected(h::DirectedHypergraph{T,V,E,D}) where {T <: Real, V, E, D <: AbstractDict{Int, T}} +function to_undirected(h::DirectedHypergraph{T,V,E,D}) where {T<:Real,V,E,D<:AbstractDict{Int,T}} - incidence = Matrix{Union{T, Nothing}}(nothing, nhv(h), nhe(h)) + incidence = Matrix{Union{T,Nothing}}(nothing, nhv(h), nhe(h)) this_nhe = nhe(h) @@ -547,7 +598,7 @@ function to_undirected(h::DirectedHypergraph{T,V,E,D}) where {T <: Real, V, E, D end end - combined_he_meta = Vector{Union{Tuple{Union{E, Nothing}, Union{E, Nothing}}, Nothing}}(undef, this_nhe) + combined_he_meta = Vector{Union{Tuple{Union{E,Nothing},Union{E,Nothing}},Nothing}}(undef, this_nhe) fill!(combined_he_meta, nothing) for he_index in 1:this_nhe tail_meta = h.he_meta_tail[he_index] @@ -558,7 +609,7 @@ function to_undirected(h::DirectedHypergraph{T,V,E,D}) where {T <: Real, V, E, D end end - Hypergraph{T, V, Tuple{Union{E, Nothing},Union{E, Nothing}}, D}( + Hypergraph{T,V,Tuple{Union{E,Nothing},Union{E,Nothing}},D}( incidence, v_meta=h.v_meta, he_meta=combined_he_meta @@ -581,24 +632,26 @@ Additionally, a value can be stored with the vertex using the `v_meta` keyword parameter. """ -function SimpleHypergraphs.add_vertex!(h::DirectedHypergraph{T, V, E, D}; - hyperedges_tail::D = D(), hyperedges_head::D = D(), v_meta::Union{V,Nothing} = nothing - ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} - @boundscheck (checkbounds(h.hg_tail,1,k) for k in keys(hyperedges_tail)) - @boundscheck (checkbounds(h.hg_head,1,k) for k in keys(hyperedges_head)) +function SimpleHypergraphs.add_vertex!( + h::DirectedHypergraph{T,V,E,D}; + hyperedges_tail::D=D(), hyperedges_head::D=D(), + v_meta::Union{V,Nothing}=nothing +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + @boundscheck (checkbounds(h.hg_tail, 1, k) for k in keys(hyperedges_tail)) + @boundscheck (checkbounds(h.hg_head, 1, k) for k in keys(hyperedges_head)) - push!(h.hg_tail.v2he,hyperedges_tail) - push!(h.hg_head.v2he,hyperedges_head) + push!(h.hg_tail.v2he, hyperedges_tail) + push!(h.hg_head.v2he, hyperedges_head) # Should always be identical to h.hg_head.v2he ix = length(h.hg_tail.v2he) for k in keys(hyperedges_tail) - h[1,ix,k]=hyperedges_tail[k] + h[1, ix, k] = hyperedges_tail[k] end for k in keys(hyperedges_head) - h[2,ix,k]=hyperedges_head[k] + h[2, ix, k] = hyperedges_head[k] end push!(h.v_meta, v_meta) @@ -621,7 +674,7 @@ function SimpleHypergraphs.remove_vertex!(h::DirectedHypergraph, v::Int) h.hg_tail.v2he[v] = h.hg_tail.v2he[n] h.hg_head.v2he[v] = h.hg_head.v2he[n] end - + for hv in h.hg_tail.he2v if v < n && haskey(hv, n) hv[v] = hv[n] @@ -667,23 +720,26 @@ a value can be stored with the hyperedge using the `he_meta_tail` and `he_meta_h keyword parameters. """ -function SimpleHypergraphs.add_hyperedge!(h::DirectedHypergraph{T, V, E, D}; - vertices_tail::D = D(), vertices_head::D = D(), - he_meta_tail::Union{E,Nothing}=nothing, he_meta_head::Union{E,Nothing}=nothing - ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} - @boundscheck (checkbounds(h.hg_tail,k,1) for k in keys(vertices_tail)) - @boundscheck (checkbounds(h.hg_head,1,k) for k in keys(vertices_head)) - - push!(h.hg_tail.he2v,vertices_tail) +function SimpleHypergraphs.add_hyperedge!( + h::DirectedHypergraph{T,V,E,D}; + vertices_tail::D=D(), + vertices_head::D=D(), + he_meta_tail::Union{E,Nothing}=nothing, + he_meta_head::Union{E,Nothing}=nothing +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + @boundscheck (checkbounds(h.hg_tail, k, 1) for k in keys(vertices_tail)) + @boundscheck (checkbounds(h.hg_head, 1, k) for k in keys(vertices_head)) + + push!(h.hg_tail.he2v, vertices_tail) push!(h.hg_head.he2v, vertices_head) # Should always be identical to length(h.hg_head.he2v) ix = length(h.hg_tail.he2v) for k in keys(vertices_tail) - h[1,k,ix]=vertices_tail[k] + h[1, k, ix] = vertices_tail[k] end for k in keys(vertices_head) - h[2,k,ix]=vertices_head[k] + h[2, k, ix] = vertices_head[k] end push!(h.he_meta_tail, he_meta_tail) push!(h.he_meta_head, he_meta_head) @@ -700,14 +756,14 @@ and the list of hyperedges (and hyperedge metadata) will be shrunk. """ function SimpleHypergraphs.remove_hyperedge!(h::DirectedHypergraph, e::Int) ne = nhe(h) - @assert(e <= ne) - if e < ne - h.he_meta_tail[e] = h.he_meta_tail[ne] + @assert(e <= ne) + if e < ne + h.he_meta_tail[e] = h.he_meta_tail[ne] h.he_meta_head[e] = h.he_meta_head[ne] h.hg_tail.he2v[e] = h.hg_tail.he2v[ne] h.hg_head.he2v[e] = h.hg_head.he2v[ne] - end + end for he in h.hg_tail.v2he if e < ne && haskey(he, ne) @@ -745,14 +801,14 @@ end Remove all vertices with degree 0 and all hyperedges of size 0. """ -function SimpleHypergraphs.prune_hypergraph!(h::H) where {H <: AbstractDirectedHypergraph} - for e in reverse(1:nhe(h)) - length(h.hg_tail.he2v[e]) == 0 && length(h.hg_head.he2v[e]) == 0 && SimpleHypergraphs.remove_hyperedge!(h,e) +function SimpleHypergraphs.prune_hypergraph!(h::H) where {H<:AbstractDirectedHypergraph} + for e in reverse(1:nhe(h)) + length(h.hg_tail.he2v[e]) == 0 && length(h.hg_head.he2v[e]) == 0 && SimpleHypergraphs.remove_hyperedge!(h, e) end - for v in reverse(1:nhv(h)) - length(h.hg_tail.v2he[v]) == 0 && length(h.hg_tail.v2he[v]) == 0 && SimpleHypergraphs.remove_vertex!(h,v) + for v in reverse(1:nhv(h)) + length(h.hg_tail.v2he[v]) == 0 && length(h.hg_tail.v2he[v]) == 0 && SimpleHypergraphs.remove_vertex!(h, v) end - h + h end """ @@ -761,8 +817,8 @@ end Remove all vertices with degree 0 and all hyperedges of size 0. """ -function SimpleHypergraphs.prune_hypergraph(h::H) where {H <: AbstractDirectedHypergraph} - prune_hypergraph!(deepcopy(h)) +function SimpleHypergraphs.prune_hypergraph(h::H) where {H<:AbstractDirectedHypergraph} + prune_hypergraph!(deepcopy(h)) end """ @@ -772,9 +828,9 @@ end Sets a new meta value `new_value` for the vertex `id` in the hypergraph `h`. """ -function SimpleHypergraphs.set_vertex_meta!(h::DirectedHypergraph{T, V, E, D}, - new_value::Union{V,Nothing}, id::Int - ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +function SimpleHypergraphs.set_vertex_meta!(h::DirectedHypergraph{T,V,E,D}, + new_value::Union{V,Nothing}, id::Int +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} checkbounds(h.v_meta, id) h.v_meta[id] = new_value h.v_meta @@ -788,8 +844,8 @@ end Returns a meta value stored at the vertex `id` in the directed hypergraph `h`. """ -function SimpleHypergraphs.get_vertex_meta(h::DirectedHypergraph{T, V, E, D}, id::Int - ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +function SimpleHypergraphs.get_vertex_meta(h::DirectedHypergraph{T,V,E,D}, id::Int +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} checkbounds(h.v_meta, id) h.v_meta[id] end @@ -803,9 +859,9 @@ end Sets a new meta value `new_value` for the hyperedge `id` in the directed hypergraph `h`. """ -function SimpleHypergraphs.set_hyperedge_meta!(h::DirectedHypergraph{T, V, E, D}, - new_value_tail::Union{E,Nothing}, new_value_head::Union{E,Nothing}, id::Int - ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +function SimpleHypergraphs.set_hyperedge_meta!(h::DirectedHypergraph{T,V,E,D}, + new_value_tail::Union{E,Nothing}, new_value_head::Union{E,Nothing}, id::Int +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} checkbounds(h.he_meta_tail, id) checkbounds(h.he_meta_head, id) @@ -825,10 +881,10 @@ Sets a new meta value `new_value` for the hyperedge `id` in the direction `side` for a directed hypergraph `h`. """ -function SimpleHypergraphs.set_hyperedge_meta!(h::DirectedHypergraph{T, V, E, D}, - new_value::Union{E,Nothing}, id::Int, side::HyperedgeDirection - ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} - +function SimpleHypergraphs.set_hyperedge_meta!(h::DirectedHypergraph{T,V,E,D}, + new_value::Union{E,Nothing}, id::Int, side::HyperedgeDirection +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + if side == tail checkbounds(h.he_meta_tail, id) h.he_meta_tail[id] = new_value @@ -848,8 +904,8 @@ end Returns a meta value stored at the hyperedge `id` in the directed hypergraph `h`. """ -function SimpleHypergraphs.get_hyperedge_meta(h::DirectedHypergraph{T, V, E, D}, id::Int - ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +function SimpleHypergraphs.get_hyperedge_meta(h::DirectedHypergraph{T,V,E,D}, id::Int +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} checkbounds(h.he_meta_tail, id) checkbounds(h.he_meta_head, id) @@ -863,8 +919,8 @@ end Returns a meta value stored at the hyperedge `id` in the directed hypergraph `h`. """ -function SimpleHypergraphs.get_hyperedge_meta(h::DirectedHypergraph{T, V, E, D}, id::Int, side::HyperedgeDirection - ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +function SimpleHypergraphs.get_hyperedge_meta(h::DirectedHypergraph{T,V,E,D}, id::Int, side::HyperedgeDirection +) where {T<:Real,V,E,D<:AbstractDict{Int,T}} if side == tail checkbounds(h.he_meta_tail, id) @@ -881,7 +937,7 @@ end Return the number of hyperedges in the directed hypergraph `h`. """ -function SimpleHypergraphs.nhe(h::H) where {H <: AbstractDirectedHypergraph} +function SimpleHypergraphs.nhe(h::H) where {H<:AbstractDirectedHypergraph} (length(h.hg_tail.he2v) == length(h.hg_head.he2v)) ? length(h.hg_tail.he2v) : throw("Tail and head sides of hypergraph have different numbers of hyperedges!") end @@ -891,12 +947,12 @@ end Return the number of vertices in the directed hypergraph `h`. """ -function SimpleHypergraphs.nhv(h::H) where {H <: AbstractDirectedHypergraph} +function SimpleHypergraphs.nhv(h::H) where {H<:AbstractDirectedHypergraph} (length(h.hg_tail.v2he) == length(h.hg_head.v2he)) ? length(h.hg_tail.v2he) : throw("Tail and head sides of hypergraph have different numbers of hyperedges!") end -function _default_heselect(h::H, v::Int; reverse::Bool=false) where {H <: AbstractDirectedHypergraph} +function _default_heselect(h::H, v::Int; reverse::Bool=false) where {H<:AbstractDirectedHypergraph} he_tail, he_head = gethyperedges(h, v) if reverse @@ -909,7 +965,7 @@ function _default_heselect(h::H, v::Int; reverse::Bool=false) where {H <: Abstra end -function _default_vselect(h::H, he::Int; reverse::Bool=false) where {H <: AbstractDirectedHypergraph} +function _default_vselect(h::H, he::Int; reverse::Bool=false) where {H<:AbstractDirectedHypergraph} vs_tail, vs_head = getvertices(h, he) if reverse @@ -943,9 +999,9 @@ a vertex identifier or a hyperedge identifier. The return values of both functio should be respectively a list of hyperedges or vertices and their weights. """ function SimpleHypergraphs.random_walk(h::H, start::Int; - heselect::Function=_default_heselect, - vselect::Function=_default_vselect, - reverse::Bool=false) where {H <: AbstractDirectedHypergraph} + heselect::Function=_default_heselect, + vselect::Function=_default_vselect, + reverse::Bool=false) where {H<:AbstractDirectedHypergraph} 1 <= start <= nhv(h) || throw(ArgumentError("invalid start vertex index")) hes, hew = heselect(h, start, reverse=reverse) he = sample(hes, Weights(hew)) @@ -962,7 +1018,7 @@ Return an array of weakly connected components in the directed hypergraph `h` into an undirected hypergraph and then obtaining the conected components of that hypergraph. """ -function get_weakly_connected_components(h::H) where {H <: AbstractDirectedHypergraph} +function get_weakly_connected_components(h::H) where {H<:AbstractDirectedHypergraph} undirected = to_undirected(h) get_connected_components(undirected) end @@ -978,7 +1034,7 @@ determines the strongly connected components of a directed hypergraph. function _visit( h::H, v::Int -) where {H <: AbstractDirectedHypergraph} +) where {H<:AbstractDirectedHypergraph} visited = zeros(Bool, nhv(h)) visited_tail_nodes = zeros(Int, nhe(h)) @@ -1022,9 +1078,9 @@ Return an array of strongly connected components in the directed hypergraph `h` Francisco José Martín-Recuerda Moyano (PhD dissertation, 2016). """ -function get_strongly_connected_components(h::H) where {H <: AbstractDirectedHypergraph} +function get_strongly_connected_components(h::H) where {H<:AbstractDirectedHypergraph} - T = Dict{Vector{Int}, Set{Int}}() + T = Dict{Vector{Int},Set{Int}}() for v in 1:nhv(h) bcc_v = _visit(h, v) @@ -1037,5 +1093,5 @@ function get_strongly_connected_components(h::H) where {H <: AbstractDirectedHyp push!(T[bcc_sorted], v) end - [sort!(collect(v)) for (k, v) in T if length(v) != 0] + [sort!(collect(v)) for (_, v) in T if length(v) != 0] end diff --git a/src/models/bipartite.jl b/src/models/bipartite.jl index 345d22b..d967704 100644 --- a/src/models/bipartite.jl +++ b/src/models/bipartite.jl @@ -179,3 +179,5 @@ end Graphs.SimpleGraphs.fadj(b::BipartiteView{H}, v::Integer) where {H<:AbstractDirectedHypergraph} = Graphs.outneighbors(b,v) Graphs.SimpleGraphs.badj(b::BipartiteView{H}, v::Integer) where {H<:AbstractDirectedHypergraph} = Graphs.inneighbors(b,v) + +# TODO: has_cycles diff --git a/src/models/random-models.jl b/src/models/random-models.jl index c0818a0..e4e81d6 100644 --- a/src/models/random-models.jl +++ b/src/models/random-models.jl @@ -164,3 +164,7 @@ function SimpleHypergraphs.random_dregular_model( HType(mx_tail, mx_head) end + +# TODO: preferential attachment +# TODO: hypercurveball algorithm +# YOU ARE HERE \ No newline at end of file diff --git a/src/models/twosection.jl b/src/models/twosection.jl index 21a8be7..b79b052 100644 --- a/src/models/twosection.jl +++ b/src/models/twosection.jl @@ -123,7 +123,7 @@ function Graphs.SimpleGraphs.fadj(t::TwoSectionView{H}) where {H<:AbstractDirect for v_tail in keys(vs_tail) for v_head in keys(vs_head) if v_head != v_tail - append!(res[v_tail], v_head) + push!(res[v_tail], v_head) end end end @@ -145,7 +145,7 @@ function Graphs.SimpleGraphs.badj(t::TwoSectionView{H}) where {H<:AbstractDirect for v_tail in keys(vs_tail) for v_head in keys(vs_head) if v_head != v_tail - append!(res[v_head], v_tail) + push!(res[v_head], v_tail) end end end diff --git a/test/runtests.jl b/test/runtests.jl index f55969a..2a99847 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,21 +4,37 @@ using StatsBase using Random using DataStructures using Graphs +import JuMP using Test # using JET - -dh1 = DirectedHypergraph{Float64, Int, String}(7,6) -dh1[1,1,1] = 1.0 -dh1.hg_head[2:3,1] .= 2.5 # Assignment on a directed hypergraph directly with slices is currently awkward -dh1[1,3,3] = 4.0 -dh1[2,4,3] = 5.5 -dh1[1,5,4] = 7.0 -dh1[2,6,4] = 8.5 -dh1[1,6,5] = 10.0 -dh1[2,7,5] = -1.5 -dh1[1,7,6] = 0.0 -dh1[2,5,6] = 1.5 +dh1 = DirectedHypergraph{Float64,Int,String}(7, 6) +dh1[1, 1, 1] = 1.0 +dh1.hg_head[2:3, 1] .= 2.5 # Assignment on a directed hypergraph directly with slices is currently awkward +dh1[1, 3, 3] = 4.0 +dh1[2, 4, 3] = 5.5 +dh1[1, 5, 4] = 7.0 +dh1[2, 6, 4] = 8.5 +dh1[1, 6, 5] = 10.0 +dh1[2, 7, 5] = -1.5 +dh1[1, 7, 6] = 0.0 +dh1[2, 5, 6] = 1.5 + +tail_2 = [ + true nothing nothing nothing nothing nothing nothing nothing nothing + nothing true nothing nothing nothing true nothing nothing nothing + nothing nothing true nothing nothing nothing true nothing nothing + nothing nothing nothing true nothing true true nothing nothing + nothing nothing nothing nothing true nothing nothing true true +] +head_2 = [ + nothing nothing nothing nothing nothing true true nothing nothing + true nothing nothing nothing nothing nothing nothing true nothing + true nothing nothing nothing nothing nothing nothing nothing true + nothing true true nothing true nothing nothing nothing nothing + nothing nothing nothing true nothing nothing nothing nothing nothing +] +dh2 = DirectedHypergraph(tail_2, head_2) # @testset "SimpleDirectedHypergraphs Code linting (JET.jl)" begin # JET.test_package(SimpleDirectedHypergraphs; target_defined_modules = true) @@ -33,13 +49,13 @@ dh1[2,5,6] = 1.5 m = Matrix(h) @test m == h @test h == [ - (1, nothing) (nothing, nothing) (nothing, nothing) - (2, nothing) (3, nothing) (nothing, nothing) - (nothing, nothing) (nothing, 0) (nothing, nothing) - (nothing, 4) (nothing, nothing) (1, nothing) - (nothing, 5) (12, nothing) (nothing, nothing) - (nothing, nothing) (nothing, nothing) (nothing, 4) - ] + (1, nothing) (nothing, nothing) (nothing, nothing) + (2, nothing) (3, nothing) (nothing, nothing) + (nothing, nothing) (nothing, 0) (nothing, nothing) + (nothing, 4) (nothing, nothing) (1, nothing) + (nothing, 5) (12, nothing) (nothing, nothing) + (nothing, nothing) (nothing, nothing) (nothing, 4) + ] mktemp("data") do path, _ println(path) SimpleHypergraphs.hg_save(path, h; format=EHGF_Format()) @@ -47,31 +63,31 @@ dh1[2,5,6] = 1.5 loaded_hg = replace(read(path, String), r"\n*$" => "") @test loaded_hg == - reduce(replace, - ["\r\n"=>"\n", - r"^\"\"\"(?s).*\"\"\"\n"=>"", #remove initial comments - r"\n*$"=>""], #remove final \n* - init=read("data/test_dhg.ehgf", String)) #no comments + reduce(replace, + ["\r\n" => "\n", + r"^\"\"\"(?s).*\"\"\"\n" => "", #remove initial comments + r"\n*$" => ""], #remove final \n* + init=read("data/test_dhg.ehgf", String)) #no comments @test loaded_hg == - reduce(replace, - ["\r\n"=>"\n", - r"^\"\"\"(?s).*\"\"\"\n"=>"", #remove initial comments - r"\n*$"=>""], #remove final \n* - init=read("data/singlelinecomment.ehgf", String)) #single line comment + reduce(replace, + ["\r\n" => "\n", + r"^\"\"\"(?s).*\"\"\"\n" => "", #remove initial comments + r"\n*$" => ""], #remove final \n* + init=read("data/singlelinecomment.ehgf", String)) #single line comment @test loaded_hg == - reduce(replace, - ["\r\n"=>"\n", - r"^\"\"\"(?s).*\"\"\"\n"=>"", #remove initial comments - r"\n*$"=>""], #remove final \n* - init=read("data/multilinecomment.ehgf", String)) #multiple lines comment + reduce(replace, + ["\r\n" => "\n", + r"^\"\"\"(?s).*\"\"\"\n" => "", #remove initial comments + r"\n*$" => ""], #remove final \n* + init=read("data/multilinecomment.ehgf", String)) #multiple lines comment - for v=1:nhv(dh1) + for v = 1:nhv(dh1) set_vertex_meta!(dh1, v, v) end - for he=1:nhe(dh1) + for he = 1:nhe(dh1) set_hyperedge_meta!(dh1, string(he), string(he), he) end @@ -91,22 +107,24 @@ dh1[2,5,6] = 1.5 @test_throws ArgumentError dhg_load("data/malformedcomment.ehgf"; format=EHGF_Format(), HType=DirectedHypergraph, T=Int) @test_throws ArgumentError dhg_load("data/argumenterror.ehgf"; format=EHGF_Format(), HType=DirectedHypergraph, T=Int) - dh2 = DirectedHypergraph{Float64}(0,0) - @test dh2 == DirectedHypergraph{Float64,Nothing}(0,0) - @test dh2 == DirectedHypergraph{Float64,Nothing,Nothing}(0,0) - @test dh2 == DirectedHypergraph{Float64,Nothing,Nothing,Dict{Int,Float64}}(0,0) + dh2 = DirectedHypergraph{Float64}(0, 0) + @test dh2 == DirectedHypergraph{Float64,Nothing}(0, 0) + @test dh2 == DirectedHypergraph{Float64,Nothing,Nothing}(0, 0) + @test dh2 == DirectedHypergraph{Float64,Nothing,Nothing,Dict{Int,Float64}}(0, 0) - dh3 = DirectedHypergraph(0,0) - @test dh3 == DirectedHypergraph{Bool, Nothing, Nothing, Dict{Int, Bool}}(0,0) + dh3 = DirectedHypergraph(0, 0) + @test dh3 == DirectedHypergraph{Bool,Nothing,Nothing,Dict{Int,Bool}}(0, 0) - for i in 1:6 SimpleHypergraphs.add_vertex!(dh2) end - SimpleHypergraphs.add_hyperedge!(dh2;vertices_tail=Dict(1 => 1.0), vertices_head=Dict(2:3 .=> 2.5)) + for i in 1:6 + SimpleHypergraphs.add_vertex!(dh2) + end + SimpleHypergraphs.add_hyperedge!(dh2; vertices_tail=Dict(1 => 1.0), vertices_head=Dict(2:3 .=> 2.5)) SimpleHypergraphs.add_hyperedge!(dh2) - SimpleHypergraphs.add_hyperedge!(dh2;vertices_tail=Dict(3 => 4.0), vertices_head=Dict(4 => 5.5)) - SimpleHypergraphs.add_hyperedge!(dh2;vertices_tail=Dict(5 => 7.0), vertices_head=Dict(6 => 8.5)) - SimpleHypergraphs.add_hyperedge!(dh2;vertices_tail=Dict(6 => 10.0)) - SimpleHypergraphs.add_hyperedge!(dh2;vertices_head=Dict(5 => 1.5)) - SimpleHypergraphs.add_vertex!(dh2;hyperedges_tail=Dict(6 => 0.0), hyperedges_head=Dict(5 => -1.5)) + SimpleHypergraphs.add_hyperedge!(dh2; vertices_tail=Dict(3 => 4.0), vertices_head=Dict(4 => 5.5)) + SimpleHypergraphs.add_hyperedge!(dh2; vertices_tail=Dict(5 => 7.0), vertices_head=Dict(6 => 8.5)) + SimpleHypergraphs.add_hyperedge!(dh2; vertices_tail=Dict(6 => 10.0)) + SimpleHypergraphs.add_hyperedge!(dh2; vertices_head=Dict(5 => 1.5)) + SimpleHypergraphs.add_vertex!(dh2; hyperedges_tail=Dict(6 => 0.0), hyperedges_head=Dict(5 => -1.5)) @test dh1 == dh2 mtail = Matrix(dh1.hg_tail) mhead = Matrix(dh1.hg_head) @@ -115,50 +133,54 @@ dh1[2,5,6] = 1.5 @test dh1 == DirectedHypergraph(mtail, mhead) @test dh1 == DirectedHypergraph{Float64}(mtail, mhead) @test dh1 == DirectedHypergraph{Float64,Nothing}(mtail, mhead) - @test dh1 == DirectedHypergraph{Float64,Nothing, Nothing}(mtail, mhead) - @test dh1 == DirectedHypergraph{Float64,Nothing, Nothing,Dict{Int,Float64}}(mtail, mhead) - @test all(Matrix(dh1.hg_tail) .== Matrix(DirectedHypergraph{Float64,Nothing, Nothing,SortedDict{Int,Float64}}(mtail, mhead).hg_tail)) - @test all(Matrix(dh1.hg_head) .== Matrix(DirectedHypergraph{Float64,Nothing, Nothing,SortedDict{Int,Float64}}(mtail, mhead).hg_head)) - @test getindex(dh1,5,4) == (7.0, nothing) - - dh4 = DirectedHypergraph{Float64,String,Nothing}(1,1) - @test SimpleHypergraphs.add_vertex!(dh4;v_meta="test") == 2 - @test SimpleHypergraphs.set_vertex_meta!(dh4,"t",1) == ["t","test"] - @test SimpleHypergraphs.get_vertex_meta(dh4,2) == "test" - @test get_hyperedge_meta(dh4,1) == (nothing, nothing) - @test_throws BoundsError get_hyperedge_meta(dh4,2) - - dh5 = DirectedHypergraph{Float64,Nothing,String}(1,1) - @test SimpleHypergraphs.add_hyperedge!(dh5;he_meta_tail="test") == 2 - @test SimpleHypergraphs.set_hyperedge_meta!(dh5,"t", "h", 1) == (["t", "test"], ["h", nothing]) - @test get_hyperedge_meta(dh5,2) == ("test", nothing) - @test get_vertex_meta(dh5,1) === nothing - @test_throws BoundsError get_vertex_meta(dh5,2) - - dh6 = DirectedHypergraph{Float64,String,String,SortedDict{Int,Float64}}(1,1) - @test typeof(dh6.hg_tail.v2he[1]) <: Dict{Int,Float64} - @test typeof(dh6.hg_head.v2he[1]) <: Dict{Int,Float64} - @test typeof(dh6.hg_tail.he2v[1]) <: Dict{Int,Float64} - @test typeof(dh6.hg_head.he2v[1]) <: Dict{Int,Float64} - @test SimpleHypergraphs.add_vertex!(dh6;v_meta="test") == 2 - @test SimpleHypergraphs.set_vertex_meta!(dh6,"t",1) == ["t","test"] - @test get_vertex_meta(dh6,2) == "test" - @test get_hyperedge_meta(dh6,1) == (nothing, nothing) - @test SimpleHypergraphs.add_hyperedge!(dh6;he_meta_tail="test") == 2 - @test SimpleHypergraphs.set_hyperedge_meta!(dh6,"t", "h", 1) == (["t", "test"], ["h", nothing]) - @test get_hyperedge_meta(dh6,2) == ("test", nothing) - @test_throws BoundsError get_vertex_meta(dh6,3) - @test_throws BoundsError get_hyperedge_meta(dh6,3) - dh6.hg_tail .= [1.0 2.0;3.0 4.0] - @test dh6[2,2] == (4.0, nothing) + @test dh1 == DirectedHypergraph{Float64,Nothing,Nothing}(mtail, mhead) + @test dh1 == DirectedHypergraph{Float64,Nothing,Nothing,Dict{Int,Float64}}(mtail, mhead) + @test all(Matrix(dh1.hg_tail) .== Matrix( + DirectedHypergraph{Float64,Nothing,Nothing,SortedDict{Int,Float64}}(mtail, mhead).hg_tail) + ) + @test all(Matrix(dh1.hg_head) .== Matrix( + DirectedHypergraph{Float64,Nothing,Nothing,SortedDict{Int,Float64}}(mtail, mhead).hg_head) + ) + @test getindex(dh1, 5, 4) == (7.0, nothing) + + dh4 = DirectedHypergraph{Float64,String,Nothing}(1, 1) + @test SimpleHypergraphs.add_vertex!(dh4; v_meta="test") == 2 + @test SimpleHypergraphs.set_vertex_meta!(dh4, "t", 1) == ["t", "test"] + @test SimpleHypergraphs.get_vertex_meta(dh4, 2) == "test" + @test get_hyperedge_meta(dh4, 1) == (nothing, nothing) + @test_throws BoundsError get_hyperedge_meta(dh4, 2) + + dh5 = DirectedHypergraph{Float64,Nothing,String}(1, 1) + @test SimpleHypergraphs.add_hyperedge!(dh5; he_meta_tail="test") == 2 + @test SimpleHypergraphs.set_hyperedge_meta!(dh5, "t", "h", 1) == (["t", "test"], ["h", nothing]) + @test get_hyperedge_meta(dh5, 2) == ("test", nothing) + @test get_vertex_meta(dh5, 1) === nothing + @test_throws BoundsError get_vertex_meta(dh5, 2) + + dh6 = DirectedHypergraph{Float64,String,String,SortedDict{Int,Float64}}(1, 1) + @test typeof(dh6.hg_tail.v2he[1]) <: AbstractDict{Int,Float64} + @test typeof(dh6.hg_head.v2he[1]) <: AbstractDict{Int,Float64} + @test typeof(dh6.hg_tail.he2v[1]) <: AbstractDict{Int,Float64} + @test typeof(dh6.hg_head.he2v[1]) <: AbstractDict{Int,Float64} + @test SimpleHypergraphs.add_vertex!(dh6; v_meta="test") == 2 + @test SimpleHypergraphs.set_vertex_meta!(dh6, "t", 1) == ["t", "test"] + @test get_vertex_meta(dh6, 2) == "test" + @test get_hyperedge_meta(dh6, 1) == (nothing, nothing) + @test SimpleHypergraphs.add_hyperedge!(dh6; he_meta_tail="test") == 2 + @test SimpleHypergraphs.set_hyperedge_meta!(dh6, "t", "h", 1) == (["t", "test"], ["h", nothing]) + @test get_hyperedge_meta(dh6, 2) == ("test", nothing) + @test_throws BoundsError get_vertex_meta(dh6, 3) + @test_throws BoundsError get_hyperedge_meta(dh6, 3) + dh6.hg_tail .= [1.0 2.0; 3.0 4.0] + @test dh6[2, 2] == (4.0, nothing) dh1_0 = deepcopy(dh1) @test SimpleHypergraphs.add_vertex!(dh1_0) == 8 - dh1_0.hg_tail[8,:] = dh1_0.hg_tail[7,:] - dh1_0.hg_head[8,:] = dh1_0.hg_head[7,:] - @test SimpleHypergraphs.remove_vertex!(dh1_0,8) == dh1 + dh1_0.hg_tail[8, :] = dh1_0.hg_tail[7, :] + dh1_0.hg_head[8, :] = dh1_0.hg_head[7, :] + @test SimpleHypergraphs.remove_vertex!(dh1_0, 8) == dh1 setindex!(dh1_0, nothing, 1, 1) - @test dh1_0[1,1] == (nothing, nothing) + @test dh1_0[1, 1] == (nothing, nothing) @test_throws BoundsError setindex!(dh1_0, nothing, 10, 9) dh1_1 = DirectedHypergraph( @@ -205,43 +227,43 @@ end; @test sum(Graphs.adjacency_matrix(Graphs.SimpleDiGraph(b))) == 11 - @test sort(collect(Graphs.outneighbors(b,5))) == [11] - @test sort(collect(Graphs.outneighbors(b,1))) == [8] - @test sort(collect(Graphs.inneighbors(b,2))) == [8] + @test sort(collect(Graphs.outneighbors(b, 5))) == [11] + @test sort(collect(Graphs.outneighbors(b, 1))) == [8] + @test sort(collect(Graphs.inneighbors(b, 2))) == [8] @test Set(Graphs.vertices(b)) == Set(1:Graphs.nv(b)) - @test SimpleHypergraphs.shortest_path(b,1,4) == [1, 3, 4] - @test SimpleHypergraphs.shortest_path(b,1,5) == Int64[] + @test SimpleHypergraphs.shortest_path(b, 1, 4) == [1, 3, 4] + @test SimpleHypergraphs.shortest_path(b, 1, 5) == Int64[] @test Graphs.is_weakly_connected(b) == false @test SimpleHypergraphs.add_vertex!(dh2) == 8 @test SimpleHypergraphs.add_hyperedge!(dh2) == 7 - dh2[1,4,7] = 1 - dh2[2,8,7] = 1 + dh2[1, 4, 7] = 1 + dh2[2, 8, 7] = 1 - @test SimpleHypergraphs.shortest_path(b,1,8) == [1,3,4,8] + @test SimpleHypergraphs.shortest_path(b, 1, 8) == [1, 3, 4, 8] bipartite_graph = Graphs.SimpleDiGraph(b) - @test Graphs.SimpleGraphs.fadj(bipartite_graph)==Graphs.SimpleGraphs.fadj(b) + @test Graphs.SimpleGraphs.fadj(bipartite_graph) == Graphs.SimpleGraphs.fadj(b) @test Graphs.nv(b) == 15 @test Graphs.ne(b) == 13 - @test sort!(Graphs.SimpleGraphs.fadj(b,1)) == [9] - @test sort!(Graphs.SimpleGraphs.fadj(b,2)) == Int64[] - @test sort!(Graphs.SimpleGraphs.badj(b,2)) == [9] + @test sort!(Graphs.SimpleGraphs.fadj(b, 1)) == [9] + @test sort!(Graphs.SimpleGraphs.fadj(b, 2)) == Int64[] + @test sort!(Graphs.SimpleGraphs.badj(b, 2)) == [9] end; @testset "SimpleDirectedHypergraphs TwoSectionView " begin - ht = DirectedHypergraph{Float64}(3,3) - ht[1,1,1] = 1 - ht.hg_head[2:3,1] .= 2 - ht[1,2,2] = 2 - ht[2,1,2] = 2 - ht[1,3,3] = 3 - ht[2,1,3] = 3 + ht = DirectedHypergraph{Float64}(3, 3) + ht[1, 1, 1] = 1 + ht.hg_head[2:3, 1] .= 2 + ht[1, 2, 2] = 2 + ht[2, 1, 2] = 2 + ht[1, 3, 3] = 3 + ht[2, 1, 3] = 3 @test Graphs.nv(Graphs.zero(TwoSectionView{DirectedHypergraph{Int64}})) == 0 @@ -258,7 +280,7 @@ end; @test Graphs.nv(t) == 7 @test Graphs.ne(t) == 6 - @test sort(Graphs.all_neighbors(t, 1)) == [2,3] + @test sort(Graphs.all_neighbors(t, 1)) == [2, 3] @test sort(Graphs.outneighbors(t, 5)) == [6] @test sort(Graphs.inneighbors(t, 4)) == [3] @inferred Graphs.all_neighbors(t, 1) @@ -267,29 +289,29 @@ end; @test Graphs.has_edge(t, 1, 5) == false @test sum(Graphs.adjacency_matrix(Graphs.SimpleDiGraph(t))) == 6 - @test SimpleHypergraphs.shortest_path(t,1,4) == [1,3,4] + @test SimpleHypergraphs.shortest_path(t, 1, 4) == [1, 3, 4] @test Graphs.is_weakly_connected(t) == false @test Graphs.is_strongly_connected(t) == false SimpleHypergraphs.add_vertex!(dh1) SimpleHypergraphs.add_hyperedge!(dh1) - dh1[1,4,7] = 1 - dh1[2,8,7] = 1 + dh1[1, 4, 7] = 1 + dh1[2, 8, 7] = 1 @test Graphs.ne(t) == 7 @test Graphs.nv(t) == 8 @test sort(Graphs.outneighbors(t, 4)) == [8] - @test SimpleHypergraphs.shortest_path(t,1,8) == [1,3,4,8] + @test SimpleHypergraphs.shortest_path(t, 1, 8) == [1, 3, 4, 8] @test sum(Graphs.adjacency_matrix(Graphs.SimpleDiGraph(t))) == 7 - Random.seed!(0); + Random.seed!(0) g = Graphs.erdos_renyi(8, 0.3; is_directed=true) h_from_g = DirectedHypergraph(g) @test Graphs.adjacency_matrix(g) == Graphs.adjacency_matrix(TwoSectionView(h_from_g)) - @test minimum([sum((h_from_g.hg_tail .== true)[:,n]) for n in 1:6] .== 1) - @test minimum([sum((h_from_g.hg_head .== true)[:,n]) for n in 1:6] .== 1) + @test minimum([sum((h_from_g.hg_tail.==true)[:, n]) for n in 1:6] .== 1) + @test minimum([sum((h_from_g.hg_head.==true)[:, n]) for n in 1:6] .== 1) @test Graphs.SimpleGraphs.fadj(g) == Graphs.SimpleGraphs.fadj(TwoSectionView(h_from_g)) @test Graphs.SimpleGraphs.badj(g) == Graphs.SimpleGraphs.badj(TwoSectionView(h_from_g)) end; @@ -299,10 +321,10 @@ end; DHᵣ = random_model(5, 5, DirectedHypergraph) @test nhv(DHᵣ) == 5 @test nhe(DHᵣ) == 5 - @test all(length.(DHᵣ.hg_tail.v2he) .> 0) - @test all(length.(DHᵣ.hg_head.v2he) .> 0) - @test all(length.(DHᵣ.hg_tail.v2he) .<= 5) - @test all(length.(DHᵣ.hg_head.v2he) .<= 5) + @test all(length.(DHᵣ.hg_tail.v2he) .> 0) + @test all(length.(DHᵣ.hg_head.v2he) .> 0) + @test all(length.(DHᵣ.hg_tail.v2he) .<= 5) + @test all(length.(DHᵣ.hg_head.v2he) .<= 5) @test_throws ErrorException random_model(1, 2, DirectedHypergraph; no_self_loops=true) @@ -312,7 +334,7 @@ end; end DHᵣ2 = random_model(5, 0, DirectedHypergraph) - add_hyperedge!(DHᵣ2;vertices_tail=Dict(2 => true, 4 => true),vertices_head=Dict(1 => true, 5 => true)) + add_hyperedge!(DHᵣ2; vertices_tail=Dict(2 => true, 4 => true), vertices_head=Dict(1 => true, 5 => true)) @test nhv(DHᵣ2) == 5 @test nhe(DHᵣ2) == 1 @@ -342,28 +364,28 @@ end; end; @testset "SimpleDirectedHypergraphs randomwalk " begin - dh = DirectedHypergraph{Float64}(8,9) - dh[1,1,1] = 1.0 - dh.hg_head[2:3,1] .= 2.5 - dh[1,3,2] = 0.5 - dh[2,4,2] = 1.5 - dh.hg_tail[3:4,3] .= 2.0 - dh[2,1,3] = 0.5 - dh[1,4,4] = 2.5 - dh[2,5,4] = 1.0 - dh[1,3,5] = 2.0 - dh[2,5,5] = 1.0 - dh[1,5,6] = 1.5 - dh[2,3,6] = 0.5 - dh[1,6,7] = 1.0 - dh[2,7,7] = 2.5 - dh[1,7,8] = 2.0 - dh[2,8,8] = 1.5 - dh[1,8,9] = 1.0 - dh[2,6,9] = 1.5 + dh = DirectedHypergraph{Float64}(8, 9) + dh[1, 1, 1] = 1.0 + dh.hg_head[2:3, 1] .= 2.5 + dh[1, 3, 2] = 0.5 + dh[2, 4, 2] = 1.5 + dh.hg_tail[3:4, 3] .= 2.0 + dh[2, 1, 3] = 0.5 + dh[1, 4, 4] = 2.5 + dh[2, 5, 4] = 1.0 + dh[1, 3, 5] = 2.0 + dh[2, 5, 5] = 1.0 + dh[1, 5, 6] = 1.5 + dh[2, 3, 6] = 0.5 + dh[1, 6, 7] = 1.0 + dh[2, 7, 7] = 2.5 + dh[1, 7, 8] = 2.0 + dh[2, 8, 8] = 1.5 + dh[1, 8, 9] = 1.0 + dh[2, 6, 9] = 1.5 dw1 = countmap([random_walk(dh, 1) for _ in 1:10^6]) - @test keys(dw1) == Set([2,3]) + @test keys(dw1) == Set([2, 3]) @test -(extrema(values(dw1))...) > -10000 dw1_rev = countmap([random_walk(dh, 1; reverse=true) for _ in 1:10^6]) @@ -381,8 +403,8 @@ end; @testset "SimpleDirectedHypergraphs connected components" begin weak_conn = sort!(get_weakly_connected_components(dh1)) @test length(weak_conn) == 2 - @test weak_conn[1] == [1,2,3,4,8] - @test weak_conn[2] == [5,6,7] + @test weak_conn[1] == [1, 2, 3, 4, 8] + @test weak_conn[2] == [5, 6, 7] strong_conn = sort!(get_strongly_connected_components(dh1)) @test length(strong_conn) == 6 @@ -390,29 +412,29 @@ end; @test strong_conn[2] == [2] @test strong_conn[3] == [3] @test strong_conn[4] == [4] - @test strong_conn[5] == [5,6,7] + @test strong_conn[5] == [5, 6, 7] @test strong_conn[6] == [8] end; @testset "SimpleDirectedHypergraphs dual " begin m_tail = [ - 1 nothing nothing nothing - nothing 2 nothing nothing - nothing nothing 3 3 - 4 nothing nothing 4 + 1 nothing nothing nothing + nothing 2 nothing nothing + nothing nothing 3 3 + 4 nothing nothing 4 ] m_head = [ - nothing 5 5 nothing - nothing nothing nothing 6 - 7 nothing nothing nothing - nothing nothing 8 nothing + nothing 5 5 nothing + nothing nothing nothing 6 + 7 nothing nothing nothing + nothing nothing 8 nothing ] - v_meta = Array{Union{Nothing, Char}, 1}(collect('a':'d')) - he_meta_tail = Array{Union{Nothing, Symbol}, 1}(Symbol.(collect('A':'D'))) - he_meta_head = Array{Union{Nothing, Symbol}, 1}(Symbol.(collect('E':'H'))) + v_meta = Array{Union{Nothing,Char},1}(collect('a':'d')) + he_meta_tail = Array{Union{Nothing,Symbol},1}(Symbol.(collect('A':'D'))) + he_meta_head = Array{Union{Nothing,Symbol},1}(Symbol.(collect('E':'H'))) dh = DirectedHypergraph{Int,Char,Symbol}(m_tail, m_head; v_meta=v_meta, he_meta_tail=he_meta_tail, he_meta_head=he_meta_head) dh_dual = dual(dh) @@ -423,10 +445,10 @@ end; m_dual_tail = Matrix(dh_dual.hg_tail) m_dual_head = Matrix(dh_dual.hg_head) - m_dual_tail[m_dual_tail .== nothing] .= 0 - m_dual_head[m_dual_head .== nothing] .= 0 - m_tail[m_tail .== nothing] .= 0 - m_head[m_head .== nothing] .= 0 + m_dual_tail[m_dual_tail.==nothing] .= 0 + m_dual_head[m_dual_head.==nothing] .= 0 + m_tail[m_tail.==nothing] .= 0 + m_head[m_head.==nothing] .= 0 @test m_tail == transpose(m_dual_tail) @test m_head == transpose(m_dual_head) @@ -435,5 +457,149 @@ end; @test dh_dual.he_meta_head == dh.v_meta @test all(dh_dual.v_meta .== [(dh.he_meta_tail[i], dh.he_meta_head[i]) for i in 1:nhe(dh)]) - @test_throws AssertionError dual(DirectedHypergraph(0,0)) + @test_throws AssertionError dual(DirectedHypergraph(0, 0)) +end; + + +@testset "SimpleDirectedHypergraphs paths/distance " begin + # Custom, strongly-connected example with loops + w2 = [3, 7, 19, 10, 13, 11, 1, 12, 8] + + # Traversing forward from vertices to vertices/hyperedges + fr = forward_reachable(dh2, 1) + @test fr[1] == Set{Int}(1:5) + @test fr[2] == Set{Int}(1:9) + fr = forward_reachable(dh2, 5) + @test fr[1] == Set{Int}(1:5) + @test fr[2] == Set{Int}(1:9) + + # Back-tracing from vertices to vertices/hyperedges + bt = backward_traceable(dh2, 1) + @test bt[1] == Set{Int}(1:5) + @test bt[2] == Set{Int}(1:9) + bt = backward_traceable(dh2, 5) + @test bt[1] == Set{Int}(1:5) + @test bt[2] == Set{Int}(1:9) + + for i in 1:nhv(dh2) + # In this example, all nodes (and hyperedges) can be reached from anywhere + for j in 1:nhv(dh2) + @test is_reachable(dh2, i, j, :vertex) + end + for e in 1:nhe(dh2) + @test is_reachable(dh2, i, e, :hyperedge) + end + end + @test_throws AssertionError is_reachable(dh2, 1, 2, :test) + + # Test `short_hyperpath_vhe` for greedy hyperpath generation + he_inedges = [ + Set{Int}([6,7]), + Set{Int}([1,8]), + Set{Int}([1,9]), + Set{Int}([2,3,5]), + Set{Int}(4), + Set{Int}([1,2,3,5,8]), + Set{Int}([1,2,3,5,9]), + Set{Int}(4), + Set{Int}(4) + ] + + # Test method to find a reasonably short path from a vertex to a hyperedge + short_path = SimpleDirectedHypergraphs.short_hyperpath_vhe(dh2, 1, 4, he_inedges, w2) + @test short_path == Set{Int}([1,2,4]) || short_path == Set{Int}([1,3,4]) + + # Test heuristic (but usually accurate) shortest-path algorithm + @test shortest_hyperpath_kk_heuristic(dh2, 1, 5, w2) == Set{Int}([1,2,4]) + @test shortest_hyperpath_kk_heuristic(dh2, 1, Set{Int}([3,4]), w2) == Set{Int}([1,2]) + @test shortest_hyperpath_kk_heuristic(dh2, Set{Int}([4,5]), 1, w2) == Set{Int}([7,9]) + @test shortest_hyperpath_kk_heuristic(dh2, Set{Int}([2, 3]), Set{Int}([4, 1]), w2) == Set{Int}([2,7]) + + # Test generation of all possible paths + @test length(all_hyperpaths(dh2, 1, 5)) == 2 + + # Example adapted from Blau et al., DOI: 10.1039/D0SC05647B + tail_3 = [ + true true true true nothing nothing nothing nothing nothing nothing nothing + nothing nothing nothing nothing true nothing nothing nothing nothing nothing nothing + nothing nothing nothing nothing nothing true nothing nothing nothing nothing nothing + nothing nothing nothing nothing nothing nothing true nothing nothing nothing nothing + nothing nothing nothing nothing nothing nothing nothing true nothing nothing nothing + nothing nothing nothing nothing nothing nothing nothing nothing true nothing nothing + nothing nothing nothing nothing nothing nothing nothing nothing nothing true nothing + nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing true + nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing + ] + head_3 = [ + nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing + nothing true nothing nothing nothing nothing nothing nothing nothing nothing nothing + true nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing + true nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing + nothing nothing true nothing nothing nothing nothing nothing nothing nothing nothing + nothing nothing true nothing nothing nothing nothing nothing nothing nothing nothing + nothing nothing nothing true nothing nothing true true nothing nothing nothing + nothing nothing nothing nothing nothing true nothing nothing true nothing nothing + nothing nothing nothing nothing true nothing nothing nothing nothing true true + ] + dh3 = DirectedHypergraph(tail_3, head_3) + + w3 = [1, 6, 2, 4, 3, 1, 2, 2, 4, 1, 1] + + # Traversing forward from vertices to vertices/hyperedges + fr = forward_reachable(dh3, 1) + @test fr[1] == Set{Int}(1:9) + @test fr[2] == Set{Int}(1:11) + fr = forward_reachable(dh3, 6) + @test fr[1] == Set{Int}([6, 8, 9]) + @test fr[2] == Set{Int}([9, 11]) + fr = forward_reachable(dh3, 9) + @test fr[1] == Set{Int}(9) + @test fr[2] == Set{Int}() + + # Back-tracing from vertices to vertices/hyperedges + bt = backward_traceable(dh3, 1) + @test bt[1] == Set{Int}(1) + @test bt[2] == Set{Int}() + bt = backward_traceable(dh3, 6) + @test bt[1] == Set{Int}([6, 1]) + @test bt[2] == Set{Int}(3) + bt = backward_traceable(dh3, 9) + @test bt[1] == Set{Int}(1:9) + @test bt[2] == Set{Int}(1:11) + + # Test heuristic (but usually accurate) shortest-path algorithm + @test shortest_hyperpath_kk_heuristic(dh3, 1, 9, w3) == Set{Int}([1, 6, 11]) + @test_throws AssertionError shortest_hyperpath_kk_heuristic(dh3, 9, 1, w2) + + # Test generation of all possible paths + @test length(all_hyperpaths(dh3, 1, 9)) == 6 + @test length(all_hyperpaths(dh3, 1, Set{Int}([8, 9]))) == 7 + @test length(all_hyperpaths(dh3, Set{Int}([1,8]), 9)) == 5 + @test length(all_hyperpaths(dh3, Set{Int}([2,6]), Set{Int}([8,9]))) == 2 + + # Test exact (integer linear programming) shortest-path algorithm + model = SimpleDirectedHypergraphs.initialize_ilp_model(dh3, 1, 9, w3) + @test JuMP.num_constraints(model[1], JuMP.VariableRef, JuMP.MOI.ZeroOne) == 11 + @test JuMP.num_constraints(model[1], JuMP.AffExpr, JuMP.MOI.GreaterThan{Float64}) == 20 + @test JuMP.solver_name(model[1]) == "GLPK" + @test JuMP.objective_sense(model[1]) == JuMP.MIN_SENSE + @test JuMP.num_variables(model[1]) == 11 + @test_throws AssertionError SimpleDirectedHypergraphs.initialize_ilp_model(dh3, 9, 1, w3) + + @test shortest_hyperpath_kk_ilp(dh3, 1, 9, w3) == Set{Int}([1,6,11]) + @test shortest_hyperpath_kk_ilp(dh3, 1, Set{Int}([7,9]), w3) == Set{Int}([1,7,10]) + @test shortest_hyperpath_kk_ilp(dh3, Set{Int}([2,6]), 9, w3) == Set{Int}(5) + @test shortest_hyperpath_kk_ilp(dh3, Set{Int}([4,6]), Set{Int}([8,9]), w3) == Set{Int}([9,11]) + + # Test distance + @test SimpleHypergraphs.distance(dh3, SnodeDistanceKKHeuristic(Set{Int}(1), Set{Int}(9)), w3) == 3 + @test SimpleHypergraphs.distance(dh3, SnodeDistanceKKILP(Set{Int}(1), Set{Int}([7,9])), w3) == 4 +end + +@testset "SimpleDirectedHypergraphs diameter " begin + @test Graphs.diameter(dh1, SnodeDistanceKKHeuristic(Set{Int}(), Set{Int}())) == Inf64 + @test Graphs.diameter(dh2, SnodeDistanceKKHeuristic(Set{Int}(), Set{Int}())) == 3 + + @test Graphs.diameter(dh1, SnodeDistanceKKILP(Set{Int}(), Set{Int}())) == Inf64 + @test Graphs.diameter(dh2, SnodeDistanceKKILP(Set{Int}(), Set{Int}())) == 3 end; \ No newline at end of file