Repository: UCSBarchlab/PyRTL

Issue:
Improve visualization of nets where order of incoming edges matters
For nets where the order of incoming edges matters (e.g. `c`, `-`, `<`, `>`, `x`, and `@`), it would be nice to improve the way visualizations present them.

For example, given the following netlist:
```
tmp6/1W <-- s -- i/8I ((4,))
tmp4/1W <-- s -- i/8I ((2,))
tmp2/1W <-- s -- i/8I ((0,))
tmp5/1W <-- s -- i/8I ((3,))
tmp7/1W <-- s -- i/8I ((5,))
o/8O <-- w -- tmp10/8W 
tmp10/8W <-- c -- tmp7/1W, tmp6/1W, tmp5/1W, tmp4/1W, tmp3/1W, tmp2/1W, const_0_0_synth_1/1C, const_0_0_synth_0/1C 
tmp3/1W <-- s -- i/8I ((1,))
```

outputting to SVG produces the following:
![shift_test_sll_new_synth_opt](https://user-images.githubusercontent.com/2482771/117214298-79403600-adb1-11eb-96bb-21f92762c4c6.png)

It would be greatly improved if `[5]` was furthest left, followed by `[4]` to the right, etc., like the actual netlist is.

This is probably an issue specifically related to using graphviz, so if that library doesn't have the capability, it might be nice to consider adding support for an output format that respects left-to-right ordering.

Comments:

Patch:
diff --git a/pyrtl/visualization.py b/pyrtl/visualization.py
index 3514091..ab9bc33 100644
--- a/pyrtl/visualization.py
+++ b/pyrtl/visualization.py
@@ -6,8 +6,9 @@ The functions provided write the block as a given visual format to the file.
 """
 
 from __future__ import print_function, unicode_literals
+import collections
 
-from .pyrtlexceptions import PyrtlError
+from .pyrtlexceptions import PyrtlError, PyrtlInternalError
 from .core import working_block, LogicNet
 from .wire import WireVector, Input, Output, Const, Register
 
@@ -298,7 +299,8 @@ def graphviz_detailed_namer(
     return namer
 
 
-def output_to_graphviz(file, block=None, namer=_graphviz_default_namer, split_state=True):
+def output_to_graphviz(file, block=None, namer=_graphviz_default_namer,
+                       split_state=True, maintain_arg_order=False):
     """ Walk the block and output it in Graphviz format to the open file.
 
     :param file: Open file to write to
@@ -306,11 +308,15 @@ def output_to_graphviz(file, block=None, namer=_graphviz_default_namer, split_st
     :param namer: Function used to label each edge and node; see 'block_to_graphviz_string'
         for more information.
     :param split_state: If True, visually split the connections to/from a register update net.
+    :param maintain_arg_order: If True, will add ordering constraints so that that incoming edges
+        are ordered left-to-right for nets where argument order matters (e.g. '<'). Keeping this
+        as False results in a cleaner, though less visually precise, graphical output.
     """
-    print(block_to_graphviz_string(block, namer, split_state), file=file)
+    print(block_to_graphviz_string(block, namer, split_state, maintain_arg_order), file=file)
 
 
-def block_to_graphviz_string(block=None, namer=_graphviz_default_namer, split_state=True):
+def block_to_graphviz_string(block=None, namer=_graphviz_default_namer,
+                             split_state=True, maintain_arg_order=False):
     """ Return a Graphviz string for the block.
 
     :param namer: A function mapping graph objects (wires/logic nets) to labels.
@@ -320,6 +326,9 @@ def block_to_graphviz_string(block=None, namer=_graphviz_default_namer, split_st
         means that registers will be appear as source nodes of the network, and
         'r' nets (i.e. the logic for setting a register's next value) will
         be treated as sink nodes of the network.
+    :param maintain_arg_order: If True, will add ordering constraints so that that incoming edges
+        are ordered left-to-right for nets where argument order matters (e.g. '<'). Keeping this
+        as False results in a cleaner, though less visually precise, graphical output.
 
     The normal namer function will label user-named wires with their names and label the nodes
     (logic nets or Input/Output/Const terminals) with their operator symbol or name/value,
@@ -371,6 +380,7 @@ digraph g {
         node_index_map[node] = index
 
     # print the list of edges
+    srcs = collections.defaultdict(list)
     for _from in sorted(graph.keys(), key=_node_sort_key):
         for _to in sorted(graph[_from].keys(), key=_node_sort_key):
             from_index = node_index_map[_from]
@@ -379,6 +389,33 @@ digraph g {
                 is_to_splitmerge = True if hasattr(_to, 'op') and _to.op in 'cs' else False
                 label = namer(edge, True, is_to_splitmerge, False)
                 rstring += '    n%d -> n%d %s;\n' % (from_index, to_index, label)
+                srcs[_to].append((_from, edge))
+
+    # Maintain left-to-right order of incoming wires for nets where order matters.
+    # This won't be visually perfect sometimes (especially for a wire used twice
+    # in a net's argument list), but for the majority of cases this will improve
+    # the visualization.
+    def index_of(w, args):
+        # Special helper so we compare id rather than using builtin operators
+        ix = 0
+        for arg in args:
+            if w is arg:
+                return ix
+            ix += 1
+        raise PyrtlInternalError('Expected to find wire in set of args')
+
+    if maintain_arg_order:
+        block = working_block(block)
+        for net in sorted(block.logic_subset(op='c-<>x@'), key=_node_sort_key):
+            args = [(node_index_map[n], wire) for (n, wire) in srcs[net]]
+            args.sort(key=lambda t: index_of(t[1], net.args))
+            s = ' -> '.join(['n%d' % n for n, _ in args])
+            rstring += '    {\n'
+            rstring += '        rank=same;\n'
+            rstring += '        edge[style=invis];\n'
+            rstring += '        ' + s + ';\n'
+            rstring += '        rankdir=LR;\n'
+            rstring += '    }\n'
 
     rstring += '}\n'
     return rstring
@@ -399,16 +436,20 @@ def output_to_svg(file, block=None, split_state=True):
     print(block_to_svg(block, split_state), file=file)
 
 
-def block_to_svg(block=None, split_state=True):
+def block_to_svg(block=None, split_state=True, maintain_arg_order=False):
     """ Return an SVG for the block.
 
     :param block: Block to use (defaults to current working block)
     :param split_state: If True, visually split the connections to/from a register update net.
+    :param maintain_arg_order: If True, will add ordering constraints so that that incoming edges
+        are ordered left-to-right for nets where argument order matters (e.g. '<'). Keeping this
+        as False results in a cleaner, though less visually precise, graphical output.
     :return: The SVG representation of the block
     """
     try:
         from graphviz import Source
-        return Source(block_to_graphviz_string(block, split_state=split_state))._repr_svg_()
+        return Source(block_to_graphviz_string(block, split_state=split_state,
+                                               maintain_arg_order=maintain_arg_order))._repr_svg_()
     except ImportError:
         raise PyrtlError('need graphviz installed (try "pip install graphviz")')
 


Test patch:
diff --git a/tests/test_visualization.py b/tests/test_visualization.py
index cabe977..fea473b 100644
--- a/tests/test_visualization.py
+++ b/tests/test_visualization.py
@@ -5,7 +5,7 @@ import pyrtl
 from .test_importexport import full_adder_blif
 
 
-graphviz_string = """\
+graphviz_string_detailed = """\
 digraph g {
     graph [splines="spline", outputorder="edgesfirst"];
     node [shape=circle, style=filled, fillcolor=lightblue1,
@@ -35,6 +35,138 @@ digraph g {
     n9 -> n11 [label="tmp3/6 (Delay: 706.50)", penwidth="6", arrowhead="none"];
     n10 -> n11 [label="tmp4/4 (Delay: 0.00)", penwidth="6", arrowhead="none"];
     n11 -> n4 [label="tmp5/10 (Delay: 706.50)", penwidth="6", arrowhead="normal"];
+    {
+        rank=same;
+        edge[style=invis];
+        n6 -> n0;
+        rankdir=LR;
+    }
+    {
+        rank=same;
+        edge[style=invis];
+        n10 -> n9;
+        rankdir=LR;
+    }
+}
+
+"""
+
+graphviz_string_arg_ordered = """\
+digraph g {
+    graph [splines="spline", outputorder="edgesfirst"];
+    node [shape=circle, style=filled, fillcolor=lightblue1,
+        fontcolor=black, fontname=helvetica, penwidth=0,
+        fixedsize=shape];
+    edge [labelfloat=false, penwidth=2, color=deepskyblue, arrowsize=.5];
+    n0 [label="0", shape=circle, fillcolor=lightgrey];
+    n1 [label="0", shape=circle, fillcolor=lightgrey];
+    n2 [label="0", shape=circle, fillcolor=lightgrey];
+    n3 [label="i", shape=invhouse, fillcolor=coral];
+    n4 [label="j", shape=invhouse, fillcolor=coral];
+    n5 [label="", height=.1, width=.1];
+    n6 [label="o", shape=house, fillcolor=lawngreen];
+    n7 [label="", height=.1, width=.1];
+    n8 [label="q", shape=house, fillcolor=lawngreen];
+    n9 [label="[0]*4", fillcolor=azure1, height=.25, width=.25];
+    n10 [label="concat", height=.1, width=.1];
+    n11 [label="<"];
+    n12 [label="[0]*7", fillcolor=azure1, height=.25, width=.25];
+    n13 [label="concat", height=.1, width=.1];
+    n14 [label="[0]*4", fillcolor=azure1, height=.25, width=.25];
+    n15 [label="concat", height=.1, width=.1];
+    n16 [label=">"];
+    n0 -> n9 [label="", penwidth="2", arrowhead="none"];
+    n1 -> n12 [label="", penwidth="2", arrowhead="none"];
+    n2 -> n14 [label="", penwidth="2", arrowhead="none"];
+    n3 -> n11 [label="", penwidth="6", arrowhead="normal"];
+    n3 -> n16 [label="", penwidth="6", arrowhead="normal"];
+    n4 -> n10 [label="", penwidth="6", arrowhead="none"];
+    n4 -> n15 [label="", penwidth="6", arrowhead="none"];
+    n5 -> n6 [label="", penwidth="6", arrowhead="normal"];
+    n7 -> n8 [label="", penwidth="2", arrowhead="normal"];
+    n9 -> n10 [label="", penwidth="6", arrowhead="none"];
+    n10 -> n11 [label="", penwidth="6", arrowhead="normal"];
+    n11 -> n13 [label="", penwidth="2", arrowhead="none"];
+    n12 -> n13 [label="", penwidth="6", arrowhead="none"];
+    n13 -> n5 [label="", penwidth="6", arrowhead="normal"];
+    n14 -> n15 [label="", penwidth="6", arrowhead="none"];
+    n15 -> n16 [label="", penwidth="6", arrowhead="normal"];
+    n16 -> n7 [label="", penwidth="2", arrowhead="normal"];
+    {
+        rank=same;
+        edge[style=invis];
+        n9 -> n4;
+        rankdir=LR;
+    }
+    {
+        rank=same;
+        edge[style=invis];
+        n3 -> n10;
+        rankdir=LR;
+    }
+    {
+        rank=same;
+        edge[style=invis];
+        n12 -> n11;
+        rankdir=LR;
+    }
+    {
+        rank=same;
+        edge[style=invis];
+        n14 -> n4;
+        rankdir=LR;
+    }
+    {
+        rank=same;
+        edge[style=invis];
+        n15 -> n3;
+        rankdir=LR;
+    }
+}
+
+"""
+
+graphviz_string_arg_unordered = """\
+digraph g {
+    graph [splines="spline", outputorder="edgesfirst"];
+    node [shape=circle, style=filled, fillcolor=lightblue1,
+        fontcolor=black, fontname=helvetica, penwidth=0,
+        fixedsize=shape];
+    edge [labelfloat=false, penwidth=2, color=deepskyblue, arrowsize=.5];
+    n0 [label="0", shape=circle, fillcolor=lightgrey];
+    n1 [label="0", shape=circle, fillcolor=lightgrey];
+    n2 [label="0", shape=circle, fillcolor=lightgrey];
+    n3 [label="i", shape=invhouse, fillcolor=coral];
+    n4 [label="j", shape=invhouse, fillcolor=coral];
+    n5 [label="", height=.1, width=.1];
+    n6 [label="o", shape=house, fillcolor=lawngreen];
+    n7 [label="", height=.1, width=.1];
+    n8 [label="q", shape=house, fillcolor=lawngreen];
+    n9 [label="[0]*4", fillcolor=azure1, height=.25, width=.25];
+    n10 [label="concat", height=.1, width=.1];
+    n11 [label="<"];
+    n12 [label="[0]*7", fillcolor=azure1, height=.25, width=.25];
+    n13 [label="concat", height=.1, width=.1];
+    n14 [label="[0]*4", fillcolor=azure1, height=.25, width=.25];
+    n15 [label="concat", height=.1, width=.1];
+    n16 [label=">"];
+    n0 -> n9 [label="", penwidth="2", arrowhead="none"];
+    n1 -> n12 [label="", penwidth="2", arrowhead="none"];
+    n2 -> n14 [label="", penwidth="2", arrowhead="none"];
+    n3 -> n11 [label="", penwidth="6", arrowhead="normal"];
+    n3 -> n16 [label="", penwidth="6", arrowhead="normal"];
+    n4 -> n10 [label="", penwidth="6", arrowhead="none"];
+    n4 -> n15 [label="", penwidth="6", arrowhead="none"];
+    n5 -> n6 [label="", penwidth="6", arrowhead="normal"];
+    n7 -> n8 [label="", penwidth="2", arrowhead="normal"];
+    n9 -> n10 [label="", penwidth="6", arrowhead="none"];
+    n10 -> n11 [label="", penwidth="6", arrowhead="normal"];
+    n11 -> n13 [label="", penwidth="2", arrowhead="none"];
+    n12 -> n13 [label="", penwidth="6", arrowhead="none"];
+    n13 -> n5 [label="", penwidth="6", arrowhead="normal"];
+    n14 -> n15 [label="", penwidth="6", arrowhead="none"];
+    n15 -> n16 [label="", penwidth="6", arrowhead="normal"];
+    n16 -> n7 [label="", penwidth="2", arrowhead="normal"];
 }
 
 """
@@ -98,9 +230,34 @@ class TestOutputGraphs(unittest.TestCase):
         with io.StringIO() as vfile:
             pyrtl.output_to_graphviz(
                 file=vfile,
-                namer=pyrtl.graphviz_detailed_namer(node_fanout, wire_delay)
+                namer=pyrtl.graphviz_detailed_namer(node_fanout, wire_delay),
+                maintain_arg_order=True
             )
-            self.assertEqual(vfile.getvalue(), graphviz_string)
+            self.assertEqual(vfile.getvalue(), graphviz_string_detailed)
+
+    def test_output_to_graphviz_correct_output_with_arg_ordering(self):
+        i = pyrtl.Input(8, 'i')
+        j = pyrtl.Input(4, 'j')
+        o = pyrtl.Output(8, 'o')
+        q = pyrtl.Output(1, 'q')
+        o <<= i < j
+        q <<= j > i
+
+        with io.StringIO() as vfile:
+            pyrtl.output_to_graphviz(file=vfile, maintain_arg_order=True)
+            self.assertEqual(vfile.getvalue(), graphviz_string_arg_ordered)
+
+    def test_output_to_graphviz_correct_output_without_arg_ordering(self):
+        i = pyrtl.Input(8, 'i')
+        j = pyrtl.Input(4, 'j')
+        o = pyrtl.Output(8, 'o')
+        q = pyrtl.Output(1, 'q')
+        o <<= i < j
+        q <<= j > i
+
+        with io.StringIO() as vfile:
+            pyrtl.output_to_graphviz(file=vfile)
+            self.assertEqual(vfile.getvalue(), graphviz_string_arg_unordered)
 
 
 class TestNetGraph(unittest.TestCase):