Skip to content

Commit

Permalink
Merge pull request #625 from epatters/graphviz-cats
Browse files Browse the repository at this point in the history
Improved Graphviz drawing of graphs and categories
  • Loading branch information
epatters committed Apr 25, 2022
2 parents dc06cce + d4149f5 commit cfde988
Show file tree
Hide file tree
Showing 19 changed files with 330 additions and 187 deletions.
9 changes: 2 additions & 7 deletions benchmark/Graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import MetaGraphs
const LG, MG = SimpleGraphs, MetaGraphs

using Catlab, Catlab.CategoricalAlgebra, Catlab.Graphs
using Catlab.Graphs.BasicGraphs: TheoryGraph
using Catlab.Graphs.BasicGraphs: TheoryGraph, TheoryLabeledGraph
using Catlab.WiringDiagrams: query
using Catlab.Programs: @relation

Expand Down Expand Up @@ -304,13 +304,8 @@ bench = SUITE["LabeledGraph"] = BenchmarkGroup()
clbench = bench["Catlab"] = BenchmarkGroup()
lgbench = bench["LightGraphs"] = BenchmarkGroup()

@present TheoryLabeledGraph <: TheoryGraph begin
Label::AttrType
label::Attr(V,Label)
end
@acset_type LabeledGraph(TheoryLabeledGraph, index=[:src,:tgt]) <: AbstractGraph
@acset_type IndexedLabeledGraph(TheoryLabeledGraph, index=[:src,:tgt],
unique_index=[:label]) <: AbstractGraph
unique_index=[:label]) <: AbstractLabeledGraph

function discrete_labeled_graph(n::Int; indexed::Bool=false)
g = (indexed ? IndexedLabeledGraph{String} : LabeledGraph{String})()
Expand Down
13 changes: 0 additions & 13 deletions docs/literate/graphics/graphviz_schema_visualization.jl

This file was deleted.

5 changes: 2 additions & 3 deletions docs/literate/graphs/graphs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ using Catlab.CategoricalAlgebra
using Catlab.Graphs
using Catlab.Graphics
using Catlab.Graphics.Graphviz
#import Catlab.Graphics.Graphviz: to_graphviz, to_graphviz_property_graph

using Colors
draw(g) = to_graphviz(g, node_labels=true, edge_labels=true)

GraphvizGraphs.to_graphviz(f::ACSetTransformation; kw...) =
to_graphviz(GraphvizGraphs.to_graphviz_property_graph(f; kw...))
to_graphviz(to_graphviz_property_graph(f; kw...))

function GraphvizGraphs.to_graphviz_property_graph(f::ACSetTransformation; kw...)
pg = GraphvizGraphs.to_graphviz_property_graph(dom(f); kw...)
pg = to_graphviz_property_graph(dom(f); kw...)
vcolors = hex.(range(colorant"#0021A5", stop=colorant"#FA4616", length=nparts(codom(f), :V)))
ecolors = hex.(range(colorant"#6C9AC3", stop=colorant"#E28F41", length=nparts(codom(f), :E)))
hex.(colormap("Oranges", nparts(codom(f), :V)))
Expand Down
16 changes: 10 additions & 6 deletions docs/literate/graphs/graphs_label.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# # Labelled Graphs
# # Labeled Graphs
# This example demonstrates how to define new C-Sets from existing C-Sets via the example of adding labels to a graph. We treat labels as members of an arbitrary FinSet of labels rather than a data attribute for pedagogical reasons. When you think of graphs where the labels are numbers, colors, or values of some kind, you would want to make them attributes. The motivation for this example is to be the simplest extension to the theory of graphs that you could possibly make.

using Catlab, Catlab.Theories
Expand All @@ -17,15 +17,19 @@ draw(g) = to_graphviz(g, node_labels=true, edge_labels=true)
tgt::Hom(E,V)
end

to_graphviz(Catlab.Graphs.BasicGraphs.TheoryGraph)
to_graphviz(TheoryGraph)

# To the theory of graphs we want to add a set of labels `L` and map that assigns to every vertex to its label in `L`.
@present TheoryLGraph <: TheoryGraph begin
L::Ob
label::Hom(V,L)
end

# Catlab will automatically generate all the data structure and algorithms (storing, mutating, serializing, etc.) our `LGraphs` for us. This snippet declares that the Julia type `LGraph` should be composed of objects of the functor category `TheoryLGraph → Skel(FinSet)`, where `Skel(FinSet)` is the subcategory of Set containing finite sets `1:n`. We want our Julia type `LGraph` to inherit from the Graphs.jl type `AbstractGraph` so that we can run graph algorithms on it. And we want the generated data structures to make an index of the maps `src`, `tgt`, and `label` so that looking up the in and outneighbors of vertex is fast and accessing all vertices by label is also fast.
to_graphviz(TheoryLGraph)

