Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/Manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622"
17 changes: 7 additions & 10 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -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],
Expand All @@ -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")
60 changes: 11 additions & 49 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.
90 changes: 90 additions & 0 deletions docs/src/tutorial.md
Original file line number Diff line number Diff line change
@@ -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
```
152 changes: 29 additions & 123 deletions src/SimpleWeightedGraphs.jl
Original file line number Diff line number Diff line change
@@ -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
Loading