Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0bfd477
Work towards new random models; some small bugfixes
espottesmith Sep 2, 2025
33db091
Early days on some pathfinding algorithms
espottesmith Nov 5, 2025
7ff3772
More fleshed out methods for pathfinding
espottesmith Nov 6, 2025
f37fb1d
Almost finished w/ v1 (minus docstrings); finishing touches and tests
espottesmith Nov 6, 2025
bed512e
Docstrings, mostly
espottesmith Nov 6, 2025
68509fa
Docstrings done; testing time, baby (or I implement the exact algorithm
espottesmith Nov 7, 2025
38b0956
Nearly a full implementation of the ILP-based exact minimum hyperpath
espottesmith Nov 9, 2025
7933a11
Paths DONE; now ready for testing
espottesmith Nov 17, 2025
28570a1
On second thought, cost function doesn't seem necessary or helpful
espottesmith Nov 17, 2025
f54e782
Notions of distance and diameter
espottesmith Nov 17, 2025
a1cae64
Exports
espottesmith Nov 17, 2025
db18871
Whole heap of changes to base dihypergraph.jl file; some initial tests
espottesmith Nov 18, 2025
a75d8eb
Forward traversal and back-tracking are tested and work
espottesmith Nov 18, 2025
b9d615a
Some progress on short_hyperpath_vhe, though it's still not working
espottesmith Nov 18, 2025
401c7fb
Very frustrating bug finally fixed; other testing still in progress
espottesmith Nov 19, 2025
279233d
Still broken
espottesmith Nov 19, 2025
ece25d7
Get ready to rumble
espottesmith Nov 25, 2025
927dade
Tests pass for heuristic shortest-path algorithm. Hell yeah
espottesmith Nov 26, 2025
4034dbd
Pass for Blau example, too
espottesmith Nov 26, 2025
ab431d6
all_hyperpaths seems to work (haven't tested exhaustively)
espottesmith Nov 26, 2025
114af3a
ILP tests pass; now just need to do distance and diameter
espottesmith Nov 27, 2025
efba184
Test pass, baby! Let's go.
espottesmith Nov 27, 2025
70461fe
CI issue - OpenSSLsucks
espottesmith Nov 27, 2025
8d37da8
Trying to appease the doctest gods
espottesmith Nov 27, 2025
7475d7b
Remove ambiguity, I hope
espottesmith Nov 27, 2025
e03efe4
Docs
espottesmith Nov 27, 2025
af2b9ab
Docs happy now, please?
espottesmith Nov 27, 2025
cc76c48
So close!
espottesmith Nov 27, 2025
77106f2
Really stuck here
espottesmith Nov 27, 2025
b95d00c
............not holding my breath
espottesmith Nov 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 10 additions & 4 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -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"]
test = ["Test"]
5 changes: 4 additions & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
@@ -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 = ".."}
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ makedocs(;
pages=[
"Home" => "index.md",
],
checkdocs=:exports
)

deploydocs(;
Expand Down
23 changes: 20 additions & 3 deletions docs/src/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,27 @@ Pathfinding
-----------
```@docs

_visit(h::H, v::Int) where {H <: AbstractDirectedHypergraph}
SimpleHypergraphs.shortest_path(b::BipartiteView{H}, source::Int, target::Int) where {H<:AbstractDirectedHypergraph}
SimpleHypergraphs.shortest_path(t::TwoSectionView{H}, source::Int, target::Int) where {H<:AbstractDirectedHypergraph}
SimpleHypergraphs.shortest_path(::BipartiteView{H}, ::Int, ::Int) where {H<:AbstractDirectedHypergraph}
SimpleHypergraphs.shortest_path(::TwoSectionView{H}, ::Int, ::Int) where {H<:AbstractDirectedHypergraph}

forward_reachable(::H, ::Int) where {H<:AbstractDirectedHypergraph}
backward_traceable(::H, ::Int) where {H<:AbstractDirectedHypergraph}
is_reachable(::H, ::Int, ::Int, ::Symbol) where {H<:AbstractDirectedHypergraph}

all_hyperpaths

shortest_hyperpath_kk_heuristic

shortest_hyperpath_kk_ilp

SnodeDistanceKKHeuristic
SnodeDistanceKKILP

SimpleHypergraphs.distance(::H, ::SnodeDistanceKKHeuristic, ::AbstractVector{T}) where {H<:AbstractDirectedHypergraph, T<:Real}
SimpleHypergraphs.distance(::H, ::SnodeDistanceKKILP, ::AbstractVector{T}) where {H<:AbstractDirectedHypergraph, T<:Real}

Graphs.diameter(::H, ::SnodeDistanceKKHeuristic) where {H<:AbstractDirectedHypergraph}
Graphs.diameter(::H, ::SnodeDistanceKKILP) where {H<:AbstractDirectedHypergraph}
```


Expand Down
10 changes: 10 additions & 0 deletions src/SimpleDirectedHypergraphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ using JSON3
using Random
using LinearAlgebra
using SimpleTraits
using InvertedIndices
import JuMP
import GLPK

export AbstractDirectedHypergraph
export DirectedHypergraph, HyperedgeDirection
Expand All @@ -20,6 +23,10 @@ export to_undirected

export get_weakly_connected_components, get_strongly_connected_components

export forward_reachable, backward_traceable, is_reachable
export all_hyperpaths, shortest_hyperpath_kk_heuristic, shortest_hyperpath_kk_ilp

export SnodeDistanceKKHeuristic, SnodeDistanceKKILP

include("abstracttypes.jl")
include("dihypergraph.jl")
Expand All @@ -30,4 +37,7 @@ include("models/twosection.jl")
include("models/random-models.jl")
include("models/dual.jl")

include("algorithms/paths.jl")
include("algorithms/distance.jl")

end # module
192 changes: 192 additions & 0 deletions src/algorithms/distance.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""
SnodeDistanceKKHeuristic <: AbstractDistance

Constructor: SnodeDistanceKKHeuristic(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance

Represent a distance between a set of *source* nodes (`sources`) and a set of *target* nodes (`targets`) in a directed
hypergraph, where a distance in this context is defined as the length of the shortest hyperpath that reaches all
`t ∈ targets`.

This approach uses the *heuristic* shortest hyperpath algorithm of Krieger & Kececioglu 2022
(DOI: 10.1186/s13015-022-00217-9). The Krieger & Kececioglu heuristic algorithm commonly (but not always) returns the
true optimal (shortest) hyperpath; this algorithm can thus be thought of as an upper bound of the true distance between
the sinks and the source. To calculate the exact (minimal) distance, instead use `SnodeDistanceKKExact`.
"""
struct SnodeDistanceKKHeuristic <: AbstractDistance
sources::Set{Int}
targets::Set{Int}
end

"""
SnodeDistanceKKILP <: AbstractDistance

Constructor: SnodeDistanceKKILP(sources::Set{Int}, targets::Set{Int}) <: AbstractDistance

Represent a distance between a set of *source* nodes (`sources`) and a set of *target* nodes (`targets`) in a directed
hypergraph, where a distance in this context is defined as the length of the shortest hyperpath that reaches all
`t ∈ targets`.

This approach uses the *exact* shortest hyperpath algorithm of Krieger & Kececioglu 2023 (DOI: 10.1089/cmb.2023.0242).
The Krieger & Kececioglu algorithm solves an integer linear programming formalism related to hypergraph cutting.

For a lower-cost heuristic algorithm, see `SnodeDistanceKKHeuristic`.
"""
struct SnodeDistanceKKILP <: AbstractDistance
sources::Set{Int}
targets::Set{Int}
end

"""
distance(
hg::H,
distance_method::SnodeDistanceKKHeuristic,
hyperedge_weights::AbstractVector{T};
) where {H <: AbstractDirectedHypergraph, T <: Real}

Return the shortest distance between the `distance_method.sources` and the `distance_method.targets`,
assuming tha the cost (i.e., distance) between two vertices is the sum of the (nonnegative) `hyperedge_weights`
of the hyperedges in the shortest path between the two sets of vertices. If there is no path between the `sources`
and `targets`, returns `typemax(T)`.

Here, the heuristic directed hypergraph pathfinding algorithm of Krieger & Kececioglu (2022),
DOI: 10.1186/s13015-022-00217-9 is used to find the shortest path. This algorithm is not guaranteed to find the
optimal pathway, but in practice, it produces the optimal pathway approximately 99% of the time.
"""
function SimpleHypergraphs.distance(
hg::H,
distance_method::SnodeDistanceKKHeuristic,
hyperedge_weights::AbstractVector{T}
) where {H<:AbstractDirectedHypergraph,T<:Real}

path = shortest_hyperpath_kk_heuristic(
hg,
distance_method.sources,
distance_method.targets,
hyperedge_weights
)

if length(path) == 0
return typemax(T)
end

return sum(
hyperedge_weights[e]
for e in path
)
end

"""
distance(
hg::H,
distance_method::SnodeDistanceKKILP,
hyperedge_weights::AbstractVector{T}
) where {H <: AbstractDirectedHypergraph}

Return the shortest distance between the `distance_method.sources` and the `distance_method.targets`,
assuming tha the cost (i.e., distance) between two vertices is the sum of the (nonnegative) `hyperedge_weights`
of the hyperedges in the shortest path between the two sets of vertices. If there is no path between the `sources`
and `targets`, returns `typemax(T)`.

Here, the *exact* shortest hyperpath algorithm of Krieger & Kececioglu 2023 (DOI: 10.1089/cmb.2023.0242) is used.
The Krieger & Kececioglu algorithm solves an integer linear programming formalism related to hypergraph cutting.

"""
function SimpleHypergraphs.distance(
hg::H,
distance_method::SnodeDistanceKKILP,
hyperedge_weights::AbstractVector{T}
) where {H<:AbstractDirectedHypergraph,T<:Real}

path = shortest_hyperpath_kk_ilp(
hg,
distance_method.sources,
distance_method.targets,
hyperedge_weights
)

if length(path) == 0
return typemax(T)
end

return sum(
hyperedge_weights[e]
for e in path
)
end

"""
diameter(
hg::H,
distance_method::SnodeDistanceKKHeuristic,
) where {H <: AbstractSimpleHypergraph, T <: Real}

Return the diameter of a hypergraph `hg` (maximum number of hyperedges required to go between any two vertices)
based on Krieger & Kececioglu's heuristic algorithm (DOI: 10.1186/s13015-022-00217-9). If there exist some vertices
not reachable from other vertices (i.e., if `hg` is not strongly connected), then this will return `Inf`.
"""
function Graphs.diameter(
hg::H,
_::SnodeDistanceKKHeuristic,
) where {H<:AbstractDirectedHypergraph}
if length(get_strongly_connected_components(hg)) != 1
return Inf64
end

# Since we're interested in the number of steps, assign each hyperedge a uniform weight of `1`
edge_weights = ones(nhe(hg))

max_dist = 0.0
for i in 1:nhv(hg)
for j in i+1:nhv(hg)
dist = SimpleHypergraphs.distance(
hg,
SnodeDistanceKKHeuristic(Set{Int}(i), Set{Int}(j)),
edge_weights
)
if dist > max_dist
max_dist = dist
end
end
end

return max_dist
end

"""
diameter(
hg::H,
distance_method::f,
) where {H <: AbstractSimpleHypergraph, T <: Real}

Return the diameter of a hypergraph `hg` (maximum number of hyperedges required to go between any two vertices)
based on Krieger & Kececioglu's exact integer linear programming algorithm (DOI: 10.1089/cmb.2023.0242). If there
exist some vertices not reachable from other vertices (i.e., if `hg` is not strongly connected), then this will
return `Inf64`.
"""
function Graphs.diameter(
hg::H,
_::SnodeDistanceKKILP,
) where {H<:AbstractDirectedHypergraph}
if length(get_strongly_connected_components(hg)) != 1
return Inf64
end

# Since we're interested in the number of steps, assign each hyperedge a uniform weight of `1`
edge_weights = ones(nhe(hg))

max_dist = 0
for i in 1:nhv(hg)
for j in i+1:nhv(hg)
dist = SimpleHypergraphs.distance(
hg,
SnodeDistanceKKILP(Set{Int}(i), Set{Int}(j)),
edge_weights
)
if dist > max_dist
max_dist = dist
end
end
end

return max_dist
end
Loading
Loading