# Catlab will automatically generate all the data structure and algorithms (storing, mutating, serializing, etc.) our `LGraphs` for us. This snippet declares that the Julia type `LGraph` should be composed of objects of the functor category `TheoryLGraph → Skel(FinSet)`, where `Skel(FinSet)` is the subcategory of Set containing finite sets of form `1:n`. We want our Julia type `LGraph` to inherit from the type `AbstractGraph` so that we can run graph algorithms on it. And we want the generated data structures to make an index of the maps `src`, `tgt`, and `label` so that looking up the in and outneighbors of vertex is fast and accessing all vertices by label is also fast.
#
# **Note**: This schema differs from that of `LabeledGraph` in `Catlab.Graphs` by making the label type an object (`Ob`) rather than attribute type (`AttrType`). In this case, the set of labels can vary from instance to instance and homomorphisms can rename labels; in the other case, the set of labels is fixed by a Julia type, such as `Int` or `Symbol`, and label values must be strictly preserved homomorphisms. The graph theory literature does not always distinguish very carefully between these two cases.

@acset_type LGraph(TheoryLGraph, index=[:src,:tgt,:label]) <: AbstractGraph

Expand All @@ -40,11 +44,11 @@ end

# Graphviz doesn't automatically know how we want to draw the labels, so we have to explicitly provide code that converts them to colors on the vertices. Note that we aren't calling these colored graphs, because that would imply some connectivity constraints on which vertices are allowed to be colored with the same colors. These labels are arbitrary, but we use color to visually encode them.
GraphvizGraphs.to_graphviz(g::LGraph; kw...) =
to_graphviz(GraphvizGraphs.to_graphviz_property_graph(g; kw...))
to_graphviz(to_graphviz_property_graph(g; kw...))

function GraphvizGraphs.to_graphviz_property_graph(g::LGraph; kw...)
h = to_graph(g)
pg = GraphvizGraphs.to_graphviz_property_graph(h; kw...)
pg = to_graphviz_property_graph(h; kw...)
vcolors = hex.(range(colorant"#0021A5", stop=colorant"#FA4616", length=nparts(g, :L)))
for v in vertices(g)
l = g[v, :label]
Expand Down Expand Up @@ -131,7 +135,7 @@ A₀ = to_graph(A)
homsᵥ(A₀, A₀)

# ## Limits and Composition by Multiplication
# Catlab has an implementation of limits for any C-Sets over any schema. So, we can just ask about labelled graphs. Notice that we get more distinct colors in the product than in either initial graph. This is because the labels of the product are pairs of labels from the factors. If `G` has `n` colors and `H` has `m` colors `G×H` will have `n×m` colors.
# Catlab has an implementation of limits for any C-Sets over any schema. So, we can just ask about labeled graphs. Notice that we get more distinct colors in the product than in either initial graph. This is because the labels of the product are pairs of labels from the factors. If `G` has `n` colors and `H` has `m` colors `G×H` will have `n×m` colors.
draw(apex(product(G,G)))

# The graph above looks weirdly disconnected and probably wasn't what you expected to see as the product. When we compose with products, we often want to add the reflexive edges in order to get the expected notion of product.
Expand Down
10 changes: 4 additions & 6 deletions docs/literate/sketches/cat_elements.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ else
return [hex.(start)]
end

GraphvizGraphs.to_graphviz(f::Elements; kw...) =
to_graphviz(GraphvizGraphs.to_graphviz_property_graph(f; kw...))

function GraphvizGraphs.to_graphviz_property_graph(f::Elements; kw...)
pg = GraphvizGraphs.to_graphviz_property_graph(graph(f); kw...)
function draw(f::Elements; kw...)
pg = to_graphviz_property_graph(graph(f);
node_labels=true, edge_labels=true, prog="neato", kw...)
vcolors = safecolors(colorant"#0021A5", colorant"#FA4616", nparts(f, :Ob))
ecolors = safecolors(colorant"#6C9AC3", colorant"#E28F41", nparts(f, :Hom))
for v in parts(f, :El)
Expand All @@ -36,7 +34,7 @@ function GraphvizGraphs.to_graphviz_property_graph(f::Elements; kw...)
fe = f[e, :πₐ]
set_eprops!(pg, e, Dict(:color => "#$(ecolors[fe])"))
end
pg
to_graphviz(pg)
end

