Thursday, November 22, 2012

Drawing Prolog-generated graphs with graphviz

While I was TAing for a "Computing with Logic" course, one of the assignments had to do with graphs and computing the transitive closure over them with various ways. There was a question in the assignment asking students to identify the form of each graph described. Most of them did a very good job in either using simple text to "draw" the graph, or either use one of the many available software that helps with this kind of stuff. However, drawing the graph yourself means that you actually understand the form. What if you have some really complex graph specification in Prolog; how can you draw it without understanding how it looks like?

Well, the answer is pretty much straightforward; just as when you use writeln's to print out helpful information while the graph is being generated, why don't just output that exact information in a more "concrete" manner which can be parsed by graphviz, and let that draw the graph for you?

And so I set programming this small idea. It ended up not being to painful to do; it was a good exercise, and the result is quite pretty :)

First, here is the specification of three distinct graphs: trees, cycles and something called a "dline":


size(cycle,8).
size(tree,4).
size(dline,12).


cycle(M,0) :- size(cycle,M). % size(M) 
cycle(I,I1) :- 
          size(cycle,M), % size(M) 
          0 =< I, I < M, I1 is I + 1. 

tree(I,I1) :- size(tree,M), % size(M)
          0 =< I, I < M, I2 is I * 2, I1 is I2 + 1. 
tree(I,I1) :- size(tree,M), % size(M) 
          0 =< I, I < M, I2 is I * 2, I1 is I2 + 2. 

dline(I,I1) :- size(dline,M), % size(M) 
          0 =< I, I < M, 0 is I mod 4, I1 is I + 1. 
dline(I,I1) :- size(dline,M), % size(M) 
          0 =< I, I < M, 0 is I mod 4, I1 is I + 2. 
dline(I1,I2) :- size(dline,M), % size(M) 
          I is I1 - 1, 0 =< I, I < M, 0 is I mod 4, I2 is I + 4. 
dline(I1,I2) :- size(dline,M), % size(M) 
          I is I1 - 2, 0 =< I, I < M, 0 is I mod 4, I2 is I + 4.

All right, now that we have our graph specifications, let's see how can we generate a valid graphviz source code and let it draw our graphs. The first thing we need to do is transform a list of generated edges into valid graphviz code:

draw_graph(_Dest,_Shape,[]).
draw_graph(Dest,Name,[Shape|Edges]) :-
        functor(Shape,Name,2),
        arg(1,Shape,X),
        arg(2,Shape,Y),
        write(Dest,'"'),write(Dest,X),write(Dest,'"'),
        write(Dest,' -> '),
        write(Dest,'"'),write(Dest,Y),write(Dest,'"'),writeln(Dest,''),
        draw_graph(Dest,Name,Edges).

The variable Dest holds the IOhandler for the file we want to write our graphviz code in. Name is the name of the graph (cycle/tree/dline) and the list contains elements of the form [name(1,2),name(2,3),...], which means that for the graph "name" there is an edge from vertex 1 to vertex 2, from vertex 2 to vertex 3 and so on. So we just split each element into a functor name and its arguments using functor/3 and arg/3, and then just output everything in graphviz syntax, i.e. lines of the form "1" -> "2" e.t.c.

Next, we need a predicate that will generate the list of edges we talked about above for a given graph name, and call draw_graph/3 to output the graphviz code:

draw(Dest,Name) :-
        size(Name,M),
        get_list(M,[],List), 
        functor(Shape,Name,2),
        arg(1,Shape,X),
        findall(Shape,(member(X,List),Shape),L), writeln(L), 
        draw_graph(Dest,Name,L).


get_list(0,List,[0|List]).
get_list(M,List1,List) :- M > 0,
        M1 is M - 1,
        get_list(M1,[M|List1],List).



Again Dest holds the IOhandler for the output file, and Name is a valid graph name (cycle/tree/dline). get_list/2 just generates a list of numbers starting from M and ending at 0. We then construct a term of the form "name(From,To)" that will be bound to the facts generated by our specifications for graphs from above, thus generating in L the list of needed edges.

The final predicate we need is the one that will do all the file opening/closing, graphviz code generating, and will also run graphviz to draw each graph:



draw1(Fil) :- writeln(Fil),
        atom_concat(Fil,'.gv',File), 
        atom_concat(Fil,'.png',Out),
        open(File,write,Dest),
        writeln(Dest,'digraph G {'),
        draw(Dest,Fil), 
        writeln(Dest,'}'),
        close(Dest),
        atom_concat('dot -Tpng ',File,Cmd1),
        atom_concat(Cmd1,' > ',Cmd2),
        atom_concat(Cmd2,Out,Cmd),
        shell(Cmd).

File holds the name of the output file (which must be a valid graph name as discussed above). We open 2 separate files; the one in which we will put the graphviz code (ending in .gv), and the final PNG picture file. I used atom_concat/3 to concatenate the necessary strings to create a valid graphviz command, and just call it with shell/1. That's pretty much it! Now you can just fire XSB, load the source file containing the code we wrote here, and ask for draw1(tree), draw1(cycle) or draw1(dline). You'll then get some really pretty pictures from graphviz :)





No comments:

Post a Comment