-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DOC: Labelled Graph example with (co)limits
- Loading branch information
1 parent
d2c1bf8
commit 1a759fd
Showing
2 changed files
with
225 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
# # Labelled 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 | ||
using Catlab.CategoricalAlgebra | ||
using Catlab.Graphs | ||
using Catlab.Graphics | ||
using Catlab.Graphics.Graphviz | ||
using Colors | ||
draw(g) = to_graphviz(g, node_labels=true, edge_labels=true) | ||
|
||
# We start with the theory of graphs, which is copied from `Catlab.Graphs.BasicGraphs`. The two objects are the edges and vertices and we have two functions `src,tgt` that assign to every edge the source vertex and target vertex respectively. Functors from this category to Set (diagrams in Set of this shape) are category theoretic graphs (directed multigraphs). | ||
@present TheoryGraph(FreeSchema) begin | ||
V::Ob | ||
E::Ob | ||
src::Hom(E,V) | ||
tgt::Hom(E,V) | ||
end | ||
|
||
to_graphviz(Catlab.Graphs.BasicGraphs.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. | ||
|
||
@acset_type LGraph(TheoryLGraph, index=[:src,:tgt,:label]) <: AbstractGraph | ||
|
||
# We need to tell Catlab how to convert our `LGraph` type into the normal `Graph` type by taking just the edges and vertices. This could be computed with Functorial Data Migration, but that is left for another sketch. | ||
to_graph(g::LGraph) = begin | ||
h = Graphs.Graph(nparts(g,:V)) | ||
for e in edges(g) | ||
add_edge!(h, g[e, :src], g[e,:tgt]) | ||
end | ||
return h | ||
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...)) | ||
|
||
function GraphvizGraphs.to_graphviz_property_graph(g::LGraph; kw...) | ||
h = to_graph(g) | ||
pg = GraphvizGraphs.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] | ||
set_vprops!(pg, v, Dict(:color => "#$(vcolors[l])")) | ||
end | ||
pg | ||
end | ||
|
||
draw(G::LGraph) = to_graphviz(G, node_labels=true, edge_labels=true) | ||
|
||
# Now we can start making some `LGraph` instances. | ||
G = @acset LGraph begin | ||
V = 4 | ||
E = 4 | ||
L = 4 | ||
src = [1,1,2,3] | ||
tgt = [2,3,4,4] | ||
label = [1,2,3,4] | ||
end | ||
|
||
draw(G) | ||
|
||
# The graph `G` has a 1-1 map between vertices and labels. That isn't necessary. | ||
H = @acset LGraph begin | ||
V = 4 | ||
E = 4 | ||
L = 3 | ||
src = [1,1,2,3] | ||
tgt = [2,3,4,4] | ||
label = [1,2,2,3] | ||
end | ||
|
||
draw(H) | ||
|
||
|
||
# We can look at some homomorphisms from G to H by their action on the labels or on the vertices. | ||
homsᵥ(G,H) = map(homomorphisms(G, H)) do α | ||
components(α).V | ||
end | ||
|
||
homsₗ(G,H) = map(homomorphisms(G, H)) do α | ||
components(α).L | ||
end | ||
|
||
# αₗ: G→ H | ||
homsₗ(G,H) | ||
|
||
# αᵥ: G→ H | ||
homsᵥ(G,H) | ||
|
||
# Note that if we reverse the direction of our homomorphism search, we get fewer matches even though the two `LGraph`s are isomorphic as graphs. The fact that in `H` vertex two and three are the same label means we have to send them to the same vertex in `G`. | ||
homsᵥ(H,G) | ||
|
||
# We can build some bigger examples like A. | ||
A = @acset LGraph begin | ||
V = 6 | ||
E = 7 | ||
L = 3 | ||
src = [1,1,2,3,2,5,4] | ||
tgt = [2,3,4,4,5,6,6] | ||
label = [1,2,2,3,2,3] | ||
end | ||
draw(A) | ||
|
||
# and B. | ||
B = @acset LGraph begin | ||
V = 6 | ||
E = 7 | ||
L = 4 | ||
src = [1,1,2,3,2,5,4] | ||
tgt = [2,3,4,4,5,6,6] | ||
label = [1,2,4,3,2,3] | ||
end | ||
draw(B) | ||
|
||
# The morphisms from A to B and B to A are also different, showing how the labels affect the structure in this category. | ||
homsᵥ(A,B) | ||
|
||
# There are more morphisms from B to A than A to B. | ||
homsᵥ(B,A) | ||
|
||
# There are two automorphisms on A | ||
homsᵥ(A,A) | ||
|
||
# And two automorphisms on B | ||
homsᵥ(B,B) | ||
|
||
# But if we forget about the labels and look at the automorphisms of the underlying graph, we get more automorphisms! | ||
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. | ||
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. | ||
add_loops!(G::LGraph) = begin | ||
for v in parts(G,:V) | ||
add_edge!(G, v,v) | ||
end | ||
return G | ||
end | ||
add_loops(G::LGraph) = add_loops!(copy(G)) | ||
Gᵣ = add_loops(G) | ||
P = apex(product(Gᵣ, G)) | ||
draw(apex(product(Gᵣ, G))) | ||
|
||
# We can look at the shape of commuting triangle, which is our favorite 3-vertex graph. | ||
T = @acset LGraph begin | ||
V = 3 | ||
E = 3 | ||
L = 3 | ||
src = [1,1,2] | ||
tgt = [2,3,3] | ||
label = [1,2,3] | ||
end | ||
Tᵣ = add_loops(T) | ||
draw(Tᵣ) | ||
|
||
E = @acset LGraph begin | ||
V = 2 | ||
E = 1 | ||
L = 2 | ||
src = [1] | ||
tgt = [2] | ||
label = [1,2] | ||
end | ||
Eᵣ = add_loops(E) | ||
draw(Eᵣ) | ||
|
||
# We can draw the product of the edge graph and the triangle graph to get the shape of a triangular prism. You can view this product as extruding `Tᵣ` along `Eᵣ`. Catlab provides a `ReflexiveGraph` as a type that handles these self-loops more intelligently than we are here. Graphviz struggles with the layout here because the product graph will include edges that are a step in both directions. This [blog post](https://www.algebraicjulia.org/blog/post/2021/04/cset-graphs-3/) does a good job explaining products in differnt kinds of graph categories. | ||
draw(apex(product(Tᵣ,Eᵣ))) | ||
components(legs(product(Tᵣ, Eᵣ))[1]).V |> collect | ||
# Another limit is the pullback. If you have a cospan, which is a diagram of the shape `X ⟶ A ⟵ Y`, you can pull back one arrow along the other by solving a system of equations. | ||
PB₂₂ = pullback(homomorphisms(Tᵣ,Eᵣ)[2],homomorphisms(Tᵣ,Eᵣ)[2]); | ||
draw(apex(PB₂₂)) | ||
|
||
# Note that the pullback depends not only on X,A,Y but also on the two arrows. You can play around with the choice of morphisms to gain an intuition of how the pullback depends on the morphisms. | ||
PB₂₃ = pullback(homomorphisms(Tᵣ,Eᵣ)[2],homomorphisms(Tᵣ,Eᵣ)[3]); | ||
draw(apex(PB₂₃)) | ||
|
||
# By constructions, the pullback is always a subobject (monic homomorphism) into the product. Catlab can enumerate all such monoic homs. | ||
homomorphisms(apex(PB₂₂), apex(product(Tᵣ,Tᵣ)), monic=true) |> length | ||
|
||
# ## Colimits and Composition by Glueing | ||
# The dual concept to limits is colimits and if limits have vibes of taking all pairs that satisfy certain constraints, colimits have the vibes of designers just gluing stuff together to make it work. | ||
|
||
# In order to illustrate this we will be gluing triangles together to make a mesh. We start by defining the point `X`, which is the shape of the boundary along which we will glue and the morphism `ℓ₁`, which is the place in `T` that we consider as the boundary. | ||
X = @acset LGraph begin | ||
E = 0 | ||
V = 1 | ||
L = 3 | ||
label=[2] | ||
end | ||
ℓ₁ = ACSetTransformation(X,T, V=[2],L=1:3) | ||
draw(X) | ||
|
||
# We have to check that the morphism is valid before we go and compute out pushout. | ||
is_natural(ℓ₁) | ||
P = pushout(ℓ₁, ℓ₁) | ||
draw(apex(P)) | ||
|
||
# Now we want to repeat the gluing process to get a bigger mesh. So we are going to need a bigger interface. | ||
I = @acset LGraph begin | ||
V = 2 | ||
L = 3 | ||
label = [1,1] | ||
end | ||
draw(I) | ||
|
||
# We have to specify how this interface embeds into both of the things we want to glue. In this case we are gluing a copy of `P` onto itself. | ||
ll = ACSetTransformation(I, apex(P), V=[3,5], L =[3,1,2]) | ||
is_natural(ll) | ||
lr = ACSetTransformation(I, apex(P), V=[1,4], L =[1,3,2]) | ||
is_natural(lr) | ||
P₂ = pushout(ll, lr); | ||
draw(apex(P₂)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters