From 0bfd477f8dea3a4193bc7bc9046c9bf0a9ae5724 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 2 Sep 2025 12:41:45 +0100 Subject: [PATCH 01/30] Work towards new random models; some small bugfixes --- LICENSE | 2 +- src/dihypergraph.jl | 4 ++-- src/models/random-models.jl | 3 +++ src/models/twosection.jl | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) 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/src/dihypergraph.jl b/src/dihypergraph.jl index 758835c..bf82239 100644 --- a/src/dihypergraph.jl +++ b/src/dihypergraph.jl @@ -147,8 +147,8 @@ struct DirectedHypergraph{T<:Real,V,E,D<:AbstractDict{Int, T}} <: AbstractDirect 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 ) diff --git a/src/models/random-models.jl b/src/models/random-models.jl index c0818a0..400b8ce 100644 --- a/src/models/random-models.jl +++ b/src/models/random-models.jl @@ -164,3 +164,6 @@ function SimpleHypergraphs.random_dregular_model( HType(mx_tail, mx_head) end + +# TODO: preferential attachment +# TODO: hypercurveball algorithm \ 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 From 33db091eff0cd6078218c3a0e7ecf14459b34b1b Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 5 Nov 2025 10:26:13 +0000 Subject: [PATCH 02/30] Early days on some pathfinding algorithms --- src/SimpleDirectedHypergraphs.jl | 1 + src/algorithms/clustering.jl | 0 src/algorithms/distance.jl | 31 +++ src/algorithms/paths.jl | 324 +++++++++++++++++++++++++++++++ src/dihypergraph.jl | 4 +- src/models/random-models.jl | 3 +- 6 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 src/algorithms/clustering.jl create mode 100644 src/algorithms/distance.jl create mode 100644 src/algorithms/paths.jl diff --git a/src/SimpleDirectedHypergraphs.jl b/src/SimpleDirectedHypergraphs.jl index 8c42f5d..a836e7d 100644 --- a/src/SimpleDirectedHypergraphs.jl +++ b/src/SimpleDirectedHypergraphs.jl @@ -9,6 +9,7 @@ using JSON3 using Random using LinearAlgebra using SimpleTraits +using InvertedIndices export AbstractDirectedHypergraph export DirectedHypergraph, HyperedgeDirection diff --git a/src/algorithms/clustering.jl b/src/algorithms/clustering.jl new file mode 100644 index 0000000..e69de29 diff --git a/src/algorithms/distance.jl b/src/algorithms/distance.jl new file mode 100644 index 0000000..d9a24fb --- /dev/null +++ b/src/algorithms/distance.jl @@ -0,0 +1,31 @@ +""" + struct SnodeDistanceKKHeuristic(sources::Vector{Int}, targets::Vector{Int}) <: AbstractDistance + +Represent a distance between a set of *source* nodes and a set of *target* nodes in a of directed hypergraph `h`, +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 + +""" + struct SnodeDistanceKKExact(sources::Vector{Int}, targets::Vector{Int}) <: AbstractDistance + +Represent a distance between a set of *source* nodes and a set of *target* nodes in a of directed hypergraph `h`, +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 algorith, see `SnodeDistanceKKHeuristic`. +""" +struct SnodeDistanceKKExact <: AbstractDistance + sources::Set{Int} + targets::Set{Int} +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..8843f74 --- /dev/null +++ b/src/algorithms/paths.jl @@ -0,0 +1,324 @@ +# TODO: weights (default vector of 1.0 of length nve(hg)) + +struct DiHyperPathState{T} where {T<:Real} + reached_vs::BitVector + marked_hes::BitVector + removed_hes::BitVector + hes_tail_count::Vector{Int} + he_inedges::Vector{Set{Int}} + edge_weights::Vector{T} + edge_costs::Vector{T} + edge_heap_points::Vector{Union{Nothing, Int}} +end + +function initialize_dihyperpath_state( + hg::H, + hyperedge_weights::Vector{T} + ) where {H <: AbstractDirectedHypergraph, T <: Real} + nv = nhv(hg) + ne = nhe(hg) + + edge_heap_points = Vector{Union{Nothing, Int}}(undef, ne) + edge_heap_points .= nothing + + return DiHyperPathState{T}( + BitVector(zeros(nv)), + BitVector(zeros(ne)), + BitVector(zeros(ne)), + length.(keys.(hg.hg_tail.he2v)), + [Set{Int}() for _ in 1:ne], + hyperedge_weights, + fill(typemax(T), ne), + edge_heap_points + ) +end + +""" +Kind of silly function +""" +function validate_weights(w::AbstractVector{T}) where {T <: Real} + @assert all(w .>= 0) +end + +""" + +""" +function forward_reachable(hg::H, source::Int, state::DiHyperPathState{T}) where {H <: AbstractDirectedHypergraph, T <: Real} + # Priority queue of reached vertices + Q = Queue{Int}() + enqueue!(Q, source) + + state.reached_vs[source] = true + + # Which vertices/hyperedges have been reached? + reached_vs = Set{Int}() + reached_es = Set{Int}() + + while length(Q) > 0 + v = popfirst!(Q) + push!(reached_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? + state.hes_tail_count[out_e] -= 1 + + if state.hes_tail_count[out_e] == 0 + push!(reached_es, out_e) + + for w in keys(hg.hg_head.he2v[out_e]) + if !state.reached_vs[w] + enqueue!(Q, w) + state.reached_vs[w] = true + end + end + end + end + end + + # Restore state + for v in reached_vs + state.reached_vs[v] = false + for out_e in hg.hg_tail.v2he[v] + state.hes_tail_count[out_e] += 1 + end + end + + # Return reached vertices and hyperedges + return reached_vs, reached_es +end + +""" + +""" +function backward_traceable(hg::H, target::Int, state::DiHyperPathState{T}) where {H <: AbstractDirectedHypergraph, T <: Real} + # Priority queue of reached vertices + Q = Queue{Int}() + enqueue!(Q, target) + + state.reached_vs[target] = true + + # Which vertices/hyperedges have been reached? + reached_vs = Set{Int}() + reached_es = Set{Int}() + + while length(Q) > 0 + v = popfirst!(Q) + push!(reached_vs, v) + + for in_e in keys(hg.hg_head.v2he[v]) + if !state.marked_hes[in_e] + push!(reached_es, in_e) + state.marked_hes[in_e] = true + + for w in keys(hg.hg_tail.he2v[in_e]) + if !state.reached_vs[w] + enqueue!(Q, w) + state.reached_vs[w] = true + end + end + end + end + end + + # Restore state + for v in reached_vs + state.reached_vs[v] = false + end + for he in reached_es + state.marked_hes[he] = false + end + + # Return reached vertices and hyperedges + return reached_vs, reached_es +end + + +""" + +""" +function shortest_hyperpath_kk_heuristic( + hg::H, + source::Int, + target::Int, + cost_function::Function, + hyperedge_weights::Vector{T} +) where {H <: AbstractDirectedHypergraph, T <: Real} + state = initialize_dihyperpath_state(hg, hyperedge_weights) + + # Doubly reachable hyperedges + dr_hes = intersect( + forward_reachable(hg, source, state)[2], + backward_traceable(hg, target, state)[2] + ) + + # Eliminate non-doubly reachable hyperedges + hg_copy = deepcopy(hg) + hg_copy[:, Not(dr_hes)] .= nothing + + # Min-heap for hyperedges + Hmin = MutableBinaryMinHeap{Tuple{Int, T}}() + 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 + # TODO: what do cost functions need? + state.edge_heap_points[out_e] = push!(Hmin, cost_function(hg, state, out_e)) + end + end + + state.reached_vs[source] = true + + while length(Hmin) > 0 + e = pop!(Hmin) + state.removed_hes[e] = true + + path = short_hyperpath_vhe(hg, source, e) + # TODO: nature of cost function for hyperedge vs. cost function for path + state.edge_costs[e] = cost_function(hg, state, path) + + out_edges = Set{Int}() + for v in keys(hg.hg_head.he2v[e]) + for f in keys(hg.hg_tail.v2he[v]) + if !state.reached_vs[v] + state.hes_tail_count[f] -= 1 + end + + if !state.marked_hes[f] + push!(out_edges, f) + state.marked_hes[f] = true + end + end + state.reached_vs[v] = true + end + + for f in out_edges + push!(state.he_inedges[f], e) + if !isnothing(state.edge_heap_points[f]) && !state.removed_hes[f] + update!(Hmin, state.edge_heap_points[f], cost_function(hg, state, short_hyperpath_vhe(hg, source, f))) + elseif isnothing(state.edge_heap_points[f]) && state.hes_tail_count[f] == 0 + state.edge_heap_points[f] = push!(Hmin, cost_function(hg, state, short_hyperpath_vhe(hg, source, f))) + end + end + end + + path = Set{Int}() + cost = typemax(T) + for in_e in keys(hg.hg_head.v2he[target]) + if !isnothing(state.edge_heap_points[in_e]) + p = short_hyperpath_vhe(hg, source, in_e) + cost_p = cost_function(hg, state, p) + if cost_p < cost + path = p + cost = cost_p + end + end + end + + return path +end + +function shortest_hyperpath_kk_heuristic( + hg::H, + source::Int, + targets::Set{Int}, + cost_function::Function +) where {H <: AbstractDirectedHypergraph} + +end + +function shortest_hyperpath_kk_heuristic( + hg::H, + sources::Set{Int}, + target::Int, + cost_function::Function +) where {H <: AbstractDirectedHypergraph} + +end + +function shortest_hyperpath_kk_heuristic( + hg::H, + sources::Set{Int}, + targets::Set{Int}, + cost_function::Function +) where {H <: AbstractDirectedHypergraph} + +end + +""" + +""" +function short_hyperpath_vhe(hg::H, v::Int, he::Int) where {H <: AbstractDirectedHypergraph} + Q = Queue{Int}() + for e in state.he_inedges[he] + enqueue!(Q, e) + state.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 = popfirst!(Q) + push!(superpath, e) + + for f in state.he_inedges[e] + if !state.marked_hes[f] + enqueue!(Q, f) + state.marked_hes[f] = true + end + end + end + + for e in superpath + state.marked_hes[e] = false + end + + # TODO: try to be more clever about this + superpath = sort(collect(superpath), by=x -> state.edge_costs[x], rev=true) + hg_copy = deepcopy(hg) + # Eliminate all hyperedges not on superpath + hg_copy[:, 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[:, e] .= nothing + + # Only if hyperedge is essential for reaching target, + if !is_reachable(hg_copy, s, he) + # Restore hyperedge to hypergraph copy + hg_copy[:, e] .= hg[:, e] + push!(path, e) + end + end + + return path +end + +""" + +""" +function is_reachable(hg::H, v::Int, he::Int) where {H <: AbstractDirectedHypergraph} + # TODO +end + +""" + +""" +function all_hyperpaths(hg::H, source::Int, target::Int) where {H <: AbstractDirectedHypergraph} + +end + +function all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} + +end + +function all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H <: AbstractDirectedHypergraph} + +end + +function all_hyperpaths(hg::H, sources::Set{Int}, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} + +end \ No newline at end of file diff --git a/src/dihypergraph.jl b/src/dihypergraph.jl index bf82239..8198899 100644 --- a/src/dihypergraph.jl +++ b/src/dihypergraph.jl @@ -133,8 +133,8 @@ an error if the vertex metadata of the two hypergraphs is not element-for-elemen 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} + 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}} diff --git a/src/models/random-models.jl b/src/models/random-models.jl index 400b8ce..e4e81d6 100644 --- a/src/models/random-models.jl +++ b/src/models/random-models.jl @@ -166,4 +166,5 @@ function SimpleHypergraphs.random_dregular_model( end # TODO: preferential attachment -# TODO: hypercurveball algorithm \ No newline at end of file +# TODO: hypercurveball algorithm +# YOU ARE HERE \ No newline at end of file From 7ff3772ac3f9de91c88e275d69c95760c22c07ac Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 6 Nov 2025 09:25:21 +0000 Subject: [PATCH 03/30] More fleshed out methods for pathfinding --- src/algorithms/paths.jl | 90 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 9 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 8843f74..e27b6c6 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -217,30 +217,102 @@ function shortest_hyperpath_kk_heuristic( end function shortest_hyperpath_kk_heuristic( - hg::H, + hg::DirectedHypergraph{T, V, E, D}, source::Int, targets::Set{Int}, - cost_function::Function -) where {H <: AbstractDirectedHypergraph} + cost_function::Function, + hyperedge_weights::Vector{T} +) 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 = add_vertex!(hg_copy) + meta_he = 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, + cost_function, + vcat(hyperedge_weights, convert(T, 0)) + ) + + # Remove the fictitious hyperedge from the targets to the metatarget + setdiff(path, Set{Int}(meta_he)) end function shortest_hyperpath_kk_heuristic( - hg::H, + hg::DirectedHypergraph{T, V, E, D}, sources::Set{Int}, target::Int, - cost_function::Function -) where {H <: AbstractDirectedHypergraph} + cost_function::Function, + hyperedge_weights::Vector{T} +) 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 = add_vertex!(hg_copy) + meta_he = 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, + cost_function, + vcat(hyperedge_weights, convert(T, 0)) + ) + # Remove the fictitious hyperedge from the metasource to the sources + setdiff(path, Set{Int}(meta_he)) end function shortest_hyperpath_kk_heuristic( - hg::H, + hg::DirectedHypergraph{T, V, E, D}, sources::Set{Int}, targets::Set{Int}, - cost_function::Function -) where {H <: AbstractDirectedHypergraph} + cost_function::Function, + hyperedge_weights::Vector{T} +) 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 = add_vertex!(hg_copy) + meta_he_source = 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 = add_vertex!(hg_copy) + meta_he_target = 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, + cost_function, + vcat(hyperedge_weights, [convert(T, 0), convert(T, 0)]) + ) + # Remove fictitious hyperedges + setdiff(path, Set{Int}([meta_he_source, meta_he_target])) end """ From f37fb1d494a62c5eed07607b9634b5b1202e5a06 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 6 Nov 2025 10:19:52 +0000 Subject: [PATCH 04/30] Almost finished w/ v1 (minus docstrings); finishing touches and tests incoming --- src/algorithms/paths.jl | 188 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 176 insertions(+), 12 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index e27b6c6..4ced4bf 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -15,6 +15,9 @@ function initialize_dihyperpath_state( hg::H, hyperedge_weights::Vector{T} ) where {H <: AbstractDirectedHypergraph, T <: Real} + # Hyperedge weights need to be nonnegative + @assert all(hyperedge_weights .>= 0) + nv = nhv(hg) ne = nhe(hg) @@ -33,13 +36,6 @@ function initialize_dihyperpath_state( ) end -""" -Kind of silly function -""" -function validate_weights(w::AbstractVector{T}) where {T <: Real} - @assert all(w .>= 0) -end - """ """ @@ -318,7 +314,12 @@ end """ """ -function short_hyperpath_vhe(hg::H, v::Int, he::Int) where {H <: AbstractDirectedHypergraph} +function short_hyperpath_vhe( + hg::H, + v::Int, + he::Int, + state::DiHyperPathState{T} +) where {H <: AbstractDirectedHypergraph, T <: Real} Q = Queue{Int}() for e in state.he_inedges[he] enqueue!(Q, e) @@ -359,7 +360,7 @@ function short_hyperpath_vhe(hg::H, v::Int, he::Int) where {H <: AbstractDirecte hg_copy[:, e] .= nothing # Only if hyperedge is essential for reaching target, - if !is_reachable(hg_copy, s, he) + if !is_reachable(hg_copy, s, he, state) # Restore hyperedge to hypergraph copy hg_copy[:, e] .= hg[:, e] push!(path, e) @@ -372,19 +373,182 @@ end """ """ -function is_reachable(hg::H, v::Int, he::Int) where {H <: AbstractDirectedHypergraph} - # TODO +function is_reachable( + hg::H, + source_v::Int, + target_he::Int, + state::DiHyperPathState{T} +) where {H <: AbstractDirectedHypergraph, T <: Real} + # Priority queue of reached vertices + Q = Queue{Int}() + enqueue!(Q, source_v) + + state.reached_vs[source_v] = true + + # Which vertices/hyperedges have been reached? + reached_vs = Set{Int}() + reached_es = Set{Int}() + + while length(Q) > 0 + v = popfirst!(Q) + push!(reached_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? + state.hes_tail_count[out_e] -= 1 + + if state.hes_tail_count[out_e] == 0 + push!(reached_es, out_e) + if out_e == target_he + return true + end + + for w in keys(hg.hg_head.he2v[out_e]) + if !state.reached_vs[w] + enqueue!(Q, w) + state.reached_vs[w] = true + end + end + end + end + end + + # Restore state + for v in reached_vs + state.reached_vs[v] = false + for out_e in hg.hg_tail.v2he[v] + state.hes_tail_count[out_e] += 1 + end + end + + return false +end + +""" + +""" +function is_reachable( + hg::H, + source_v::Int, + target_v::Int, + state::DiHyperPathState{T} +) where {H <: AbstractDirectedHypergraph, T <: Real} + # Priority queue of reached vertices + Q = Queue{Int}() + enqueue!(Q, source_v) + + state.reached_vs[source_v] = true + + # Which vertices/hyperedges have been reached? + reached_vs = Set{Int}() + reached_es = Set{Int}() + + while length(Q) > 0 + v = popfirst!(Q) + push!(reached_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? + state.hes_tail_count[out_e] -= 1 + + if state.hes_tail_count[out_e] == 0 + push!(reached_es, out_e) + + for w in keys(hg.hg_head.he2v[out_e]) + if w == target_v + return true + end + + if !state.reached_vs[w] + enqueue!(Q, w) + state.reached_vs[w] = true + end + end + end + end + end + + # Restore state + for v in reached_vs + state.reached_vs[v] = false + for out_e in hg.hg_tail.v2he[v] + state.hes_tail_count[out_e] += 1 + end + end + + return false +end + +""" + +""" +function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H <: AbstractDirectedHypergraph} + # Remove excluded hyperedges + hg_copy = deepcopy(hg) + hg_copy[:, sort(collect(out))] .= nothing + + weights = ones(nhe(hg_copy)) + state = initialize_dihyperpath_state(hg_copy, weights) + + reached_vs, reached_es = forward_reachable(hg_copy, source, state) + + # 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[:, e] .= nothing + + # Only if hyperedge is essential for reaching target, + if !isreachable(hg_copy, source, target, state) + # Restore hyperedge to hypergraph copy + hg_copy[:, e] .= hg[:, e] + push!(path, e) + end + end + + return path end """ """ 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 = popfirst!(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::H, source::Int, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} - + # TODO: you are here end function all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H <: AbstractDirectedHypergraph} From bed512e70a48151b7741eece1ff10d47ca983fea Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 6 Nov 2025 15:50:17 +0000 Subject: [PATCH 05/30] Docstrings, mostly --- src/algorithms/paths.jl | 161 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 154 insertions(+), 7 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 4ced4bf..a014e34 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -1,5 +1,29 @@ # TODO: weights (default vector of 1.0 of length nve(hg)) +""" + struct DiHyperPathState{T}( + reached_vs::BitVector, + marked_hes::BitVector, + removed_hes::BitVector, + hes_tail_count::Vector{Int}, + he_inedges::Vector{Set{Int}}, + edge_weights::Vector{T}, + edge_costs::Vector{T}, + edge_heap_points::Vector{Union{Nothing, Int}} + ) where {T<:Real} + + Stores the state of a pathfinding operation. + + `reached_vs` and `marked_hes` track which vertices and hyperedges, respectively, have been visited and/or need to + be searched. `removed_hes` tracks which hyperedges have been removed from the edge heap during the main heuristic + pathfinding operation (see `shortest_hyperpath_kk_heuristic`). `hes_tail_count` is a vector containing the size of + the tail of each hyperedge. `he_inedges` tracks which hyperedges flow into which other hyperedges (i.e., for a + hyperedge `f`, the *inedges* are those hyperedges `e` where there exists some vertex `v` that is in both the head + of `e` and the tail of `f`). `edge_weights` are a set of initially-defined costs associated with each directed + hyperedge, and `edge_costs` are the (estimated) costs from the source vertex to each hyperedge. Finally, + `edge_heap_points` tracks the references for each hyperedge in the heap used during heuristic pathfinding. + +""" struct DiHyperPathState{T} where {T<:Real} reached_vs::BitVector marked_hes::BitVector @@ -11,6 +35,15 @@ struct DiHyperPathState{T} where {T<:Real} edge_heap_points::Vector{Union{Nothing, Int}} end +""" + initialize_dihyperpath_state( + hg::H, + hyperedge_weights::Vector{T} + ) where {H <: AbstractDirectedHypergraph, T <: Real} + + Construct an initial state for directed hypergraph pathfinding on hypergraph `hg` with hyperedge weights (i.e., + costs) `hyperedge_weights`. +""" function initialize_dihyperpath_state( hg::H, hyperedge_weights::Vector{T} @@ -37,9 +70,19 @@ function initialize_dihyperpath_state( end """ + forward_reachable( + hg::H, + source::Int, + state::DiHyperPathState{T} + ) where {H <: AbstractDirectedHypergraph, T <: Real} + """ -function forward_reachable(hg::H, source::Int, state::DiHyperPathState{T}) where {H <: AbstractDirectedHypergraph, T <: Real} +function forward_reachable( + hg::H, + source::Int, + state::DiHyperPathState{T} +) where {H <: AbstractDirectedHypergraph, T <: Real} # Priority queue of reached vertices Q = Queue{Int}() enqueue!(Q, source) @@ -130,7 +173,48 @@ end """ + shortest_hyperpath_kk_heuristic( + hg::H, + source::Int, + target::Int, + cost_function::Function, + hyperedge_weights::Vector{T} + ) where {H <: AbstractDirectedHypergraph, T <: Real} + + shortest_hyperpath_kk_heuristic( + hg::DirectedHypergraph{T, V, E, D}, + source::Int, + targets::Set{Int}, + cost_function::Function, + hyperedge_weights::Vector{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, + cost_function::Function, + hyperedge_weights::Vector{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}, + cost_function::Function, + hyperedge_weights::Vector{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 `cost_function` (which must take in the hypergraph `hg`, the current `state`, and some + pathway (collection of hyperedge)), 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, @@ -167,7 +251,7 @@ function shortest_hyperpath_kk_heuristic( e = pop!(Hmin) state.removed_hes[e] = true - path = short_hyperpath_vhe(hg, source, e) + path = short_hyperpath_vhe(hg, source, e, state) # TODO: nature of cost function for hyperedge vs. cost function for path state.edge_costs[e] = cost_function(hg, state, path) @@ -189,9 +273,9 @@ function shortest_hyperpath_kk_heuristic( for f in out_edges push!(state.he_inedges[f], e) if !isnothing(state.edge_heap_points[f]) && !state.removed_hes[f] - update!(Hmin, state.edge_heap_points[f], cost_function(hg, state, short_hyperpath_vhe(hg, source, f))) + update!(Hmin, state.edge_heap_points[f], cost_function(hg, state, short_hyperpath_vhe(hg, source, f, state))) elseif isnothing(state.edge_heap_points[f]) && state.hes_tail_count[f] == 0 - state.edge_heap_points[f] = push!(Hmin, cost_function(hg, state, short_hyperpath_vhe(hg, source, f))) + state.edge_heap_points[f] = push!(Hmin, cost_function(hg, state, short_hyperpath_vhe(hg, source, f, state))) end end end @@ -200,7 +284,7 @@ function shortest_hyperpath_kk_heuristic( cost = typemax(T) for in_e in keys(hg.hg_head.v2he[target]) if !isnothing(state.edge_heap_points[in_e]) - p = short_hyperpath_vhe(hg, source, in_e) + p = short_hyperpath_vhe(hg, source, in_e, state) cost_p = cost_function(hg, state, p) if cost_p < cost path = p @@ -360,7 +444,7 @@ function short_hyperpath_vhe( hg_copy[:, e] .= nothing # Only if hyperedge is essential for reaching target, - if !is_reachable(hg_copy, s, he, state) + if !is_reachable(hg_copy, v, he, state) # Restore hyperedge to hypergraph copy hg_copy[:, e] .= hg[:, e] push!(path, e) @@ -548,13 +632,76 @@ function all_hyperpaths(hg::H, source::Int, target::Int) where {H <: AbstractDir end function all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} - # TODO: you are here + 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 = add_vertex!(hg_copy) + meta_he = 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::H, sources::Set{Int}, target::Int) where {H <: AbstractDirectedHypergraph} + 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 = add_vertex!(hg_copy) + meta_he = 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::H, sources::Set{Int}, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} + 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 = add_vertex!(hg_copy) + meta_he_source = 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 = add_vertex!(hg_copy) + meta_he_target = 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 \ No newline at end of file From 68509fae4b96322dd0b6cb20d1f96c884d7ca4d2 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Fri, 7 Nov 2025 10:59:33 +0000 Subject: [PATCH 06/30] Docstrings done; testing time, baby (or I implement the exact algorithm first) --- src/algorithms/paths.jl | 66 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index a014e34..41c71b6 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -1,5 +1,3 @@ -# TODO: weights (default vector of 1.0 of length nve(hg)) - """ struct DiHyperPathState{T}( reached_vs::BitVector, @@ -76,7 +74,10 @@ end state::DiHyperPathState{T} ) where {H <: AbstractDirectedHypergraph, T <: Real} + 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). + `DiHyperPathState` `state` is used to track what vertices/hyperedges have been traversed. """ function forward_reachable( hg::H, @@ -127,9 +128,22 @@ function forward_reachable( end """ + backward_traceable( + hg::H, + target::Int, + state::DiHyperPathState{T} + ) where {H <: AbstractDirectedHypergraph, T <: Real} + + 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). + `DiHyperPathState` `state` is used to track what vertices/hyperedges have been traversed. """ -function backward_traceable(hg::H, target::Int, state::DiHyperPathState{T}) where {H <: AbstractDirectedHypergraph, T <: Real} +function backward_traceable( + hg::H, + target::Int, + state::DiHyperPathState{T} +) where {H <: AbstractDirectedHypergraph, T <: Real} # Priority queue of reached vertices Q = Queue{Int}() enqueue!(Q, target) @@ -396,7 +410,17 @@ function shortest_hyperpath_kk_heuristic( end """ + short_hyperpath_vhe( + hg::H, + v::Int, + he::Int, + state::DiHyperPathState{T} + ) 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`, using `state` to track the hypergraph traversal during pathfinding. `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, @@ -455,7 +479,16 @@ function short_hyperpath_vhe( end """ + is_reachable( + hg::H, + source_v::Int, + target_he::Int, + state::DiHyperPathState{T} + ) where {H <: AbstractDirectedHypergraph, T <: Real} + A short-circuiting version of `forward_reachable` that returns `true` if a hyperpath in hypergraph `hg` exists from + a source vertex with index `source_v` to a target hyperedge with index `target_he`. A `DiHyperPathState` object + `state` is used to keep track of what vertices and hyperedges have been visited during a traversal. """ function is_reachable( hg::H, @@ -509,7 +542,16 @@ function is_reachable( end """ + is_reachable( + hg::H, + source_v::Int, + target_v::Int, + state::DiHyperPathState{T} + ) where {H <: AbstractDirectedHypergraph, T <: Real} + A short-circuiting version of `forward_reachable` that returns `true` if a hyperpath in hypergraph `hg` exists from + a source vertex with index `source_v` to a target vertex with index `target_v`. A `DiHyperPathState` object + `state` is used to keep track of what vertices and hyperedges have been visited during a traversal. """ function is_reachable( hg::H, @@ -564,7 +606,10 @@ function is_reachable( 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 @@ -599,7 +644,22 @@ function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H end """ + all_hyperpaths(hg::H, source::Int, target::Int) where {H <: AbstractDirectedHypergraph} + all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} + + all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H <: AbstractDirectedHypergraph} + + all_hyperpaths(hg::H, sources::Set{Int}, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} + + 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 From 38b095644e299061b46c156df22b1e9f35eeed60 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Sun, 9 Nov 2025 18:14:29 +0000 Subject: [PATCH 07/30] Nearly a full implementation of the ILP-based exact minimum hyperpath algorithm --- Project.toml | 14 +- src/SimpleDirectedHypergraphs.jl | 2 + src/algorithms/paths.jl | 212 +++++++++++++++++++++++++++++++ src/models/bipartite.jl | 2 + 4 files changed, 226 insertions(+), 4 deletions(-) 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/src/SimpleDirectedHypergraphs.jl b/src/SimpleDirectedHypergraphs.jl index a836e7d..3dccec3 100644 --- a/src/SimpleDirectedHypergraphs.jl +++ b/src/SimpleDirectedHypergraphs.jl @@ -10,6 +10,8 @@ using Random using LinearAlgebra using SimpleTraits using InvertedIndices +using JuMP +import GLPK export AbstractDirectedHypergraph export DirectedHypergraph, HyperedgeDirection diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 41c71b6..24de1d8 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -764,4 +764,216 @@ function all_hyperpaths(hg::H, sources::Set{Int}, targets::Set{Int}) where {H <: # Remove fictitious hyperedges return Set(setdiff(p, Set{Int}([meta_he_source, meta_he_target])) for p in paths) +end + +function initialize_ilp_model( + hg::H, + source::Int, + target::Int, + cost_function::Function, + hyperedge_weights::Vector{T}) where {H<:AbstractDirectedHypergraph, T<:Real} + + # Initialize state + state = initialize_dihyperpath_state(hg, hyperedge_weights) + + # First, verify that the problem is well-posed + # That is, can `target` be reached from `source` + @assert is_reachable(hg, source, target, state) + + # Initialize integer linear programming model + model = Model(GLPK.Optimizer) + + # Create one binary variable for each hyperedge in `hg` + @variable(model, x[1:nhe(hg)], Bin) + 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(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])) + @constraint(model, con_scalar, sum([x[ih] for ih in in_hes]) >= x[i]) + end + + # Head-hitting inequalities + if target ∉ 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 + + @constraint(model, con_scalar, sum([x[j] for j in hits]) >= x[i]) + end + end + + # Target-production inequality + @constraint(model, con_scalar, 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, state)[1] + # TODO: this is inefficient + # Currently, will repeat a lot of work + dist_ests[v] = cost_function( + hg, + state, + shortest_hyperpath_kk_heuristic(hg, source, v, cost_function, 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)) + + @constraint(model, con_scalar, dot(x, cross) >= 1) + end + + # Define objective function + @objective(model, Min, dot(x, state.edge_weights)) + + return model, x, cuts, crosses + +end + +""" + +""" +function shortest_hyperpath_kk_ilp( + hg::H, + source::Int, + target::Int, + cost_function::Function, + hyperedge_weights::Vector{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, cost_function, hyperedge_weights) + + optimize!(model) + + # Convert floating-point solution into BitVector + # TODO: Is this necessary w/ JuMP? Or will the output really be binary? + solution = 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 + @constraint(model, con_scalar, dot(x, cross) >= 1) + end + + # Re-optimize model with new cut-constraints + optimize!(model) + solution = value.(x) .> 0.5 + + new_cuts, new_crosses = expand_cuts(hg, source, target, new_cuts, new_crosses, solution) + end + + return Set(findall(solution)) +end + +# TODO: alternates of the above with multiple sources and/or targets + +""" + +""" +function expand_cuts( + hg::H, + source::Int, + target::Int, + cuts::Vector{Set{Int}}, + crosses::Vector{BitVector}, + curr_sol::BitVector +) + 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}}() + 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) + ] + end + + greedy_v = minimum(q -> length(findall(!old_cross .&& new_cross_ev[q])), keys(new_cross_ev)) + + 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_corss) && 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/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 From 7933a117e2296b7d7ac07f04570016af033ba10b Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 17 Nov 2025 12:07:01 +0000 Subject: [PATCH 08/30] Paths DONE; now ready for testing --- src/SimpleDirectedHypergraphs.jl | 4 + src/algorithms/paths.jl | 154 ++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 2 deletions(-) diff --git a/src/SimpleDirectedHypergraphs.jl b/src/SimpleDirectedHypergraphs.jl index 3dccec3..f23e1e4 100644 --- a/src/SimpleDirectedHypergraphs.jl +++ b/src/SimpleDirectedHypergraphs.jl @@ -23,6 +23,8 @@ export to_undirected export get_weakly_connected_components, get_strongly_connected_components +export DiHyperPathState, initialize_dihyperpath_state, forward_reachable, backward_traceable, is_reachable +export all_hyperpaths, shortest_hyperpath_kk_heuristic, initialize_ilp_model, shortest_hyperpath_kk_ilp include("abstracttypes.jl") include("dihypergraph.jl") @@ -33,4 +35,6 @@ include("models/twosection.jl") include("models/random-models.jl") include("models/dual.jl") +include("algorithms/paths.jl") + end # module diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 24de1d8..589d763 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -862,7 +862,48 @@ function initialize_ilp_model( end """ + shortest_hyperpath_kk_ilp( + hg::H, + source::Int, + target::Int, + cost_function::Function, + hyperedge_weights::Vector{T} + ) where {H<:AbstractDirectedHypergraph, T<:Real} + + shortest_hyperpath_kk_ilp( + hg::H, + source::Int, + targets::Set{Int}, + cost_function::Function, + hyperedge_weights::Vector{T} + ) where {H<:AbstractDirectedHypergraph, T<:Real} + + shortest_hyperpath_kk_ilp( + hg::H, + sources::Set{Int}, + target::Int, + cost_function::Function, + hyperedge_weights::Vector{T} + ) where {H<:AbstractDirectedHypergraph, T<:Real} + + shortest_hyperpath_kk_ilp( + hg::H, + sources::Set{Int}, + targets::Set{Int}, + cost_function::Function, + hyperedge_weights::Vector{T} + ) where {H<:AbstractDirectedHypergraph, T<:Real} + + 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 `cost_function` (which must take in the hypergraph `hg`, the current `state`, and some + pathway (collection of hyperedge)), 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, @@ -900,10 +941,119 @@ function shortest_hyperpath_kk_ilp( return Set(findall(solution)) end -# TODO: alternates of the above with multiple sources and/or targets +function shortest_hyperpath_kk_ilp( + hg::H, + source::Int, + targets::Set{Int}, + cost_function::Function, + hyperedge_weights::Vector{T} +) where {H<:AbstractDirectedHypergraph, T<:Real} + 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 = add_vertex!(hg_copy) + meta_he = 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, + cost_function, + hyperedge_weights + ) + + # Remove the fictitious hyperedge from the targets to the metatarget + return setdiff(path, Set{Int}(meta_he)) +end + +function shortest_hyperpath_kk_ilp( + hg::H, + sources::Set{Int}, + target::Int, + cost_function::Function, + hyperedge_weights::Vector{T} +) where {H<:AbstractDirectedHypergraph, T<:Real} + 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 = add_vertex!(hg_copy) + meta_he = 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, + cost_function, + vcat(hyperedge_weights, convert(T, 0)) + ) + + # Remove the fictitious hyperedge from the metasource to the sources + setdiff(path, Set{Int}(meta_he)) +end + +function shortest_hyperpath_kk_ilp( + hg::H, + sources::Set{Int}, + targets::Set{Int}, + cost_function::Function, + hyperedge_weights::Vector{T} +) where {H<:AbstractDirectedHypergraph, T<:Real} + 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 = add_vertex!(hg_copy) + meta_he_source = 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 = add_vertex!(hg_copy) + meta_he_target = 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, + cost_function, + 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, @@ -912,7 +1062,7 @@ function expand_cuts( cuts::Vector{Set{Int}}, crosses::Vector{BitVector}, curr_sol::BitVector -) +) where {H <: AbstractDirectedHypergraph} new_cuts = Set{Int}[] new_crosses = BitVector[] From 28570a1818df475d1a232a2978bc0e283fa818a3 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 17 Nov 2025 12:26:29 +0000 Subject: [PATCH 09/30] On second thought, cost function doesn't seem necessary or helpful --- src/algorithms/distance.jl | 17 +++++++++--- src/algorithms/paths.jl | 54 ++++++++++++-------------------------- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/src/algorithms/distance.jl b/src/algorithms/distance.jl index d9a24fb..dc824d9 100644 --- a/src/algorithms/distance.jl +++ b/src/algorithms/distance.jl @@ -1,5 +1,5 @@ """ - struct SnodeDistanceKKHeuristic(sources::Vector{Int}, targets::Vector{Int}) <: AbstractDistance + struct SnodeDistanceKKHeuristic(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance Represent a distance between a set of *source* nodes and a set of *target* nodes in a of directed hypergraph `h`, where a distance in this context is defined as the length of the shortest hyperpath that reaches all `t ∈ targets`. @@ -15,7 +15,7 @@ struct SnodeDistanceKKHeuristic <: AbstractDistance end """ - struct SnodeDistanceKKExact(sources::Vector{Int}, targets::Vector{Int}) <: AbstractDistance + struct SnodeDistanceKKILP(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance Represent a distance between a set of *source* nodes and a set of *target* nodes in a of directed hypergraph `h`, where a distance in this context is defined as the length of the shortest hyperpath that reaches all `t ∈ targets`. @@ -25,7 +25,18 @@ The Krieger & Kececioglu algorithm solves an integer linear programming formalis For a lower-cost heuristic algorith, see `SnodeDistanceKKHeuristic`. """ -struct SnodeDistanceKKExact <: AbstractDistance +struct SnodeDistanceKKILP <: AbstractDistance sources::Set{Int} targets::Set{Int} +end + +function SimpleHypergraphs.distance( + h::H, + distance_method::SnodeDistanceKKHeuristic, + cost_function::Function, + hyperedge_weights::Vector{T} +) where {H <: AbstractDirectedHypergraph} + + return cost_function() + end \ No newline at end of file diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 589d763..cb39b26 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -191,7 +191,6 @@ end hg::H, source::Int, target::Int, - cost_function::Function, hyperedge_weights::Vector{T} ) where {H <: AbstractDirectedHypergraph, T <: Real} @@ -199,7 +198,6 @@ end hg::DirectedHypergraph{T, V, E, D}, source::Int, targets::Set{Int}, - cost_function::Function, hyperedge_weights::Vector{T} ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} @@ -207,7 +205,6 @@ end hg::DirectedHypergraph{T, V, E, D}, sources::Set{Int}, target::Int, - cost_function::Function, hyperedge_weights::Vector{T} ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} @@ -215,14 +212,13 @@ end hg::DirectedHypergraph{T, V, E, D}, sources::Set{Int}, targets::Set{Int}, - cost_function::Function, hyperedge_weights::Vector{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 `cost_function` (which must take in the hypergraph `hg`, the current `state`, and some - pathway (collection of hyperedge)), but in practice, it produces the optimal pathway approximately 99% of the time. + `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 @@ -234,7 +230,6 @@ function shortest_hyperpath_kk_heuristic( hg::H, source::Int, target::Int, - cost_function::Function, hyperedge_weights::Vector{T} ) where {H <: AbstractDirectedHypergraph, T <: Real} state = initialize_dihyperpath_state(hg, hyperedge_weights) @@ -255,7 +250,7 @@ function shortest_hyperpath_kk_heuristic( # If only the source is needed for this hyperedge if length(hg.hg_tail.he2v[out_e]) == 1 # TODO: what do cost functions need? - state.edge_heap_points[out_e] = push!(Hmin, cost_function(hg, state, out_e)) + state.edge_heap_points[out_e] = push!(Hmin, state.edge_weights[out_e]) end end @@ -267,7 +262,7 @@ function shortest_hyperpath_kk_heuristic( path = short_hyperpath_vhe(hg, source, e, state) # TODO: nature of cost function for hyperedge vs. cost function for path - state.edge_costs[e] = cost_function(hg, state, path) + state.edge_costs[e] = sum(state.edge_weights[x] for x in path) out_edges = Set{Int}() for v in keys(hg.hg_head.he2v[e]) @@ -287,9 +282,15 @@ function shortest_hyperpath_kk_heuristic( for f in out_edges push!(state.he_inedges[f], e) if !isnothing(state.edge_heap_points[f]) && !state.removed_hes[f] - update!(Hmin, state.edge_heap_points[f], cost_function(hg, state, short_hyperpath_vhe(hg, source, f, state))) + update!( + Hmin, + state.edge_heap_points[f], + sum(state.edge_weights[x] for x in short_hyperpath_vhe(hg, source, f, state)) + ) elseif isnothing(state.edge_heap_points[f]) && state.hes_tail_count[f] == 0 - state.edge_heap_points[f] = push!(Hmin, cost_function(hg, state, short_hyperpath_vhe(hg, source, f, state))) + state.edge_heap_points[f] = push!( + Hmin, + sum(state.edge_weights[x] for x in short_hyperpath_vhe(hg, source, f, state)) end end end @@ -299,7 +300,7 @@ function shortest_hyperpath_kk_heuristic( for in_e in keys(hg.hg_head.v2he[target]) if !isnothing(state.edge_heap_points[in_e]) p = short_hyperpath_vhe(hg, source, in_e, state) - cost_p = cost_function(hg, state, p) + cost_p = sum(state.edge_weights[e] for e in p) if cost_p < cost path = p cost = cost_p @@ -314,7 +315,6 @@ function shortest_hyperpath_kk_heuristic( hg::DirectedHypergraph{T, V, E, D}, source::Int, targets::Set{Int}, - cost_function::Function, hyperedge_weights::Vector{T} ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} hg_copy = deepcopy(hg) @@ -332,7 +332,6 @@ function shortest_hyperpath_kk_heuristic( hg_copy, source, metatarget, - cost_function, vcat(hyperedge_weights, convert(T, 0)) ) @@ -344,7 +343,6 @@ function shortest_hyperpath_kk_heuristic( hg::DirectedHypergraph{T, V, E, D}, sources::Set{Int}, target::Int, - cost_function::Function, hyperedge_weights::Vector{T} ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} hg_copy = deepcopy(hg) @@ -362,7 +360,6 @@ function shortest_hyperpath_kk_heuristic( hg_copy, metasource, target, - cost_function, vcat(hyperedge_weights, convert(T, 0)) ) @@ -374,7 +371,6 @@ function shortest_hyperpath_kk_heuristic( hg::DirectedHypergraph{T, V, E, D}, sources::Set{Int}, targets::Set{Int}, - cost_function::Function, hyperedge_weights::Vector{T} ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} hg_copy = deepcopy(hg) @@ -401,7 +397,6 @@ function shortest_hyperpath_kk_heuristic( hg_copy, metasource, metatarget, - cost_function, vcat(hyperedge_weights, [convert(T, 0), convert(T, 0)]) ) @@ -770,7 +765,6 @@ function initialize_ilp_model( hg::H, source::Int, target::Int, - cost_function::Function, hyperedge_weights::Vector{T}) where {H<:AbstractDirectedHypergraph, T<:Real} # Initialize state @@ -823,10 +817,8 @@ function initialize_ilp_model( for v in forward_reachable(hg, source, state)[1] # TODO: this is inefficient # Currently, will repeat a lot of work - dist_ests[v] = cost_function( - hg, - state, - shortest_hyperpath_kk_heuristic(hg, source, v, cost_function, hyperedge_weights) + dist_ests[v] = sum( + state.edge_weights[e] for e in shortest_hyperpath_kk_heuristic(hg, source, v, hyperedge_weights) ) end @@ -866,7 +858,6 @@ end hg::H, source::Int, target::Int, - cost_function::Function, hyperedge_weights::Vector{T} ) where {H<:AbstractDirectedHypergraph, T<:Real} @@ -874,7 +865,6 @@ end hg::H, source::Int, targets::Set{Int}, - cost_function::Function, hyperedge_weights::Vector{T} ) where {H<:AbstractDirectedHypergraph, T<:Real} @@ -882,7 +872,6 @@ end hg::H, sources::Set{Int}, target::Int, - cost_function::Function, hyperedge_weights::Vector{T} ) where {H<:AbstractDirectedHypergraph, T<:Real} @@ -890,14 +879,12 @@ end hg::H, sources::Set{Int}, targets::Set{Int}, - cost_function::Function, hyperedge_weights::Vector{T} ) where {H<:AbstractDirectedHypergraph, T<:Real} 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 `cost_function` (which must take in the hypergraph `hg`, the current `state`, and some - pathway (collection of hyperedge)), if and only if such a path exists. + `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 @@ -909,12 +896,11 @@ function shortest_hyperpath_kk_ilp( hg::H, source::Int, target::Int, - cost_function::Function, hyperedge_weights::Vector{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, cost_function, hyperedge_weights) + model, x, cuts, crosses = initialize_ilp_model(hg, source, target, hyperedge_weights) optimize!(model) @@ -945,7 +931,6 @@ function shortest_hyperpath_kk_ilp( hg::H, source::Int, targets::Set{Int}, - cost_function::Function, hyperedge_weights::Vector{T} ) where {H<:AbstractDirectedHypergraph, T<:Real} hg_copy = deepcopy(hg) @@ -963,7 +948,6 @@ function shortest_hyperpath_kk_ilp( hg_copy, source, metatarget, - cost_function, hyperedge_weights ) @@ -975,7 +959,6 @@ function shortest_hyperpath_kk_ilp( hg::H, sources::Set{Int}, target::Int, - cost_function::Function, hyperedge_weights::Vector{T} ) where {H<:AbstractDirectedHypergraph, T<:Real} hg_copy = deepcopy(hg) @@ -993,7 +976,6 @@ function shortest_hyperpath_kk_ilp( hg_copy, metasource, target, - cost_function, vcat(hyperedge_weights, convert(T, 0)) ) @@ -1005,7 +987,6 @@ function shortest_hyperpath_kk_ilp( hg::H, sources::Set{Int}, targets::Set{Int}, - cost_function::Function, hyperedge_weights::Vector{T} ) where {H<:AbstractDirectedHypergraph, T<:Real} hg_copy = deepcopy(hg) @@ -1032,7 +1013,6 @@ function shortest_hyperpath_kk_ilp( hg_copy, metasource, metatarget, - cost_function, vcat(hyperedge_weights, [convert(T, 0), convert(T, 0)]) ) From f54e782313d5526f616b158d54b93d18f0fa700c Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 17 Nov 2025 13:04:04 +0000 Subject: [PATCH 10/30] Notions of distance and diameter --- src/algorithms/distance.jl | 140 +++++++++++++++++++++++++++++++++++-- src/dihypergraph.jl | 2 +- 2 files changed, 137 insertions(+), 5 deletions(-) diff --git a/src/algorithms/distance.jl b/src/algorithms/distance.jl index dc824d9..e3559d6 100644 --- a/src/algorithms/distance.jl +++ b/src/algorithms/distance.jl @@ -30,13 +30,145 @@ struct SnodeDistanceKKILP <: AbstractDistance targets::Set{Int} end +""" + SimpleHypergraphs.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( - h::H, + hg::H, distance_method::SnodeDistanceKKHeuristic, - cost_function::Function, - hyperedge_weights::Vector{T} + 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 + +""" + SimpleHypergraphs.distance( + hg::H, + distance_method::SnodeDistanceKKHeuristic, + 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} + + 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(Int, nhe(hg)) + + max_dist = 0 + for i in 1:nhv(hg) + for j in i+1:nhv(hg) + dist = distance(hg, SnodeDistanceKKHeuristic(Set{Int}(i), Set{Int}(j)), edge_weights) + if dist > max_dist + max_dist = dist + end + end + end +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 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, + _::SnodeDistanceKKHeuristic, ) where {H <: AbstractDirectedHypergraph} + if length(get_strongly_connected_components(hg)) != 1 + return Inf64 + end - return cost_function() + # Since we're interested in the number of steps, assign each hyperedge a uniform weight of `1` + edge_weights = ones(Int64, nhe(hg)) + max_dist = 0 + for i in 1:nhv(hg) + for j in i+1:nhv(hg) + dist = distance(hg, SnodeDistanceKKILP(Set{Int}(i), Set{Int}(j)), edge_weights) + if dist > max_dist + max_dist = dist + end + end + end end \ No newline at end of file diff --git a/src/dihypergraph.jl b/src/dihypergraph.jl index 8198899..f1f30b9 100644 --- a/src/dihypergraph.jl +++ b/src/dihypergraph.jl @@ -1037,5 +1037,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 From a1cae64c2339cd7e3349c7d6257ddae8d6cbda30 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Mon, 17 Nov 2025 13:13:04 +0000 Subject: [PATCH 11/30] Exports --- src/SimpleDirectedHypergraphs.jl | 3 +++ src/algorithms/distance.jl | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/SimpleDirectedHypergraphs.jl b/src/SimpleDirectedHypergraphs.jl index f23e1e4..68f2f43 100644 --- a/src/SimpleDirectedHypergraphs.jl +++ b/src/SimpleDirectedHypergraphs.jl @@ -26,6 +26,8 @@ export get_weakly_connected_components, get_strongly_connected_components export DiHyperPathState, initialize_dihyperpath_state, forward_reachable, backward_traceable, is_reachable export all_hyperpaths, shortest_hyperpath_kk_heuristic, initialize_ilp_model, shortest_hyperpath_kk_ilp +export SnodeDistanceKKHeuristic, SnodeDistanceKKILP + include("abstracttypes.jl") include("dihypergraph.jl") include("io.jl") @@ -36,5 +38,6 @@ 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 index e3559d6..99e9e6e 100644 --- a/src/algorithms/distance.jl +++ b/src/algorithms/distance.jl @@ -1,5 +1,5 @@ """ - struct SnodeDistanceKKHeuristic(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance + SnodeDistanceKKHeuristic(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance Represent a distance between a set of *source* nodes and a set of *target* nodes in a of directed hypergraph `h`, where a distance in this context is defined as the length of the shortest hyperpath that reaches all `t ∈ targets`. @@ -15,7 +15,7 @@ struct SnodeDistanceKKHeuristic <: AbstractDistance end """ - struct SnodeDistanceKKILP(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance + SnodeDistanceKKILP(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance Represent a distance between a set of *source* nodes and a set of *target* nodes in a of directed hypergraph `h`, where a distance in this context is defined as the length of the shortest hyperpath that reaches all `t ∈ targets`. @@ -31,7 +31,7 @@ struct SnodeDistanceKKILP <: AbstractDistance end """ - SimpleHypergraphs.distance( + distance( hg::H, distance_method::SnodeDistanceKKHeuristic, hyperedge_weights::AbstractVector{T}; @@ -70,7 +70,7 @@ function SimpleHypergraphs.distance( end """ - SimpleHypergraphs.distance( + distance( hg::H, distance_method::SnodeDistanceKKHeuristic, hyperedge_weights::AbstractVector{T} From db188710504f72903a1cb68391433f7f81e3ab74 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 18 Nov 2025 12:27:15 +0000 Subject: [PATCH 12/30] Whole heap of changes to base dihypergraph.jl file; some initial tests --- src/algorithms/distance.jl | 2 +- src/algorithms/paths.jl | 37 ++- src/dihypergraph.jl | 456 +++++++++++++++++++++---------------- test/runtests.jl | 60 +++++ 4 files changed, 349 insertions(+), 206 deletions(-) diff --git a/src/algorithms/distance.jl b/src/algorithms/distance.jl index 99e9e6e..14ed7bf 100644 --- a/src/algorithms/distance.jl +++ b/src/algorithms/distance.jl @@ -89,7 +89,7 @@ function SimpleHypergraphs.distance( hg::H, distance_method::SnodeDistanceKKILP, hyperedge_weights::AbstractVector{T} -) where {H <: AbstractDirectedHypergraph} +) where {H <: AbstractDirectedHypergraph, T <: Real} path = shortest_hyperpath_kk_ilp( hg, diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index cb39b26..50f2f30 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -22,7 +22,7 @@ `edge_heap_points` tracks the references for each hyperedge in the heap used during heuristic pathfinding. """ -struct DiHyperPathState{T} where {T<:Real} +struct DiHyperPathState{T<:Real} reached_vs::BitVector marked_hes::BitVector removed_hes::BitVector @@ -291,6 +291,7 @@ function shortest_hyperpath_kk_heuristic( state.edge_heap_points[f] = push!( Hmin, sum(state.edge_weights[x] for x in short_hyperpath_vhe(hg, source, f, state)) + ) end end end @@ -475,6 +476,34 @@ end """ is_reachable( + hg::H, + source::Int, + target::Int, + target_type::Symbol, + state::DiHyperPathState{T} + ) where {H <: AbstractDirectedHypergraph, T <: Real} + + TODO: this + +""" +function is_reachable( + hg::H, + source::Int, + target::Int, + target_type::Symbol, + state::DiHyperPathState{T} +) where {H <: AbstractDirectedHypergraph, T <: Real} + @assert target_type ∈ [:vertex, :hyperedge] "`target_type` must be :vertex or :hyperedge" + + if target_type == :vertex + is_reachable_vertex(hg, source, target, state) + else + is_reachable_hyperedge(hg, source, target, state) + end +end + +""" + is_reachable_hyperedge( hg::H, source_v::Int, target_he::Int, @@ -485,7 +514,7 @@ end a source vertex with index `source_v` to a target hyperedge with index `target_he`. A `DiHyperPathState` object `state` is used to keep track of what vertices and hyperedges have been visited during a traversal. """ -function is_reachable( +function is_reachable_hyperedge( hg::H, source_v::Int, target_he::Int, @@ -537,7 +566,7 @@ function is_reachable( end """ - is_reachable( + is_reachable_vertex( hg::H, source_v::Int, target_v::Int, @@ -548,7 +577,7 @@ end a source vertex with index `source_v` to a target vertex with index `target_v`. A `DiHyperPathState` object `state` is used to keep track of what vertices and hyperedges have been visited during a traversal. """ -function is_reachable( +function is_reachable_vertex( hg::H, source_v::Int, target_v::Int, diff --git a/src/dihypergraph.jl b/src/dihypergraph.jl index f1f30b9..1f7d43f 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,7 +133,7 @@ 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}}} +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} @@ -142,10 +143,10 @@ 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,Nothing,Nothing,D}(n, k), Hypergraph{T,Nothing,Nothing,D}(n, k), @@ -153,17 +154,17 @@ struct DirectedHypergraph{T<:Real,V,E,D<:AbstractDict{Int, T}} <: AbstractDirect ) 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,24 @@ 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 +754,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 +799,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 +815,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 +826,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 +842,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 +857,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 +879,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 +902,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 +917,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 +935,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 +945,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 +963,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 +997,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 +1016,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 +1032,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 +1076,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) diff --git a/test/runtests.jl b/test/runtests.jl index f55969a..577a785 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,6 +20,22 @@ 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 true nothing nothing + 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) # end @@ -436,4 +452,48 @@ end; @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)) +end; + + +@testset "SimpleDirectedHypergraphs paths/distance " begin + w2 = [3, 7, 19, 10, 13, 11, 1, 12, 8] + init_state = initialize_dihyperpath_state(dh2, w2) + @test init_state.reached_vs .== BitVector(zeros(nhv(dh2))) + @test init_state.marked_hes .== BitVector(zeros(nhe(dh2))) + @test init_state.removed_hes .== BitVector(zeros(nhe(dh2))) + @test init_state.hes_tail_count .== length.(keys.(dh2.hg_tail.he2v)) + @test init_state.he_inedges .== [Set{Int}() for _ in 1:nhe(dh2)] + @test init_state.edge_weights .== w2 + @test init_state.edge_costs .== fill(typemax(Int), nhe(dh2)) + @test init_state.edge_heap_points .== fill(nothing, nhe(dh2)) + + # 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) + +end; + +@testset "SimpleDirectedHypergraphs diameter " begin + end; \ No newline at end of file From a75d8ebd5953cfd2a7ab3ef81258b87e312a4898 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 18 Nov 2025 14:27:23 +0000 Subject: [PATCH 13/30] Forward traversal and back-tracking are tested and work --- src/algorithms/distance.jl | 12 +- src/algorithms/paths.jl | 132 +++++------ test/runtests.jl | 434 ++++++++++++++++++++----------------- 3 files changed, 313 insertions(+), 265 deletions(-) diff --git a/src/algorithms/distance.jl b/src/algorithms/distance.jl index 14ed7bf..f90410b 100644 --- a/src/algorithms/distance.jl +++ b/src/algorithms/distance.jl @@ -50,7 +50,7 @@ function SimpleHypergraphs.distance( hg::H, distance_method::SnodeDistanceKKHeuristic, hyperedge_weights::AbstractVector{T} -) where {H <: AbstractDirectedHypergraph, T <: Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} path = shortest_hyperpath_kk_heuristic( hg, @@ -89,7 +89,7 @@ function SimpleHypergraphs.distance( hg::H, distance_method::SnodeDistanceKKILP, hyperedge_weights::AbstractVector{T} -) where {H <: AbstractDirectedHypergraph, T <: Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} path = shortest_hyperpath_kk_ilp( hg, @@ -121,7 +121,7 @@ end function Graphs.diameter( hg::H, _::SnodeDistanceKKHeuristic, -) where {H <: AbstractDirectedHypergraph} +) where {H<:AbstractDirectedHypergraph} if length(get_strongly_connected_components(hg)) != 1 return Inf64 end @@ -143,7 +143,7 @@ end """ diameter( hg::H, - distance_method::SnodeDistanceKKHeuristic, + 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) @@ -153,8 +153,8 @@ end """ function Graphs.diameter( hg::H, - _::SnodeDistanceKKHeuristic, -) where {H <: AbstractDirectedHypergraph} + _::SnodeDistanceKKILP, +) where {H<:AbstractDirectedHypergraph} if length(get_strongly_connected_components(hg)) != 1 return Inf64 end diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 50f2f30..6cedc13 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -30,7 +30,7 @@ struct DiHyperPathState{T<:Real} he_inedges::Vector{Set{Int}} edge_weights::Vector{T} edge_costs::Vector{T} - edge_heap_points::Vector{Union{Nothing, Int}} + edge_heap_points::Vector{Union{Nothing,Int}} end """ @@ -43,22 +43,22 @@ end costs) `hyperedge_weights`. """ function initialize_dihyperpath_state( - hg::H, - hyperedge_weights::Vector{T} - ) where {H <: AbstractDirectedHypergraph, T <: Real} + hg::H, + hyperedge_weights::Vector{T} +) where {H<:AbstractDirectedHypergraph,T<:Real} # Hyperedge weights need to be nonnegative @assert all(hyperedge_weights .>= 0) nv = nhv(hg) ne = nhe(hg) - edge_heap_points = Vector{Union{Nothing, Int}}(undef, ne) + edge_heap_points = Vector{Union{Nothing,Int}}(undef, ne) edge_heap_points .= nothing return DiHyperPathState{T}( - BitVector(zeros(nv)), - BitVector(zeros(ne)), - BitVector(zeros(ne)), + BitVector(falses(nv)), + BitVector(falses(ne)), + BitVector(falses(ne)), length.(keys.(hg.hg_tail.he2v)), [Set{Int}() for _ in 1:ne], hyperedge_weights, @@ -83,11 +83,11 @@ function forward_reachable( hg::H, source::Int, state::DiHyperPathState{T} -) where {H <: AbstractDirectedHypergraph, T <: Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} # Priority queue of reached vertices Q = Queue{Int}() enqueue!(Q, source) - + state.reached_vs[source] = true # Which vertices/hyperedges have been reached? @@ -95,7 +95,7 @@ function forward_reachable( reached_es = Set{Int}() while length(Q) > 0 - v = popfirst!(Q) + v = dequeue!(Q) push!(reached_vs, v) for out_e in keys(hg.hg_tail.v2he[v]) @@ -118,7 +118,7 @@ function forward_reachable( # Restore state for v in reached_vs state.reached_vs[v] = false - for out_e in hg.hg_tail.v2he[v] + for out_e in keys(hg.hg_tail.v2he[v]) state.hes_tail_count[out_e] += 1 end end @@ -143,11 +143,11 @@ function backward_traceable( hg::H, target::Int, state::DiHyperPathState{T} -) where {H <: AbstractDirectedHypergraph, T <: Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} # Priority queue of reached vertices Q = Queue{Int}() enqueue!(Q, target) - + state.reached_vs[target] = true # Which vertices/hyperedges have been reached? @@ -155,7 +155,7 @@ function backward_traceable( reached_es = Set{Int}() while length(Q) > 0 - v = popfirst!(Q) + v = dequeue!(Q) push!(reached_vs, v) for in_e in keys(hg.hg_head.v2he[v]) @@ -231,7 +231,7 @@ function shortest_hyperpath_kk_heuristic( source::Int, target::Int, hyperedge_weights::Vector{T} -) where {H <: AbstractDirectedHypergraph, T <: Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} state = initialize_dihyperpath_state(hg, hyperedge_weights) # Doubly reachable hyperedges @@ -245,7 +245,7 @@ function shortest_hyperpath_kk_heuristic( hg_copy[:, Not(dr_hes)] .= nothing # Min-heap for hyperedges - Hmin = MutableBinaryMinHeap{Tuple{Int, T}}() + Hmin = MutableBinaryMinHeap{Tuple{Int,T}}() 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 @@ -313,11 +313,11 @@ function shortest_hyperpath_kk_heuristic( end function shortest_hyperpath_kk_heuristic( - hg::DirectedHypergraph{T, V, E, D}, + hg::DirectedHypergraph{T,V,E,D}, source::Int, targets::Set{Int}, hyperedge_weights::Vector{T} -) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +) 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 @@ -325,7 +325,7 @@ function shortest_hyperpath_kk_heuristic( metatarget = add_vertex!(hg_copy) meta_he = add_hyperedge!( hg_copy; - vertices_tail=D( x => convert(T, 0) for x in targets), + vertices_tail=D(x => convert(T, 0) for x in targets), vertices_head=D(metatarget, convert(T, 0)) ) @@ -341,11 +341,11 @@ function shortest_hyperpath_kk_heuristic( end function shortest_hyperpath_kk_heuristic( - hg::DirectedHypergraph{T, V, E, D}, + hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, target::Int, hyperedge_weights::Vector{T} -) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +) 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 @@ -354,7 +354,7 @@ function shortest_hyperpath_kk_heuristic( meta_he = add_hyperedge!( hg_copy; vertices_tail=D(metasource, convert(T, 0)), - vertices_head=D( x => convert(T, 0) for x in sources) + vertices_head=D(x => convert(T, 0) for x in sources) ) path = shortest_hyperpath_kk_heuristic( @@ -369,11 +369,11 @@ function shortest_hyperpath_kk_heuristic( end function shortest_hyperpath_kk_heuristic( - hg::DirectedHypergraph{T, V, E, D}, + hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, targets::Set{Int}, hyperedge_weights::Vector{T} -) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +) 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 @@ -382,7 +382,7 @@ function shortest_hyperpath_kk_heuristic( meta_he_source = add_hyperedge!( hg_copy; vertices_tail=D(metasource, convert(T, 0)), - vertices_head=D( x => convert(T, 0) for x in sources) + 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 @@ -390,7 +390,7 @@ function shortest_hyperpath_kk_heuristic( metatarget = add_vertex!(hg_copy) meta_he_target = add_hyperedge!( hg_copy; - vertices_tail=D( x => convert(T, 0) for x in targets), + vertices_tail=D(x => convert(T, 0) for x in targets), vertices_head=D(metatarget, convert(T, 0)) ) @@ -423,7 +423,7 @@ function short_hyperpath_vhe( v::Int, he::Int, state::DiHyperPathState{T} -) where {H <: AbstractDirectedHypergraph, T <: Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} Q = Queue{Int}() for e in state.he_inedges[he] enqueue!(Q, e) @@ -435,7 +435,7 @@ function short_hyperpath_vhe( # Construct (likely redundant) superpath by backtracking from target while length(Q) > 0 - e = popfirst!(Q) + e = dequeue!(Q) push!(superpath, e) for f in state.he_inedges[e] @@ -492,7 +492,7 @@ function is_reachable( target::Int, target_type::Symbol, state::DiHyperPathState{T} -) where {H <: AbstractDirectedHypergraph, T <: Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} @assert target_type ∈ [:vertex, :hyperedge] "`target_type` must be :vertex or :hyperedge" if target_type == :vertex @@ -519,11 +519,11 @@ function is_reachable_hyperedge( source_v::Int, target_he::Int, state::DiHyperPathState{T} -) where {H <: AbstractDirectedHypergraph, T <: Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} # Priority queue of reached vertices Q = Queue{Int}() enqueue!(Q, source_v) - + state.reached_vs[source_v] = true # Which vertices/hyperedges have been reached? @@ -531,7 +531,7 @@ function is_reachable_hyperedge( reached_es = Set{Int}() while length(Q) > 0 - v = popfirst!(Q) + v = dequeue!(Q) push!(reached_vs, v) for out_e in keys(hg.hg_tail.v2he[v]) @@ -582,11 +582,11 @@ function is_reachable_vertex( source_v::Int, target_v::Int, state::DiHyperPathState{T} -) where {H <: AbstractDirectedHypergraph, T <: Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} # Priority queue of reached vertices Q = Queue{Int}() enqueue!(Q, source_v) - + state.reached_vs[source_v] = true # Which vertices/hyperedges have been reached? @@ -594,7 +594,7 @@ function is_reachable_vertex( reached_es = Set{Int}() while length(Q) > 0 - v = popfirst!(Q) + v = dequeue!(Q) push!(reached_vs, v) for out_e in keys(hg.hg_tail.v2he[v]) @@ -635,7 +635,7 @@ end 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} +function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H<:AbstractDirectedHypergraph} # Remove excluded hyperedges hg_copy = deepcopy(hg) hg_copy[:, sort(collect(out))] .= nothing @@ -649,7 +649,7 @@ function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H if target ∉ reached_vs return Set{Int}() end - + path = Set{Int}() # Try to minimize the size of the path by eliminating unnecessary hyperedges @@ -685,19 +685,19 @@ end *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} +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}}}() - + 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 = popfirst!(Q) + out, keep = dequeue!(Q) path = get_hyperpath(hg, source, target, out) @@ -715,7 +715,7 @@ function all_hyperpaths(hg::H, source::Int, target::Int) where {H <: AbstractDir paths end -function all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} +function all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H<:AbstractDirectedHypergraph} hg_copy = deepcopy(hg) # Add a single "metatarget" vertex to reformulate as single-source, single-sink pathfinding problem @@ -723,7 +723,7 @@ function all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H <: Abstr metatarget = add_vertex!(hg_copy) meta_he = add_hyperedge!( hg_copy; - vertices_tail=D( x => convert(T, 0) for x in targets), + vertices_tail=D(x => convert(T, 0) for x in targets), vertices_head=D(metatarget, convert(T, 0)) ) @@ -737,7 +737,7 @@ function all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H <: Abstr return Set(setdiff(p, Set{Int}(meta_he)) for p in paths) end -function all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H <: AbstractDirectedHypergraph} +function all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H<:AbstractDirectedHypergraph} hg_copy = deepcopy(hg) # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem @@ -746,7 +746,7 @@ function all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H <: Abstr meta_he = add_hyperedge!( hg_copy; vertices_tail=D(metasource, convert(T, 0)), - vertices_head=D( x => convert(T, 0) for x in sources) + vertices_head=D(x => convert(T, 0) for x in sources) ) paths = all_hyperpaths( @@ -759,7 +759,7 @@ function all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H <: Abstr return Set(setdiff(p, Set{Int}(meta_he)) for p in paths) end -function all_hyperpaths(hg::H, sources::Set{Int}, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} +function all_hyperpaths(hg::H, sources::Set{Int}, targets::Set{Int}) where {H<:AbstractDirectedHypergraph} hg_copy = deepcopy(hg) # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem @@ -768,7 +768,7 @@ function all_hyperpaths(hg::H, sources::Set{Int}, targets::Set{Int}) where {H <: meta_he_source = add_hyperedge!( hg_copy; vertices_tail=D(metasource, convert(T, 0)), - vertices_head=D( x => convert(T, 0) for x in sources) + 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 @@ -776,7 +776,7 @@ function all_hyperpaths(hg::H, sources::Set{Int}, targets::Set{Int}) where {H <: metatarget = add_vertex!(hg_copy) meta_he_target = add_hyperedge!( hg_copy; - vertices_tail=D( x => convert(T, 0) for x in targets), + vertices_tail=D(x => convert(T, 0) for x in targets), vertices_head=D(metatarget, convert(T, 0)) ) @@ -794,18 +794,18 @@ function initialize_ilp_model( hg::H, source::Int, target::Int, - hyperedge_weights::Vector{T}) where {H<:AbstractDirectedHypergraph, T<:Real} + hyperedge_weights::Vector{T}) where {H<:AbstractDirectedHypergraph,T<:Real} # Initialize state state = initialize_dihyperpath_state(hg, hyperedge_weights) - + # First, verify that the problem is well-posed # That is, can `target` be reached from `source` @assert is_reachable(hg, source, target, state) # Initialize integer linear programming model model = Model(GLPK.Optimizer) - + # Create one binary variable for each hyperedge in `hg` @variable(model, x[1:nhe(hg)], Bin) set_start_value.(x, 1) @@ -879,7 +879,7 @@ function initialize_ilp_model( @objective(model, Min, dot(x, state.edge_weights)) return model, x, cuts, crosses - + end """ @@ -926,13 +926,13 @@ function shortest_hyperpath_kk_ilp( source::Int, target::Int, hyperedge_weights::Vector{T} -) where {H<:AbstractDirectedHypergraph, T<:Real} +) 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) optimize!(model) - + # Convert floating-point solution into BitVector # TODO: Is this necessary w/ JuMP? Or will the output really be binary? solution = value.(x) .> 0.5 @@ -961,7 +961,7 @@ function shortest_hyperpath_kk_ilp( source::Int, targets::Set{Int}, hyperedge_weights::Vector{T} -) where {H<:AbstractDirectedHypergraph, T<:Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} hg_copy = deepcopy(hg) # Add a single "metatarget" vertex to reformulate as single-source, single-sink pathfinding problem @@ -969,7 +969,7 @@ function shortest_hyperpath_kk_ilp( metatarget = add_vertex!(hg_copy) meta_he = add_hyperedge!( hg_copy; - vertices_tail=D( x => convert(T, 0) for x in targets), + vertices_tail=D(x => convert(T, 0) for x in targets), vertices_head=D(metatarget, convert(T, 0)) ) @@ -989,7 +989,7 @@ function shortest_hyperpath_kk_ilp( sources::Set{Int}, target::Int, hyperedge_weights::Vector{T} -) where {H<:AbstractDirectedHypergraph, T<:Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} hg_copy = deepcopy(hg) # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem @@ -998,7 +998,7 @@ function shortest_hyperpath_kk_ilp( meta_he = add_hyperedge!( hg_copy; vertices_tail=D(metasource, convert(T, 0)), - vertices_head=D( x => convert(T, 0) for x in sources) + vertices_head=D(x => convert(T, 0) for x in sources) ) path = shortest_hyperpath_kk_ilp( @@ -1017,7 +1017,7 @@ function shortest_hyperpath_kk_ilp( sources::Set{Int}, targets::Set{Int}, hyperedge_weights::Vector{T} -) where {H<:AbstractDirectedHypergraph, T<:Real} +) where {H<:AbstractDirectedHypergraph,T<:Real} hg_copy = deepcopy(hg) # Add a single "metasource" vertex to reformulate as single-source, single-sink pathfinding problem @@ -1026,7 +1026,7 @@ function shortest_hyperpath_kk_ilp( meta_he_source = add_hyperedge!( hg_copy; vertices_tail=D(metasource, convert(T, 0)), - vertices_head=D( x => convert(T, 0) for x in sources) + 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 @@ -1034,7 +1034,7 @@ function shortest_hyperpath_kk_ilp( metatarget = add_vertex!(hg_copy) meta_he_target = add_hyperedge!( hg_copy; - vertices_tail=D( x => convert(T, 0) for x in targets), + vertices_tail=D(x => convert(T, 0) for x in targets), vertices_head=D(metatarget, convert(T, 0)) ) @@ -1071,7 +1071,7 @@ function expand_cuts( cuts::Vector{Set{Int}}, crosses::Vector{BitVector}, curr_sol::BitVector -) where {H <: AbstractDirectedHypergraph} +) where {H<:AbstractDirectedHypergraph} new_cuts = Set{Int}[] new_crosses = BitVector[] @@ -1090,7 +1090,7 @@ function expand_cuts( ] 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) @@ -1107,8 +1107,8 @@ function expand_cuts( # 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}}() + new_cut_ev = Dict{Int,Set{Int}}() + new_cross_ev = Dict{Int,Vector{Bool}}() for v in keys(hg.hg_tail.he2v[e]) new_cut_ev[v] = setdiff(new_cut, Set(v)) new_cross_ev[v] = [ diff --git a/test/runtests.jl b/test/runtests.jl index 577a785..a09e433 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,31 +8,31 @@ 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 + 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 true nothing nothing - nothing true true nothing true nothing nothing nothing nothing - nothing nothing nothing true nothing nothing nothing nothing nothing + nothing nothing nothing nothing nothing true true nothing nothing + true nothing nothing nothing nothing nothing nothing true nothing + true nothing nothing nothing nothing nothing true 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) @@ -49,13 +49,13 @@ dh2 = DirectedHypergraph(tail_2, head_2) 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()) @@ -63,31 +63,31 @@ dh2 = DirectedHypergraph(tail_2, head_2) 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 @@ -107,22 +107,24 @@ dh2 = DirectedHypergraph(tail_2, head_2) @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) @@ -131,50 +133,54 @@ dh2 = DirectedHypergraph(tail_2, head_2) @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( @@ -221,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 @@ -274,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) @@ -283,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; @@ -315,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) @@ -328,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 @@ -358,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]) @@ -397,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 @@ -406,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) @@ -439,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) @@ -451,49 +457,91 @@ 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] init_state = initialize_dihyperpath_state(dh2, w2) - @test init_state.reached_vs .== BitVector(zeros(nhv(dh2))) - @test init_state.marked_hes .== BitVector(zeros(nhe(dh2))) - @test init_state.removed_hes .== BitVector(zeros(nhe(dh2))) - @test init_state.hes_tail_count .== length.(keys.(dh2.hg_tail.he2v)) - @test init_state.he_inedges .== [Set{Int}() for _ in 1:nhe(dh2)] - @test init_state.edge_weights .== w2 - @test init_state.edge_costs .== fill(typemax(Int), nhe(dh2)) - @test init_state.edge_heap_points .== fill(nothing, nhe(dh2)) + @test init_state.reached_vs == BitVector(falses(nhv(dh2))) + @test init_state.marked_hes == BitVector(falses(nhe(dh2))) + @test init_state.removed_hes == BitVector(falses(nhe(dh2))) + @test init_state.hes_tail_count == length.(keys.(dh2.hg_tail.he2v)) + @test init_state.he_inedges == [Set{Int}() for _ in 1:nhe(dh2)] + @test init_state.edge_weights == w2 + @test init_state.edge_costs == fill(typemax(Int), nhe(dh2)) + @test init_state.edge_heap_points == fill(nothing, nhe(dh2)) + + # Traversing forward from vertices to vertices/hyperedges + fr = forward_reachable(dh2, 1, init_state) + @test fr[1] == Set{Int}(1:5) + @test fr[2] == Set{Int}(1:9) + fr = forward_reachable(dh2, 5, init_state) + @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, init_state) + @test bt[1] == Set{Int}(1:5) + @test bt[2] == Set{Int}(1:9) + bt = backward_traceable(dh2, 5, init_state) + @test bt[1] == Set{Int}(1:5) + @test bt[2] == Set{Int}(1:9) # 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 + 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 + 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 = [3, 7, 19, 10, 13, 11, 1, 12, 8, 2, 22] + init_state = initialize_dihyperpath_state(dh3, w3) + + # Traversing forward from vertices to vertices/hyperedges + fr = forward_reachable(dh3, 1, init_state) + @test fr[1] == Set{Int}(1:9) + @test fr[2] == Set{Int}(1:11) + fr = forward_reachable(dh3, 6, init_state) + @test fr[1] == Set{Int}([6, 8, 9]) + @test fr[2] == Set{Int}([9, 11]) + fr = forward_reachable(dh3, 9, init_state) + @test fr[1] == Set{Int}(9) + @test fr[2] == Set{Int}() + + # Back-tracing from vertices to vertices/hyperedges + bt = backward_traceable(dh3, 1, init_state) + @test bt[1] == Set{Int}(1) + @test bt[2] == Set{Int}() + bt = backward_traceable(dh3, 6, init_state) + @test bt[1] == Set{Int}([6, 1]) + @test bt[2] == Set{Int}(3) + bt = backward_traceable(dh3, 9, init_state) + @test bt[1] == Set{Int}(1:9) + @test bt[2] == Set{Int}(1:11) + end; @testset "SimpleDirectedHypergraphs diameter " begin - + end; \ No newline at end of file From b9d615a82cc25b93aa695f149bb4d24978248558 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 18 Nov 2025 15:10:48 +0000 Subject: [PATCH 14/30] Some progress on short_hyperpath_vhe, though it's still not working --- src/algorithms/paths.jl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 6cedc13..8074030 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -242,7 +242,7 @@ function shortest_hyperpath_kk_heuristic( # Eliminate non-doubly reachable hyperedges hg_copy = deepcopy(hg) - hg_copy[:, Not(dr_hes)] .= nothing + hg_copy[:, InvertedIndices.Not(dr_hes)] .= (nothing, nothing) # Min-heap for hyperedges Hmin = MutableBinaryMinHeap{Tuple{Int,T}}() @@ -261,7 +261,6 @@ function shortest_hyperpath_kk_heuristic( state.removed_hes[e] = true path = short_hyperpath_vhe(hg, source, e, state) - # TODO: nature of cost function for hyperedge vs. cost function for path state.edge_costs[e] = sum(state.edge_weights[x] for x in path) out_edges = Set{Int}() @@ -454,17 +453,19 @@ function short_hyperpath_vhe( superpath = sort(collect(superpath), by=x -> state.edge_costs[x], rev=true) hg_copy = deepcopy(hg) # Eliminate all hyperedges not on superpath - hg_copy[:, Not(superpath)] .= nothing + 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[:, e] .= nothing + 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, state) + if !is_reachable(hg_copy, v, he, :hyperedge, state) # Restore hyperedge to hypergraph copy hg_copy[:, e] .= hg[:, e] push!(path, e) @@ -801,7 +802,7 @@ function initialize_ilp_model( # First, verify that the problem is well-posed # That is, can `target` be reached from `source` - @assert is_reachable(hg, source, target, state) + @assert is_reachable(hg, source, target, :vertex, state) # Initialize integer linear programming model model = Model(GLPK.Optimizer) From 401c7fb8422e9bbc89836e459888eb5d8e9c1a63 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 19 Nov 2025 10:48:01 +0000 Subject: [PATCH 15/30] Very frustrating bug finally fixed; other testing still in progress --- src/algorithms/paths.jl | 138 ++-------------------------------------- test/runtests.jl | 30 ++++++++- 2 files changed, 35 insertions(+), 133 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 8074030..ebe152f 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -496,138 +496,14 @@ function is_reachable( ) where {H<:AbstractDirectedHypergraph,T<:Real} @assert target_type ∈ [:vertex, :hyperedge] "`target_type` must be :vertex or :hyperedge" - if target_type == :vertex - is_reachable_vertex(hg, source, target, state) - else - is_reachable_hyperedge(hg, source, target, state) - end -end - -""" - is_reachable_hyperedge( - hg::H, - source_v::Int, - target_he::Int, - state::DiHyperPathState{T} - ) where {H <: AbstractDirectedHypergraph, T <: Real} - - A short-circuiting version of `forward_reachable` that returns `true` if a hyperpath in hypergraph `hg` exists from - a source vertex with index `source_v` to a target hyperedge with index `target_he`. A `DiHyperPathState` object - `state` is used to keep track of what vertices and hyperedges have been visited during a traversal. -""" -function is_reachable_hyperedge( - hg::H, - source_v::Int, - target_he::Int, - state::DiHyperPathState{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} - # Priority queue of reached vertices - Q = Queue{Int}() - enqueue!(Q, source_v) - - state.reached_vs[source_v] = true - - # Which vertices/hyperedges have been reached? - reached_vs = Set{Int}() - reached_es = Set{Int}() - - while length(Q) > 0 - v = dequeue!(Q) - push!(reached_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? - state.hes_tail_count[out_e] -= 1 - - if state.hes_tail_count[out_e] == 0 - push!(reached_es, out_e) - if out_e == target_he - return true - end - - for w in keys(hg.hg_head.he2v[out_e]) - if !state.reached_vs[w] - enqueue!(Q, w) - state.reached_vs[w] = true - end - end - end - end - end - - # Restore state - for v in reached_vs - state.reached_vs[v] = false - for out_e in hg.hg_tail.v2he[v] - state.hes_tail_count[out_e] += 1 - end - end + fr = forward_reachable(hg, source, state) - return false -end - -""" - is_reachable_vertex( - hg::H, - source_v::Int, - target_v::Int, - state::DiHyperPathState{T} - ) where {H <: AbstractDirectedHypergraph, T <: Real} - - A short-circuiting version of `forward_reachable` that returns `true` if a hyperpath in hypergraph `hg` exists from - a source vertex with index `source_v` to a target vertex with index `target_v`. A `DiHyperPathState` object - `state` is used to keep track of what vertices and hyperedges have been visited during a traversal. -""" -function is_reachable_vertex( - hg::H, - source_v::Int, - target_v::Int, - state::DiHyperPathState{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} - # Priority queue of reached vertices - Q = Queue{Int}() - enqueue!(Q, source_v) - - state.reached_vs[source_v] = true - - # Which vertices/hyperedges have been reached? - reached_vs = Set{Int}() - reached_es = Set{Int}() - - while length(Q) > 0 - v = dequeue!(Q) - push!(reached_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? - state.hes_tail_count[out_e] -= 1 - - if state.hes_tail_count[out_e] == 0 - push!(reached_es, out_e) - - for w in keys(hg.hg_head.he2v[out_e]) - if w == target_v - return true - end - - if !state.reached_vs[w] - enqueue!(Q, w) - state.reached_vs[w] = true - end - end - end - end - end - - # Restore state - for v in reached_vs - state.reached_vs[v] = false - for out_e in hg.hg_tail.v2he[v] - state.hes_tail_count[out_e] += 1 - end + #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 - - return false end """ @@ -658,7 +534,7 @@ function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H< hg_copy[:, e] .= nothing # Only if hyperedge is essential for reaching target, - if !isreachable(hg_copy, source, target, state) + if !is_reachable(hg_copy, source, target, :vertex, state) # Restore hyperedge to hypergraph copy hg_copy[:, e] .= hg[:, e] push!(path, e) diff --git a/test/runtests.jl b/test/runtests.jl index a09e433..09baa80 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,7 +7,6 @@ using Graphs 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 @@ -30,7 +29,7 @@ tail_2 = [ 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 true nothing true + 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 ] @@ -490,6 +489,33 @@ end; @test bt[1] == Set{Int}(1:5) @test bt[2] == Set{Int}(1:9) + # 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]), + Set{Int}([1,2,3,5]), + Set{Int}(4), + Set{Int}(4) + ] + + for i in 1:nhv(dh2) + # In this example, all nodes (and hyperedges) can be reached from anywhere + for j in 1:nhv(dh2) + @testset "Test DH2: v$i\t -> v$j" begin + @test is_reachable(dh2, i, j, :vertex, init_state) + end + end + for e in 1:nhe(dh2) + @testset "Test DH2: v$i\t -> he$e" begin + @test is_reachable(dh2, i, e, :hyperedge, init_state) + end + end + end + # Example adapted from Blau et al., DOI: 10.1039/D0SC05647B tail_3 = [ true true true true nothing nothing nothing nothing nothing nothing nothing From 279233d27b2532bd4e8f32bf377e79c8584e5f8d Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 19 Nov 2025 12:38:40 +0000 Subject: [PATCH 16/30] Still broken --- src/algorithms/paths.jl | 70 +++++++++++++++++++++++++++++++---------- test/runtests.jl | 33 +++++++++++-------- 2 files changed, 73 insertions(+), 30 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index ebe152f..604c0b1 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -1,3 +1,18 @@ +#TODO: STATUS +# Somehow state.he_inedges is getting screwy +# DH2: +# [ Info: All inedges: +# [ Info: 1: Set([6, 7]) +# [ Info: 2: Set([8, 1]) +# [ Info: 3: Set([7, 9, 1]) +# [ Info: 4: Set([5, 2, 3]) +# [ Info: 5: Set([4]) +# [ Info: 6: Set([5, 2, 8, 3, 1]) +# [ Info: 7: Set([5, 7, 2, 9, 3, 1]) +# [ Info: 8: Set([4]) +# [ Info: 9: Set([4]) + + """ struct DiHyperPathState{T}( reached_vs::BitVector, @@ -124,7 +139,7 @@ function forward_reachable( end # Return reached vertices and hyperedges - return reached_vs, reached_es + return (reached_vs, reached_es) end """ @@ -182,7 +197,7 @@ function backward_traceable( end # Return reached vertices and hyperedges - return reached_vs, reached_es + return (reached_vs, reached_es) end @@ -232,32 +247,37 @@ function shortest_hyperpath_kk_heuristic( target::Int, hyperedge_weights::Vector{T} ) where {H<:AbstractDirectedHypergraph,T<:Real} + state = initialize_dihyperpath_state(hg, hyperedge_weights) + # Verify that the target can be reached + fr = forward_reachable(hg, source, state) + @assert target ∈ fr[1] + # Doubly reachable hyperedges - dr_hes = intersect( - forward_reachable(hg, source, state)[2], + dr_hes = sort!(collect(intersect( + fr[2], backward_traceable(hg, target, state)[2] - ) + ))) # Eliminate non-doubly reachable hyperedges hg_copy = deepcopy(hg) - hg_copy[:, InvertedIndices.Not(dr_hes)] .= (nothing, nothing) + 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{Int,T}}() + 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 - # TODO: what do cost functions need? - state.edge_heap_points[out_e] = push!(Hmin, state.edge_weights[out_e]) + state.edge_heap_points[out_e] = push!(Hmin, (state.edge_weights[out_e], out_e)) end end state.reached_vs[source] = true while length(Hmin) > 0 - e = pop!(Hmin) + e = pop!(Hmin)[2] state.removed_hes[e] = true path = short_hyperpath_vhe(hg, source, e, state) @@ -266,30 +286,38 @@ function shortest_hyperpath_kk_heuristic( out_edges = Set{Int}() for v in keys(hg.hg_head.he2v[e]) for f in keys(hg.hg_tail.v2he[v]) - if !state.reached_vs[v] - state.hes_tail_count[f] -= 1 - end - if !state.marked_hes[f] push!(out_edges, f) state.marked_hes[f] = true + + if !state.reached_vs[v] + state.hes_tail_count[f] -= 1 + end end end state.reached_vs[v] = true end for f in out_edges + state.marked_hes[f] = false + push!(state.he_inedges[f], e) if !isnothing(state.edge_heap_points[f]) && !state.removed_hes[f] update!( Hmin, state.edge_heap_points[f], - sum(state.edge_weights[x] for x in short_hyperpath_vhe(hg, source, f, state)) + ( + sum(state.edge_weights[x] for x in short_hyperpath_vhe(hg, source, f, state)), + f + ) ) elseif isnothing(state.edge_heap_points[f]) && state.hes_tail_count[f] == 0 state.edge_heap_points[f] = push!( Hmin, - sum(state.edge_weights[x] for x in short_hyperpath_vhe(hg, source, f, state)) + ( + sum(state.edge_weights[x] for x in short_hyperpath_vhe(hg, source, f, state)), + f + ) ) end end @@ -297,11 +325,19 @@ function shortest_hyperpath_kk_heuristic( path = Set{Int}() cost = typemax(T) - for in_e in keys(hg.hg_head.v2he[target]) + inedges = keys(hg.hg_head.v2he[target]) + @info "Target inedges: $inedges" + @info "All inedges:" + for i in 1:nhe(hg) + inedges_i = state.he_inedges[i] + @info "\t$i: $inedges_i" + end + for in_e in inedges if !isnothing(state.edge_heap_points[in_e]) p = short_hyperpath_vhe(hg, source, in_e, state) cost_p = sum(state.edge_weights[e] for e in p) if cost_p < cost + @info "Updating to: path $p with cost $cost_p" path = p cost = cost_p end diff --git a/test/runtests.jl b/test/runtests.jl index 09baa80..1c27c27 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -489,19 +489,6 @@ end; @test bt[1] == Set{Int}(1:5) @test bt[2] == Set{Int}(1:9) - # 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]), - Set{Int}([1,2,3,5]), - Set{Int}(4), - Set{Int}(4) - ] - for i in 1:nhv(dh2) # In this example, all nodes (and hyperedges) can be reached from anywhere for j in 1:nhv(dh2) @@ -515,6 +502,26 @@ end; end end end + @test_throws AssertionError is_reachable(dh2, i, e, :test, init_state) + + # 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]), + Set{Int}([1,2,3,5]), + Set{Int}(4), + Set{Int}(4) + ] + init_state.he_inedges .= he_inedges + + @test ( + SimpleDirectedHypergraphs.short_hyperpath_vhe(dh2, 1, 4, init_state) == Set{Int}([1,2,4]) || + SimpleDirectedHypergraphs.short_hyperpath_vhe(dh2, 1, 4, init_state) == Set{Int}([1,3,4]) + ) # Example adapted from Blau et al., DOI: 10.1039/D0SC05647B tail_3 = [ From ece25d719e91aeb59d93477874060a5b926d41bc Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Tue, 25 Nov 2025 13:30:20 +0000 Subject: [PATCH 17/30] Get ready to rumble --- src/algorithms/paths.jl | 309 ++++++++++++++-------------------------- test/runtests.jl | 47 +++--- 2 files changed, 121 insertions(+), 235 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 604c0b1..4038443 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -1,203 +1,102 @@ -#TODO: STATUS -# Somehow state.he_inedges is getting screwy -# DH2: -# [ Info: All inedges: -# [ Info: 1: Set([6, 7]) -# [ Info: 2: Set([8, 1]) -# [ Info: 3: Set([7, 9, 1]) -# [ Info: 4: Set([5, 2, 3]) -# [ Info: 5: Set([4]) -# [ Info: 6: Set([5, 2, 8, 3, 1]) -# [ Info: 7: Set([5, 7, 2, 9, 3, 1]) -# [ Info: 8: Set([4]) -# [ Info: 9: Set([4]) - - -""" - struct DiHyperPathState{T}( - reached_vs::BitVector, - marked_hes::BitVector, - removed_hes::BitVector, - hes_tail_count::Vector{Int}, - he_inedges::Vector{Set{Int}}, - edge_weights::Vector{T}, - edge_costs::Vector{T}, - edge_heap_points::Vector{Union{Nothing, Int}} - ) where {T<:Real} - - Stores the state of a pathfinding operation. - - `reached_vs` and `marked_hes` track which vertices and hyperedges, respectively, have been visited and/or need to - be searched. `removed_hes` tracks which hyperedges have been removed from the edge heap during the main heuristic - pathfinding operation (see `shortest_hyperpath_kk_heuristic`). `hes_tail_count` is a vector containing the size of - the tail of each hyperedge. `he_inedges` tracks which hyperedges flow into which other hyperedges (i.e., for a - hyperedge `f`, the *inedges* are those hyperedges `e` where there exists some vertex `v` that is in both the head - of `e` and the tail of `f`). `edge_weights` are a set of initially-defined costs associated with each directed - hyperedge, and `edge_costs` are the (estimated) costs from the source vertex to each hyperedge. Finally, - `edge_heap_points` tracks the references for each hyperedge in the heap used during heuristic pathfinding. - -""" -struct DiHyperPathState{T<:Real} - reached_vs::BitVector - marked_hes::BitVector - removed_hes::BitVector - hes_tail_count::Vector{Int} - he_inedges::Vector{Set{Int}} - edge_weights::Vector{T} - edge_costs::Vector{T} - edge_heap_points::Vector{Union{Nothing,Int}} -end - -""" - initialize_dihyperpath_state( - hg::H, - hyperedge_weights::Vector{T} - ) where {H <: AbstractDirectedHypergraph, T <: Real} - - Construct an initial state for directed hypergraph pathfinding on hypergraph `hg` with hyperedge weights (i.e., - costs) `hyperedge_weights`. -""" -function initialize_dihyperpath_state( - hg::H, - hyperedge_weights::Vector{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} - # Hyperedge weights need to be nonnegative - @assert all(hyperedge_weights .>= 0) - - nv = nhv(hg) - ne = nhe(hg) - - edge_heap_points = Vector{Union{Nothing,Int}}(undef, ne) - edge_heap_points .= nothing - - return DiHyperPathState{T}( - BitVector(falses(nv)), - BitVector(falses(ne)), - BitVector(falses(ne)), - length.(keys.(hg.hg_tail.he2v)), - [Set{Int}() for _ in 1:ne], - hyperedge_weights, - fill(typemax(T), ne), - edge_heap_points - ) -end - """ forward_reachable( hg::H, source::Int, - state::DiHyperPathState{T} - ) where {H <: AbstractDirectedHypergraph, T <: Real} + ) 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). - `DiHyperPathState` `state` is used to track what vertices/hyperedges have been traversed. """ function forward_reachable( hg::H, source::Int, - state::DiHyperPathState{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} +) where {H<:AbstractDirectedHypergraph} # Priority queue of reached vertices Q = Queue{Int}() enqueue!(Q, source) - state.reached_vs[source] = true + 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? - reached_vs = Set{Int}() - reached_es = Set{Int}() + vs = Set{Int}() + es = Set{Int}() while length(Q) > 0 v = dequeue!(Q) - push!(reached_vs, v) + 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? - state.hes_tail_count[out_e] -= 1 + hes_tail_count[out_e] -= 1 - if state.hes_tail_count[out_e] == 0 - push!(reached_es, out_e) + if hes_tail_count[out_e] == 0 + push!(es, out_e) for w in keys(hg.hg_head.he2v[out_e]) - if !state.reached_vs[w] + if !reached_vs[w] enqueue!(Q, w) - state.reached_vs[w] = true + reached_vs[w] = true end end end end end - # Restore state - for v in reached_vs - state.reached_vs[v] = false - for out_e in keys(hg.hg_tail.v2he[v]) - state.hes_tail_count[out_e] += 1 - end - end - # Return reached vertices and hyperedges - return (reached_vs, reached_es) + return (vs, es) end """ backward_traceable( hg::H, target::Int, - state::DiHyperPathState{T} - ) where {H <: AbstractDirectedHypergraph, T <: Real} + ) 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). - - `DiHyperPathState` `state` is used to track what vertices/hyperedges have been traversed. """ function backward_traceable( hg::H, target::Int, - state::DiHyperPathState{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} +) where {H<:AbstractDirectedHypergraph} # Priority queue of reached vertices Q = Queue{Int}() enqueue!(Q, target) - state.reached_vs[target] = true + reached_vs = BitVector(falses(nhv(hg))) + reached_vs[target] = true + + marked_hes = BitVector(falses(nhe(hg))) # Which vertices/hyperedges have been reached? - reached_vs = Set{Int}() - reached_es = Set{Int}() + vs = Set{Int}() + es = Set{Int}() while length(Q) > 0 v = dequeue!(Q) - push!(reached_vs, v) + push!(vs, v) for in_e in keys(hg.hg_head.v2he[v]) - if !state.marked_hes[in_e] - push!(reached_es, in_e) - state.marked_hes[in_e] = true + 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 !state.reached_vs[w] + if !reached_vs[w] enqueue!(Q, w) - state.reached_vs[w] = true + reached_vs[w] = true end end end end end - # Restore state - for v in reached_vs - state.reached_vs[v] = false - end - for he in reached_es - state.marked_hes[he] = false - end - # Return reached vertices and hyperedges - return (reached_vs, reached_es) + return (vs, es) end @@ -248,16 +147,27 @@ function shortest_hyperpath_kk_heuristic( hyperedge_weights::Vector{T} ) where {H<:AbstractDirectedHypergraph,T<:Real} - state = initialize_dihyperpath_state(hg, hyperedge_weights) + 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, state) + fr = forward_reachable(hg, source) @assert target ∈ fr[1] # Doubly reachable hyperedges dr_hes = sort!(collect(intersect( fr[2], - backward_traceable(hg, target, state)[2] + backward_traceable(hg, target)[2] ))) # Eliminate non-doubly reachable hyperedges @@ -270,52 +180,60 @@ function shortest_hyperpath_kk_heuristic( 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 - state.edge_heap_points[out_e] = push!(Hmin, (state.edge_weights[out_e], out_e)) + hyperedge_heap_points[out_e] = push!(Hmin, (hyperedge_weights[out_e], out_e)) end end - state.reached_vs[source] = true - while length(Hmin) > 0 e = pop!(Hmin)[2] - state.removed_hes[e] = true + removed_hes[e] = true - path = short_hyperpath_vhe(hg, source, e, state) - state.edge_costs[e] = sum(state.edge_weights[x] for x in path) + 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 !state.marked_hes[f] - push!(out_edges, f) - state.marked_hes[f] = true - - if !state.reached_vs[v] - state.hes_tail_count[f] -= 1 + 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 - state.reached_vs[v] = true + reached_vs[v] = true end for f in out_edges - state.marked_hes[f] = false + marked_hes[f] = false + end - push!(state.he_inedges[f], e) - if !isnothing(state.edge_heap_points[f]) && !state.removed_hes[f] + for f in out_edges + push!(hyperedge_inedges[f], e) + if !isnothing(hyperedge_heap_points[f]) && !removed_hes[f] update!( Hmin, - state.edge_heap_points[f], + hyperedge_heap_points[f], ( - sum(state.edge_weights[x] for x in short_hyperpath_vhe(hg, source, f, state)), + sum( + hyperedge_weights[x] + for x in short_hyperpath_vhe(hg, source, f, hyperedge_inedges, hyperedge_costs) + ), f ) ) - elseif isnothing(state.edge_heap_points[f]) && state.hes_tail_count[f] == 0 - state.edge_heap_points[f] = push!( + elseif isnothing(hyperedge_heap_points[f]) && hes_tail_count[f] == 0 + hyperedge_heap_points[f] = push!( Hmin, ( - sum(state.edge_weights[x] for x in short_hyperpath_vhe(hg, source, f, state)), + sum( + hyperedge_weights[x] + for x in short_hyperpath_vhe(hg, source, f, hyperedge_inedges, hyperedge_costs) + ), f ) ) @@ -325,19 +243,11 @@ function shortest_hyperpath_kk_heuristic( path = Set{Int}() cost = typemax(T) - inedges = keys(hg.hg_head.v2he[target]) - @info "Target inedges: $inedges" - @info "All inedges:" - for i in 1:nhe(hg) - inedges_i = state.he_inedges[i] - @info "\t$i: $inedges_i" - end - for in_e in inedges - if !isnothing(state.edge_heap_points[in_e]) - p = short_hyperpath_vhe(hg, source, in_e, state) - cost_p = sum(state.edge_weights[e] for e in p) + 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 - @info "Updating to: path $p with cost $cost_p" path = p cost = cost_p end @@ -445,24 +355,25 @@ end hg::H, v::Int, he::Int, - state::DiHyperPathState{T} ) 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`, using `state` to track the hypergraph traversal during pathfinding. `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. + 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, - state::DiHyperPathState{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} + he_inedges::Vector{Set{Int}}, + he_costs::Vector{T} +) where {H<:AbstractDirectedHypergraph, T<:Real} + marked_hes = BitVector(falses(nhe(hg))) + Q = Queue{Int}() - for e in state.he_inedges[he] + for e in he_inedges[he] enqueue!(Q, e) - state.marked_hes[e] = true + marked_hes[e] = true end superpath = Set{Int}(he) @@ -473,20 +384,16 @@ function short_hyperpath_vhe( e = dequeue!(Q) push!(superpath, e) - for f in state.he_inedges[e] - if !state.marked_hes[f] + for f in he_inedges[e] + if !marked_hes[f] enqueue!(Q, f) - state.marked_hes[f] = true + marked_hes[f] = true end end end - for e in superpath - state.marked_hes[e] = false - end - # TODO: try to be more clever about this - superpath = sort(collect(superpath), by=x -> state.edge_costs[x], rev=true) + 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 @@ -501,7 +408,7 @@ function short_hyperpath_vhe( hg_copy.hg_head[:, e] .= nothing # Only if hyperedge is essential for reaching target, - if !is_reachable(hg_copy, v, he, :hyperedge, state) + if !is_reachable(hg_copy, v, he, :hyperedge) # Restore hyperedge to hypergraph copy hg_copy[:, e] .= hg[:, e] push!(path, e) @@ -517,8 +424,7 @@ end source::Int, target::Int, target_type::Symbol, - state::DiHyperPathState{T} - ) where {H <: AbstractDirectedHypergraph, T <: Real} + ) where {H <: AbstractDirectedHypergraph} TODO: this @@ -527,12 +433,11 @@ function is_reachable( hg::H, source::Int, target::Int, - target_type::Symbol, - state::DiHyperPathState{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} + target_type::Symbol +) where {H<:AbstractDirectedHypergraph} @assert target_type ∈ [:vertex, :hyperedge] "`target_type` must be :vertex or :hyperedge" - fr = forward_reachable(hg, source, state) + 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 @@ -553,10 +458,7 @@ function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H< hg_copy = deepcopy(hg) hg_copy[:, sort(collect(out))] .= nothing - weights = ones(nhe(hg_copy)) - state = initialize_dihyperpath_state(hg_copy, weights) - - reached_vs, reached_es = forward_reachable(hg_copy, source, state) + reached_vs, reached_es = forward_reachable(hg_copy, source) # Path does not exist if target ∉ reached_vs @@ -569,8 +471,8 @@ function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H< for e in reached_es hg_copy[:, e] .= nothing - # Only if hyperedge is essential for reaching target, - if !is_reachable(hg_copy, source, target, :vertex, state) + # 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) @@ -707,14 +609,12 @@ function initialize_ilp_model( hg::H, source::Int, target::Int, - hyperedge_weights::Vector{T}) where {H<:AbstractDirectedHypergraph,T<:Real} - - # Initialize state - state = initialize_dihyperpath_state(hg, hyperedge_weights) + hyperedge_weights::Vector{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, state) + @assert is_reachable(hg, source, target, :vertex) # Initialize integer linear programming model model = Model(GLPK.Optimizer) @@ -756,11 +656,11 @@ function initialize_ilp_model( # Distance-based inequalities dist_ests = fill(typemax(T), nhv(hg)) - for v in forward_reachable(hg, source, state)[1] + for v in forward_reachable(hg, source)[1] # TODO: this is inefficient # Currently, will repeat a lot of work dist_ests[v] = sum( - state.edge_weights[e] for e in shortest_hyperpath_kk_heuristic(hg, source, v, hyperedge_weights) + hyperedge_weights[e] for e in shortest_hyperpath_kk_heuristic(hg, source, v, hyperedge_weights) ) end @@ -789,10 +689,9 @@ function initialize_ilp_model( end # Define objective function - @objective(model, Min, dot(x, state.edge_weights)) + @objective(model, Min, dot(x, hyperedge_weights)) return model, x, cuts, crosses - end """ diff --git a/test/runtests.jl b/test/runtests.jl index 1c27c27..d5e37d6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -463,29 +463,20 @@ end; @testset "SimpleDirectedHypergraphs paths/distance " begin # Custom, strongly-connected example with loops w2 = [3, 7, 19, 10, 13, 11, 1, 12, 8] - init_state = initialize_dihyperpath_state(dh2, w2) - @test init_state.reached_vs == BitVector(falses(nhv(dh2))) - @test init_state.marked_hes == BitVector(falses(nhe(dh2))) - @test init_state.removed_hes == BitVector(falses(nhe(dh2))) - @test init_state.hes_tail_count == length.(keys.(dh2.hg_tail.he2v)) - @test init_state.he_inedges == [Set{Int}() for _ in 1:nhe(dh2)] - @test init_state.edge_weights == w2 - @test init_state.edge_costs == fill(typemax(Int), nhe(dh2)) - @test init_state.edge_heap_points == fill(nothing, nhe(dh2)) # Traversing forward from vertices to vertices/hyperedges - fr = forward_reachable(dh2, 1, init_state) + fr = forward_reachable(dh2, 1) @test fr[1] == Set{Int}(1:5) @test fr[2] == Set{Int}(1:9) - fr = forward_reachable(dh2, 5, init_state) + 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, init_state) + bt = backward_traceable(dh2, 1) @test bt[1] == Set{Int}(1:5) @test bt[2] == Set{Int}(1:9) - bt = backward_traceable(dh2, 5, init_state) + bt = backward_traceable(dh2, 5) @test bt[1] == Set{Int}(1:5) @test bt[2] == Set{Int}(1:9) @@ -493,16 +484,16 @@ end; # In this example, all nodes (and hyperedges) can be reached from anywhere for j in 1:nhv(dh2) @testset "Test DH2: v$i\t -> v$j" begin - @test is_reachable(dh2, i, j, :vertex, init_state) + @test is_reachable(dh2, i, j, :vertex) end end for e in 1:nhe(dh2) @testset "Test DH2: v$i\t -> he$e" begin - @test is_reachable(dh2, i, e, :hyperedge, init_state) + @test is_reachable(dh2, i, e, :hyperedge) end end end - @test_throws AssertionError is_reachable(dh2, i, e, :test, init_state) + @test_throws AssertionError is_reachable(dh2, 1, 2, :test) # Test `short_hyperpath_vhe` for greedy hyperpath generation he_inedges = [ @@ -511,17 +502,14 @@ end; Set{Int}([1,9]), Set{Int}([2,3,5]), Set{Int}(4), - Set{Int}([1,2,3,5]), - Set{Int}([1,2,3,5]), + Set{Int}([1,2,3,5,8]), + Set{Int}([1,2,3,5,9]), Set{Int}(4), Set{Int}(4) ] - init_state.he_inedges .= he_inedges - @test ( - SimpleDirectedHypergraphs.short_hyperpath_vhe(dh2, 1, 4, init_state) == Set{Int}([1,2,4]) || - SimpleDirectedHypergraphs.short_hyperpath_vhe(dh2, 1, 4, init_state) == Set{Int}([1,3,4]) - ) + 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]) # Example adapted from Blau et al., DOI: 10.1039/D0SC05647B tail_3 = [ @@ -549,27 +537,26 @@ end; dh3 = DirectedHypergraph(tail_3, head_3) w3 = [3, 7, 19, 10, 13, 11, 1, 12, 8, 2, 22] - init_state = initialize_dihyperpath_state(dh3, w3) # Traversing forward from vertices to vertices/hyperedges - fr = forward_reachable(dh3, 1, init_state) + fr = forward_reachable(dh3, 1) @test fr[1] == Set{Int}(1:9) @test fr[2] == Set{Int}(1:11) - fr = forward_reachable(dh3, 6, init_state) + 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, init_state) + 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, init_state) + bt = backward_traceable(dh3, 1) @test bt[1] == Set{Int}(1) @test bt[2] == Set{Int}() - bt = backward_traceable(dh3, 6, init_state) + bt = backward_traceable(dh3, 6) @test bt[1] == Set{Int}([6, 1]) @test bt[2] == Set{Int}(3) - bt = backward_traceable(dh3, 9, init_state) + bt = backward_traceable(dh3, 9) @test bt[1] == Set{Int}(1:9) @test bt[2] == Set{Int}(1:11) From 927dade49920743582c0ac1e11ab9fa0d3521630 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 26 Nov 2025 09:41:57 +0000 Subject: [PATCH 18/30] Tests pass for heuristic shortest-path algorithm. Hell yeah --- src/algorithms/paths.jl | 90 ++++++++++++++++++++--------------------- src/dihypergraph.jl | 6 ++- test/runtests.jl | 15 ++++--- 3 files changed, 58 insertions(+), 53 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 4038443..eb92f3d 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -261,24 +261,24 @@ function shortest_hyperpath_kk_heuristic( hg::DirectedHypergraph{T,V,E,D}, source::Int, targets::Set{Int}, - hyperedge_weights::Vector{T} -) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + hyperedge_weights::Vector{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 = add_vertex!(hg_copy) - meta_he = add_hyperedge!( + 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)) + vertices_head=D(metatarget => convert(T, 0)) ) path = shortest_hyperpath_kk_heuristic( hg_copy, source, metatarget, - vcat(hyperedge_weights, convert(T, 0)) + vcat(hyperedge_weights, convert(S, 0)) ) # Remove the fictitious hyperedge from the targets to the metatarget @@ -289,16 +289,16 @@ function shortest_hyperpath_kk_heuristic( hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, target::Int, - hyperedge_weights::Vector{T} -) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + hyperedge_weights::Vector{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 = add_vertex!(hg_copy) - meta_he = add_hyperedge!( + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he = SimpleHypergraphs.add_hyperedge!( hg_copy; - vertices_tail=D(metasource, convert(T, 0)), + vertices_tail=D(metasource => convert(T, 0)), vertices_head=D(x => convert(T, 0) for x in sources) ) @@ -306,7 +306,7 @@ function shortest_hyperpath_kk_heuristic( hg_copy, metasource, target, - vcat(hyperedge_weights, convert(T, 0)) + vcat(hyperedge_weights, convert(S, 0)) ) # Remove the fictitious hyperedge from the metasource to the sources @@ -317,33 +317,33 @@ function shortest_hyperpath_kk_heuristic( hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, targets::Set{Int}, - hyperedge_weights::Vector{T} -) where {T<:Real,V,E,D<:AbstractDict{Int,T}} + hyperedge_weights::Vector{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 = add_vertex!(hg_copy) - meta_he_source = add_hyperedge!( + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he_source = SimpleHypergraphs.add_hyperedge!( hg_copy; - vertices_tail=D(metasource, convert(T, 0)), + 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 = add_vertex!(hg_copy) - meta_he_target = add_hyperedge!( + 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)) + vertices_head=D(metatarget => convert(T, 0)) ) path = shortest_hyperpath_kk_heuristic( hg_copy, metasource, metatarget, - vcat(hyperedge_weights, [convert(T, 0), convert(T, 0)]) + vcat(hyperedge_weights, [convert(S, 0), convert(S, 0)]) ) # Remove fictitious hyperedges @@ -535,11 +535,11 @@ function all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H<:Abstrac # 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 = add_vertex!(hg_copy) - meta_he = add_hyperedge!( + 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)) + vertices_head=D(metatarget => convert(T, 0)) ) paths = all_hyperpaths( @@ -557,10 +557,10 @@ function all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H<:Abstrac # 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 = add_vertex!(hg_copy) - meta_he = add_hyperedge!( + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he = SimpleHypergraphs.add_hyperedge!( hg_copy; - vertices_tail=D(metasource, convert(T, 0)), + vertices_tail=D(metasource => convert(T, 0)), vertices_head=D(x => convert(T, 0) for x in sources) ) @@ -579,20 +579,20 @@ function all_hyperpaths(hg::H, sources::Set{Int}, targets::Set{Int}) where {H<:A # 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 = add_vertex!(hg_copy) - meta_he_source = add_hyperedge!( + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he_source = SimpleHypergraphs.add_hyperedge!( hg_copy; - vertices_tail=D(metasource, convert(T, 0)), + 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 = add_vertex!(hg_copy) - meta_he_target = add_hyperedge!( + 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)) + vertices_head=D(metatarget => convert(T, 0)) ) paths = all_hyperpaths( @@ -778,11 +778,11 @@ function shortest_hyperpath_kk_ilp( # 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 = add_vertex!(hg_copy) - meta_he = add_hyperedge!( + 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)) + vertices_head=D(metatarget => convert(T, 0)) ) path = shortest_hyperpath_kk_ilp( @@ -806,10 +806,10 @@ function shortest_hyperpath_kk_ilp( # 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 = add_vertex!(hg_copy) - meta_he = add_hyperedge!( + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he = SimpleHypergraphs.add_hyperedge!( hg_copy; - vertices_tail=D(metasource, convert(T, 0)), + vertices_tail=D(metasource => convert(T, 0)), vertices_head=D(x => convert(T, 0) for x in sources) ) @@ -834,20 +834,20 @@ function shortest_hyperpath_kk_ilp( # 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 = add_vertex!(hg_copy) - meta_he_source = add_hyperedge!( + metasource = SimpleHypergraphs.add_vertex!(hg_copy) + meta_he_source = SimpleHypergraphs.add_hyperedge!( hg_copy; - vertices_tail=D(metasource, convert(T, 0)), + 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 = add_vertex!(hg_copy) - meta_he_target = add_hyperedge!( + 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)) + vertices_head=D(metatarget => convert(T, 0)) ) path = shortest_hyperpath_kk_ilp( diff --git a/src/dihypergraph.jl b/src/dihypergraph.jl index 1f7d43f..a9cc481 100644 --- a/src/dihypergraph.jl +++ b/src/dihypergraph.jl @@ -722,8 +722,10 @@ 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 + 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)) diff --git a/test/runtests.jl b/test/runtests.jl index d5e37d6..d37b491 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -483,14 +483,10 @@ end; for i in 1:nhv(dh2) # In this example, all nodes (and hyperedges) can be reached from anywhere for j in 1:nhv(dh2) - @testset "Test DH2: v$i\t -> v$j" begin - @test is_reachable(dh2, i, j, :vertex) - end + @test is_reachable(dh2, i, j, :vertex) end for e in 1:nhe(dh2) - @testset "Test DH2: v$i\t -> he$e" begin - @test is_reachable(dh2, i, e, :hyperedge) - end + @test is_reachable(dh2, i, e, :hyperedge) end end @test_throws AssertionError is_reachable(dh2, 1, 2, :test) @@ -508,9 +504,16 @@ end; 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]) + # Example adapted from Blau et al., DOI: 10.1039/D0SC05647B tail_3 = [ true true true true nothing nothing nothing nothing nothing nothing nothing From 4034dbd7f5911051c75479157a1e5f7ed65e2658 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 26 Nov 2025 09:58:06 +0000 Subject: [PATCH 19/30] Pass for Blau example, too --- test/runtests.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index d37b491..4110ffe 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -539,7 +539,7 @@ end; ] dh3 = DirectedHypergraph(tail_3, head_3) - w3 = [3, 7, 19, 10, 13, 11, 1, 12, 8, 2, 22] + w3 = [1, 6, 2, 4, 3, 1, 2, 2, 4, 1, 1] # Traversing forward from vertices to vertices/hyperedges fr = forward_reachable(dh3, 1) @@ -563,6 +563,8 @@ end; @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]) end; @testset "SimpleDirectedHypergraphs diameter " begin From ab431d616fb7c3053a514519d25f90d43cb00df4 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Wed, 26 Nov 2025 10:43:47 +0000 Subject: [PATCH 20/30] all_hyperpaths seems to work (haven't tested exhaustively) --- src/algorithms/paths.jl | 25 ++++++++++++++++++++----- test/runtests.jl | 9 +++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index eb92f3d..aa2379d 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -456,7 +456,9 @@ end function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H<:AbstractDirectedHypergraph} # Remove excluded hyperedges hg_copy = deepcopy(hg) - hg_copy[:, sort(collect(out))] .= nothing + 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) @@ -469,7 +471,8 @@ function get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H< # Try to minimize the size of the path by eliminating unnecessary hyperedges for e in reached_es - hg_copy[:, e] .= nothing + 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) @@ -530,7 +533,11 @@ function all_hyperpaths(hg::H, source::Int, target::Int) where {H<:AbstractDirec paths end -function all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H<:AbstractDirectedHypergraph} +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 @@ -552,7 +559,11 @@ function all_hyperpaths(hg::H, source::Int, targets::Set{Int}) where {H<:Abstrac return Set(setdiff(p, Set{Int}(meta_he)) for p in paths) end -function all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H<:AbstractDirectedHypergraph} +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 @@ -574,7 +585,11 @@ function all_hyperpaths(hg::H, sources::Set{Int}, target::Int) where {H<:Abstrac return Set(setdiff(p, Set{Int}(meta_he)) for p in paths) end -function all_hyperpaths(hg::H, sources::Set{Int}, targets::Set{Int}) where {H<:AbstractDirectedHypergraph} +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 diff --git a/test/runtests.jl b/test/runtests.jl index 4110ffe..e27cf06 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -514,6 +514,9 @@ end; @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 @@ -565,6 +568,12 @@ end; # Test heuristic (but usually accurate) shortest-path algorithm @test shortest_hyperpath_kk_heuristic(dh3, 1, 9, w3) == Set{Int}([1, 6, 11]) + + # Test generation of all possible paths + @test length(all_hyperpaths(dh3, 1, 9)) == 6 + @test length(all_hyperpaths(dh3, 1, Set{Int}([8, 9]))) == 9 + @test length(all_hyperpaths(dh3, Set{Int}([1,8]), 9)) == 1 + @test length(all_hyperpaths(dh3, Set{Int}([2,6]), Set{Int}([8,9]))) == 2 end; @testset "SimpleDirectedHypergraphs diameter " begin From 114af3ada82b20f4dad9d38cafd7e5ab99f08fc1 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 09:17:34 +0000 Subject: [PATCH 21/30] ILP tests pass; now just need to do distance and diameter --- src/algorithms/clustering.jl | 0 src/algorithms/paths.jl | 48 ++++++++++++++++++++---------------- test/runtests.jl | 21 +++++++++++++--- 3 files changed, 45 insertions(+), 24 deletions(-) delete mode 100644 src/algorithms/clustering.jl diff --git a/src/algorithms/clustering.jl b/src/algorithms/clustering.jl deleted file mode 100644 index e69de29..0000000 diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index aa2379d..fdc7355 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -650,11 +650,11 @@ function initialize_ilp_model( end in_hes = collect(keys(hg.hg_head.v2he[v])) - @constraint(model, con_scalar, sum([x[ih] for ih in in_hes]) >= x[i]) + @constraint(model, sum([x[ih] for ih in in_hes]) >= x[i]) end # Head-hitting inequalities - if target ∉ hg.hg_head.he2v[i] + 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 @@ -662,20 +662,21 @@ function initialize_ilp_model( end end - @constraint(model, con_scalar, sum([x[j] for j in hits]) >= x[i]) + @constraint(model, sum([x[j] for j in hits]) >= x[i]) end end # Target-production inequality - @constraint(model, con_scalar, sum([x[e] for e in keys(hg.hg_head.v2he[target])]) >= 1) + @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) + [hyperedge_weights[e] for e in shortest_hyperpath_kk_heuristic(hg, source, v, hyperedge_weights)] ) end @@ -700,7 +701,7 @@ function initialize_ilp_model( ] push!(crosses, BitVector(cross)) - @constraint(model, con_scalar, dot(x, cross) >= 1) + @constraint(model, dot(x, cross) >= 1) end # Define objective function @@ -770,7 +771,7 @@ function shortest_hyperpath_kk_ilp( while length(new_cuts) > 0 # Add new constraints to the model for cross in new_crosses - @constraint(model, con_scalar, dot(x, cross) >= 1) + @constraint(model, dot(x, cross) >= 1) end # Re-optimize model with new cut-constraints @@ -784,11 +785,11 @@ function shortest_hyperpath_kk_ilp( end function shortest_hyperpath_kk_ilp( - hg::H, + hg::DirectedHypergraph{T,V,E,D}, source::Int, targets::Set{Int}, - hyperedge_weights::Vector{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} + hyperedge_weights::Vector{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 @@ -804,7 +805,7 @@ function shortest_hyperpath_kk_ilp( hg_copy, source, metatarget, - hyperedge_weights + vcat(hyperedge_weights, convert(S, 0)) ) # Remove the fictitious hyperedge from the targets to the metatarget @@ -812,11 +813,11 @@ function shortest_hyperpath_kk_ilp( end function shortest_hyperpath_kk_ilp( - hg::H, + hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, target::Int, - hyperedge_weights::Vector{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} + hyperedge_weights::Vector{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 @@ -832,7 +833,7 @@ function shortest_hyperpath_kk_ilp( hg_copy, metasource, target, - vcat(hyperedge_weights, convert(T, 0)) + vcat(hyperedge_weights, convert(S, 0)) ) # Remove the fictitious hyperedge from the metasource to the sources @@ -840,11 +841,11 @@ function shortest_hyperpath_kk_ilp( end function shortest_hyperpath_kk_ilp( - hg::H, + hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, targets::Set{Int}, - hyperedge_weights::Vector{T} -) where {H<:AbstractDirectedHypergraph,T<:Real} + hyperedge_weights::Vector{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 @@ -936,6 +937,8 @@ function expand_cuts( # 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] = [ @@ -943,10 +946,13 @@ function expand_cuts( !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 - greedy_v = minimum(q -> length(findall(!old_cross .&& new_cross_ev[q])), keys(new_cross_ev)) - new_cut = new_cut_ev[greedy_v] new_cross = new_cross_ev[greedy_v] end @@ -954,7 +960,7 @@ function expand_cuts( end # If this is still a valid s,t-cut - if !any(new_corss) && source ∈ new_cut + if !any(new_cross) && source ∈ new_cut push!(new_cuts, new_cut) push!(new_crosses, new_cross) end diff --git a/test/runtests.jl b/test/runtests.jl index e27cf06..c2c219f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,6 +4,7 @@ using StatsBase using Random using DataStructures using Graphs +import JuMP using Test # using JET @@ -568,13 +569,27 @@ end; # 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]))) == 9 - @test length(all_hyperpaths(dh3, Set{Int}([1,8]), 9)) == 1 + @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 -end; + + model = 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 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]) +end @testset "SimpleDirectedHypergraphs diameter " begin From efba18409027ac160f9dea4d3bd08596f72a9eb4 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 09:32:28 +0000 Subject: [PATCH 22/30] Test pass, baby! Let's go. --- src/algorithms/distance.jl | 22 +++++++++++++++++----- test/runtests.jl | 9 +++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/algorithms/distance.jl b/src/algorithms/distance.jl index f90410b..70dbbae 100644 --- a/src/algorithms/distance.jl +++ b/src/algorithms/distance.jl @@ -127,17 +127,23 @@ function Graphs.diameter( end # Since we're interested in the number of steps, assign each hyperedge a uniform weight of `1` - edge_weights = ones(Int, nhe(hg)) + edge_weights = ones(nhe(hg)) - max_dist = 0 + max_dist = 0.0 for i in 1:nhv(hg) for j in i+1:nhv(hg) - dist = distance(hg, SnodeDistanceKKHeuristic(Set{Int}(i), Set{Int}(j)), edge_weights) + 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 """ @@ -160,15 +166,21 @@ function Graphs.diameter( end # Since we're interested in the number of steps, assign each hyperedge a uniform weight of `1` - edge_weights = ones(Int64, nhe(hg)) + edge_weights = ones(nhe(hg)) max_dist = 0 for i in 1:nhv(hg) for j in i+1:nhv(hg) - dist = distance(hg, SnodeDistanceKKILP(Set{Int}(i), Set{Int}(j)), edge_weights) + 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/test/runtests.jl b/test/runtests.jl index c2c219f..00bc4b7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -577,6 +577,7 @@ end; @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 = 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 @@ -589,8 +590,16 @@ end; @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 From 70461feee007e14b35fa1d643e61f8ecc4b48275 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 09:51:29 +0000 Subject: [PATCH 23/30] CI issue - OpenSSLsucks --- .github/workflows/CI.yml | 74 +++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 1c5a2fb..953691e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -12,42 +12,42 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} jobs: - test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} - runs-on: ${{ matrix.os }} - timeout-minutes: 60 - permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created - actions: write - contents: read - strategy: - fail-fast: false - matrix: - version: - - 'lts' - - '1.11' - - 'pre' - os: - - ubuntu-latest - arch: - - x64 - steps: - - name: Install matplotlib - run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi - shell: bash - - uses: actions/checkout@v4 - - uses: julia-actions/setup-julia@v2 - with: - version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - - uses: julia-actions/cache@v2 - - uses: julia-actions/julia-buildpkg@v1 - - uses: julia-actions/julia-runtest@v1 - - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v5 - with: - files: lcov.info - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false + # test: + # name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + # runs-on: ${{ matrix.os }} + # timeout-minutes: 60 + # permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created + # actions: write + # contents: read + # strategy: + # fail-fast: false + # matrix: + # version: + # - 'lts' + # - '1.11' + # - 'pre' + # os: + # - ubuntu-latest + # arch: + # - x64 + # steps: + # - name: Install matplotlib + # run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi + # shell: bash + # - uses: actions/checkout@v4 + # - uses: julia-actions/setup-julia@v2 + # with: + # version: ${{ matrix.version }} + # arch: ${{ matrix.arch }} + # - uses: julia-actions/cache@v2 + # - uses: julia-actions/julia-buildpkg@v1 + # - uses: julia-actions/julia-runtest@v1 + # - uses: julia-actions/julia-processcoverage@v1 + # - uses: codecov/codecov-action@v5 + # with: + # files: lcov.info + # token: ${{ secrets.CODECOV_TOKEN }} + # fail_ci_if_error: false docs: name: Documentation runs-on: ubuntu-latest @@ -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) From 8d37da83ecbb22724a3237807816012731877d34 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 10:12:13 +0000 Subject: [PATCH 24/30] Trying to appease the doctest gods --- src/SimpleDirectedHypergraphs.jl | 4 ++-- src/algorithms/distance.jl | 20 +++++++++++------ src/algorithms/paths.jl | 38 ++++++++++++++++++-------------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/SimpleDirectedHypergraphs.jl b/src/SimpleDirectedHypergraphs.jl index 68f2f43..3f4e391 100644 --- a/src/SimpleDirectedHypergraphs.jl +++ b/src/SimpleDirectedHypergraphs.jl @@ -10,7 +10,7 @@ using Random using LinearAlgebra using SimpleTraits using InvertedIndices -using JuMP +using JuMP: constraint, is_binary, Min, Model, objective, optimize!, set_start_value, value, variable import GLPK export AbstractDirectedHypergraph @@ -23,7 +23,7 @@ export to_undirected export get_weakly_connected_components, get_strongly_connected_components -export DiHyperPathState, initialize_dihyperpath_state, forward_reachable, backward_traceable, is_reachable +export forward_reachable, backward_traceable, is_reachable, get_hyperpath export all_hyperpaths, shortest_hyperpath_kk_heuristic, initialize_ilp_model, shortest_hyperpath_kk_ilp export SnodeDistanceKKHeuristic, SnodeDistanceKKILP diff --git a/src/algorithms/distance.jl b/src/algorithms/distance.jl index 70dbbae..be4072c 100644 --- a/src/algorithms/distance.jl +++ b/src/algorithms/distance.jl @@ -1,8 +1,11 @@ """ - SnodeDistanceKKHeuristic(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance + SnodeDistanceKKHeuristic <: AbstractDistance -Represent a distance between a set of *source* nodes and a set of *target* nodes in a of directed hypergraph `h`, -where a distance in this context is defined as the length of the shortest hyperpath that reaches all `t ∈ targets`. +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 @@ -15,15 +18,18 @@ struct SnodeDistanceKKHeuristic <: AbstractDistance end """ - SnodeDistanceKKILP(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance + SnodeDistanceKKILP <: AbstractDistance + +Constructor: SnodeDistanceKKILP(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance -Represent a distance between a set of *source* nodes and a set of *target* nodes in a of directed hypergraph `h`, -where a distance in this context is defined as the length of the shortest hyperpath that reaches all `t ∈ targets`. +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 algorith, see `SnodeDistanceKKHeuristic`. +For a lower-cost heuristic algorithm, see `SnodeDistanceKKHeuristic`. """ struct SnodeDistanceKKILP <: AbstractDistance sources::Set{Int} diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index fdc7355..9a22547 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -423,11 +423,12 @@ end hg::H, source::Int, target::Int, - target_type::Symbol, - ) where {H <: AbstractDirectedHypergraph} - - TODO: this + 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, @@ -448,7 +449,12 @@ function is_reachable( end """ - get_hyperpath(hg::H, source::Int, target::Int, out::Set{Int}) where {H <: AbstractDirectedHypergraph} + 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`. @@ -719,25 +725,25 @@ end ) where {H<:AbstractDirectedHypergraph, T<:Real} shortest_hyperpath_kk_ilp( - hg::H, + hg::DirectedHypergraph{T,V,E,D}, source::Int, targets::Set{Int}, - hyperedge_weights::Vector{T} - ) where {H<:AbstractDirectedHypergraph, T<:Real} + hyperedge_weights::Vector{S} + ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} shortest_hyperpath_kk_ilp( - hg::H, + hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, target::Int, - hyperedge_weights::Vector{T} - ) where {H<:AbstractDirectedHypergraph, T<:Real} + hyperedge_weights::Vector{S} + ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} shortest_hyperpath_kk_ilp( - hg::H, + hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, targets::Set{Int}, - hyperedge_weights::Vector{T} - ) where {H<:AbstractDirectedHypergraph, T<:Real} + hyperedge_weights::Vector{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 @@ -885,9 +891,9 @@ end cuts::Vector{Set{Int}}, crosses::Vector{BitVector}, curr_sol::BitVector - ) where {H <: AbstractDirectedHypergraph} + ) where {H<:AbstractDirectedHypergraph} - A helper function for `shortest_hyperpath_kk_ilp` + 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`. From 7475d7b672f15e8effb9a4b86c9b74cca256c879 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 10:27:40 +0000 Subject: [PATCH 25/30] Remove ambiguity, I hope --- src/SimpleDirectedHypergraphs.jl | 2 +- src/algorithms/paths.jl | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/SimpleDirectedHypergraphs.jl b/src/SimpleDirectedHypergraphs.jl index 3f4e391..1996007 100644 --- a/src/SimpleDirectedHypergraphs.jl +++ b/src/SimpleDirectedHypergraphs.jl @@ -10,7 +10,7 @@ using Random using LinearAlgebra using SimpleTraits using InvertedIndices -using JuMP: constraint, is_binary, Min, Model, objective, optimize!, set_start_value, value, variable +import JuMP import GLPK export AbstractDirectedHypergraph diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 9a22547..870dce1 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -638,14 +638,14 @@ function initialize_ilp_model( @assert is_reachable(hg, source, target, :vertex) # Initialize integer linear programming model - model = Model(GLPK.Optimizer) + model = JuMP.Model(GLPK.Optimizer) # Create one binary variable for each hyperedge in `hg` - @variable(model, x[1:nhe(hg)], Bin) - set_start_value.(x, 1) + @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(is_binary.(x)) + @assert all(JuMP.is_binary.(x)) # Define initial constraints for i in 1:nhe(hg) @@ -656,7 +656,7 @@ function initialize_ilp_model( end in_hes = collect(keys(hg.hg_head.v2he[v])) - @constraint(model, sum([x[ih] for ih in in_hes]) >= x[i]) + @JuMP.constraint(model, sum([x[ih] for ih in in_hes]) >= x[i]) end # Head-hitting inequalities @@ -668,12 +668,12 @@ function initialize_ilp_model( end end - @constraint(model, sum([x[j] for j in hits]) >= x[i]) + @JuMP.constraint(model, sum([x[j] for j in hits]) >= x[i]) end end # Target-production inequality - @constraint(model, sum([x[e] for e in keys(hg.hg_head.v2he[target])]) >= 1) + @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)) @@ -707,11 +707,11 @@ function initialize_ilp_model( ] push!(crosses, BitVector(cross)) - @constraint(model, dot(x, cross) >= 1) + @JuMP.constraint(model, dot(x, cross) >= 1) end # Define objective function - @objective(model, Min, dot(x, hyperedge_weights)) + @JuMP.objective(model, Min, dot(x, hyperedge_weights)) return model, x, cuts, crosses end @@ -765,11 +765,11 @@ function shortest_hyperpath_kk_ilp( # 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) - optimize!(model) + JuMP.optimize!(model) # Convert floating-point solution into BitVector # TODO: Is this necessary w/ JuMP? Or will the output really be binary? - solution = value.(x) .> 0.5 + 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) @@ -777,12 +777,12 @@ function shortest_hyperpath_kk_ilp( while length(new_cuts) > 0 # Add new constraints to the model for cross in new_crosses - @constraint(model, dot(x, cross) >= 1) + @JuMP.constraint(model, dot(x, cross) >= 1) end # Re-optimize model with new cut-constraints - optimize!(model) - solution = value.(x) .> 0.5 + JuMP.optimize!(model) + solution = JuMP.value.(x) .> 0.5 new_cuts, new_crosses = expand_cuts(hg, source, target, new_cuts, new_crosses, solution) end From e03efe45a40d9f082a8badce7855f43550058160 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 10:49:48 +0000 Subject: [PATCH 26/30] Docs --- .github/workflows/CI.yml | 72 ++++++++++++++++---------------- docs/src/reference.md | 27 ++++++++++++ src/SimpleDirectedHypergraphs.jl | 4 +- src/algorithms/distance.jl | 2 +- src/algorithms/paths.jl | 66 +++++++++++++++++++---------- test/runtests.jl | 4 +- 6 files changed, 113 insertions(+), 62 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 953691e..7814366 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -12,42 +12,42 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} jobs: - # test: - # name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} - # runs-on: ${{ matrix.os }} - # timeout-minutes: 60 - # permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created - # actions: write - # contents: read - # strategy: - # fail-fast: false - # matrix: - # version: - # - 'lts' - # - '1.11' - # - 'pre' - # os: - # - ubuntu-latest - # arch: - # - x64 - # steps: - # - name: Install matplotlib - # run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi - # shell: bash - # - uses: actions/checkout@v4 - # - uses: julia-actions/setup-julia@v2 - # with: - # version: ${{ matrix.version }} - # arch: ${{ matrix.arch }} - # - uses: julia-actions/cache@v2 - # - uses: julia-actions/julia-buildpkg@v1 - # - uses: julia-actions/julia-runtest@v1 - # - uses: julia-actions/julia-processcoverage@v1 - # - uses: codecov/codecov-action@v5 - # with: - # files: lcov.info - # token: ${{ secrets.CODECOV_TOKEN }} - # fail_ci_if_error: false + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created + actions: write + contents: read + strategy: + fail-fast: false + matrix: + version: + - 'lts' + - '1.11' + - 'pre' + os: + - ubuntu-latest + arch: + - x64 + steps: + - name: Install matplotlib + run: if [ "$RUNNER_OS" = "Linux" ]; then sudo apt-get install -y python3-matplotlib; fi + shell: bash + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v5 + with: + files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false docs: name: Documentation runs-on: ubuntu-latest diff --git a/docs/src/reference.md b/docs/src/reference.md index 84be58b..9541bc2 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -125,6 +125,33 @@ _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} +forward_reachable(hg::H, source::Int) where {H<:AbstractDirectedHypergraph} +backward_traceable(hg::H, target::Int) where {H<:AbstractDirectedHypergraph} +is_reachable(hg::H, source::Int, target::Int, target_type::Symbol) where {H<:AbstractDirectedHypergraph} + +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}} + +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}} + +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}} + +SnodeDistanceKKHeuristic <: AbstractDistance +SnodeDistanceKKILP <: AbstractDistance + +SimpleHypergraphs.distance(hg::H, distance_method::SnodeDistanceKKHeuristic, hyperedge_weights::AbstractVector{T}) where {H<:AbstractDirectedHypergraph,T<:Real} +distance(hg::H, distance_method::SnodeDistanceKKILP, hyperedge_weights::AbstractVector{T}) where {H <: AbstractDirectedHypergraph} + +Graphs.diameter(hg::H, _::SnodeDistanceKKHeuristic) where {H<:AbstractDirectedHypergraph} +Graphs.diameter(hg::H, _::SnodeDistanceKKILP) where {H<:AbstractDirectedHypergraph} ``` diff --git a/src/SimpleDirectedHypergraphs.jl b/src/SimpleDirectedHypergraphs.jl index 1996007..7d8b84e 100644 --- a/src/SimpleDirectedHypergraphs.jl +++ b/src/SimpleDirectedHypergraphs.jl @@ -23,8 +23,8 @@ export to_undirected export get_weakly_connected_components, get_strongly_connected_components -export forward_reachable, backward_traceable, is_reachable, get_hyperpath -export all_hyperpaths, shortest_hyperpath_kk_heuristic, initialize_ilp_model, shortest_hyperpath_kk_ilp +export forward_reachable, backward_traceable, is_reachable +export all_hyperpaths, shortest_hyperpath_kk_heuristic, shortest_hyperpath_kk_ilp export SnodeDistanceKKHeuristic, SnodeDistanceKKILP diff --git a/src/algorithms/distance.jl b/src/algorithms/distance.jl index be4072c..abfa7e9 100644 --- a/src/algorithms/distance.jl +++ b/src/algorithms/distance.jl @@ -78,7 +78,7 @@ end """ distance( hg::H, - distance_method::SnodeDistanceKKHeuristic, + distance_method::SnodeDistanceKKILP, hyperedge_weights::AbstractVector{T} ) where {H <: AbstractDirectedHypergraph} diff --git a/src/algorithms/paths.jl b/src/algorithms/paths.jl index 870dce1..2cc36df 100644 --- a/src/algorithms/paths.jl +++ b/src/algorithms/paths.jl @@ -105,28 +105,28 @@ end hg::H, source::Int, target::Int, - hyperedge_weights::Vector{T} + 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::Vector{T} + 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::Vector{T} + 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::Vector{T} + hyperedge_weights::AbstractVector{T} ) where {T <: Real, V, E, D <: AbstractDict{Int,T}} Implements the heuristic directed hypergraph pathfinding algorithm of Krieger & Kececioglu (2022), @@ -144,7 +144,7 @@ function shortest_hyperpath_kk_heuristic( hg::H, source::Int, target::Int, - hyperedge_weights::Vector{T} + hyperedge_weights::AbstractVector{T} ) where {H<:AbstractDirectedHypergraph,T<:Real} reached_vs = BitVector(falses(nhv(hg))) @@ -261,7 +261,7 @@ function shortest_hyperpath_kk_heuristic( hg::DirectedHypergraph{T,V,E,D}, source::Int, targets::Set{Int}, - hyperedge_weights::Vector{S} + hyperedge_weights::AbstractVector{S} ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} hg_copy = deepcopy(hg) @@ -289,7 +289,7 @@ function shortest_hyperpath_kk_heuristic( hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, target::Int, - hyperedge_weights::Vector{S} + hyperedge_weights::AbstractVector{S} ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} hg_copy = deepcopy(hg) @@ -317,7 +317,7 @@ function shortest_hyperpath_kk_heuristic( hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, targets::Set{Int}, - hyperedge_weights::Vector{S} + hyperedge_weights::AbstractVector{S} ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} hg_copy = deepcopy(hg) @@ -366,7 +366,7 @@ function short_hyperpath_vhe( v::Int, he::Int, he_inedges::Vector{Set{Int}}, - he_costs::Vector{T} + he_costs::AbstractVector{T} ) where {H<:AbstractDirectedHypergraph, T<:Real} marked_hes = BitVector(falses(nhe(hg))) @@ -494,11 +494,23 @@ end """ all_hyperpaths(hg::H, source::Int, target::Int) where {H <: AbstractDirectedHypergraph} - all_hyperpaths(hg::H, source::Int, targets::Set{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::H, sources::Set{Int}, target::Int) where {H <: AbstractDirectedHypergraph} + 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::H, sources::Set{Int}, targets::Set{Int}) where {H <: AbstractDirectedHypergraph} + 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. @@ -626,11 +638,23 @@ function all_hyperpaths( 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::Vector{T} + hyperedge_weights::AbstractVector{T} ) where {H<:AbstractDirectedHypergraph, T<:Real} # First, verify that the problem is well-posed @@ -721,28 +745,28 @@ end hg::H, source::Int, target::Int, - hyperedge_weights::Vector{T} + 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::Vector{S} + 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::Vector{S} + 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::Vector{S} + 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), @@ -759,7 +783,7 @@ function shortest_hyperpath_kk_ilp( hg::H, source::Int, target::Int, - hyperedge_weights::Vector{T} + hyperedge_weights::AbstractVector{T} ) where {H<:AbstractDirectedHypergraph,T<:Real} # TODO: do I need to carry `x` over like this? Not sure about variable scope @@ -794,7 +818,7 @@ function shortest_hyperpath_kk_ilp( hg::DirectedHypergraph{T,V,E,D}, source::Int, targets::Set{Int}, - hyperedge_weights::Vector{S} + hyperedge_weights::AbstractVector{S} ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} hg_copy = deepcopy(hg) @@ -822,7 +846,7 @@ function shortest_hyperpath_kk_ilp( hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, target::Int, - hyperedge_weights::Vector{S} + hyperedge_weights::AbstractVector{S} ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} hg_copy = deepcopy(hg) @@ -850,7 +874,7 @@ function shortest_hyperpath_kk_ilp( hg::DirectedHypergraph{T,V,E,D}, sources::Set{Int}, targets::Set{Int}, - hyperedge_weights::Vector{S} + hyperedge_weights::AbstractVector{S} ) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} hg_copy = deepcopy(hg) diff --git a/test/runtests.jl b/test/runtests.jl index 00bc4b7..2a99847 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -578,13 +578,13 @@ end; @test length(all_hyperpaths(dh3, Set{Int}([2,6]), Set{Int}([8,9]))) == 2 # Test exact (integer linear programming) shortest-path algorithm - model = initialize_ilp_model(dh3, 1, 9, w3) + 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 initialize_ilp_model(dh3, 9, 1, w3) + @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]) From af2b9abef62dc8c96c871ca181153eb0704340ae Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 11:00:19 +0000 Subject: [PATCH 27/30] Docs happy now, please? --- docs/make.jl | 1 + docs/src/reference.md | 42 +++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 21 deletions(-) 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 9541bc2..ee729c7 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -125,33 +125,33 @@ _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} -forward_reachable(hg::H, source::Int) where {H<:AbstractDirectedHypergraph} -backward_traceable(hg::H, target::Int) where {H<:AbstractDirectedHypergraph} -is_reachable(hg::H, source::Int, target::Int, target_type::Symbol) 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(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}} +all_hyperpaths(::H, ::Int, ::Int) where {H <: AbstractDirectedHypergraph} +all_hyperpaths(::DirectedHypergraph{T,V,E,D}, ::Int, ::Set{Int}) where {T<:Real,V,E,D<:AbstractDict{Int,T}} +all_hyperpaths(::DirectedHypergraph{T,V,E,D}, ::Set{Int}, ::Int) where {T<:Real,V,E,D<:AbstractDict{Int,T}} +all_hyperpaths(::DirectedHypergraph{T,V,E,D}, ::Set{Int}, ::Set{Int}) where {T<:Real,V,E,D<:AbstractDict{Int,T}} -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}} +shortest_hyperpath_kk_heuristic(::H, ::Int, ::Int, ::AbstractVector{T}) where {H <: AbstractDirectedHypergraph, T <: Real} +shortest_hyperpath_kk_heuristic(::DirectedHypergraph{T, V, E, D}, ::Int, ::Set{Int}, hyperedge_weights::AbstractVector{T}) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +shortest_hyperpath_kk_heuristic(::DirectedHypergraph{T, V, E, D}, ::Set{Int}, ::Int, ::AbstractVector{T}) where {T <: Real, V, E, D <: AbstractDict{Int,T}} +shortest_hyperpath_kk_heuristic(::DirectedHypergraph{T, V, E, D}, ::Set{Int}, ::Set{Int}, ::AbstractVector{T}) where {T <: Real, V, E, D <: AbstractDict{Int,T}} -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}} +shortest_hyperpath_kk_ilp(::H, ::Int, ::Int, ::AbstractVector{T}) where {H<:AbstractDirectedHypergraph, T<:Real} +shortest_hyperpath_kk_ilp(::DirectedHypergraph{T,V,E,D}, ::Int, ::Set{Int}, ::AbstractVector{S}) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} +shortest_hyperpath_kk_ilp(::DirectedHypergraph{T,V,E,D}, ::Set{Int}, ::Int, ::AbstractVector{S}) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} +shortest_hyperpath_kk_ilp(::DirectedHypergraph{T,V,E,D}, ::Set{Int}, ::Set{Int}, ::AbstractVector{S}) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} -SnodeDistanceKKHeuristic <: AbstractDistance -SnodeDistanceKKILP <: AbstractDistance +SnodeDistanceKKHeuristic +SnodeDistanceKKILP -SimpleHypergraphs.distance(hg::H, distance_method::SnodeDistanceKKHeuristic, hyperedge_weights::AbstractVector{T}) where {H<:AbstractDirectedHypergraph,T<:Real} -distance(hg::H, distance_method::SnodeDistanceKKILP, hyperedge_weights::AbstractVector{T}) where {H <: AbstractDirectedHypergraph} +SimpleHypergraphs.distance(::H, ::SnodeDistanceKKHeuristic, ::AbstractVector{T}) where {H<:AbstractDirectedHypergraph,T<:Real} +distance(::H, ::SnodeDistanceKKILP, ::AbstractVector{T}) where {H <: AbstractDirectedHypergraph} -Graphs.diameter(hg::H, _::SnodeDistanceKKHeuristic) where {H<:AbstractDirectedHypergraph} -Graphs.diameter(hg::H, _::SnodeDistanceKKILP) where {H<:AbstractDirectedHypergraph} +Graphs.diameter(::H, ::SnodeDistanceKKHeuristic) where {H<:AbstractDirectedHypergraph} +Graphs.diameter(::H, ::SnodeDistanceKKILP) where {H<:AbstractDirectedHypergraph} ``` From cc76c4823563a5e145eb69a3783e29b71c4fa6fe Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 11:08:42 +0000 Subject: [PATCH 28/30] So close! --- docs/src/reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/reference.md b/docs/src/reference.md index ee729c7..438739e 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -147,8 +147,8 @@ shortest_hyperpath_kk_ilp(::DirectedHypergraph{T,V,E,D}, ::Set{Int}, ::Set{Int}, SnodeDistanceKKHeuristic SnodeDistanceKKILP -SimpleHypergraphs.distance(::H, ::SnodeDistanceKKHeuristic, ::AbstractVector{T}) where {H<:AbstractDirectedHypergraph,T<:Real} -distance(::H, ::SnodeDistanceKKILP, ::AbstractVector{T}) where {H <: AbstractDirectedHypergraph} +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} From 77106f2ba7bd397a6da1e820ec25c8d6c2ceea38 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 11:23:32 +0000 Subject: [PATCH 29/30] Really stuck here --- docs/src/reference.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/reference.md b/docs/src/reference.md index 438739e..18b4735 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -121,9 +121,8 @@ 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} From b95d00c195a993e4ded865a5b9f13b0bb81c93b0 Mon Sep 17 00:00:00 2001 From: "Evan Walter Clark Spotte-Smith, PhD" Date: Thu, 27 Nov 2025 11:30:41 +0000 Subject: [PATCH 30/30] ............not holding my breath --- docs/Project.toml | 5 ++++- docs/src/reference.md | 19 +++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) 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/src/reference.md b/docs/src/reference.md index 18b4735..0f7073d 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -128,20 +128,11 @@ 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(::H, ::Int, ::Int) where {H <: AbstractDirectedHypergraph} -all_hyperpaths(::DirectedHypergraph{T,V,E,D}, ::Int, ::Set{Int}) where {T<:Real,V,E,D<:AbstractDict{Int,T}} -all_hyperpaths(::DirectedHypergraph{T,V,E,D}, ::Set{Int}, ::Int) where {T<:Real,V,E,D<:AbstractDict{Int,T}} -all_hyperpaths(::DirectedHypergraph{T,V,E,D}, ::Set{Int}, ::Set{Int}) where {T<:Real,V,E,D<:AbstractDict{Int,T}} - -shortest_hyperpath_kk_heuristic(::H, ::Int, ::Int, ::AbstractVector{T}) where {H <: AbstractDirectedHypergraph, T <: Real} -shortest_hyperpath_kk_heuristic(::DirectedHypergraph{T, V, E, D}, ::Int, ::Set{Int}, hyperedge_weights::AbstractVector{T}) where {T <: Real, V, E, D <: AbstractDict{Int,T}} -shortest_hyperpath_kk_heuristic(::DirectedHypergraph{T, V, E, D}, ::Set{Int}, ::Int, ::AbstractVector{T}) where {T <: Real, V, E, D <: AbstractDict{Int,T}} -shortest_hyperpath_kk_heuristic(::DirectedHypergraph{T, V, E, D}, ::Set{Int}, ::Set{Int}, ::AbstractVector{T}) where {T <: Real, V, E, D <: AbstractDict{Int,T}} - -shortest_hyperpath_kk_ilp(::H, ::Int, ::Int, ::AbstractVector{T}) where {H<:AbstractDirectedHypergraph, T<:Real} -shortest_hyperpath_kk_ilp(::DirectedHypergraph{T,V,E,D}, ::Int, ::Set{Int}, ::AbstractVector{S}) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} -shortest_hyperpath_kk_ilp(::DirectedHypergraph{T,V,E,D}, ::Set{Int}, ::Int, ::AbstractVector{S}) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} -shortest_hyperpath_kk_ilp(::DirectedHypergraph{T,V,E,D}, ::Set{Int}, ::Set{Int}, ::AbstractVector{S}) where {S<:Real,T<:Real,V,E,D<:AbstractDict{Int,T}} +all_hyperpaths + +shortest_hyperpath_kk_heuristic + +shortest_hyperpath_kk_ilp SnodeDistanceKKHeuristic SnodeDistanceKKILP