Drawing wiring diagrams in Graphviz

Catlab can draw wiring diagrams using Graphviz. Directed wiring diagrams are drawn using the dot program and undirected wiring diagrams using neato and fdp. This feature requires that Graphviz be installed, but does not require any additional Julia packages.

using Catlab.WiringDiagrams, Catlab.Graphics

Directed wiring diagrams

Symmetric monoidal category

using Catlab.Theories

A, B = Ob(FreeSymmetricMonoidalCategory, :A, :B)
f = Hom(:f, A, B)
g = Hom(:g, B, A)
h = Hom(:h, otimes(A,B), otimes(A,B));

To start, here are a few very simple examples.

to_graphviz(f)
G n1 f n0in1:s->n1:n n1:s->n0out1:n
to_graphviz(compose(f,g))
G n1 f n0in1:s->n1:n n2 g n1:s->n2:n n2:s->n0out1:n
to_graphviz(otimes(f,g))
G n1 f n0in1:s->n1:n n2 g n0in2:s->n2:n n1:s->n0out1:n n2:s->n0out2:n

In the next example, notice how Graphviz automatically "untwists" the double braiding to minimize edge crossings.

to_graphviz(compose(braid(A,A), otimes(f,f), braid(B,B)))
G n2 f n0in1:s->n2:n n1 f n0in2:s->n1:n n1:s->n0out2:n n2:s->n0out1:n

Here is a larger composite morphism.

composite = compose(otimes(g,f), h, otimes(f,g))
to_graphviz(composite)
G n1 g n0in1:s->n1:n n2 f n0in2:s->n2:n n3 h n1:s->n3:n n2:s->n3:n n4 f n3:s->n4:n n5 g n3:s->n5:n n4:s->n0out1:n n5:s->n0out2:n

By default, the wiring diagram is laid out from top to bottom. Other layout orientations can be requested, such as left-to-right or bottom-to-top:

to_graphviz(composite, orientation=LeftToRight)
G n1 g n0in1:e->n1:w n2 f n0in2:e->n2:w n3 h n1:e->n3:w n2:e->n3:w n4 f n3:e->n4:w n5 g n3:e->n5:w n4:e->n0out1:w n5:e->n0out2:w
to_graphviz(composite, orientation=BottomToTop)
G n1 g n0in1:n->n1:s n2 f n0in2:n->n2:s n3 h n1:n->n3:s n2:n->n3:s n4 f n3:n->n4:s n5 g n3:n->n5:s n4:n->n0out1:s n5:n->n0out2:s

When working with very large diagrams (larger than the ones shown here), it is sometimes convenient to omit the ports of the outer box and any wires attached to them.

to_graphviz(composite, outer_ports=false)
G n1 g n3 h n1:s->n3:n n2 f n2:s->n3:n n4 f n3:s->n4:n n5 g n3:s->n5:n

Biproduct category

A, B = Ob(FreeBiproductCategory, :A, :B)
f = Hom(:f, A, B)
g = Hom(:g, B, A);

By default, copies and merges are drawn the way they are represented internally, as multiple wires.

f1 = compose(mcopy(A), otimes(f,f))
to_graphviz(f1)
G n1 f n0in1:s->n1:n n2 f n0in1:s->n2:n n1:s->n0out1:n n2:s->n0out2:n
f2 = compose(mcopy(A), otimes(f,f), mmerge(B))
to_graphviz(f2)
G n1 f n0in1:s->n1:n n2 f n0in1:s->n2:n n1:s->n0out1:n n2:s->n0out1:n

To draw nodes for copies and merges, we need to add junctions to the wiring diagram.

to_graphviz(add_junctions!(to_wiring_diagram(f1)))
G n3 n0in1:s->n3 n1 f n1:s->n0out1:n n2 f n2:s->n0out2:n n3->n1:n n3->n2:n
to_graphviz(add_junctions!(to_wiring_diagram(f2)))
G n3 n0in1:s->n3 n1 f n4 n1:s->n4 n2 f n2:s->n4 n3->n1:n n3->n2:n n4->n0out1:n

Traced monoidal category

A, B, X, Y = Ob(FreeTracedMonoidalCategory, :A, :B, :X, :Y)
f = Hom(:f, otimes(X,A), otimes(X,B))

to_graphviz(trace(X, A, B, f))
G n1 f n0in1:s->n1:n n1:s->n0out1:n n1:s->n1:n
to_graphviz(trace(X, A, B, f), orientation=LeftToRight)
G n1 f n0in1:e->n1:w n1:e->n0out1:w n1:e->n1:w
g, h = Hom(:g, A, A), Hom(:h, B, B)

trace_naturality = trace(X, A, B, compose(otimes(id(X),g), f, otimes(id(X),h)))
to_graphviz(trace_naturality, orientation=LeftToRight)
G n1 g n0in1:e->n1:w n2 f n1:e->n2:w n2:e->n2:w n3 h n2:e->n3:w n3:e->n0out1:w

Undirected wiring diagrams

The composite of two binary relations:

using Catlab.Programs: @relation

diagram = @relation (x,z) where (x,y,z) begin
    R(x,y)
    S(y,z)
end
to_graphviz(diagram, box_labels=:name)
G n1 R n5 n1--n5 n6 n1--n6 n2 S n2--n6 n7 n2--n7 n3--n5 n4--n7

