diff --git a/docs/Manifest.toml b/docs/Manifest.toml index 028e4da..dfeb2ba 100644 --- a/docs/Manifest.toml +++ b/docs/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.8.5" manifest_format = "2.0" -project_hash = "bdbda428ff9bda3e6edacec7c2836953b8b12fea" +project_hash = "09b59749909e1f918f05aece3dff03fef67a9e88" [[deps.ANSIColoredPrinters]] git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" diff --git a/docs/Project.toml b/docs/Project.toml index f01fb7a..5d3957f 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,4 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622" diff --git a/docs/make.jl b/docs/make.jl index 53e00a5..4628821 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,7 +1,10 @@ -using SimpleWeightedGraphs +using Graphs using Documenter +using SimpleWeightedGraphs -DocMeta.setdocmeta!(SimpleWeightedGraphs, :DocTestSetup, :(using SimpleWeightedGraphs); recursive=true) +DocMeta.setdocmeta!( + SimpleWeightedGraphs, :DocTestSetup, :(using SimpleWeightedGraphs); recursive=true +) makedocs(; modules=[SimpleWeightedGraphs], @@ -14,15 +17,9 @@ makedocs(; edit_link="master", assets=String[], ), - pages=[ - "Home" => "index.md", - "API reference" => "api.md", - ], + pages=["Home" => "index.md", "Tutorial" => "tutorial.md", "API reference" => "api.md"], linkcheck=true, strict=true, ) -deploydocs(; - repo="github.com/JuliaGraphs/SimpleWeightedGraphs.jl", - devbranch="master", -) +deploydocs(; repo="github.com/JuliaGraphs/SimpleWeightedGraphs.jl", devbranch="master") diff --git a/docs/src/index.md b/docs/src/index.md index 9c53d03..0712df6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -6,58 +6,20 @@ CurrentModule = SimpleWeightedGraphs Documentation for [SimpleWeightedGraphs](https://github.com/JuliaGraphs/SimpleWeightedGraphs.jl). - ## Quick start -```julia -using Graphs, SimpleWeightedGraphs - -g = SimpleWeightedGraph(3) # or use `SimpleWeightedDiGraph` for directed graphs -add_edge!(g, 1, 2, 0.5) -add_edge!(g, 2, 3, 0.8) -add_edge!(g, 1, 3, 2.0) - -# get weight of edge from vertex 1 to vertex 2 -get_weight(g, 1, 2) - -# find the shortest path from vertex 1 to vertex 3 taking weights into account. -enumerate_paths(dijkstra_shortest_paths(g, 1), 3) -3-element Array{Int64,1}: - 1 - 2 - 3 - -# reweight the edge from 1 to 2 -add_edge!(g, 1, 2, 1.6) - -# rerun the shortest path calculation from 1 to 3 -enumerate_paths(dijkstra_shortest_paths(g, 1), 3) -2-element Array{Int64,1}: - 1 - 3 - -# it's possible to build the graph from arrays of sources, destinations and weights -sources = [1,2,1] -destinations = [2,3,3] -weights = [0.5, 0.8, 2.0] -g = SimpleWeightedGraph(sources, destinations, weights) - -# the combine keyword handles repeated pairs (sum by default) -g = SimpleWeightedGraph([1,2,1], [2,1,2], [1,1,1]; combine = +) -g.weights[2,1] == g.weights[1,2] == 3 # true - -# WARNING: unexpected results might occur with non-associative combine functions - -# notice that weights are indexed by [destination, source] -s = SimpleWeightedDiGraph([1,2,1], [2,1,2], [1,1,1]; combine = +) -s.weights[1,2] == 1 # true -s.weights[2,1] == 2 # true -``` +This package defines two new graph types: [`SimpleWeightedGraph`](@ref) and [`SimpleWeightedDiGraph`](@ref). +See the tutorial to discover what you can do with them. +Also refer to the [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl) package for more complex algorithms. ## Caveats -Please pay attention to the fact that zero-weight edges are discarded by `add_edge!`. -This is due to the way the graph is stored (a sparse matrix). A possible workaround -is to [set a very small weight instead](https://stackoverflow.com/questions/48977068/how-to-add-free-edge-to-graph-in-lightgraphs-julia/48994712#48994712). +Because `SimpleWeighted(Di)Graph`s are stored in sparse matrices, they have two major flaws: + +- Iteratively adding or removing vertices or edges is not very efficient. Building the graph in one go from a list of edge sources, destinations and weights is much faster. + +- Zero-weight edges are discarded by `add_edge!`. A possible workaround is to [set a very small weight instead](https://stackoverflow.com/questions/48977068/how-to-add-free-edge-to-graph-in-lightgraphs-julia/48994712#48994712). + +## Alternatives -Note that adding or removing vertices or edges from these graph types is not particularly performant. See [MetaGraphsNext.jl](https://github.com/JuliaGraphs/MetaGraphsNext.jl) or [MetaGraphs.jl](https://github.com/JuliaGraphs/MetaGraphs.jl) for possible alternatives. \ No newline at end of file +If your graphs have more than just edge weights to store, take a look at [MetaGraphsNext.jl](https://github.com/JuliaGraphs/MetaGraphsNext.jl) or [MetaGraphs.jl](https://github.com/JuliaGraphs/MetaGraphs.jl) for more complex formats. diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md new file mode 100644 index 0000000..a16d8c0 --- /dev/null +++ b/docs/src/tutorial.md @@ -0,0 +1,90 @@ +```@meta +CurrentModule = SimpleWeightedGraphs +``` + +# Tutorial + +```jldoctest tuto +julia> using Graphs, SimpleWeightedGraphs +``` + +Here's how to construct an undirected graph (use `SimpleWeightedDiGraph` for directed graphs): + +```jldoctest tuto +julia> g = SimpleWeightedGraph(3) +{3, 0} undirected simple Int64 graph with Float64 weights + +julia> add_edge!(g, 1, 2, 0.5); + +julia> add_edge!(g, 2, 3, 0.8); + +julia> add_edge!(g, 1, 3, 2.0); +``` + +Get the weight of edge from vertex 1 to vertex 2: + +```jldoctest tuto +julia> get_weight(g, 1, 2) +0.5 +``` + +Find the shortest path from vertex 1 to vertex 3, taking weights into account: + +```jldoctest tuto +julia> enumerate_paths(dijkstra_shortest_paths(g, 1), 3) +3-element Vector{Int64}: + 1 + 2 + 3 +``` + +Reweight the edge from 1 to 2: + +```jldoctest tuto +julia> add_edge!(g, 1, 2, 1.6); +``` + +Rerun the shortest path calculation from 1 to 3: + +```jldoctest tuto +julia> enumerate_paths(dijkstra_shortest_paths(g, 1), 3) +2-element Vector{Int64}: + 1 + 3 +``` + +It's possible (and faster) to build the graph from arrays of sources, destinations and weights: + +```jldoctest tuto +julia> sources = [1, 2, 1]; + +julia> destinations = [2, 3, 3]; + +julia> weights = [0.5, 0.8, 2.0]; + +julia> g = SimpleWeightedGraph(sources, destinations, weights) +{3, 3} undirected simple Int64 graph with Float64 weights +``` + +The combine keyword handles repeated pairs (sum by default). +Unexpected results might occur with non-associative combine functions. + +```jldoctest tuto +julia> g = SimpleWeightedGraph([1,2,1], [2,1,2], [1,1,1]; combine = +) +{2, 1} undirected simple Int64 graph with Int64 weights + +julia> g.weights[2,1] == g.weights[1,2] == 3 +true +``` + +Notice that weights are indexed by `[destination, source]` internally: + +```jldoctest tuto +julia> s = SimpleWeightedDiGraph([1,2,1], [2,1,2], [1,1,1]; combine = +); + +julia> s.weights[1,2] == 1 +true + +julia> s.weights[2,1] == 2 +true +``` diff --git a/src/SimpleWeightedGraphs.jl b/src/SimpleWeightedGraphs.jl index ccf6393..3111485 100644 --- a/src/SimpleWeightedGraphs.jl +++ b/src/SimpleWeightedGraphs.jl @@ -1,134 +1,40 @@ +""" + SimpleWeightedGraphs + +A package for graphs with edge weights, stored as sparse adjacency matrices. +""" module SimpleWeightedGraphs -using Graphs using LinearAlgebra using Markdown -using SparseArrays - -import Base: - convert, eltype, show, ==, Pair, Tuple, copy, length, issubset, zero - -import Graphs: - _NI, AbstractGraph, AbstractEdge, AbstractEdgeIter, - src, dst, edgetype, nv, ne, vertices, edges, is_directed, - add_vertex!, add_edge!, rem_vertex!, rem_edge!, - has_vertex, has_edge, inneighbors, outneighbors, - indegree, outdegree, degree, has_self_loops, num_self_loops, - - add_vertices!, adjacency_matrix, laplacian_matrix, weights, - connected_components, cartesian_product, - - AbstractGraphFormat, loadgraph, loadgraphs, savegraph, - pagerank, induced_subgraph - -export - AbstractSimpleWeightedGraph, - AbstractSimpleWeightedEdge, - SimpleWeightedEdge, - SimpleWeightedGraph, - SimpleWeightedGraphEdge, - SimpleWeightedDiGraph, - SimpleWeightedDiGraphEdge, - weight, - weighttype, - get_weight, - WGraph, - WDiGraph, - SWGFormat, - degree_matrix - +using SparseArrays: SparseMatrixCSC, sparse, spzeros, nnz, findnz, spdiagm, nzrange + +using Graphs: Graphs +using Graphs: AbstractGraph, AbstractEdge, AbstractEdgeIter, AbstractGraphFormat +using Graphs: SimpleGraph, SimpleDiGraph +using Graphs: src, dst +using Graphs: edgetype, is_directed, nv, ne, vertices, edges +using Graphs: add_vertex!, add_vertices!, add_edge!, rem_vertex!, rem_edge! +using Graphs: has_vertex, has_edge, inneighbors, outneighbors +using Graphs: indegree, outdegree, degree, has_self_loops, num_self_loops +using Graphs: adjacency_matrix, laplacian_matrix, weights +using Graphs: connected_components, cartesian_product, induced_subgraph, pagerank +using Graphs: loadgraph, loadgraphs, savegraph +using Graphs: _NI + +export AbstractSimpleWeightedGraph, AbstractSimpleWeightedEdge +export SimpleWeightedGraph, SimpleWeightedDiGraph +export SimpleWeightedEdge, SimpleWeightedGraphEdge, SimpleWeightedDiGraphEdge +export WGraph, WDiGraph, SWGFormat +export weight, weighttype, get_weight, degree_matrix + +include("utils.jl") include("simpleweightededge.jl") - -""" - AbstractSimpleWeightedGraph - -An abstract type representing a simple graph structure. -AbstractSimpleWeightedGraphs must have the following elements: -- weightmx::AbstractSparseMatrix{Real} -""" -abstract type AbstractSimpleWeightedGraph{T<:Integer,U<:Real} <: AbstractGraph{T} end - -function show(io::IO, g::AbstractSimpleWeightedGraph{T, U}) where T where U - dir = is_directed(g) ? "directed" : "undirected" - print(io, "{$(nv(g)), $(ne(g))} $dir simple $T graph with $U weights") -end - -# conversion to SparseMatrixCSC -convert(::Type{SparseMatrixCSC{T, U}}, g::AbstractSimpleWeightedGraph) where T<:Real where U<:Integer = SparseMatrixCSC{T, U}(g.weights) - - -### INTERFACE - -nv(g::AbstractSimpleWeightedGraph{T, U}) where T where U = T(size(weights(g), 1)) -vertices(g::AbstractSimpleWeightedGraph{T, U}) where T where U = one(T):nv(g) -eltype(x::AbstractSimpleWeightedGraph{T, U}) where T where U = T -weighttype(x::AbstractSimpleWeightedGraph{T, U}) where T where U = U - -# handles single-argument edge constructors such as pairs and tuples -has_edge(g::AbstractSimpleWeightedGraph{T, U}, x) where T where U = has_edge(g, edgetype(g)(x)) -add_edge!(g::AbstractSimpleWeightedGraph{T, U}, x) where T where U = add_edge!(g, edgetype(g)(x)) - -# handles two-argument edge constructors like src,dst -has_edge(g::AbstractSimpleWeightedGraph, x, y) = has_edge(g, edgetype(g)(x, y, 0)) -add_edge!(g::AbstractSimpleWeightedGraph, x, y) = add_edge!(g, edgetype(g)(x, y, 1)) -add_edge!(g::AbstractSimpleWeightedGraph, x, y, z) = add_edge!(g, edgetype(g)(x, y, z)) - -function issubset(g::T, h::T) where T<:AbstractSimpleWeightedGraph - (gmin, gmax) = extrema(vertices(g)) - (hmin, hmax) = extrema(vertices(h)) - return (hmin <= gmin <= gmax <= hmax) && issubset(edges(g), edges(h)) -end - -has_vertex(g::AbstractSimpleWeightedGraph, v::Integer) = v in vertices(g) - -function rem_edge!(g::AbstractSimpleWeightedGraph{T, U}, u::Integer, v::Integer) where {T, U} - rem_edge!(g, edgetype(g)(T(u), T(v), one(U))) -end - -get_weight(g::AbstractSimpleWeightedGraph, u::Integer, v::Integer) = weights(g)[v, u] - -zero(g::T) where T<:AbstractSimpleWeightedGraph = T() - -# TODO: manipulte SparseMatrixCSC directly -add_vertex!(g::AbstractSimpleWeightedGraph) = add_vertices!(g, 1) - -copy(g::T) where T <: AbstractSimpleWeightedGraph = T(copy(weights(g))) - - -const SimpleWeightedGraphEdge = SimpleWeightedEdge -const SimpleWeightedDiGraphEdge = SimpleWeightedEdge +include("abstractsimpleweightedgraph.jl") include("simpleweighteddigraph.jl") include("simpleweightedgraph.jl") +include("conversion.jl") include("overrides.jl") include("persistence.jl") -const WGraph = SimpleWeightedGraph -const WDiGraph = SimpleWeightedDiGraph - - -# return the index in nzval of mat[i, j] -# we assume bounds are already checked -# see https://github.com/JuliaSparse/SparseArrays.jl/blob/fa547689947fadd6c2f3d09ddfcb5f26536f18c8/src/sparsematrix.jl#L2492 for implementation -@inbounds function _get_nz_index!(mat::SparseMatrixCSC, i::Integer, j::Integer) - # r1 and r2 are start and end of the column - r1 = Int(mat.colptr[j]) - r2 = Int(mat.colptr[j+1]-1) - (r1 > r2) && return 0 # column is empty so we have a non structural zero - # search if i correspond to a stored value - indx = searchsortedfirst(mat.rowval, i, r1, r2, Base.Forward) - ((indx > r2) || (mat.rowval[indx] != i)) && return 0 - return indx -end - -SimpleWeightedDiGraph(g::SimpleWeightedGraph) = SimpleWeightedDiGraph(copy(g.weights)) -function SimpleWeightedDiGraph{T, U}(g::SimpleWeightedGraph) where {T<:Integer, U<:Real} - return SimpleWeightedDiGraph(SparseMatrixCSC{U, T}(copy(g.weights))) end - -SimpleWeightedGraph(g::SimpleWeightedDiGraph) = SimpleWeightedGraph(g.weights .+ g.weights') - -function SimpleWeightedGraph{T, U}(g::SimpleWeightedDiGraph) where {T<:Integer, U<:Real} - return SimpleWeightedGraph(SparseMatrixCSC{U, T}(g.weights .+ g.weights')) -end - -end # module diff --git a/src/abstractsimpleweightedgraph.jl b/src/abstractsimpleweightedgraph.jl new file mode 100644 index 0000000..f7f1e6a --- /dev/null +++ b/src/abstractsimpleweightedgraph.jl @@ -0,0 +1,125 @@ + +""" + AbstractSimpleWeightedGraph + +An abstract type representing a simple graph structure with edge weights. + +The only requirement for concrete subtypes is that they should implement a method `Graphs.weights(g)` which returns the weighted adjacency matrix. +""" +abstract type AbstractSimpleWeightedGraph{T<:Integer,U<:Real} <: AbstractGraph{T} end + +function Base.show(io::IO, g::AbstractSimpleWeightedGraph{T,U}) where {T} where {U} + dir = is_directed(g) ? "directed" : "undirected" + return print(io, "{$(nv(g)), $(ne(g))} $dir simple $T graph with $U weights") +end + +## Interface + +Base.eltype(::AbstractSimpleWeightedGraph{T,U}) where {T} where {U} = T +Base.zero(::T) where {T<:AbstractSimpleWeightedGraph} = T() +Base.copy(g::T) where {T<:AbstractSimpleWeightedGraph} = T(copy(weights(g))) + +""" + weighttype(g) + +Return the subtype of `Real` used to represent edge weights. +""" +weighttype(::AbstractSimpleWeightedGraph{T,U}) where {T} where {U} = U + +""" + get_weight(g, u, v) + +Retrieve the weight of edge `(u, v)`. +""" +get_weight(g::AbstractSimpleWeightedGraph, u::Integer, v::Integer) = weights(g)[v, u] + +## Vertices + +Graphs.nv(g::AbstractSimpleWeightedGraph{T,U}) where {T} where {U} = T(size(weights(g), 1)) +Graphs.vertices(g::AbstractSimpleWeightedGraph{T,U}) where {T} where {U} = one(T):nv(g) +Graphs.has_vertex(g::AbstractSimpleWeightedGraph, v::Integer) = v in vertices(g) + +# TODO: manipulate SparseMatrixCSC directly +Graphs.add_vertex!(g::AbstractSimpleWeightedGraph) = add_vertices!(g, 1) + +function Graphs.add_vertices!(g::AbstractSimpleWeightedGraph, n::Integer) + T = eltype(g) + U = weighttype(g) + (nv(g) + one(T) <= nv(g)) && return false # test for overflow + emptycols = spzeros(U, nv(g) + n, n) + g.weights = hcat(g.weights, emptycols[1:nv(g), :]) + g.weights = vcat(g.weights, emptycols') + return true +end + +## Edges + +# Handle single-argument edge constructors such as pairs and tuples + +""" + Graphs.has_edge(g, e) + +Check the existence of the edge `e` in the graph, where `e` is any object that can be converted into `edgetype(g)`. +""" +function Graphs.has_edge(g::AbstractSimpleWeightedGraph{T,U}, e) where {T,U} + return has_edge(g, edgetype(g)(e)) +end + +""" + Graphs.add_edge!(g, e) + +Add the edge `e` to the graph, where `e` is any object that can be converted into `edgetype(g)`. +""" +function Graphs.add_edge!(g::AbstractSimpleWeightedGraph{T,U}, e) where {T,U} + return add_edge!(g, edgetype(g)(e)) +end + +# Handle two-argument edge constructors like src,dst + +""" + Graphs.has_edge(g, u, v) + +Check the existence of the edge `(u, v)` in the graph. +""" +function Graphs.has_edge(g::AbstractSimpleWeightedGraph{T,U}, u, v) where {T,U} + return has_edge(g, edgetype(g)(u, v, 0)) +end + +""" + Graphs.add_edge!(g, u, v) + +Add the edge `(u, v)` to the graph with a default weight of 1. +""" +function Graphs.add_edge!(g::AbstractSimpleWeightedGraph{T,U}, u, v) where {T,U} + return add_edge!(g, edgetype(g)(T(u), T(v), one(U))) +end + +""" + Graphs.rem_edge!(g, u, v) + +Remove the edge `(u, v)` from the graph. +""" +function Graphs.rem_edge!( + g::AbstractSimpleWeightedGraph{T,U}, u::Integer, v::Integer +) where {T,U} + return rem_edge!(g, edgetype(g)(T(u), T(v), one(U))) +end + +# Handle three-argument constructors + +""" + Graphs.add_edge!(g, u, v, w) + +Add the edge `(u, v)` to the graph with a weight of `w`. +""" +function Graphs.add_edge!(g::AbstractSimpleWeightedGraph{T,U}, u, v, w) where {T,U} + return add_edge!(g, edgetype(g)(T(u), T(v), U(w))) +end + +## Miscellaneous + +function Base.issubset(g::T, h::T) where {T<:AbstractSimpleWeightedGraph} + (gmin, gmax) = extrema(vertices(g)) + (hmin, hmax) = extrema(vertices(h)) + return (hmin <= gmin <= gmax <= hmax) && issubset(edges(g), edges(h)) +end diff --git a/src/conversion.jl b/src/conversion.jl new file mode 100644 index 0000000..924bf7d --- /dev/null +++ b/src/conversion.jl @@ -0,0 +1,17 @@ +SimpleWeightedDiGraph(g::SimpleWeightedGraph) = SimpleWeightedDiGraph(copy(g.weights)) + +function SimpleWeightedDiGraph{T,U}(g::SimpleWeightedGraph) where {T<:Integer,U<:Real} + return SimpleWeightedDiGraph(SparseMatrixCSC{U,T}(copy(g.weights))) +end + +SimpleWeightedGraph(g::SimpleWeightedDiGraph) = SimpleWeightedGraph(g.weights .+ g.weights') + +function SimpleWeightedGraph{T,U}(g::SimpleWeightedDiGraph) where {T<:Integer,U<:Real} + return SimpleWeightedGraph(SparseMatrixCSC{U,T}(g.weights .+ g.weights')) +end + +function Base.convert( + ::Type{SparseMatrixCSC{U,T}}, g::AbstractSimpleWeightedGraph +) where {U<:Real,T<:Integer} + return SparseMatrixCSC{U,T}(g.weights) +end diff --git a/src/overrides.jl b/src/overrides.jl index 8add985..fdb6325 100644 --- a/src/overrides.jl +++ b/src/overrides.jl @@ -1,33 +1,35 @@ -##### OVERRIDES FOR EFFICIENCY / CORRECTNESS - -function add_vertices!(g::AbstractSimpleWeightedGraph, n::Integer) - T = eltype(g) - U = weighttype(g) - (nv(g) + one(T) <= nv(g)) && return false # test for overflow - emptycols = spzeros(U, nv(g) + n, n) - g.weights = hcat(g.weights, emptycols[1:nv(g), :]) - g.weights = vcat(g.weights, emptycols') - return true -end +""" + degree_matrix(g, T; dir) -function degree_matrix(g::AbstractSimpleWeightedGraph, T::DataType=weighttype(g); dir::Symbol=:out) +Construct the weighted diagonal degree matrix, filled with element type `T` and considering edge direction `dir ∈ [:in, :out, :both]`. +""" +function degree_matrix( + g::AbstractSimpleWeightedGraph, T::DataType=weighttype(g); dir::Symbol=:out +) if is_directed(g) if dir == :out - d = vec(sum(g.weights, dims=1)) + d = vec(sum(g.weights; dims=1)) elseif dir == :in - d = vec(sum(g.weights, dims=2)) + d = vec(sum(g.weights; dims=2)) elseif dir == :both - d = vec(sum(g.weights, dims=1)) + vec(sum(g.weights, dims=2)) + d = vec(sum(g.weights; dims=1)) + vec(sum(g.weights; dims=2)) else throw(DomainError(dir, "invalid argument, only accept :in, :out and :both")) end else - d = vec(sum(g.weights, dims=1)) + d = vec(sum(g.weights; dims=1)) end - return spdiagm( 0 => T.(d) ) + return spdiagm(0 => T.(d)) end -function adjacency_matrix(g::AbstractSimpleWeightedGraph, T::DataType=weighttype(g); dir::Symbol=:out) +""" + Graphs.adjacency_matrix(g, T; dir) + +Construct the weighted adjacency matrix, filled with element type `T` and considering edge direction `dir ∈ [:in, :out, :both]`. +""" +function Graphs.adjacency_matrix( + g::AbstractSimpleWeightedGraph, T::DataType=weighttype(g); dir::Symbol=:out +) if dir == :out return SparseMatrixCSC(T.(copy(g.weights))') else @@ -35,13 +37,25 @@ function adjacency_matrix(g::AbstractSimpleWeightedGraph, T::DataType=weighttype end end -function laplacian_matrix(g::AbstractSimpleWeightedGraph, T::DataType=weighttype(g); dir::Symbol=:out) - degree_matrix(g, T; dir=dir) - adjacency_matrix(g, T; dir=dir) +""" + Graphs.laplacian_matrix(g, T; dir) + +Subtract the adjacency matrix to the degree matrix, both filled with element type `T` and considering edge direction `dir ∈ [:in, :out, :both]`. +""" +function Graphs.laplacian_matrix( + g::AbstractSimpleWeightedGraph, T::DataType=weighttype(g); dir::Symbol=:out +) + return degree_matrix(g, T; dir=dir) - adjacency_matrix(g, T; dir=dir) end -function pagerank(g::SimpleWeightedDiGraph, α=0.85, n::Integer=100, ϵ=1.0e-6) +""" + Graphs.pagerank(g, α=0.85, n=100, ϵ=1.0e-6) + +Apply the page rank algorithm on a weighted graph. +""" +function Graphs.pagerank(g::SimpleWeightedDiGraph, α=0.85, n::Integer=100, ϵ=1.0e-6) A = weights(g) - S = vec(sum(A, dims=1)) + S = vec(sum(A; dims=1)) S = 1 ./ S S[findall(S .== Inf)] .= 0.0 M = A' # need a separate line due to bug #17456 in julia @@ -71,22 +85,23 @@ function pagerank(g::SimpleWeightedDiGraph, α=0.85, n::Integer=100, ϵ=1.0e-6) return x end end - error("Pagerank did not converge after $n iterations.") + return error("Pagerank did not converge after $n iterations.") end -savegraph(fn::AbstractString, g::AbstractSimpleWeightedGraph, gname::AbstractString="graph"; compress=true) = - savegraph(fn, g, gname, SWGFormat(), compress=compress) +""" +Graphs.cartesian_product(g, h) -savegraph(fn::AbstractString, d::Dict{T, U}; compress=true) where T <: AbstractString where U <: AbstractSimpleWeightedGraph = - savegraph(fn, d, SWGFormat(), compress=compress) +Compute the weighted cartesian product of two weighted graphs. -# It is possible that this is suboptimal, but it is the most trivial extension of the implementation used in Graphs.jl -function cartesian_product(g::G, h::G) where G <: AbstractSimpleWeightedGraph +!!! warning "Warning" + It is possible that this is suboptimal, but it is the most trivial extension of the implementation used in Graphs.jl. +""" +function Graphs.cartesian_product(g::G, h::G) where {G<:AbstractSimpleWeightedGraph} z = G(nv(g) * nv(h)) id(i, j) = (i - 1) * nv(h) + j for e in edges(g) i1, i2 = Tuple(e) - for j = 1:nv(h) + for j in 1:nv(h) add_edge!(z, id(i1, j), id(i2, j), weight(e)) end end @@ -102,20 +117,20 @@ end # Connected Components on a Sparse Matrix -function _cc(g::SimpleWeightedGraph{T,U}) where T where U +function _cc(g::SimpleWeightedGraph{T,U}) where {T} where {U} a = weights(g) comp = 0 n = size(a, 1) marks = zeros(T, n) queue = Vector{T}() - for i = 1:n + for i in 1:n if marks[i] == 0 comp += 1 push!(queue, i) while !isempty(queue) v = pop!(queue) marks[v] = comp - for index in nzrange(a,v) + for index in nzrange(a, v) n = a.rowval[index] if marks[n] == 0 push!(queue, n) @@ -124,19 +139,33 @@ function _cc(g::SimpleWeightedGraph{T,U}) where T where U end end end - marks, comp + return marks, comp end -function connected_components(g::SimpleWeightedGraph{T,U}) where T where U +""" + Graphs.connected_components(g) + +Compute the connected components of a weighted graph. Note that an edge with weight `0` will still be counted as an edge if it exists in the sparse weights matrix. +""" +function Graphs.connected_components(g::SimpleWeightedGraph{T,U}) where {T,U} marks, num_cc = _cc(g) - cc = [Vector{T}() for i = 1:num_cc] - for (i,v) in enumerate(marks) + cc = [Vector{T}() for i in 1:num_cc] + for (i, v) in enumerate(marks) push!(cc[v], i) end - cc + return cc end -function induced_subgraph(g::T, vlist::AbstractVector{U}) where T <: AbstractSimpleWeightedGraph where U <: Integer +""" + Graphs.induced_subgraph(g, vlist) + +Compute the weighted subgraph induced by a list of vertices. + +Return a tuple containing the new graph and the list of vertices. +""" +function Graphs.induced_subgraph( + g::T, vlist::AbstractVector{U} +) where {T<:AbstractSimpleWeightedGraph,U<:Integer} E = eltype(g) allunique(vlist) || throw(ArgumentError("Vertices in subgraph list must be unique")) new_weights = g.weights[E.(vlist), E.(vlist)] diff --git a/src/persistence.jl b/src/persistence.jl index 2c9f5bf..b4dccdf 100644 --- a/src/persistence.jl +++ b/src/persistence.jl @@ -1,21 +1,39 @@ -# The format of simpleweightedgraph files is as follows: for each graph, -# a one line header: "LightGraphs.SimpleWeightedGraph", , , {"d" | "u"}, [, , , , ] -# - "LightGraphs.SimpleWeightedGraph" is a fixed string -# - num_vertices is an integer -# - num_edges is an integer -# - "d" for directed graph, "u" for undirected. Note that this -# option does not perform any additional edge construction; it's -# merely used to return the correct type of graph. -# - name is a string -# - ver is an int -# - vdatatype is a string ("UInt8", etc.) -# - wdatatype is a string describing the data type of the weights -# - graphcode is a string. -# header followed by a list of (comma-delimited) edges - src,dst,weight -# Multiple graphs may be present in one file. - -const FIXEDSTR = "LightGraphs.SimpleWeightedGraph" +const FIXEDSTR = "LightGraphs.SimpleWeightedGraph" # TODO: rename it +""" + SWGFormat + +The storage format of SimpleWeightedGraph files. +Multiple graphs may be present in one file. + +For each graph, the file contains a one line header: + +``` +"LightGraphs.SimpleWeightedGraph", , , {"d" | "u"}, [, , , , ] +``` + +- `"LightGraphs.SimpleWeightedGraph"` is a fixed string +- `` is an integer +- `` is an integer +- `"d"` for directed graph, `"u"` for undirected (note that this + option does not perform any additional edge construction, it's + merely used to return the correct type of graph) +- `` is a string +- `` is an int +- `` is a string ("UInt8", etc.) +- `` is a string describing the data type of the weights +- `` is a string. + +The header is followed by a list of (comma-delimited) edges, each on a separate line: + +``` +,, +``` + +- `` is an int giving the source of the edge +- `` is an int giving the destination of the edge +- `` is a real giving the weight of the edge +""" struct SWGFormat <: AbstractGraphFormat end struct SWGHeader @@ -28,20 +46,24 @@ struct SWGHeader wdtype::DataType # weight data type code::String end -function show(io::IO, h::SWGHeader) + +function Base.show(io::IO, h::SWGHeader) isdir = h.is_directed ? "d" : "u" - print(io, "$FIXEDSTR,$(h.nv),$(h.ne),$isdir,$(h.name),$(h.ver),$(h.vdtype),$(h.wdtype),$(h.code)") + return print( + io, + "$FIXEDSTR,$(h.nv),$(h.ne),$isdir,$(h.name),$(h.ver),$(h.vdtype),$(h.wdtype),$(h.code)", + ) end function _swg_read_one_graph(f::IO, header::SWGHeader) T = header.vdtype U = header.wdtype if header.is_directed - g = SimpleWeightedDiGraph{T, U}(header.nv) + g = SimpleWeightedDiGraph{T,U}(header.nv) else - g = SimpleWeightedGraph{T, U}(header.nv) + g = SimpleWeightedGraph{T,U}(header.nv) end - for i = 1:header.ne + for i in 1:(header.ne) line = chomp(readline(f)) if length(line) > 0 src_s, dst_s, weight_s = split(line, r"\s*,\s*") @@ -62,7 +84,9 @@ end function _parse_header(s::AbstractString) addl_info = false - fixedstr, nvstr, nestr, dirundir, graphname, _ver, _vdtype, _wdtype, graphcode = split(s, r"s*,s*", limit=9) + fixedstr, nvstr, nestr, dirundir, graphname, _ver, _vdtype, _wdtype, graphcode = split( + s, r"s*,s*"; limit=9 + ) fixedstr != FIXEDSTR && error("Error parsing header.") n_v = parse(Int, nvstr) @@ -80,7 +104,7 @@ end """ loadswg_mult(io) -Return a dictionary of (name=>graph) loaded from IO stream `io`. +Return a dictionary of `name => graph` pairs loaded from the IO stream `io` using `SWGFormat`. """ function loadswg_mult(io::IO) graphs = Dict{String,AbstractGraph}() @@ -95,6 +119,11 @@ function loadswg_mult(io::IO) return graphs end +""" + loadswg(io, gname) + +Return a single graph with name `gname` loaded from the IO stream `io` using `SWGFormat`. +""" function loadswg(io::IO, gname::String) while !eof(io) line = strip(chomp(readline(io))) @@ -106,17 +135,25 @@ function loadswg(io::IO, gname::String) _swg_skip_one_graph(io, header.ne) end end - error("Graph $gname not found") + return error("Graph $gname not found") end """ saveswg(io, g, gname) -Write a graph `g` with name `gname` in a proprietary format -to the IO stream designated by `io`. Return 1 (number of graphs written). +Write a graph `g` with name `gname` to the IO stream `io` using `SWGFormat`, and return 1 (number of graphs written). """ function saveswg(io::IO, g::AbstractGraph, gname::String) - header = SWGHeader(nv(g), ne(g), is_directed(g), gname, 1, eltype(g), weighttype(g), "simpleweightedgraph") + header = SWGHeader( + nv(g), + ne(g), + is_directed(g), + gname, + 1, + eltype(g), + weighttype(g), + "simpleweightedgraph", + ) # write header line line = string(header) write(io, "$line\n") @@ -128,22 +165,84 @@ function saveswg(io::IO, g::AbstractGraph, gname::String) end """ - saveswg_mult(io, graphs) + saveswg_mult(io, d) -Write a dictionary of (name=>graph) to an IO stream `io`, -with default `GZip` compression. Return number of graphs written. +Write a dictionary `d` of `name => graph` pairs to the IO stream `io` using `SWGFormat`, and return the number of graphs written. """ -function saveswg_mult(io::IO, graphs::Dict) +function saveswg_mult(io::IO, d::Dict) ng = 0 - for (gname, g) in graphs + for (gname, g) in d ng += saveswg(io, g, gname) end return ng end +## Graphs functions with IO + +""" + Graphs.loadgraph(io, gname, SWGFormat()) + +Return a single graph with name `gname` loaded from the IO stream `io` using `SWGFormat`. +""" +Graphs.loadgraph(io::IO, gname::String, ::SWGFormat) = loadswg(io, gname) + +""" + Graphs.loadgraphs(io, SWGFormat()) + +Return a dictionary of `name => graph` pairs loaded from the IO stream `io` using `SWGFormat`. +""" +Graphs.loadgraphs(io::IO, ::SWGFormat) = loadswg_mult(io) + +""" + Graphs.savegraph(io, g, gname, SWGFormat()) + +Write a graph `g` with name `gname` to the IO stream `io` using `SWGFormat`, and return 1 (number of graphs written). +""" +function Graphs.savegraph(io::IO, g::AbstractGraph, gname::String, ::SWGFormat) + return saveswg(io, g, gname) +end + +""" + Graphs.savegraph(io, g, SWGFormat()) + +Write a graph `g` with default name `"graph"` to the IO stream `io` using `SWGFormat`, and return 1 (number of graphs written). +""" +Graphs.savegraph(io::IO, g::AbstractGraph, ::SWGFormat) = saveswg(io, g, "graph") + +""" + Graphs.savegraph(io, d, SWGFormat()) + +Write a dictionary `d` of `name => graph` pairs to the IO stream `io` using `SWGFormat`, and return the number of graphs written. +""" +Graphs.savegraph(io::IO, d::Dict, ::SWGFormat) = saveswg_mult(io, d) + +## Graphs functions with file names + +# TODO: check what they do and whether compress is still required -loadgraph(io::IO, gname::String, ::SWGFormat) = loadswg(io, gname) -loadgraphs(io::IO, ::SWGFormat) = loadswg_mult(io) -savegraph(io::IO, g::AbstractGraph, gname::String, ::SWGFormat) = saveswg(io, g, gname) -savegraph(io::IO, g::AbstractGraph, ::SWGFormat) = saveswg(io, g, "graph") -savegraph(io::IO, d::Dict, ::SWGFormat) = saveswg_mult(io, d) \ No newline at end of file +""" + Graphs.savegraph(fn, g, gname) + +!!! warning "Warning" + This function needs to be checked +""" +function Graphs.savegraph( + fn::AbstractString, + g::AbstractSimpleWeightedGraph, + gname::AbstractString="graph"; + compress=true, +) + return savegraph(fn, g, gname, SWGFormat(); compress=compress) +end + +""" + Graphs.savegraph(fn, d) + +!!! warning "Warning" + This function needs to be checked +""" +function Graphs.savegraph( + fn::AbstractString, d::Dict{T,U}; compress=true +) where {T<:AbstractString,U<:AbstractSimpleWeightedGraph} + return savegraph(fn, d, SWGFormat(); compress=compress) +end diff --git a/src/simpleweighteddigraph.jl b/src/simpleweighteddigraph.jl index 1b46e7c..dbc0766 100644 --- a/src/simpleweighteddigraph.jl +++ b/src/simpleweighteddigraph.jl @@ -1,112 +1,173 @@ """ - SimpleWeightedDiGraph{T, U} + SimpleWeightedDiGraph{T,U} -A type representing a directed graph with weights of type `U`. +A type representing a directed weighted graph with vertices of type `T` and edge weights of type `U`. -Note that adding or removing vertices or edges is not particularly performant; -see MetaGraphs.jl for possible alternatives. +# Fields +- `weights::SparseMatrixCSC{U,T}`: weighted adjacency matrix, indexed by `(dst, src)` -The primary constructor takes a sparse adjacency matrix as input, of which -the transpose is stored. -To provide the transpose directly, the keyword argument `permute=true` can be used. +!!! tip "Performance" + Iteratively adding/removing vertices or edges is not very efficient for this type of graph: better construct the graph in one shot if possible. + +# Basic constructors +``` +SimpleWeightedDiGraph() # empty +SimpleWeightedDiGraph(n) # n vertices, no edges +SimpleWeightedDiGraph(graph) # from graph +SimpleWeightedDiGraph(adjmx; permute) # from adjacency matrix, possibly transposed +SimpleWeightedDiGraph(sources, destinations, weights) # from list of edges +``` +Use `methods(SimpleWeightedDiGraph)` for the full list of constructors. """ -mutable struct SimpleWeightedDiGraph{T<:Integer, U<:Real} <: AbstractSimpleWeightedGraph{T, U} - weights::SparseMatrixCSC{U, T} # indexed by [dst, src] - function SimpleWeightedDiGraph{T, U}(adjmx::SparseMatrixCSC{U,T}; permute=true) where {T <: Integer, U <: Real} +mutable struct SimpleWeightedDiGraph{T<:Integer,U<:Real} <: AbstractSimpleWeightedGraph{T,U} + weights::SparseMatrixCSC{U,T} + function SimpleWeightedDiGraph{T,U}( + adjmx::SparseMatrixCSC{U,T}; permute=true + ) where {T<:Integer,U<:Real} dima, dimb = size(adjmx) isequal(dima, dimb) || error("Adjacency / distance matrices must be square") - permute ? new{T, U}(permutedims(adjmx)) : new{T, U}(adjmx) + return permute ? new{T,U}(permutedims(adjmx)) : new{T,U}(adjmx) end +end + +""" + WDiGraph + +Alias for `SimpleWeightedDiGraph`. +""" +const WDiGraph = SimpleWeightedDiGraph +function SimpleWeightedDiGraph{T}( + adjmx::SparseMatrixCSC{U,T}; permute=true +) where {T<:Integer,U<:Real} + return SimpleWeightedDiGraph{T,U}(adjmx; permute=permute) end -SimpleWeightedDiGraph{T}(adjmx::SparseMatrixCSC{U, T}; permute=true) where {T<:Integer, U<:Real} = - SimpleWeightedDiGraph{T, U}(adjmx; permute=permute) +function SimpleWeightedDiGraph( + adjmx::SparseMatrixCSC{U,T}; permute=true +) where {T<:Integer,U<:Real} + return SimpleWeightedDiGraph{T,U}(adjmx; permute=permute) +end -SimpleWeightedDiGraph(adjmx::SparseMatrixCSC{U, T}; permute=true) where {T<:Integer, U<:Real} = - SimpleWeightedDiGraph{T, U}(adjmx; permute=permute) +function SimpleWeightedDiGraph(m::AbstractMatrix{U}) where {U<:Real} + return SimpleWeightedDiGraph{Int,U}(SparseMatrixCSC{U,Int}(m)) +end -SimpleWeightedDiGraph(m::AbstractMatrix{U}) where {U <: Real} = - SimpleWeightedDiGraph{Int, U}(SparseMatrixCSC{U, Int}(m)) -SimpleWeightedDiGraph{T}(m::AbstractMatrix{U}) where {T <: Integer, U <: Real} = - SimpleWeightedDiGraph{T, U}(SparseMatrixCSC{U, T}(m)) -SimpleWeightedDiGraph{T, U}(m::AbstractMatrix) where {T <: Integer, U <: Real} = - SimpleWeightedDiGraph{T, U}(SparseMatrixCSC{U, T}(m)) +function SimpleWeightedDiGraph{T}(m::AbstractMatrix{U}) where {T<:Integer,U<:Real} + return SimpleWeightedDiGraph{T,U}(SparseMatrixCSC{U,T}(m)) +end -SimpleWeightedDiGraph(g::SimpleWeightedDiGraph) = SimpleWeightedDiGraph(copy(g.weights), permute=false) -SimpleWeightedDiGraph{T,U}(g::SimpleWeightedDiGraph) where {T <: Integer, U <: Real} = - SimpleWeightedDiGraph(SparseMatrixCSC{U, T}(copy(g.weights)), permute=false) +function SimpleWeightedDiGraph{T,U}(m::AbstractMatrix) where {T<:Integer,U<:Real} + return SimpleWeightedDiGraph{T,U}(SparseMatrixCSC{U,T}(m)) +end +function SimpleWeightedDiGraph(g::SimpleWeightedDiGraph) + return SimpleWeightedDiGraph(copy(g.weights); permute=false) +end + +function SimpleWeightedDiGraph{T,U}(g::SimpleWeightedDiGraph) where {T<:Integer,U<:Real} + return SimpleWeightedDiGraph(SparseMatrixCSC{U,T}(copy(g.weights)); permute=false) +end -ne(g::SimpleWeightedDiGraph) = nnz(g.weights) +Graphs.ne(g::SimpleWeightedDiGraph) = nnz(g.weights) -function has_edge(g::SimpleWeightedDiGraph, u::Integer, v::Integer) - (u ∈ vertices(g) && v ∈ vertices(g)) || return false - _get_nz_index!(g.weights, v, u) != 0 # faster than Base.isstored +function Graphs.has_edge(g::SimpleWeightedDiGraph, u::Integer, v::Integer) + (u in vertices(g) && v in vertices(g)) || return false + return _get_nz_index!(g.weights, v, u) != 0 # faster than Base.isstored end -has_edge(g::SimpleWeightedDiGraph, e::AbstractEdge) = - has_edge(g, src(e), dst(e)) +Graphs.has_edge(g::SimpleWeightedDiGraph, e::AbstractEdge) = has_edge(g, src(e), dst(e)) -function SimpleWeightedDiGraph{T,U}(n::Integer = 0) where {T<:Integer, U<:Real} +function SimpleWeightedDiGraph{T,U}(n::Integer=0) where {T<:Integer,U<:Real} weights = spzeros(U, T, T(n), T(n)) - return SimpleWeightedDiGraph{T, U}(weights) + return SimpleWeightedDiGraph{T,U}(weights) end # Graph() -SimpleWeightedDiGraph() = SimpleWeightedDiGraph{Int, Float64}() +SimpleWeightedDiGraph() = SimpleWeightedDiGraph{Int,Float64}() # Graph(6), Graph(0x5) -SimpleWeightedDiGraph(n::T) where T<:Integer = SimpleWeightedDiGraph{T, Float64}(n) +SimpleWeightedDiGraph(n::T) where {T<:Integer} = SimpleWeightedDiGraph{T,Float64}(n) # Graph(UInt8) -SimpleWeightedDiGraph(::Type{T}) where T<:Integer = SimpleWeightedDiGraph{T, Float64}(zero(T)) +function SimpleWeightedDiGraph(::Type{T}) where {T<:Integer} + return SimpleWeightedDiGraph{T,Float64}(zero(T)) +end # Graph(UInt8, Float32) -SimpleWeightedDiGraph(::Type{T}, ::Type{U}) where {T <: Integer, U <: Real} = SimpleWeightedDiGraph{T, U}(zero(T)) +function SimpleWeightedDiGraph(::Type{T}, ::Type{U}) where {T<:Integer,U<:Real} + return SimpleWeightedDiGraph{T,U}(zero(T)) +end # DiGraph(AbstractGraph, ::Type{U}) -function SimpleWeightedDiGraph(g::Graphs.AbstractGraph{T}, ::Type{U}=Float64) where {U <: Real, T} +function SimpleWeightedDiGraph( + g::Graphs.AbstractGraph{T}, ::Type{U}=Float64 +) where {U<:Real,T} return SimpleWeightedDiGraph{T}(adjacency_matrix(g, U)) end -""" - DiGraph(g::AbstractGraph, x::Real) - -Construct a weighted digraph from other graph `g` with initial weight `x`. -""" -function SimpleWeightedDiGraph(g::Graphs.AbstractGraph{T}, x::U) where {U <: Real, T} +function SimpleWeightedDiGraph(g::Graphs.AbstractGraph{T}, x::U) where {U<:Real,T} m = adjacency_matrix(g, U)' - return SimpleWeightedDiGraph{T, U}(x .* m, permute=false) + return SimpleWeightedDiGraph{T,U}(x .* m; permute=false) end # DiGraph(srcs, dsts, weights) -function SimpleWeightedDiGraph(i::AbstractVector{T}, j::AbstractVector{T}, v::AbstractVector{U}; combine = +) where {T<:Integer, U<:Real} +function SimpleWeightedDiGraph( + i::AbstractVector{T}, j::AbstractVector{T}, v::AbstractVector{U}; combine=+ +) where {T<:Integer,U<:Real} m = max(maximum(j), maximum(i)) - SimpleWeightedDiGraph{T, U}(sparse(j, i, v, m, m, combine), permute=false) + return SimpleWeightedDiGraph{T,U}(sparse(j, i, v, m, m, combine); permute=false) end Graphs.SimpleDiGraph(g::SimpleWeightedDiGraph) = SimpleDiGraph(g.weights') -edgetype(::SimpleWeightedDiGraph{T, U}) where T<:Integer where U<:Real = SimpleWeightedGraphEdge{T,U} +function Graphs.edgetype(::SimpleWeightedDiGraph{T,U}) where {T<:Integer} where {U<:Real} + return SimpleWeightedGraphEdge{T,U} +end -edges(g::SimpleWeightedDiGraph) = (SimpleWeightedEdge(x[2], x[1], x[3]) for x in zip(findnz(g.weights)...)) -weights(g::SimpleWeightedDiGraph) = g.weights' +function Graphs.edges(g::SimpleWeightedDiGraph) + return (SimpleWeightedEdge(x[2], x[1], x[3]) for x in zip(findnz(g.weights)...)) +end + +""" + Graphs.weights(g::SimpleWeightedDiGraph) + +Return the weighted adjacency matrix, stored as an `Adjoint`. +""" +Graphs.weights(g::SimpleWeightedDiGraph) = g.weights' -function outneighbors(g::SimpleWeightedDiGraph, v::Integer) +""" + Graphs.outneighbors(g::SimpleWeightedDiGraph, v) + +Return the vector of outneighbors of vertex `v`. + +!!! tip "Performance" + This function is more efficient than `inneighbors` for directed weighted graphs. +""" +function Graphs.outneighbors(g::SimpleWeightedDiGraph, v::Integer) mat = g.weights - return view(mat.rowval, mat.colptr[v]:(mat.colptr[v+1]-1)) + return view(mat.rowval, mat.colptr[v]:(mat.colptr[v + 1] - 1)) end -inneighbors(g::SimpleWeightedDiGraph, v::Integer) = g.weights[v,:].nzind + +""" + Graphs.inneighbors(g::SimpleWeightedDiGraph, v) + +Return the vector of inneighbors of vertex `v`. + +!!! tip "Performance" + This function is less efficient than `inneighbors` for directed weighted graphs (it allocates a new vector). +""" +Graphs.inneighbors(g::SimpleWeightedDiGraph, v::Integer) = g.weights[v, :].nzind # add_edge! will overwrite weights. -function add_edge!(g::SimpleWeightedDiGraph, e::SimpleWeightedGraphEdge) +function Graphs.add_edge!(g::SimpleWeightedDiGraph, e::SimpleWeightedGraphEdge) T = eltype(g) U = weighttype(g) s_, d_, w = Tuple(e) if w == zero(U) - @warn "Note: adding edges with a zero weight to this graph type has no effect." maxlog=1 _id=:swd_add_edge_zero + @warn "Note: adding edges with a zero weight to this graph type has no effect." maxlog = + 1 _id = :swd_add_edge_zero return false end @@ -117,15 +178,19 @@ function add_edge!(g::SimpleWeightedDiGraph, e::SimpleWeightedGraphEdge) return true end -rem_edge!(g::SimpleWeightedDiGraph, e::AbstractEdge) = - rem_edge!(g, src(e), dst(e)) +""" + Graphs.rem_edge!(g, e) + +Remove the edge `e` from the graph. +""" +Graphs.rem_edge!(g::SimpleWeightedDiGraph, e::AbstractEdge) = rem_edge!(g, src(e), dst(e)) -function rem_edge!(g::SimpleWeightedDiGraph{T}, u::Integer, v::Integer) where {T} - (u ∈ vertices(g) && v ∈ vertices(g)) || return false +function Graphs.rem_edge!(g::SimpleWeightedDiGraph{T}, u::Integer, v::Integer) where {T} + (u in vertices(g) && v in vertices(g)) || return false w = g.weights indx = _get_nz_index!(w, v, u) # get the index in nzval indx == 0 && return false # the edge does not exist - @view(w.colptr[(u+one(u)):end]) .-= T(1) # there is one value less in column u + @view(w.colptr[(u + one(u)):end]) .-= T(1) # there is one value less in column u # we remove the stored value deleteat!(w.rowval, indx) deleteat!(w.nzval, indx) @@ -133,19 +198,15 @@ function rem_edge!(g::SimpleWeightedDiGraph{T}, u::Integer, v::Integer) where {T return true end -@doc_str """ +""" rem_vertex!(g::SimpleWeightedDiGraph, v) -Remove the vertex `v` from graph `g`. Return false if removal fails -(e.g., if vertex is not in the graph); true otherwise. +Remove the vertex `v` from graph `g`. Return false if removal fails (e.g., if vertex is not in the graph) and true otherwise. -### Implementation Notes -This operation has to be performed carefully if one keeps external -data structures indexed by edges or vertices in the graph, since -internally the removal results in all vertices with indices greater than `v` -being shifted down one. +!!! tip "Correctness" + This operation has to be performed carefully if one keeps external data structures indexed by edges or vertices in the graph, since internally the removal results in all vertices with indices greater than `v` being shifted down one. """ -function rem_vertex!(g::SimpleWeightedDiGraph, v::Integer) +function Graphs.rem_vertex!(g::SimpleWeightedDiGraph, v::Integer) v in vertices(g) || return false n = nv(g) all_except_v = (1:n) .!= v @@ -153,25 +214,25 @@ function rem_vertex!(g::SimpleWeightedDiGraph, v::Integer) return true end -copy(g::SimpleWeightedDiGraph) = SimpleWeightedDiGraph(copy(g.weights')) +Base.copy(g::SimpleWeightedDiGraph) = SimpleWeightedDiGraph(copy(g.weights')) -==(g::SimpleWeightedDiGraph, h::SimpleWeightedDiGraph) = g.weights == h.weights +Base.:(==)(g::SimpleWeightedDiGraph, h::SimpleWeightedDiGraph) = g.weights == h.weights -is_directed(::Type{<:SimpleWeightedDiGraph}) = true +Graphs.is_directed(::Type{<:SimpleWeightedDiGraph}) = true """ - g[e::SimpleWeightedGraph, Val{:weight}()] + g[e, :weight] -Equivalent to g[src(e), dst(e)]. +Return the weight of edge `e`. """ function Base.getindex(g::SimpleWeightedDiGraph, e::AbstractEdge, ::Val{:weight}) return g.weights[dst(e), src(e)] end """ - g[e::SimpleWeightedGraph, i::Integer, j::Integer, Val{:weight}()] + g[i, j, :weight] -Return the weight of edge (i, j). +Return the weight of edge `(i, j)`. """ function Base.getindex(g::SimpleWeightedDiGraph, i::Integer, j::Integer, ::Val{:weight}) return g.weights[j, i] diff --git a/src/simpleweightededge.jl b/src/simpleweightededge.jl index 7b0b783..0220c21 100644 --- a/src/simpleweightededge.jl +++ b/src/simpleweightededge.jl @@ -1,39 +1,122 @@ -import Base: Pair, Tuple, show, == -import Graphs: AbstractEdge, src, dst, reverse +""" + AbstractSimpleWeightedEdge{T} +Abstract type for weighted edges with endpoints of type `T`. +""" abstract type AbstractSimpleWeightedEdge{T} <: AbstractEdge{T} end -struct SimpleWeightedEdge{T<:Integer, U<:Real} <: AbstractSimpleWeightedEdge{T} +""" + SimpleWeightedEdge{T,U} + +Concrete struct for a weighted edge with endpoints of type `T` and a weight of type `U<:Real`. + +# Fields +- `src::T`: edge source +- `dst::T`: edge destination +- `weight::U`: edge weight +""" +struct SimpleWeightedEdge{T<:Integer,U<:Real} <: AbstractSimpleWeightedEdge{T} src::T dst::T weight::U end +""" + SimpleWeightedGraphEdge + +Alias for `SimpleWeightedEdge`. +""" +const SimpleWeightedGraphEdge = SimpleWeightedEdge + +""" + SimpleWeightedDiGraphEdge + +Alias for `SimpleWeightedEdge`. +""" +const SimpleWeightedDiGraphEdge = SimpleWeightedEdge + +""" + SimpleWeightedEdge((u, v)) + +Construct a `SimpleWeightedEdge` from `u` to `v` with a default weight of 1.0. +""" SimpleWeightedEdge(t::NTuple{2}) = SimpleWeightedEdge(t[1], t[2], one(Float64)) -SimpleWeightedEdge(t::NTuple{3}) = SimpleWeightedEdge(t[1], t[2], t[3]) + +function SimpleWeightedEdge{T,U}(t::NTuple{2}) where {T<:Integer,U<:Real} + return SimpleWeightedEdge(T(t[1]), T(t[2]), one(U)) +end + +""" + SimpleWeightedEdge(u => v) + +Construct a `SimpleWeightedEdge` from `u` to `v` with a default weight of 1.0. +""" SimpleWeightedEdge(p::Pair) = SimpleWeightedEdge(p.first, p.second, one(Float64)) -SimpleWeightedEdge{T, U}(p::Pair) where T<:Integer where U <: Real = SimpleWeightedEdge(T(p.first), T(p.second), one(U)) -SimpleWeightedEdge{T, U}(t::NTuple{3}) where T<:Integer where U <: Real = SimpleWeightedEdge(T(t[1]), T(t[2]), U(t[3])) -SimpleWeightedEdge{T, U}(t::NTuple{2}) where T<:Integer where U <: Real = SimpleWeightedEdge(T(t[1]), T(t[2]), one(U)) -SimpleWeightedEdge{T, U}(x,y) where T<: Integer where U<:Real = SimpleWeightedEdge(x,y,one(U)) + +function SimpleWeightedEdge{T,U}(p::Pair) where {T<:Integer,U<:Real} + return SimpleWeightedEdge(T(p.first), T(p.second), one(U)) +end + +""" + SimpleWeightedEdge((u, v, w)) + +Construct a `SimpleWeightedEdge` from `u` to `v` with a weight of `w`. +""" +SimpleWeightedEdge(t::NTuple{3}) = SimpleWeightedEdge(t[1], t[2], t[3]) + +function SimpleWeightedEdge{T,U}(t::NTuple{3}) where {T<:Integer,U<:Real} + return SimpleWeightedEdge(T(t[1]), T(t[2]), U(t[3])) +end + +""" + SimpleWeightedEdge(u, v) + +Construct a `SimpleWeightedEdge` from `u` to `v` with a default weight of 1.0. +""" SimpleWeightedEdge(x, y) = SimpleWeightedEdge(x, y, one(Float64)) -eltype(e::T) where T<:AbstractSimpleWeightedEdge= eltype(src(e)) + +function SimpleWeightedEdge{T,U}(x, y) where {T<:Integer,U<:Real} + return SimpleWeightedEdge(x, y, one(U)) +end + +Base.eltype(::AbstractSimpleWeightedEdge{T}) where {T} = T # Accessors -src(e::AbstractSimpleWeightedEdge) = e.src -dst(e::AbstractSimpleWeightedEdge) = e.dst +Graphs.src(e::AbstractSimpleWeightedEdge) = e.src +Graphs.dst(e::AbstractSimpleWeightedEdge) = e.dst + +""" + weight(e) + +Return the weight of a weighted edge. +""" weight(e::AbstractSimpleWeightedEdge) = e.weight # I/O -show(io::IO, e::AbstractSimpleWeightedEdge) = print(io, "Edge $(e.src) => $(e.dst) with weight $(e.weight)") +function Base.show(io::IO, e::AbstractSimpleWeightedEdge) + return print(io, "Edge $(e.src) => $(e.dst) with weight $(e.weight)") +end # Conversions -Tuple(e::AbstractSimpleWeightedEdge) = (src(e), dst(e), weight(e)) +Base.Tuple(e::AbstractSimpleWeightedEdge) = (src(e), dst(e), weight(e)) -(::Type{SimpleWeightedEdge{T, U}})(e::AbstractSimpleWeightedEdge) where {T <: Integer, U <: Real} = SimpleWeightedEdge{T, U}(T(e.src), T(e.dst), U(e.weight)) +function (::Type{SimpleWeightedEdge{T,U}})( + e::AbstractSimpleWeightedEdge +) where {T<:Integer,U<:Real} + return SimpleWeightedEdge{T,U}(T(e.src), T(e.dst), U(e.weight)) +end # Convenience functions - note that these do not use weight. -reverse(e::T) where T<:AbstractSimpleWeightedEdge = T(dst(e), src(e), weight(e)) -==(e1::AbstractSimpleWeightedEdge, e2::AbstractSimpleWeightedEdge) = (src(e1) == src(e2) && dst(e1) == dst(e2)) -==(e1::AbstractSimpleWeightedEdge, e2::AbstractEdge) = (src(e1) == src(e2) && dst(e1) == dst(e2)) -==(e1::AbstractEdge, e2::AbstractSimpleWeightedEdge) = (src(e1) == src(e2) && dst(e1) == dst(e2)) +Base.reverse(e::T) where {T<:AbstractSimpleWeightedEdge} = T(dst(e), src(e), weight(e)) + +function Base.:(==)(e1::AbstractSimpleWeightedEdge, e2::AbstractSimpleWeightedEdge) + return (src(e1) == src(e2) && dst(e1) == dst(e2)) +end + +function Base.:(==)(e1::AbstractSimpleWeightedEdge, e2::AbstractEdge) + return (src(e1) == src(e2) && dst(e1) == dst(e2)) +end + +function Base.:(==)(e1::AbstractEdge, e2::AbstractSimpleWeightedEdge) + return (src(e1) == src(e2) && dst(e1) == dst(e2)) +end diff --git a/src/simpleweightedgraph.jl b/src/simpleweightedgraph.jl index b19993b..3d7d6ec 100644 --- a/src/simpleweightedgraph.jl +++ b/src/simpleweightedgraph.jl @@ -2,31 +2,51 @@ """ SimpleWeightedGraph{T, U} -A type representing an undirected graph with weights of type `U`. - -Note that adding or removing vertices or edges is not particularly performant; -see MetaGraphs.jl for possible alternatives. +A type representing an undirected weighted graph with vertices of type `T` and edge weights of type `U`. + +# Fields +- `weights::SparseMatrixCSC{U,T}`: weighted adjacency matrix, indexed by `(dst, src)` + +!!! tip "Performance" + Iteratively adding/removing vertices or edges is not very efficient for this type of graph: better construct the graph in one shot if possible. + +# Basic constructors +``` +SimpleWeightedGraph() # empty +SimpleWeightedGraph(n) # n vertices, no edges +SimpleWeightedGraph(graph) # from graph +SimpleWeightedGraph(adjmx) # from adjacency matrix +SimpleWeightedGraph(sources, destinations, weights) # from list of edges +``` +Use `methods(SimpleWeightedGraph)` for the full list of constructors. """ -mutable struct SimpleWeightedGraph{T<:Integer, U<:Real} <: AbstractSimpleWeightedGraph{T, U} +mutable struct SimpleWeightedGraph{T<:Integer,U<:Real} <: AbstractSimpleWeightedGraph{T,U} weights::SparseMatrixCSC{U,T} - function SimpleWeightedGraph{T, U}(adjmx::SparseMatrixCSC{U, T}) where {T<:Integer, U<:Real} - dima,dimb = size(adjmx) - isequal(dima,dimb) || error("Adjacency / distance matrices must be square") + function SimpleWeightedGraph{T,U}( + adjmx::SparseMatrixCSC{U,T} + ) where {T<:Integer,U<:Real} + dima, dimb = size(adjmx) + isequal(dima, dimb) || error("Adjacency / distance matrices must be square") issymmetric(adjmx) || error("Adjacency / distance matrices must be symmetric") - new{T, U}(adjmx) + return new{T,U}(adjmx) end - end -ne(g::SimpleWeightedGraph) = (nnz(g.weights) + nselfloop(g)) ÷ 2 +""" + WGraph + +Alias for `SimpleWeightedGraph`. +""" +const WGraph = SimpleWeightedGraph + +Graphs.ne(g::SimpleWeightedGraph) = (nnz(g.weights) + nselfloop(g)) ÷ 2 -function has_edge(g::SimpleWeightedGraph, u::Integer, v::Integer) - u ∈ vertices(g) && v ∈ vertices(g) || return false - _get_nz_index!(g.weights, v, u) != 0 # faster than Base.isstored +function Graphs.has_edge(g::SimpleWeightedGraph, u::Integer, v::Integer) + u in vertices(g) && v in vertices(g) || return false + return _get_nz_index!(g.weights, v, u) != 0 # faster than Base.isstored end -has_edge(g::SimpleWeightedGraph, e::AbstractEdge) = - has_edge(g, src(e), dst(e)) +Graphs.has_edge(g::SimpleWeightedGraph, e::AbstractEdge) = has_edge(g, src(e), dst(e)) function nselfloop(g::SimpleWeightedGraph) n = 0 @@ -36,104 +56,130 @@ function nselfloop(g::SimpleWeightedGraph) return n end -SimpleWeightedGraph{T}(adjmx::SparseMatrixCSC{U, T}) where {T <: Integer, U <: Real} = - SimpleWeightedGraph{T, U}(adjmx) +function SimpleWeightedGraph{T}(adjmx::SparseMatrixCSC{U,T}) where {T<:Integer,U<:Real} + return SimpleWeightedGraph{T,U}(adjmx) +end + +function SimpleWeightedGraph(adjmx::SparseMatrixCSC{U,T}) where {T<:Integer,U<:Real} + return SimpleWeightedGraph{T,U}(adjmx) +end -SimpleWeightedGraph(adjmx::SparseMatrixCSC{U, T}) where {T <: Integer, U <: Real} = - SimpleWeightedGraph{T, U}(adjmx) +function SimpleWeightedGraph(m::AbstractMatrix{U}) where {U<:Real} + return SimpleWeightedGraph{Int,U}(SparseMatrixCSC{U,Int}(m)) +end -SimpleWeightedGraph(m::AbstractMatrix{U}) where {U <: Real} = - SimpleWeightedGraph{Int, U}(SparseMatrixCSC{U, Int}(m)) -SimpleWeightedGraph{T}(m::AbstractMatrix{U}) where T<:Integer where U<:Real = - SimpleWeightedGraph{T, U}(SparseMatrixCSC{U, T}(m)) -SimpleWeightedGraph{T, U}(m::AbstractMatrix) where T<:Integer where U<:Real = - SimpleWeightedGraph{T, U}(SparseMatrixCSC{U, T}(m)) +function SimpleWeightedGraph{T}(m::AbstractMatrix{U}) where {T<:Integer,U<:Real} + return SimpleWeightedGraph{T,U}(SparseMatrixCSC{U,T}(m)) +end +function SimpleWeightedGraph{T,U}(m::AbstractMatrix) where {T<:Integer,U<:Real} + return SimpleWeightedGraph{T,U}(SparseMatrixCSC{U,T}(m)) +end SimpleWeightedGraph(g::SimpleWeightedGraph) = SimpleWeightedGraph(copy(g.weights)) -function SimpleWeightedGraph{T,U}(g::SimpleWeightedGraph) where {T<:Integer, U<:Real} - return SimpleWeightedGraph(SparseMatrixCSC{U, T}(copy(g.weights))) +function SimpleWeightedGraph{T,U}(g::SimpleWeightedGraph) where {T<:Integer,U<:Real} + return SimpleWeightedGraph(SparseMatrixCSC{U,T}(copy(g.weights))) end # Graph{UInt8}(6), Graph{Int16}(7), Graph{UInt8}() -function (::Type{SimpleWeightedGraph{T, U}})(n::Integer = 0) where T<:Integer where U<:Real +function (::Type{SimpleWeightedGraph{T,U}})(n::Integer=0) where {T<:Integer} where {U<:Real} weights = spzeros(U, T, T(n), T(n)) - return SimpleWeightedGraph{T, U}(weights) + return SimpleWeightedGraph{T,U}(weights) end # Graph() SimpleWeightedGraph() = SimpleWeightedGraph(Matrix{Float64}(undef, 0, 0)) # Graph(6), Graph(0x5) -SimpleWeightedGraph(n::T) where T<:Integer = SimpleWeightedGraph{T, Float64}(n) +SimpleWeightedGraph(n::T) where {T<:Integer} = SimpleWeightedGraph{T,Float64}(n) # Graph(UInt8) -SimpleWeightedGraph(::Type{T}) where T<:Integer = SimpleWeightedGraph{T, Float64}(zero(T)) +SimpleWeightedGraph(::Type{T}) where {T<:Integer} = SimpleWeightedGraph{T,Float64}(zero(T)) # Graph(UInt8, Float32) -SimpleWeightedGraph(::Type{T}, ::Type{U}) where {T<:Integer, U<:Real} = SimpleWeightedGraph{T, U}(zero(T)) +function SimpleWeightedGraph(::Type{T}, ::Type{U}) where {T<:Integer,U<:Real} + return SimpleWeightedGraph{T,U}(zero(T)) +end # Graph(SimpleGraph) -function SimpleWeightedGraph(g::Graphs.AbstractGraph{T}, ::Type{U}=Float64) where {T <: Integer, U <: Real} - adj_matrix = if Graphs.is_directed(g) - # TODO abstract function instead of SimpleGraph constructor - adjacency_matrix(Graphs.SimpleGraphs.SimpleGraph(g), U) +function SimpleWeightedGraph( + g::Graphs.AbstractGraph{T}, ::Type{U}=Float64 +) where {T<:Integer,U<:Real} + adj_matrix = if is_directed(g) + # TODO: abstract function instead of SimpleGraph constructor + adjacency_matrix(SimpleGraph(g), U) else adjacency_matrix(g, U) end - return SimpleWeightedGraph{T, U}(adj_matrix) + return SimpleWeightedGraph{T,U}(adj_matrix) end -function SimpleWeightedGraph(g::Graphs.AbstractGraph{T}, x::U) where {T <: Integer, U <: Real} - adj_matrix = if Graphs.is_directed(g) - # TODO abstract function instead of SimpleGraph constructor - adjacency_matrix(Graphs.SimpleGraphs.SimpleGraph(g), U) +function SimpleWeightedGraph(g::Graphs.AbstractGraph{T}, x::U) where {T<:Integer,U<:Real} + adj_matrix = if is_directed(g) + # TODO: abstract function instead of SimpleGraph constructor + adjacency_matrix(SimpleGraph(g), U) else adjacency_matrix(g, U) end - return SimpleWeightedGraph{T, U}(x .* adj_matrix) + return SimpleWeightedGraph{T,U}(x .* adj_matrix) end # SimpleWeightedGraph{T, U}(SimpleGraph) -function (::Type{SimpleWeightedGraph{T, U}})(g::Graphs.AbstractGraph) where {T<:Integer, U <: Real} - adj_matrix = if Graphs.is_directed(g) +function (::Type{SimpleWeightedGraph{T,U}})( + g::Graphs.AbstractGraph +) where {T<:Integer,U<:Real} + adj_matrix = if is_directed(g) # TODO abstract function instead of SimpleGraph constructor - adjacency_matrix(Graphs.SimpleGraphs.SimpleGraph{T}(g), U) + adjacency_matrix(SimpleGraph{T}(g), U) else adjacency_matrix(g, U) end - return SimpleWeightedGraph{T, U}(adj_matrix) + return SimpleWeightedGraph{T,U}(adj_matrix) end # Graph(srcs, dsts, weights) -function SimpleWeightedGraph(i::AbstractVector{T}, j::AbstractVector{T}, v::AbstractVector{U}; combine = +) where {T<:Integer, U<:Real} +function SimpleWeightedGraph( + i::AbstractVector{T}, j::AbstractVector{T}, v::AbstractVector{U}; combine=+ +) where {T<:Integer,U<:Real} m = max(maximum(i), maximum(j)) - s = sparse(vcat(i,j), vcat(j,i), vcat(v,v), m, m, combine) - SimpleWeightedGraph{T, U}(s) + s = sparse(vcat(i, j), vcat(j, i), vcat(v, v), m, m, combine) + return SimpleWeightedGraph{T,U}(s) end Graphs.SimpleGraph(g::SimpleWeightedGraph) = SimpleGraph(g.weights) -edgetype(::SimpleWeightedGraph{T, U}) where {T<:Integer, U<:Real} = SimpleWeightedGraphEdge{T,U} +function Graphs.edgetype(::SimpleWeightedGraph{T,U}) where {T<:Integer,U<:Real} + return SimpleWeightedGraphEdge{T,U} +end + +function Graphs.edges(g::SimpleWeightedGraph) + return (SimpleWeightedEdge(x[1], x[2], x[3]) for x in zip(findnz(triu(g.weights))...)) +end + +""" + Graphs.weights(g::SimpleWeightedGraph) -edges(g::SimpleWeightedGraph) = (SimpleWeightedEdge(x[1], x[2], x[3]) for x in zip(findnz(triu(g.weights))...)) -weights(g::SimpleWeightedGraph) = g.weights +Return the weighted adjacency matrix. +""" +Graphs.weights(g::SimpleWeightedGraph) = g.weights -function outneighbors(g::SimpleWeightedGraph, v::Integer) +function Graphs.outneighbors(g::SimpleWeightedGraph, v::Integer) mat = g.weights - return view(mat.rowval, mat.colptr[v]:mat.colptr[v+1]-1) + return view(mat.rowval, mat.colptr[v]:(mat.colptr[v + 1] - 1)) end -inneighbors(g::SimpleWeightedGraph, x...) = outneighbors(g, x...) + +Graphs.inneighbors(g::SimpleWeightedGraph, x...) = outneighbors(g, x...) # add_edge! will overwrite weights. -function add_edge!(g::SimpleWeightedGraph, e::SimpleWeightedGraphEdge) +function Graphs.add_edge!(g::SimpleWeightedGraph, e::SimpleWeightedGraphEdge) T = eltype(g) U = weighttype(g) s_, d_, w = Tuple(e) if w == zero(U) - @warn "Note: adding edges with a zero weight to this graph type has no effect." maxlog=1 _id=:swg_add_edge_zero + @warn "Note: adding edges with a zero weight to this graph type has no effect." maxlog = + 1 _id = :swg_add_edge_zero return false end @@ -145,40 +191,42 @@ function add_edge!(g::SimpleWeightedGraph, e::SimpleWeightedGraphEdge) return true end -rem_edge!(g::SimpleWeightedGraph, e::AbstractEdge) = - rem_edge!(g, src(e), dst(e)) +""" + Graphs.rem_edge!(g, e) + +Remove the edge `e` from the graph. +""" +Graphs.rem_edge!(g::SimpleWeightedGraph, e::AbstractEdge) = rem_edge!(g, src(e), dst(e)) -function rem_edge!(g::SimpleWeightedGraph{T, U}, u::Integer, v::Integer) where {T<:Integer, U<:Real} - (u ∈ vertices(g) && v ∈ vertices(g)) || return false +function Graphs.rem_edge!( + g::SimpleWeightedGraph{T,U}, u::Integer, v::Integer +) where {T<:Integer,U<:Real} + (u in vertices(g) && v in vertices(g)) || return false w = g.weights indx_uv = _get_nz_index!(w, u, v) # get the index in nzval indx_uv == 0 && return false # the edge does not exist - @view(w.colptr[(v+one(v)):end]) .-= T(1) # there is one value less in column v + @view(w.colptr[(v + one(v)):end]) .-= T(1) # there is one value less in column v # we remove the stored value deleteat!(w.rowval, indx_uv) deleteat!(w.nzval, indx_uv) (u == v) && return true # same for the reverse edge indx_vu = _get_nz_index!(w, v, u) - @view(w.colptr[(u+one(u)):end]) .-= T(1) + @view(w.colptr[(u + one(u)):end]) .-= T(1) deleteat!(w.rowval, indx_vu) deleteat!(w.nzval, indx_vu) return true end -@doc_str """ +""" rem_vertex!(g::SimpleWeightedGraph, v) -Remove the vertex `v` from graph `g`. Return false if removal fails -(e.g., if vertex is not in the graph); true otherwise. +Remove the vertex `v` from graph `g`. Return false if removal fails (e.g., if vertex is not in the graph) and true otherwise. -### Implementation Notes -This operation has to be performed carefully if one keeps external -data structures indexed by edges or vertices in the graph, since -internally the removal results in all vertices with indices greater than `v` -being shifted down one. +!!! tip "Correctness" + This operation has to be performed carefully if one keeps external data structures indexed by edges or vertices in the graph, since internally the removal results in all vertices with indices greater than `v` being shifted down one. """ -function rem_vertex!(g::SimpleWeightedGraph, v::Integer) +function Graphs.rem_vertex!(g::SimpleWeightedGraph, v::Integer) v in vertices(g) || return false n = nv(g) all_except_v = (1:n) .!= v @@ -186,16 +234,24 @@ function rem_vertex!(g::SimpleWeightedGraph, v::Integer) return true end +Base.:(==)(g::SimpleWeightedGraph, h::SimpleWeightedGraph) = g.weights == h.weights -==(g::SimpleWeightedGraph, h::SimpleWeightedGraph) = g.weights == h.weights - -is_directed(::Type{<:SimpleWeightedGraph}) = false +Graphs.is_directed(::Type{<:SimpleWeightedGraph}) = false +""" + g[e, :weight] +Return the weight of edge `e`. +""" function Base.getindex(g::SimpleWeightedGraph, e::AbstractEdge, ::Val{:weight}) return g.weights[src(e), dst(e)] end +""" + g[i, j, :weight] + +Return the weight of edge `(i, j)`. +""" function Base.getindex(g::SimpleWeightedGraph, i::Integer, j::Integer, ::Val{:weight}) return g.weights[i, j] end diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..c433131 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,17 @@ +""" + _get_nz_index!(mat::SparseMatrixCSC, i, j) + +Return the index in nzval of `mat[i, j]`. We assume bounds are already checked + +See https://github.com/JuliaSparse/SparseArrays.jl/blob/fa547689947fadd6c2f3d09ddfcb5f26536f18c8/src/sparsematrix.jl#L2492 for implementation +""" +function _get_nz_index!(mat::SparseMatrixCSC, i::Integer, j::Integer) + # r1 and r2 are start and end of the column + @inbounds r1 = Int(mat.colptr[j]) + @inbounds r2 = Int(mat.colptr[j + 1] - 1) + (r1 > r2) && return 0 # column is empty so we have a non structural zero + # search if i correspond to a stored value + @inbounds indx = searchsortedfirst(mat.rowval, i, r1, r2, Base.Forward) + @inbounds ((indx > r2) || (mat.rowval[indx] != i)) && return 0 + return indx +end diff --git a/test/connectivity.jl b/test/connectivity.jl index 4787fc8..a630225 100644 --- a/test/connectivity.jl +++ b/test/connectivity.jl @@ -1,17 +1,13 @@ -const graphs = [ -:bull, -:chvatal, -:house -] +const graphs = [:bull, :chvatal, :house] @testset verbose = true "Connectivity" begin - for s in graphs - gx = smallgraph(s) + for s in graphs + gx = smallgraph(s) - for g in testgraphs(gx) - a = adjacency_matrix(g) - x = connected_components(g) - y = connected_components(SimpleWeightedGraph(a)) - @test x == y - end - end - end + for g in testgraphs(gx) + a = adjacency_matrix(g) + x = connected_components(g) + y = connected_components(SimpleWeightedGraph(a)) + @test x == y + end + end +end diff --git a/test/overrides.jl b/test/overrides.jl index d9ab49c..dda32ef 100644 --- a/test/overrides.jl +++ b/test/overrides.jl @@ -1,43 +1,55 @@ @testset verbose = true "Overrides" begin g3 = SimpleWeightedGraph(path_graph(5)) - g3_d = [1. 0. 0. 0. 0.; - 0. 2. 0. 0. 0.; - 0. 0. 2. 0. 0.; - 0. 0. 0. 2. 0.; - 0. 0. 0. 0. 1.] - g3_l = [1. -1. 0. 0. 0.; - -1. 2. -1. 0. 0.; - 0. -1. 2. -1. 0.; - 0. 0. -1. 2. -1.; - 0. 0. 0. -1. 1.] + g3_d = [ + 1.0 0.0 0.0 0.0 0.0 + 0.0 2.0 0.0 0.0 0.0 + 0.0 0.0 2.0 0.0 0.0 + 0.0 0.0 0.0 2.0 0.0 + 0.0 0.0 0.0 0.0 1.0 + ] + g3_l = [ + 1.0 -1.0 0.0 0.0 0.0 + -1.0 2.0 -1.0 0.0 0.0 + 0.0 -1.0 2.0 -1.0 0.0 + 0.0 0.0 -1.0 2.0 -1.0 + 0.0 0.0 0.0 -1.0 1.0 + ] g5 = SimpleWeightedDiGraph(4) - g5_din = [0. 0. 0. 0.; - 0. 2. 0. 0.; - 0. 0. 3. 0.; - 0. 0. 0. 5.] - g5_dout = [3. 0. 0. 0.; - 0. 2. 0. 0.; - 0. 0. 5. 0.; - 0. 0. 0. 0.] - g5_dboth = [3. 0. 0. 0.; - 0. 4. 0. 0.; - 0. 0. 8. 0.; - 0. 0. 0. 5.] - g5_l = [3. -2. -1. 0.; - 0. 2. -2. 0.; - 0. 0. 5. -5.; - 0. 0. 0. 0.] + g5_din = [ + 0.0 0.0 0.0 0.0 + 0.0 2.0 0.0 0.0 + 0.0 0.0 3.0 0.0 + 0.0 0.0 0.0 5.0 + ] + g5_dout = [ + 3.0 0.0 0.0 0.0 + 0.0 2.0 0.0 0.0 + 0.0 0.0 5.0 0.0 + 0.0 0.0 0.0 0.0 + ] + g5_dboth = [ + 3.0 0.0 0.0 0.0 + 0.0 4.0 0.0 0.0 + 0.0 0.0 8.0 0.0 + 0.0 0.0 0.0 5.0 + ] + g5_l = [ + 3.0 -2.0 -1.0 0.0 + 0.0 2.0 -2.0 0.0 + 0.0 0.0 5.0 -5.0 + 0.0 0.0 0.0 0.0 + ] for g in testgraphs(g3) @test @inferred(adjacency_matrix(g, Bool)) == adjacency_matrix(g, Bool; dir=:out) @test @inferred(adjacency_matrix(g))[3, 2] == 1 - @test degree_matrix(g, Float64, dir=:out) == g3_d - @test degree_matrix(g, Float64, dir=:out) == degree_matrix(g, Float64, dir=:in) + @test degree_matrix(g, Float64; dir=:out) == g3_d + @test degree_matrix(g, Float64; dir=:out) == degree_matrix(g, Float64; dir=:in) @test adjacency_matrix(g)[2, 4] == 0 @test adjacency_matrix(g; dir=:out) == adjacency_matrix(g; dir=:in)' @test issymmetric(laplacian_matrix(g)) @test laplacian_matrix(g, Float64) ≈ g3_l - @test g[1:3] == SimpleWeightedGraph{eltype(g), weighttype(g)}(path_graph(3)) + @test g[1:3] == SimpleWeightedGraph{eltype(g),weighttype(g)}(path_graph(3)) gx = copy(g) add_edge!(gx, 2, 3, 99) gi = gx[2:4] @@ -48,26 +60,29 @@ @test ne(h) == 40 gz = g3[1:4] add_edge!(gz, 3, 4, 87) - @test weights(cartesian_product(g3,gz))[11,12]==weights(gz)[3,4] + @test weights(cartesian_product(g3, gz))[11, 12] == weights(gz)[3, 4] end - add_edge!(g5, 1, 2, 2); add_edge!(g5, 2, 3, 2); add_edge!(g5, 1, 3, 1); add_edge!(g5, 3, 4, 5) + add_edge!(g5, 1, 2, 2) + add_edge!(g5, 2, 3, 2) + add_edge!(g5, 1, 3, 1) + add_edge!(g5, 3, 4, 5) for g in testdigraphs(g5) - @test degree_matrix(g, Float64, dir=:out) == g5_dout - @test degree_matrix(g, Float64, dir=:in) == g5_din - @test degree_matrix(g, Float64, dir=:both) == g5_dboth + @test degree_matrix(g, Float64; dir=:out) == g5_dout + @test degree_matrix(g, Float64; dir=:in) == g5_din + @test degree_matrix(g, Float64; dir=:both) == g5_dboth @test_throws DomainError degree_matrix(g, dir=:other) @test @inferred(adjacency_matrix(g, Int64)) == adjacency_matrix(g, Int64; dir=:out) @test adjacency_matrix(g; dir=:out) == adjacency_matrix(g; dir=:in)' @test !issymmetric(laplacian_matrix(g)) @test laplacian_matrix(g, Float64) ≈ g5_l - @test @inferred(pagerank(g))[3] ≈ 0.2266 atol=0.001 + @test @inferred(pagerank(g))[3] ≈ 0.2266 atol = 0.001 @test length(@inferred(pagerank(g))) == nv(g) @test_throws ErrorException pagerank(g, 2) @test_throws ErrorException pagerank(g, 0.85, 2) gc = SimpleWeightedDiGraph(path_digraph(2), 2) - @test g[2:3] == SimpleWeightedDiGraph{eltype(g5), weighttype(g5)}(gc) + @test g[2:3] == SimpleWeightedDiGraph{eltype(g5),weighttype(g5)}(gc) @test weights(g[2:3])[1, 2] == 2 end end diff --git a/test/persistence.jl b/test/persistence.jl index a664af1..6c34a97 100644 --- a/test/persistence.jl +++ b/test/persistence.jl @@ -1,34 +1,36 @@ @testset verbose = true "Persistence" begin -gpath = joinpath(testdir, "testdata") -pdict = loadgraphs(joinpath(gpath, "swgs.jgz"), SWGFormat()) -p1 = pdict["g"] -p2 = pdict["h"] + gpath = joinpath(testdir, "testdata") + pdict = loadgraphs(joinpath(gpath, "swgs.jgz"), SWGFormat()) + p1 = pdict["g"] + p2 = pdict["h"] -(f, fio) = mktemp() -# test :lg -@test savegraph(f, p1, SWGFormat()) == 1 -@test savegraph(f, p1, SWGFormat(); compress=true) == 1 -@test savegraph(f, p1, SWGFormat(); compress=true) == 1 -@test savegraph(f, p2, SWGFormat(); compress=true) == 1 -@test (ne(p2), nv(p2)) == (3, 3) + (f, fio) = mktemp() + # test :lg + @test savegraph(f, p1, SWGFormat()) == 1 + @test savegraph(f, p1, SWGFormat(); compress=true) == 1 + @test savegraph(f, p1, SWGFormat(); compress=true) == 1 + @test savegraph(f, p2, SWGFormat(); compress=true) == 1 + @test (ne(p2), nv(p2)) == (3, 3) -g2 = loadgraph(f, SWGFormat()) -j2 = loadgraph(f, "graph", SWGFormat()) -@test g2 == j2 -@test (ne(g2), nv(g2)) == (3, 3) + g2 = loadgraph(f, SWGFormat()) + j2 = loadgraph(f, "graph", SWGFormat()) + @test g2 == j2 + @test (ne(g2), nv(g2)) == (3, 3) -(f, fio) = mktemp() -@test length(sprint(savegraph, p1, SWGFormat())) == 104 -@test length(sprint(savegraph, p2, SWGFormat())) == 104 -gs = loadgraph(joinpath(gpath, "swgs.jgz"), "h", SWGFormat()) -@test gs == p2 -@test_throws ErrorException loadgraph(joinpath(gpath, "swgs.jgz"), "badname", SWGFormat()) + (f, fio) = mktemp() + @test length(sprint(savegraph, p1, SWGFormat())) == 104 + @test length(sprint(savegraph, p2, SWGFormat())) == 104 + gs = loadgraph(joinpath(gpath, "swgs.jgz"), "h", SWGFormat()) + @test gs == p2 + @test_throws ErrorException loadgraph( + joinpath(gpath, "swgs.jgz"), "badname", SWGFormat() + ) -@test savegraph(f, p1) == 1 -d = Dict{String,AbstractGraph}("p1" => p1, "p2" => p2) -@test savegraph(f, d, SWGFormat()) == 2 -@test savegraph(f, d) == 2 + @test savegraph(f, p1) == 1 + d = Dict{String,AbstractGraph}("p1" => p1, "p2" => p2) + @test savegraph(f, d, SWGFormat()) == 2 + @test savegraph(f, d) == 2 -close(fio) -rm(f) + close(fio) + rm(f) end diff --git a/test/runtests.jl b/test/runtests.jl index f106347..642e407 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -38,7 +38,7 @@ tests = [ Aqua.test_all(SimpleWeightedGraphs; ambiguities=false) end @testset verbose = false "Code formatting (JuliaFormatter.jl)" begin - @test_broken format(SimpleWeightedGraphs; verbose=false, overwrite=false) + @test format(SimpleWeightedGraphs; verbose=false, overwrite=false) end @testset verbose = false "Doctests (Documenter.jl)" begin doctest(SimpleWeightedGraphs) diff --git a/test/simpleweightededge.jl b/test/simpleweightededge.jl index a3bfd01..02f841a 100644 --- a/test/simpleweightededge.jl +++ b/test/simpleweightededge.jl @@ -4,16 +4,16 @@ import SimpleWeightedGraphs.SimpleWeightedEdge e2 = SimpleWeightedEdge(1, 2) e3 = SimpleWeightedEdge(1, 2, 1) re = SimpleWeightedEdge(2, 1, 1.0) - ep = SimpleWeightedEdge(Pair(1,2)) + ep = SimpleWeightedEdge(Pair(1, 2)) for s in [0x01, UInt16(1), 1] T = typeof(s) - d = s+one(T) + d = s + one(T) t = (s, d, 1) ep1 = SimpleWeightedEdge(t) - ep2 = SimpleWeightedEdge{UInt8, Int64}(t) - ep3 = SimpleWeightedEdge{Int16, Int64}(t) + ep2 = SimpleWeightedEdge{UInt8,Int64}(t) + ep3 = SimpleWeightedEdge{Int16,Int64}(t) t1 = (s, d) t2 = (s, d, 1) @@ -25,8 +25,10 @@ import SimpleWeightedGraphs.SimpleWeightedEdge @test eltype(t) == typeof(s) @test SimpleWeightedEdge(t) == e3 @test SimpleWeightedEdge(t1) == SimpleWeightedEdge(t2) - @test SimpleWeightedEdge(t1) == SimpleWeightedEdge{UInt8, Float64}(t1) == SimpleWeightedEdge{Int16, Float64}(t1) - @test SimpleWeightedEdge{Int64, Float64}(ep1) == e3 + @test SimpleWeightedEdge(t1) == + SimpleWeightedEdge{UInt8,Float64}(t1) == + SimpleWeightedEdge{Int16,Float64}(t1) + @test SimpleWeightedEdge{Int64,Float64}(ep1) == e3 @test Tuple(ep1) == t2 @test reverse(ep1) == re diff --git a/test/simpleweightedgraph.jl b/test/simpleweightedgraph.jl index b536a5d..d67ddc1 100644 --- a/test/simpleweightedgraph.jl +++ b/test/simpleweightedgraph.jl @@ -19,7 +19,6 @@ using SimpleWeightedGraphs @test @inferred(ne(SimpleWeightedDiGraph(adjmx2))) == 6 @test @inferred(is_directed(SimpleWeightedDiGraph)) - for gbig in [SimpleWeightedGraph(0xff), SimpleWeightedDiGraph(0xff)] @test @inferred(!add_vertex!(gbig)) # overflow @test @inferred(!add_vertices!(gbig, 10)) @@ -46,27 +45,32 @@ using SimpleWeightedGraphs gx = SimpleWeightedGraph(path_graph(4)) gc = copy(gx) - @test_logs (:warn, "Note: adding edges with a zero weight to this graph type has no effect.") add_edge!(gc, 4, 1, 0.0) + @test_logs ( + :warn, "Note: adding edges with a zero weight to this graph type has no effect." + ) add_edge!(gc, 4, 1, 0.0) @test !(add_edge!(gc, 4, 1, 0.0)) for g in testgraphs(gx) @test @inferred(vertices(g)) == 1:4 - @test SimpleWeightedEdge(2,3) in edges(g) + @test SimpleWeightedEdge(2, 3) in edges(g) @test @inferred(nv(g)) == 4 - @test @inferred(outneighbors(g,2)) == inneighbors(g,2) == neighbors(g,2) + @test @inferred(outneighbors(g, 2)) == inneighbors(g, 2) == neighbors(g, 2) @test @inferred(has_edge(g, 2, 3)) @test @inferred(has_edge(g, 3, 2)) gc = copy(g) - @test @inferred(add_edge!(gc, 4=>1)) && gc == SimpleWeightedGraph(cycle_graph(4)) - @test @inferred(has_edge(gc, 4=>1)) && has_edge(gc, 0x04=>0x01) + @test @inferred(add_edge!(gc, 4 => 1)) && gc == SimpleWeightedGraph(cycle_graph(4)) + @test @inferred(has_edge(gc, 4 => 1)) && has_edge(gc, 0x04 => 0x01) gc = copy(g) - @test @inferred(add_edge!(gc, (4,1))) && gc == SimpleWeightedGraph(cycle_graph(4)) - @test @inferred(has_edge(gc, (4,1))) && has_edge(gc, (0x04, 0x01)) + @test @inferred(add_edge!(gc, (4, 1))) && gc == SimpleWeightedGraph(cycle_graph(4)) + @test @inferred(has_edge(gc, (4, 1))) && has_edge(gc, (0x04, 0x01)) gc = copy(g) @test add_edge!(gc, 4, 1) && gc == SimpleWeightedGraph(cycle_graph(4)) - @test @inferred(inneighbors(g, 2)) == @inferred(outneighbors(g, 2)) == @inferred(neighbors(g,2)) == [1,3] + @test @inferred(inneighbors(g, 2)) == + @inferred(outneighbors(g, 2)) == + @inferred(neighbors(g, 2)) == + [1, 3] @test @inferred(add_vertex!(gc)) # out of order, but we want it for issubset @test @inferred(g ⊆ gc) @test @inferred(has_vertex(gc, 5)) @@ -74,37 +78,40 @@ using SimpleWeightedGraphs @test @inferred(ne(g)) == 3 @test @inferred(rem_edge!(gc, 1, 2)) && @inferred(!has_edge(gc, 1, 2)) - @test @inferred(inneighbors(gc, 2)) == @inferred(outneighbors(gc, 2)) == @inferred(neighbors(gc,2)) == [3] + @test @inferred(inneighbors(gc, 2)) == + @inferred(outneighbors(gc, 2)) == + @inferred(neighbors(gc, 2)) == + [3] ga = @inferred(copy(g)) @test @inferred(rem_vertex!(ga, 2)) && ne(ga) == 1 @test @inferred(!rem_vertex!(ga, 10)) - @test @inferred(zero(g)) == SimpleWeightedGraph{eltype(g), weighttype(g)}() + @test @inferred(zero(g)) == SimpleWeightedGraph{eltype(g),weighttype(g)}() # concrete tests below - @test @inferred(eltype(g)) == eltype(outneighbors(g,1)) == eltype(nv(g)) + @test @inferred(eltype(g)) == eltype(outneighbors(g, 1)) == eltype(nv(g)) T = @inferred(eltype(g)) U = @inferred(weighttype(g)) - @test @inferred(nv(SimpleWeightedGraph{T, U}(6))) == 6 + @test @inferred(nv(SimpleWeightedGraph{T,U}(6))) == 6 # @test @inferred(eltype(SimpleWeightedGraph(T))) == T - @test @inferred(eltype(SimpleWeightedGraph{T, U}(adjmx1))) == T + @test @inferred(eltype(SimpleWeightedGraph{T,U}(adjmx1))) == T ga = SimpleWeightedGraph(10) - @test @inferred(eltype(SimpleWeightedGraph{T, U}(ga))) == T + @test @inferred(eltype(SimpleWeightedGraph{T,U}(ga))) == T for gd in testdigraphs(gdx) T2 = eltype(gd) @test @inferred(eltype(SimpleWeightedGraph(gd))) == T2 end - @test @inferred(edgetype(g)) == SimpleWeightedGraphEdge{T, U} + @test @inferred(edgetype(g)) == SimpleWeightedGraphEdge{T,U} @test @inferred(copy(g)) == g @test @inferred(!is_directed(g)) # test that edgetype(g) is a valid constructor when called with just the source and destination vertex - @test edgetype(g)(1,2) == SimpleWeightedGraphEdge{T, U}(1,2,1.0) + @test edgetype(g)(1, 2) == SimpleWeightedGraphEdge{T,U}(1, 2, 1.0) e = first(edges(g)) @test @inferred(has_edge(g, e)) @@ -113,13 +120,15 @@ using SimpleWeightedGraphs gdx = SimpleWeightedDiGraph(path_digraph(4)) gc = copy(gdx) - @test_logs (:warn, "Note: adding edges with a zero weight to this graph type has no effect.") add_edge!(gc, 4, 1, 0.0) + @test_logs ( + :warn, "Note: adding edges with a zero weight to this graph type has no effect." + ) add_edge!(gc, 4, 1, 0.0) @test !(add_edge!(gc, 4, 1, 0.0)) for g in testdigraphs(gdx) @test @inferred(vertices(g)) == 1:4 - @test SimpleWeightedEdge(2,3) in edges(g) - @test !(SimpleWeightedEdge(3,2) in edges(g)) + @test SimpleWeightedEdge(2, 3) in edges(g) + @test !(SimpleWeightedEdge(3, 2) in edges(g)) @test @inferred(nv(g)) == 4 @test outneighbors(g, 2) == [3] @test inneighbors(g, 2) == [1] @@ -128,16 +137,19 @@ using SimpleWeightedGraphs @test @inferred(!has_edge(g, 3, 2)) gc = copy(g) - @test @inferred(add_edge!(gc, 4=>1)) && gc == SimpleWeightedDiGraph(cycle_digraph(4)) - @test @inferred(has_edge(gc, 4=>1)) && has_edge(gc, 0x04=>0x01) + @test @inferred(add_edge!(gc, 4 => 1)) && + gc == SimpleWeightedDiGraph(cycle_digraph(4)) + @test @inferred(has_edge(gc, 4 => 1)) && has_edge(gc, 0x04 => 0x01) gc = copy(g) - @test @inferred(add_edge!(gc, (4,1))) && gc == SimpleWeightedDiGraph(cycle_digraph(4)) - @test @inferred(has_edge(gc, (4,1))) && has_edge(gc, (0x04, 0x01)) + @test @inferred(add_edge!(gc, (4, 1))) && + gc == SimpleWeightedDiGraph(cycle_digraph(4)) + @test @inferred(has_edge(gc, (4, 1))) && has_edge(gc, (0x04, 0x01)) gc = @inferred(copy(g)) - @test @inferred(add_edge!(gc, 4, 1)) && gc == SimpleWeightedDiGraph(cycle_digraph(4)) + @test @inferred(add_edge!(gc, 4, 1)) && + gc == SimpleWeightedDiGraph(cycle_digraph(4)) @test @inferred(inneighbors(g, 2)) == [1] - @test @inferred(outneighbors(g, 2)) == @inferred(neighbors(g,2)) == [3] + @test @inferred(outneighbors(g, 2)) == @inferred(neighbors(g, 2)) == [3] @test @inferred(add_vertex!(gc)) # out of order, but we want it for issubset @test @inferred(g ⊆ gc) @test @inferred(has_vertex(gc, 5)) @@ -151,32 +163,32 @@ using SimpleWeightedGraphs @test @inferred(rem_vertex!(ga, 2)) && ne(ga) == 1 @test @inferred(!rem_vertex!(ga, 10)) - @test @inferred(zero(g)) == SimpleWeightedDiGraph{eltype(g), weighttype(g)}() + @test @inferred(zero(g)) == SimpleWeightedDiGraph{eltype(g),weighttype(g)}() # concrete tests below - @test @inferred(eltype(g)) == eltype(@inferred(outneighbors(g,1))) == eltype(nv(g)) + @test @inferred(eltype(g)) == eltype(@inferred(outneighbors(g, 1))) == eltype(nv(g)) T = @inferred(eltype(g)) U = @inferred(weighttype(g)) - @test @inferred(nv(SimpleWeightedDiGraph{T, U}(6))) == 6 + @test @inferred(nv(SimpleWeightedDiGraph{T,U}(6))) == 6 # @test @inferred(eltype(SimpleWeightedDiGraph(T))) == T - @test @inferred(eltype(SimpleWeightedDiGraph{T, U}(adjmx2))) == T + @test @inferred(eltype(SimpleWeightedDiGraph{T,U}(adjmx2))) == T ga = SimpleWeightedDiGraph(10) - @test @inferred(eltype(SimpleWeightedDiGraph{T, U}(ga))) == T + @test @inferred(eltype(SimpleWeightedDiGraph{T,U}(ga))) == T for gu in testgraphs(gx) T2 = @inferred(eltype(gu)) @test @inferred(eltype(SimpleWeightedDiGraph(gu))) == T2 end - @test @inferred(edgetype(g)) == SimpleWeightedDiGraphEdge{T, U} + @test @inferred(edgetype(g)) == SimpleWeightedDiGraphEdge{T,U} @test @inferred(copy(g)) == g @test @inferred(is_directed(g)) # test that edgetype(g) is a valid constructor when called with just the source and destination vertex - @test edgetype(g)(1,2) == SimpleWeightedDiGraphEdge{T, U}(1,2,1.0) + @test edgetype(g)(1, 2) == SimpleWeightedDiGraphEdge{T,U}(1, 2, 1.0) e = first(@inferred(edges(g))) @test @inferred(has_edge(g, e)) @@ -194,67 +206,74 @@ using SimpleWeightedGraphs g = SimpleWeightedDiGraph(path_graph(5), 4.0) @test sum(weights(g)) == ne(g) * 4.0 - gx = Graph(4,3) + gx = Graph(4, 3) for g in testsimplegraphs(gx) @test eltype(SimpleWeightedGraph(g)) == eltype(g) end - gx = DiGraph(4,3) + gx = DiGraph(4, 3) for g in testsimpledigraphs(gx) @test eltype(SimpleWeightedGraph(g)) == eltype(g) end s = SimpleWeightedGraph(path_graph(5), 2) - s2 = SimpleWeightedGraph([1,2,3,4], [2,3,4,5], [2,2,2,2]) + s2 = SimpleWeightedGraph([1, 2, 3, 4], [2, 3, 4, 5], [2, 2, 2, 2]) @test s == s2 s = SimpleWeightedDiGraph(path_digraph(5), 2) - s2 = SimpleWeightedDiGraph([1,2,3,4], [2,3,4,5], [2,2,2,2]) + s2 = SimpleWeightedDiGraph([1, 2, 3, 4], [2, 3, 4, 5], [2, 2, 2, 2]) @test s == s2 - s = SimpleWeightedGraph([10,20,30,40], [20,30,40,50], [2,2,2,2]) + s = SimpleWeightedGraph([10, 20, 30, 40], [20, 30, 40, 50], [2, 2, 2, 2]) @test size(s.weights) == (50, 50) - s = SimpleWeightedDiGraph([10,20,30,40], [20,30,40,50], [2,2,2,2]) + s = SimpleWeightedDiGraph([10, 20, 30, 40], [20, 30, 40, 50], [2, 2, 2, 2]) @test size(s.weights) == (50, 50) - s = SimpleWeightedGraph([1,2,1], [2,1,2], [1,1,1]; combine = +) - @test s.weights[2,1] == s.weights[1,2] == 3 + s = SimpleWeightedGraph([1, 2, 1], [2, 1, 2], [1, 1, 1]; combine=+) + @test s.weights[2, 1] == s.weights[1, 2] == 3 - s = SimpleWeightedDiGraph([1,2,1], [2,1,2], [1,1,1]; combine = +) - @test s.weights[1,2] == 1 - @test s.weights[2,1] == 2 + s = SimpleWeightedDiGraph([1, 2, 1], [2, 1, 2], [1, 1, 1]; combine=+) + @test s.weights[1, 2] == 1 + @test s.weights[2, 1] == 2 - s = SimpleWeightedDiGraph([1,2,1], [2,1,2], [1,1,2]; combine = (x,y) -> y) - @test s.weights[1,2] == 1 - @test s.weights[2,1] == 2 + s = SimpleWeightedDiGraph([1, 2, 1], [2, 1, 2], [1, 1, 2]; combine=(x, y) -> y) + @test s.weights[1, 2] == 1 + @test s.weights[2, 1] == 2 - @test SimpleDiGraph(SimpleWeightedDiGraph(cycle_graph(4))) == SimpleDiGraph(cycle_graph(4)) + @test SimpleDiGraph(SimpleWeightedDiGraph(cycle_graph(4))) == + SimpleDiGraph(cycle_graph(4)) @test SimpleGraph(SimpleWeightedGraph(path_graph(5))) == path_graph(5) - @test SimpleWeightedGraph(cycle_graph(4)) == SimpleWeightedGraph(SimpleWeightedGraph(cycle_graph(4))) - @test SimpleWeightedDiGraph(cycle_digraph(4)) == SimpleWeightedDiGraph(SimpleWeightedDiGraph(cycle_digraph(4))) + @test SimpleWeightedGraph(cycle_graph(4)) == + SimpleWeightedGraph(SimpleWeightedGraph(cycle_graph(4))) + @test SimpleWeightedDiGraph(cycle_digraph(4)) == + SimpleWeightedDiGraph(SimpleWeightedDiGraph(cycle_digraph(4))) - @test SimpleWeightedDiGraph(Matrix(adjacency_matrix(cycle_digraph(4)))) == SimpleWeightedDiGraph(cycle_digraph(4)) - @test SimpleWeightedDiGraph{Int32}(Matrix(adjacency_matrix(cycle_digraph(4)))) == SimpleWeightedDiGraph{Int32, Float64}(SimpleWeightedDiGraph(cycle_digraph(4))) + @test SimpleWeightedDiGraph(Matrix(adjacency_matrix(cycle_digraph(4)))) == + SimpleWeightedDiGraph(cycle_digraph(4)) + @test SimpleWeightedDiGraph{Int32}(Matrix(adjacency_matrix(cycle_digraph(4)))) == + SimpleWeightedDiGraph{Int32,Float64}(SimpleWeightedDiGraph(cycle_digraph(4))) - @test SimpleWeightedGraph{Int32}(Matrix(adjacency_matrix(cycle_graph(4)))) == SimpleWeightedGraph{Int32, Float64}(SimpleWeightedGraph(cycle_graph(4))) - @test SimpleWeightedGraph{Int32}(adjacency_matrix(cycle_graph(4))) == SimpleWeightedGraph{Int32, Float64}(SimpleWeightedGraph(cycle_graph(4))) + @test SimpleWeightedGraph{Int32}(Matrix(adjacency_matrix(cycle_graph(4)))) == + SimpleWeightedGraph{Int32,Float64}(SimpleWeightedGraph(cycle_graph(4))) + @test SimpleWeightedGraph{Int32}(adjacency_matrix(cycle_graph(4))) == + SimpleWeightedGraph{Int32,Float64}(SimpleWeightedGraph(cycle_graph(4))) @testset "Typed constructors $T" for T in (UInt8, Int32) g = SimpleWeightedGraph(T) @test g isa AbstractGraph{T} - @test g isa SimpleWeightedGraph{T, Float64} + @test g isa SimpleWeightedGraph{T,Float64} dg = SimpleWeightedDiGraph(T) @test dg isa AbstractGraph{T} - @test dg isa SimpleWeightedDiGraph{T, Float64} + @test dg isa SimpleWeightedDiGraph{T,Float64} for U in (Float16, Float32) g = SimpleWeightedGraph(T, U) @test g isa AbstractGraph{T} - @test g isa SimpleWeightedGraph{T, U} + @test g isa SimpleWeightedGraph{T,U} dg = SimpleWeightedDiGraph(T, U) @test dg isa AbstractGraph{T} - @test dg isa SimpleWeightedDiGraph{T, U} + @test dg isa SimpleWeightedDiGraph{T,U} end end @@ -291,11 +310,11 @@ using SimpleWeightedGraphs @test add_edge!(dg, 1, 3, 2.5) @test dg[1, 3, Val{:weight}()] ≈ 2.5 @test dg2[1, 3, Val{:weight}()] ≈ 0 - dg3 = SimpleWeightedDiGraph{Int, Float64}(dg) + dg3 = SimpleWeightedDiGraph{Int,Float64}(dg) @test add_edge!(dg, 1, 4, 3.5) @test dg[1, 4, Val{:weight}()] ≈ 3.5 @test dg3[1, 4, Val{:weight}()] ≈ 0 - g1 = SimpleWeightedGraph{Int, Float64}(dg) + g1 = SimpleWeightedGraph{Int,Float64}(dg) g2 = SimpleWeightedGraph(dg) @test g1 == g2 @test ne(g1) == 5 # 1-2 1-3 2-3 3-4 4-1 @@ -308,7 +327,7 @@ using SimpleWeightedGraphs @test add_edge!(g, 1, 3, 2.5) @test g[1, 3, Val{:weight}()] ≈ 2.5 @test g2[1, 3, Val{:weight}()] ≈ 0.0 - g3 = SimpleWeightedDiGraph{Int, Float64}(g) + g3 = SimpleWeightedDiGraph{Int,Float64}(g) @test add_edge!(g, 1, 4, 3.5) @test g[1, 4, Val{:weight}()] ≈ 3.5 @test g3[1, 4, Val{:weight}()] ≈ 0 @@ -316,10 +335,10 @@ using SimpleWeightedGraphs # copy from undirected to directed g = SimpleWeightedGraph(cycle_graph(4), 0.5) dg = SimpleWeightedDiGraph(g) - @test g[1,3,Val{:weight}()] ≈ 0 + @test g[1, 3, Val{:weight}()] ≈ 0 add_edge!(g, 1, 3, 6.5) - @test g[1,3,Val{:weight}()] ≈ 6.5 - @test dg[1,3,Val{:weight}()] ≈ 0 + @test g[1, 3, Val{:weight}()] ≈ 6.5 + @test dg[1, 3, Val{:weight}()] ≈ 0 dg = SimpleWeightedDiGraph(cycle_digraph(4), 0.5) @test dg[2, 1, Val{:weight}()] ≈ 0