draw(g) = to_graphviz(g, node_labels=true, edge_labels=true, prog="neato")
Expand Down
1 change: 0 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ makedocs(
"generated/graphics/graphviz_wiring_diagrams.md",
"generated/graphics/tikz_wiring_diagrams.md",
"generated/graphics/layouts_vs_drawings.md",
"generated/graphics/graphviz_schema_visualization.md",
],
],
"Modules" => Any[
Expand Down
10 changes: 10 additions & 0 deletions src/graphics/Graphviz.jl
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ Base.@kwdef struct Label <: Statement
label::String = ""
end

# Useful in unit tests. Not exported.

function filter_statements(graph::Graph, type::Type)
[ stmt for stmt in graph.stmts if stmt isa type ]
end
function filter_statements(graph::Graph, type::Type, attr::Symbol)
[ stmt.attrs[attr] for stmt in graph.stmts
if stmt isa type && haskey(stmt.attrs, attr) ]
end

# Bindings
##########

Expand Down
107 changes: 104 additions & 3 deletions src/graphics/GraphvizCategories.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,93 @@
""" Graphviz drawing of categories, functors, diagrams, and related structures.
"""
module GraphvizCategories
export to_graphviz
export to_graphviz, to_graphviz_property_graph

using ...Syntax, ...Present, ...Theories
using ...CategoricalAlgebra, ...Graphs, ..GraphvizGraphs
import ..Graphviz
import ..GraphvizGraphs: to_graphviz, to_graphviz_property_graph

function to_graphviz(F::FinFunctor; kw...)
to_graphviz(to_graphviz_property_graph(F; kw...))
# Presentations
###############

to_graphviz(pres::Presentation, args...; kw...) =
to_graphviz(to_graphviz_property_graph(pres, args...; kw...))

function to_graphviz_property_graph(pres::Presentation, Ob::Symbol, Hom::Symbol;
prog::AbstractString="neato", graph_attrs::AbstractDict=Dict(),
node_attrs::AbstractDict=Dict(), edge_attrs::AbstractDict=Dict())
pg = PropertyGraph{Any}(; prog = prog,
graph = graph_attrs,
node = merge!(Dict(:shape => "ellipse", :margin => "0"), node_attrs),
edge = edge_attrs,
)

obs = generators(pres, Ob)
add_vertices!(pg, length(obs))
for (v, ob) in enumerate(obs)
set_vprop!(pg, v, :label, string(first(ob)))
end

homs = generators(pres, Hom)
add_edges!(pg, map(f -> generator_index(pres, first(gat_type_args(f))), homs),
map(f -> generator_index(pres, last(gat_type_args(f))), homs))
for (e, hom) in enumerate(homs)
set_eprop!(pg, e, :label, string(first(hom)))
end
pg
end

# Categories
############

function to_graphviz_property_graph(pres::Presentation{Category}; kw...)
to_graphviz_property_graph(pres, :Ob, :Hom; kw...)
end

function to_graphviz_property_graph(pres::Presentation{MCategory};
tight_attrs::AbstractDict=Dict(:arrowhead => "empty"), kw...)
pg = to_graphviz_property_graph(pres, :Ob, :Hom; kw...)
for tight_hom in generators(pres, :Tight)
e = generator_index(pres, hom(tight_hom))
set_eprops!(pg, e, tight_attrs)
end
pg
end

to_graphviz(X::AbstractElements; kw...) =
to_graphviz(to_graphviz_property_graph(X; kw...))

function to_graphviz_property_graph(X::AbstractElements;
prog::AbstractString="neato", graph_attrs::AbstractDict=Dict(),
node_attrs::AbstractDict=Dict(), edge_attrs::AbstractDict=Dict(),
node_labels::Bool=false, edge_labels::Bool=false)
pg = PropertyGraph{Any}(; prog = prog,
graph = graph_attrs,
node = merge!(Dict(:shape => "box", :width => "0", :height => "0",
:margin => "0.025"), node_attrs),
edge = edge_attrs)

add_vertices!(pg, nparts(X, :El))
add_edges!(pg, X[:src], X[:tgt])

for v in parts(X, :El)
ob_name = X[X[v, :πₑ], :nameo]
set_vprop!(pg, v, :label, node_labels ? "$v:$ob_name" : "$ob_name")
end
for e in parts(X, :Arr)
hom_name = X[X[e, :πₐ], :nameh]
set_eprop!(pg, e, :label, edge_labels ? "$e:$hom_name" : "$hom_name")
end
pg
end

# Diagrams
##########

to_graphviz(F::FinFunctor; kw...) =
to_graphviz(to_graphviz_property_graph(F; kw...))

function to_graphviz_property_graph(F::FinFunctor; kw...)
g = graph(dom(F))
pg = to_graphviz_property_graph(g; kw...)
Expand All @@ -26,4 +103,28 @@ function to_graphviz_property_graph(F::FinFunctor; kw...)
pg
end

# Schemas
#########

function to_graphviz_property_graph(pres::Presentation{Schema}; kw...)
pg = to_graphviz_property_graph(pres, :Ob, :Hom; kw...)
ob_vertices = vertices(pg)
hom_edges = edges(pg)

attr_types = generators(pres, :AttrType)
attr_vertices = add_vertices!(pg, length(attr_types))
for (v, attr_type) in zip(attr_vertices, attr_types)
set_vprops!(pg, v, xlabel=string(first(attr_type)), shape="point")
end

attrs = generators(pres, :Attr)
attr_edges = add_edges!(pg,
map(attr -> ob_vertices[generator_index(pres, dom(attr))], attrs),
map(attr -> attr_vertices[generator_index(pres, codom(attr))], attrs))
for (e, attr) in zip(attr_edges, attrs)
set_eprop!(pg, e, :label, string(first(attr)))
end
pg
end

end
Loading

0 comments on commit cfde988

Please sign in to comment.