diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7276ffe..834c4be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,14 +12,17 @@ jobs: fail-fast: false matrix: version: + - '1.6' + - '1.7' - '1.8' - - nightly + - '1.9' + - '1' os: - ubuntu-latest arch: - x64 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} @@ -45,7 +48,7 @@ jobs: name: Documentation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: version: '1' diff --git a/Project.toml b/Project.toml index d12131c..db146b5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,14 +1,16 @@ name = "SMDGraphs" uuid = "b792745b-7241-45be-ba96-70eb67e8468f" -authors = ["Andrea Pasquale and Michele Ceresoli "] -version = "0.1.2" +authors = ["JSMD Team"] +version = "0.2.0" [deps] Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +JSMDInterfaces = "6b30ee2f-618e-4a15-bf4e-7df7b496e609" [compat] Graphs = "1" -julia = "1" +JSMDInterfaces = "1.5" +julia = "1.6" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/docs/src/mappedgraph.md b/docs/src/mappedgraph.md index e966000..267e06f 100644 --- a/docs/src/mappedgraph.md +++ b/docs/src/mappedgraph.md @@ -5,6 +5,7 @@ CurrentModule = SMDGraphs DocTestSetup = quote using SMDGraphs + import JSMDInterfaces.Graph: AbstractJSMDGraphNode end ``` @@ -19,10 +20,12 @@ items in the nodes through this ID. ## Usage Suppose that you want to create a graph to connect items that store planetary bodies properties. First, we will define our custom node type, which must be -a sub-type of [`SMDGraphs.AbstractGraphNode`](@ref): +a sub-type of `AbstractJSMDGraphNode`: ```julia -struct SpaceBody{T} <: SMDGraphs.AbstractGraphNode +import JSMDInterfaces.Graph: AbstractJSMDGraphNode + +struct SpaceBody{T} <: AbstractGraphNode radius::T density::T id::Int @@ -87,7 +90,7 @@ Connections between the items in the graph are easily added as follows: SMDGraphs.add_edge!(graph, 10, 399) SMDGraphs.add_edge!(graph, 399, 301) ``` -By providing an additional integer input to [`SMDGraphs.add_edge!`](@ref), a weight factor +By providing an additional integer input to `add_edge!`, a weight factor can be associated to the edge. The default weight is null. Finally, the path between two nodes is retrived as: @@ -97,4 +100,4 @@ julia> print(path) [2, 1, 3] ``` -Note that [`SMDGraphs.get_path`](@ref) returns an integer vector of internal IDs instead of the user-defined ones. This enables a faster retrieval of the nodes via [`SMDGraphs.get_mappednode`](@ref), allowing to skip the dictionary lookup for the ID mapping of each node in the path. \ No newline at end of file +Note that `get_path` returns an integer vector of internal IDs instead of the user-defined ones. This enables a faster retrieval of the nodes via [`SMDGraphs.get_mappednode`](@ref), allowing to skip the dictionary lookup for the ID mapping of each node in the path. \ No newline at end of file diff --git a/src/SMDGraphs.jl b/src/SMDGraphs.jl index cc4e038..52b4ccd 100644 --- a/src/SMDGraphs.jl +++ b/src/SMDGraphs.jl @@ -1,17 +1,33 @@ module SMDGraphs +using JSMDInterfaces.Interface + +import JSMDInterfaces.Graph: + AbstractJSMDGraphNode, + AbstractJSMDGraph, + add_edge!, + add_vertex!, + edges, + edgetype, + get_path, + has_vertex, + has_edge, + has_path, + inneighbors, + is_directed, + ne, + nv, + outneighbors, + vertices + import Graphs: + AbstractGraph, + SimpleEdge, SimpleGraph, SimpleDiGraph, - add_edge!, - add_vertex!, - nv, dijkstra_shortest_paths, - enumerate_paths, - has_vertex, - has_path + enumerate_paths -include("abstract.jl") include("graphs/MappedGraphs.jl") end diff --git a/src/abstract.jl b/src/abstract.jl deleted file mode 100644 index 71446d2..0000000 --- a/src/abstract.jl +++ /dev/null @@ -1,20 +0,0 @@ -import Graphs: AbstractGraph - -""" -AbstractGraphNode - -Abstract type for all graph nodes types. -""" -abstract type AbstractGraphNode end - -""" - get_node_id(b::AbstractGraphNode) - -Get the mapped-id of an `AbstractGraphNode`. - -!!! warning - This method is abstract! A concrete implementation for each concrete node shall be defined. -""" -function get_node_id(b::T) where {T<:AbstractGraphNode} - throw(ErrorException("`get_node_id` shall be implemented for $T")) -end diff --git a/src/graphs/MappedGraphs.jl b/src/graphs/MappedGraphs.jl index 047f35e..8a99cdd 100644 --- a/src/graphs/MappedGraphs.jl +++ b/src/graphs/MappedGraphs.jl @@ -1,6 +1,17 @@ """ - MappedNodeGraph{N, G} + get_node_id(b::AbstractJSMDGraphNode) + +Get the mapped-id of an `AbstractJSMDGraphNode`. + +!!! warning + This method is abstract! A concrete implementation for each concrete node shall be defined. +""" +@interface function get_node_id(::AbstractJSMDGraphNode) end + + +""" + MappedNodeGraph{N, G} <: AbstractJSMDGraph{Int} Create a graph with mapped nodes. @@ -14,14 +25,14 @@ Create a graph with mapped nodes. ### Constructors - `MappedNodeGraph{N}(g::G) where {G <: AbstractGraph, N <: AbstractGraphNode}` """ -struct MappedNodeGraph{N,G} +struct MappedNodeGraph{N,G} <: AbstractJSMDGraph{Int} graph::G mid::Dict{Int,Int} # mapped ids nodes::Vector{N} paths::Dict{Int,Dict{Int,Vector{Int}}} edges::Dict{Int,Dict{Int,Int}} - function MappedNodeGraph{N}(g::G) where {G<:AbstractGraph,N<:AbstractGraphNode} + function MappedNodeGraph{N}(g::G) where {G<:AbstractGraph, N<:AbstractJSMDGraphNode} return new{N,G}( g, Dict{Int,Int}(), @@ -51,10 +62,19 @@ MappedDiGraph(::Type{N}) where {N} = MappedNodeGraph{N}(SimpleDiGraph{Int}()) """ get_mappedid(g::MappedNodeGraph, node::Int) -Get the mappedid associated with a node. +Get the mappedid associated with a node. The mappedid is the internal ID that is assigned +to the node within the graph. """ @inline get_mappedid(g::MappedNodeGraph, node::Int) = g.mid[node] +""" + get_outerid(g::MappedNodeGraph, id::Int) + +Return the id of the node associated to the mapped id `id`. The outer ID is the ID that is +assigned to the node by the user. +""" +@inline get_outerid(g::MappedNodeGraph, id::Int) = get_node_id(g.nodes[id]) + """ get_mappednode(g::MappedNodeGraph, mid::Int) @@ -71,28 +91,48 @@ Get the node associated with a node index. Base.isempty(g::MappedNodeGraph) = Base.isempty(g.nodes) -""" - has_vertex(g, node) -Return true if `node` is contained in the graph `g`. -""" -@inline has_vertex(g::MappedNodeGraph, node::Int) = haskey(g.mid, node) +# Graphs Interfaces +# ======================= -""" - has_path(g, from, to) +has_vertex(g::MappedNodeGraph, node::Int) = haskey(g.mid, node) -Return true if there is a path between `from` and `to` in the graph `g`. -""" -function has_path(g::MappedNodeGraph, from::Int, to::Int) - return has_path(g.graph, get_mappedid(g, from), get_mappedid(g, to)) +function has_edge(g::MappedNodeGraph, from::Int, to::Int) + # Check whether from and to are registered in the graph + (!haskey(g.mid, from) || !haskey(g.mid, to)) && return false + + fid = get_mappedid(g, from) + tid = get_mappedid(g, to) + + has_edge(g.graph, fid, tid) +end + +function edges(g::MappedNodeGraph) + map(e->SimpleEdge(get_outerid(g, e.src), get_outerid(g, e.dst)), edges(g.graph)) end -""" - add_vertex!(g, node) +edgetype(g::MappedNodeGraph) = edgetype(g.graph) -Add `node` to the graph `g`. -""" -function add_vertex!(g::MappedNodeGraph{T}, node::T) where {T<:AbstractGraphNode} +is_directed(g::MappedNodeGraph) = is_directed(g.graph) + +ne(g::MappedNodeGraph) = ne(g.graph) +nv(g::MappedNodeGraph) = nv(g.graph) + +function inneighbors(g::MappedNodeGraph, node::Int) + map(x->get_outerid(g, x), inneighbors(g.graph, get_mappedid(g, node))) +end + +function outneighbors(g::MappedNodeGraph, node::Int) + map(x->get_outerid(g, x), outneighbors(g.graph, get_mappedid(g, node))) +end + +vertices(g::MappedNodeGraph) = map(get_node_id, g.nodes) + + +# JSMD Interfaces +# ======================= + +function add_vertex!(g::MappedNodeGraph{T}, node::T) where {T<:AbstractJSMDGraphNode} nodeid = get_node_id(node) has_vertex(g, nodeid) && return nothing @@ -108,12 +148,6 @@ function add_vertex!(g::MappedNodeGraph{T}, node::T) where {T<:AbstractGraphNode return nothing end -""" - add_edge!(g::MappedNodeGraph, from::Int, to::Int, [cost]) - -Add an edge between `from` and `to` to `g`. -Optionally assign a `cost` to the edge. -""" function add_edge!(g::MappedNodeGraph{T}, from::Int, to::Int, cost::Int=0) where {T} # ensure the two vertexes already exist in the graph if !(has_vertex(g, from) && has_vertex(g, to)) @@ -131,6 +165,19 @@ function add_edge!(g::MappedNodeGraph{T}, from::Int, to::Int, cost::Int=0) where return nothing end +function has_path(g::MappedNodeGraph, from::Int, to::Int) + return has_path(g.graph, get_mappedid(g, from), get_mappedid(g, to)) +end + +function get_path(g::MappedNodeGraph{T}, from::Int, to::Int) where {T} + (has_vertex(g, from) && has_vertex(g, to)) || return Int[] + return g.paths[from][to] +end + + +# Internal routines +# ======================= + """ add_edge_cost!(g::MappedNodeGraph, fid::Int, tid::Int, cost::Int) @@ -183,17 +230,6 @@ function compute_paths!(g::MappedNodeGraph{T}) where {T} return nothing end -""" - get_path(g::MappedNodeGraph, from::Int, to::Int) - -Get the nodes on the path between and including `from` and `to`. Returns an empty array if -either `from` or `to` are not a part of `g` or if there is no path between them. -""" -function get_path(g::MappedNodeGraph{T}, from::Int, to::Int) where {T} - (has_vertex(g, from) && has_vertex(g, to)) || return Int[] - return g.paths[from][to] -end - """ get_edgecosts(g::MappedNodeGraph, from::Int, to::Int) diff --git a/test/runtests.jl b/test/runtests.jl index 31bc9c9..4e475d2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,15 +1,19 @@ using SMDGraphs -using Graphs using Test +using Graphs +using Graphs: SimpleEdge + +import JSMDInterfaces.Errors: NotImplementedError +import JSMDInterfaces.Graph as jGraph import SMDGraphs: MappedGraph, MappedDiGraph, MappedNodeGraph # Simple Node graph for testing purposes -struct IntNode <: SMDGraphs.AbstractGraphNode +struct IntNode <: jGraph.AbstractJSMDGraphNode id::Int end -struct FakeNode <: SMDGraphs.AbstractGraphNode +struct FakeNode <: jGraph.AbstractJSMDGraphNode id::Int end @@ -17,73 +21,130 @@ SMDGraphs.get_node_id(n::IntNode) = n.id @testset "SMDGraphs.jl" begin - # Check Error Enforcement - @test_throws ErrorException SMDGraphs.get_node_id(FakeNode(7)) + @testset "MappedGraphs" begin + + # Check Error Enforcement + @test_throws NotImplementedError SMDGraphs.get_node_id(FakeNode(7)) + + # Test MappedGraph Constructor + graph = MappedGraph(IntNode) + @test isa(graph, SMDGraphs.MappedNodeGraph{IntNode,SimpleGraph{Int64}}) + + graph2 = MappedNodeGraph{IntNode}() + @test isa(graph, SMDGraphs.MappedNodeGraph{IntNode,SimpleGraph{Int64}}) + + dgraph = MappedDiGraph(IntNode) + @test isa(dgraph, SMDGraphs.MappedNodeGraph{IntNode,SimpleDiGraph{Int64}}) + + @test isempty(graph) + @test isempty(dgraph) + + @test SMDGraphs.nv(graph) == 0 + @test SMDGraphs.ne(graph) == 0 + + @test SMDGraphs.vertices(graph) == [] + + node_a = IntNode(10) + node_b = IntNode(7) + node_c = IntNode(1) + + # Test vertex addition + SMDGraphs.add_vertex!(graph, node_a) + SMDGraphs.add_vertex!(graph, node_b) + + SMDGraphs.add_vertex!(dgraph, node_a) + SMDGraphs.add_vertex!(dgraph, node_b) + SMDGraphs.add_vertex!(dgraph, node_c) + + @test !isempty(graph) + + # Test JSMDInterfaces + # ========================= + + @test !SMDGraphs.is_directed(graph) + @test SMDGraphs.is_directed(dgraph) + + @test SMDGraphs.nv(graph) == 2 + @test SMDGraphs.nv(dgraph) == 3 + + @test SMDGraphs.ne(graph) == 0 + @test SMDGraphs.ne(dgraph) == 0 + + @test SMDGraphs.edgetype(graph) == SimpleEdge{Int64} + @test SMDGraphs.edgetype(dgraph) == SimpleEdge{Int64} - # Test MappedGraph Constructor - graph = MappedGraph(IntNode) - @test isa(graph, SMDGraphs.MappedNodeGraph{IntNode,SimpleGraph{Int64}}) + @test SMDGraphs.vertices(graph) == [10, 7] + @test SMDGraphs.vertices(dgraph) == [10, 7, 1] - graph2 = MappedNodeGraph{IntNode}() - @test isa(graph, SMDGraphs.MappedNodeGraph{IntNode,SimpleGraph{Int64}}) + @test SMDGraphs.has_vertex(graph, 10) + @test SMDGraphs.has_vertex(graph, 7) + @test !SMDGraphs.has_vertex(graph, 1) - dgraph = MappedDiGraph(IntNode) - @test isa(dgraph, SMDGraphs.MappedNodeGraph{IntNode,SimpleDiGraph{Int64}}) + @test isempty(SMDGraphs.inneighbors(graph, 10)) - @test isempty(graph) - @test isempty(dgraph) + SMDGraphs.add_vertex!(graph, node_c) + @test SMDGraphs.has_vertex(graph, 1) - node_a = IntNode(10) - node_b = IntNode(7) - node_c = IntNode(1) + # Test outer IDs + @test SMDGraphs.get_outerid(graph, 1) == 10 + @test SMDGraphs.get_outerid(dgraph, 3) == 1 - # Test vertex addition - SMDGraphs.add_vertex!(graph, node_a) - SMDGraphs.add_vertex!(graph, node_b) + # Test Mapped IDs + @test SMDGraphs.get_mappedid(graph, 10) == 1 + @test SMDGraphs.get_mappedid(graph, 7) == 2 - SMDGraphs.add_vertex!(dgraph, node_a) - SMDGraphs.add_vertex!(dgraph, node_b) - SMDGraphs.add_vertex!(dgraph, node_c) + # Test Mapped Nodes + @test SMDGraphs.get_node(graph, 1) == node_c - @test !isempty(graph) + # Add path + @test !SMDGraphs.has_path(graph, 7, 10) + SMDGraphs.add_edge!(graph, 7, 10) + @test SMDGraphs.has_path(graph, 7, 10) + @test SMDGraphs.has_edge(graph, 7, 10) - @test SMDGraphs.has_vertex(graph, 10) - @test SMDGraphs.has_vertex(graph, 7) - @test !SMDGraphs.has_vertex(graph, 1) + @test SMDGraphs.inneighbors(graph, 10) == [7] + @test SMDGraphs.outneighbors(graph, 10) == [7] + @test SMDGraphs.inneighbors(graph, 7) == [10] - SMDGraphs.add_vertex!(graph, node_c) - @test SMDGraphs.has_vertex(graph, 1) + @test !SMDGraphs.has_edge(graph, 10, 1) + SMDGraphs.add_edge!(graph, 10, 1, 7) + @test SMDGraphs.has_path(graph, 1, 7) + @test SMDGraphs.has_edge(graph, 10, 1) + @test !SMDGraphs.has_edge(graph, 7, 1) - # Test Mapped IDs - @test SMDGraphs.get_mappedid(graph, 10) == 1 - @test SMDGraphs.get_mappedid(graph, 7) == 2 + @test SMDGraphs.ne(graph) == 2 + @test SMDGraphs.edges(graph) == [ + SimpleEdge(10, 7), + SimpleEdge(10, 1) + ] - # Test Mapped Nodes - @test SMDGraphs.get_node(graph, 1) == node_c + SMDGraphs.add_edge!(dgraph, 10, 7, 6) + SMDGraphs.add_edge!(dgraph, 7, 1, 3) - # Add path - @test !SMDGraphs.has_path(graph, 7, 10) - SMDGraphs.add_edge!(graph, 7, 10) - @test SMDGraphs.has_path(graph, 7, 10) + @test SMDGraphs.edges(dgraph) == [ + SimpleEdge(10, 7), + SimpleEdge(7, 1) + ] - SMDGraphs.add_edge!(graph, 10, 1, 7) - @test SMDGraphs.has_path(graph, 1, 7) + @test isempty(SMDGraphs.inneighbors(dgraph, 10)) + @test SMDGraphs.inneighbors(dgraph, 7) == [10] + @test SMDGraphs.outneighbors(dgraph, 10) == [7] - SMDGraphs.add_edge!(dgraph, 10, 7, 6) - SMDGraphs.add_edge!(dgraph, 7, 1, 3) + @test SMDGraphs.ne(dgraph) == 2 - @test_throws ErrorException SMDGraphs.add_edge!(graph, 10, 8) + @test_throws ErrorException SMDGraphs.add_edge!(graph, 10, 8) - @test SMDGraphs.get_path(graph, 1, 7) == [3, 1, 2] - @test SMDGraphs.get_path(graph, 1, 10) == [3, 1] + @test SMDGraphs.get_path(graph, 1, 7) == [3, 1, 2] + @test SMDGraphs.get_path(graph, 1, 10) == [3, 1] - # Check edge cost - @test SMDGraphs.get_edgecosts(graph, 8, 2) == Int64[] - @test SMDGraphs.get_edgecosts(graph, 1, 10) == [7] - @test SMDGraphs.get_edgecosts(graph, 1, 7) == [7, 0] - @test SMDGraphs.get_edgecosts(graph, 10, 1) == [7] + # Check edge cost + @test SMDGraphs.get_edgecosts(graph, 8, 2) == Int64[] + @test SMDGraphs.get_edgecosts(graph, 1, 10) == [7] + @test SMDGraphs.get_edgecosts(graph, 1, 7) == [7, 0] + @test SMDGraphs.get_edgecosts(graph, 10, 1) == [7] - @test SMDGraphs.get_edgecosts(dgraph, 10, 1) == [6, 3] + @test SMDGraphs.get_edgecosts(dgraph, 10, 1) == [6, 3] + end end;