A "wheel"-shaped composition of relations:

diagram = @relation (x,y,z) where (w,x,y,z) begin
    R(x,w)
    S(y,w)
    T(z,w)
end
to_graphviz(diagram, box_labels=:name)
G n1 R n7 n1--n7 n8 n1--n8 n2 S n2--n7 n9 n2--n9 n3 T n3--n7 n10 n3--n10 n4--n8 n5--n9 n6--n10

As these examples show, the box_labels keyword argument specifies the data attribute of boxes to use for box labels, if any. The boolean argument port_labels controls the labeling of ports by numerical values and the argument junction_labels specifies the data attribute of junctions to use for junction labels. Note that the macro @relation creates wiring diagrams with name attribute for boxes and variable attribute for junctions.

to_graphviz(diagram, box_labels=:name,
            port_labels=false, junction_labels=:variable)
G n1 R n7 w n1--n7 n8 x n1--n8 n2 S n2--n7 n9 y n2--n9 n3 T n3--n7 n10 z n3--n10 n4--n8 n5--n9 n6--n10

By default, all junctions are shown. The keyword argument implicit_junctions omits any junctions which have exactly two incident ports.

to_graphviz(diagram, box_labels=:name,
            port_labels=false, implicit_junctions=true)
G n1 R n1--n4 n7 n1--n7 n2 S n2--n5 n2--n7 n3 T n3--n6 n3--n7

Custom styles

The visual appearance of wiring diagrams can be customized by setting Graphviz attributes at the graph, node, edge, and cell levels. Graph, node, and edge attributes are described in the Graphviz documentation. Cell attributes are passed to the primary cell of the HTML-like label used for the boxes.

A, B, C = Ob(FreeSymmetricMonoidalCategory, :A, :B, :C)
f, g = Hom(:f, A, B), Hom(:g, B, C)

to_graphviz(compose(f,g),
  labels = true, label_attr=:headlabel,
  node_attrs = Dict(
    :fontname => "Courier",
  ),
  edge_attrs = Dict(
    :fontname => "Courier",
    :labelangle => "25",
    :labeldistance => "2",
  ),
  cell_attrs = Dict(
    :bgcolor => "lavender",
  )
)
G n1 f n0in1:s->n1:n A n2 g n1:s->n2:n B n2:s->n0out1:n C

Output formats

The function to_graphviz returns an object of a type Graphviz.Graph, representing a Graphviz graph as an abstract syntax tree. When displayed interactively, this object is automatically run through Graphviz and rendered as an SVG image. Sometimes it is convenient to perform this process manually, to change the output format or further customize the generated dot file.

To generate a dot file, use the builtin pretty-printer. This feature does not require Graphviz to be installed.

using Catlab.Graphics: Graphviz

graph = to_graphviz(compose(f,g))
Graphviz.pprint(graph)
digraph G {
  graph [fontname="Serif",rankdir="TB"];
  node [fontname="Serif",shape="none",width="0",height="0",margin="0"];
  edge [arrowsize="0.5",fontname="Serif"];
  {
    graph [rank="source",rankdir="LR"];
    node [style="invis",shape="none",label="",width="0.333",height="0"];
    edge [style="invis"];
    n0in1 [id="in1"];
  }
  {
    graph [rank="sink",rankdir="LR"];
    node [style="invis",shape="none",label="",width="0.333",height="0"];
    edge [style="invis"];
    n0out1 [id="out1"];
  }
  n1 [comment="f",id="n1",label=<<TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
<TR><TD><TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0"><TR><TD HEIGHT="0" WIDTH="24" PORT="in1"></TD></TR></TABLE></TD></TR>
<TR><TD BORDER="1" CELLPADDING="4">f</TD></TR>
<TR><TD><TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0"><TR><TD HEIGHT="0" WIDTH="24" PORT="out1"></TD></TR></TABLE></TD></TR>
</TABLE>>];
  n2 [comment="g",id="n2",label=<<TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
<TR><TD><TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0"><TR><TD HEIGHT="0" WIDTH="24" PORT="in1"></TD></TR></TABLE></TD></TR>
<TR><TD BORDER="1" CELLPADDING="4">g</TD></TR>
<TR><TD><TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0"><TR><TD HEIGHT="0" WIDTH="24" PORT="out1"></TD></TR></TABLE></TD></TR>
</TABLE>>];
  n0in1:s -> n1:in1:n [comment="A",id="e1"];
  n1:out1:s -> n2:in1:n [comment="B",id="e2"];
  n2:out1:s -> n0out1:n [comment="C",id="e3"];
}

Catlab provides a simple wrapper around the Graphviz command-line programs. For example, here is the JSON output for the graph.

import JSON

JSON.parse(Graphviz.run_graphviz(graph, format="json0"))
Dict{String, Any} with 9 entries:
  "name"          => "G"
  "strict"        => false
  "bb"            => "0,0,24,160"
  "objects"       => Any[Dict{String, Any}("rank"=>"source", "name"=>"%3", "nod…
  "fontname"      => "Serif"
  "rankdir"       => "TB"
  "directed"      => true
  "edges"         => Any[Dict{String, Any}("headport"=>"in1:n", "tail"=>2, "hea…
  "_subgraph_cnt" => 2