pythontreegenetic-programmingdeap

Visualizing nested function calls in python


I need a way to visualize nested function calls in python, preferably in a tree-like structure. So, if I have a string that contains f(g(x,h(y))), I'd like to create a tree that makes the levels more readable. For example:

     f()
      |
     g()
     / \
    x  h()
        |
        y

Or, of course, even better, a tree plot like the one that sklearn.tree.plot_tree creates.

This seems like a problem that someone has probably solved long ago, but it has so far resisted my attempts to find it. FYI, this is for the visualization of genetic programming output that tends to have very complex strings like this.

thanks!

update: toytree and toyplot get pretty close, but just not quite there: enter image description here

This is generated with:

 import toytree, toyplot
 mystyle = {"layout": 'down','node_labels':True}
 s = '((x,(y)));'
 toytree.tree(s).draw(**mystyle);

It's close, but the node labels aren't strings...

Update 2: I found another potential solution that gets me closer in text form: https://rosettacode.org/wiki/Visualize_a_tree#Python

tree2 = Node('f')([ 
            Node('g')([
                Node('x')([]),
                Node('h')([
                    Node('y')([])
                ])
            ])
        ])
print('\n\n'.join([drawTree2(True)(False)(tree2)]))

This results in the following:

enter image description here

That's right, but I had to hand convert my string to the Node notation the drawTree2 function needs.


Solution

  • Here's a solution using pyparsing and asciitree. This can be adapted to parse just about anything and to generate whatever data structure is required for plotting. In this case, the code generates nested dictionaries suitable for input to asciitree.

    #!/usr/bin/env python3
    
    from collections import OrderedDict
    
    from asciitree import LeftAligned
    from pyparsing import Suppress, Word, alphas, Forward, delimitedList, ParseException, Optional
    
    
    def grammar():
        lpar = Suppress('(')
        rpar = Suppress(')')
    
        identifier = Word(alphas).setParseAction(lambda t: (t[0], {}))
        function_name = Word(alphas)
        expr = Forward()
        function_arg = delimitedList(expr)
        function = (function_name + lpar + Optional(function_arg) + rpar).setParseAction(lambda t: (t[0] + '()', OrderedDict(t[1:])))
        expr << (function | identifier)
    
        return function
    
    
    def parse(expr):
        g = grammar()
    
        try:
            parsed = g.parseString(expr, parseAll=True)
        except ParseException as e:
            print()
            print(expr)
            print(' ' * e.loc + '^')
            print(e.msg)
            raise
    
        return dict([parsed[0]])
    
    
    if __name__ == '__main__':
        expr = 'f(g(x,h(y)))'
        tree = parse(expr)
        print(LeftAligned()(tree))
    

    Output:

    f()
     +-- g()
         +-- x
         +-- h()
             +-- y
    

    Edit

    With some tweaks, you can build an edge list suitable for plotting in your favorite graph library (igraph example below).

    #!/usr/bin/env python3
    
    import igraph
    
    from pyparsing import Suppress, Word, alphas, Forward, delimitedList, ParseException, Optional
    
    
    class GraphBuilder(object):
        def __init__(self):
            self.labels = {}
            self.edges = []
    
        def add_edges(self, source, targets):
            for target in targets:
                self.add_edge(source, target)
    
            return source
    
        def add_edge(self, source, target):
            x = self.labels.setdefault(source, len(self.labels))
            y = self.labels.setdefault(target, len(self.labels))
            self.edges.append((x, y))
    
        def build(self):
            g = igraph.Graph()
            g.add_vertices(len(self.labels))
            g.vs['label'] = sorted(self.labels.keys(), key=lambda l: self.labels[l])
            g.add_edges(self.edges)
    
            return g
    
    
    def grammar(gb):
        lpar = Suppress('(')
        rpar = Suppress(')')
    
        identifier = Word(alphas)
        function_name = Word(alphas).setParseAction(lambda t: t[0] + '()')
        expr = Forward()
        function_arg = delimitedList(expr)
        function = (function_name + lpar + Optional(function_arg) + rpar).setParseAction(lambda t: gb.add_edges(t[0], t[1:]))
        expr << (function | identifier)
    
        return function
    
    
    def parse(expr, gb):
        g = grammar(gb)
        g.parseString(expr, parseAll=True)
    
    
    if __name__ == '__main__':
        expr = 'f(g(x,h(y)))'
    
        gb = GraphBuilder()
        parse(expr, gb)
    
        g = gb.build()
        layout = g.layout('tree', root=len(gb.labels)-1)
        igraph.plot(g, layout=layout, vertex_size=30, vertex_color='white')
    

    Output plot