Following a walkthrough of the package and the features. An AttributeGraph is quite straightforwardingly defined by 4 fields:

  • the underlying wrapped graph
  • the vertex attribute data structure
  • the edge attribute data structure
  • the graph attribute data structure

That is because we tried to manufacture the least speculative way to define a such. As this might not be able to do some easy out-of-the-box operations we also proposed a more Opinionated approach

Opinionated approach

The default operation of OAttributeGraphs is an opinionated one. By calling OAttributeGraph(), you are already within the realm of our preferences.

julia> using AttributeGraphs, Graphs

julia> OAttributeGraph() |> typeof
AttributeGraph{Int64, SimpleGraph{Int64}, Vector{Missing}, Dict{Tuple{Int64, Int64, Int64}, Missing}, Missing}

This approach treats graph data as following:

  • The vertex attributes data structure is a vector.

The index of the vector corresponds to the index of the vertex. This way we want to be closer to the Graphs.jl implementation.

  • The edge attributes data structure is a Dict{Tuple{Int, Int, Int}}.

Non surpisingly, each edge attribute is indexed by the source and destination node. The third integer is the multiplicity for MultiGraphs and if non given, 1 is assumed.

  • The graph attributes data structure is non existent. The user type is directly what it is.

However some functionality is provided from {add,rem,has,get}graphattr[!] for AbstractDict. For trivial situation you should directly use graph_attr.

Following we define a AttributeGraph with vertex and edge attributes of String and graph attributes of Dict{Symbol, String}

julia> oag = OAttributeGraph(;vertex_type=String, edge_type=String, graph_type=Dict{Symbol, String})
{0, 0} undirected attribute Int64 graph

You will not be surpised to see that probably all of the obvious operations are supported.

For example starting with the graph attributes

julia> addgraphattr!(oag, :ena, "ena")

julia> addgraphattr!(oag, :dio, "dio")

julia> graph_attr(oag)
Dict{Symbol, String} with 2 entries:
  :ena => "ena"
  :dio => "dio"

Now let's add some vertices and edges. Note we use the addvertex!, addedge! and friends instead of add_vertex!, add_edge! and friends to be 100% consistent. The second ones will also work, but you sooner or later you will reach a corrupted state.

julia> foreach(_ -> addvertex!(oag), 1:10) # add 10 nodes

julia> foreach(x -> addedge!(oag,x[1],x[2]), [(1,2), (1,3), (3,4), (1,5), (7,8)]) # add some edges

addvertex! immeadiately adds missing to the vertex attribute:

julia> vertex_attr(oag)
10-element Vector{Union{Missing, String}}:
julia> addvertexattr!(oag, 4, "customattr4")

julia> vertex_attr(oag)
10-element Vector{Union{Missing, String}}:

Similar for edge attributes:

julia> addedgeattr!(oag, 1, 2, "data_for_1-2")

julia> addedgeattr!(oag, 3, 4, "data_for_3-4")

julia> edge_attr(oag)
Dict{Tuple{Int64, Int64, Int64}, String} with 2 entries:
  (1, 2, 1) => "data_for_1-2"
  (3, 4, 1) => "data_for_3-4"

Deleting a node will automatically update the indices and attributes

julia> remvertex!(oag, 2)

julia> edge_attr(oag)
Dict{Tuple{Int64, Int64, Int64}, String} with 1 entry:
  (2, 3, 1) => "data_for_3-4"

General approach

Basically, AttributeGraph is a parametric type that can be anything. So you can customize it to do whatever you want. You might say that the lack of features is the basic feature here.

The basic use case for this is that you might often have some data data accompanying your graph graph. You don't want to always pass around a tuple of (graph, data). Instead, you can simply use this package to load data in a AttributeGraph and pass it around more compactly.

An outrageous example follows

julia> using DataStructures, Random, Test

julia> Random.seed!(0); # for reproducibility

julia> gag = AttributeGraph(SimpleGraph(), (v) -> v+randn(), DefaultDict{Tuple{Int, Int, String},Float64}(10.0), missing)
{0, 0} undirected attribute Int64 graph

Here the vertex attributes are given by a normally distributed random function with mean the vertex index. The edge attributes are a default dictionary indexed by a tuple {Int, Int, String} The graph attributes are non-existant so we pass in missing.

The following function will return our initializations

julia> vertex_attr(gag); # returns a function

julia> edge_attr(gag)
DataStructures.DefaultDict{Tuple{Int64, Int64, String}, Float64, Float64}()

julia> graph_attr(gag)

We can continue on constructing our graph

julia> add_vertices!(gag, 10)

julia> foreach(v -> add_edge!(gag, v, v+1), 1:nv(gag))

julia> gag 
{10, 9} undirected attribute Int64 graph

You can query right away the vertex stochastic attributes

julia> [vertex_attr(gag)(v) for v in vertices(gag)]
10-element Vector{Float64}:

I hear you saying: "But I can even do vertex_attr(gag)(1235)" and it will still return a value even though the vertex doesn't exist. True. You can move on to customize your approach if you want that to fail. Eg:

julia> myvertexattr(ag::AttributeGraph, x) = has_vertex(ag, x) ? vertex_attr(gag)(x) : error("no attribute")
myvertexattr (generic function with 1 method)
julia> myvertexattr(gag, 4)

julia> @test_throws Exception myvertexattr(gag, 123) # now it throws an error
Test Passed
      Thrown: ErrorException

Similarly you will need to manually fill up the attributes for the graph edge, e.g.:

julia> let e = first(edges(gag))
           edge_attr(gag)[src(e), dst(e), "class1"] = 100
           edge_attr(gag)[src(e), dst(e), "class2"] = 200

julia> edge_attr(gag)
DefaultDict{Tuple{Int64, Int64, String}, Float64, Float64} with 2 entries:
  (1, 2, "class1") => 100.0
  (1, 2, "class2") => 200.0

Since we decided to use a DefaultDict though you can still query other edges

julia> edge_attr(gag)[1, 4, ""]

The general approach is mostly useful for customizable or quick and dirty situations.